diff --git a/Kirika_test.go b/Kirika_test.go index 959afc4..a248aa8 100644 --- a/Kirika_test.go +++ b/Kirika_test.go @@ -1,11 +1,12 @@ package Kirika import ( + "bytes" + "encoding/hex" + "git.gammaspectra.live/S.O.N.G/Kirika/audio" "git.gammaspectra.live/S.O.N.G/Kirika/audio/format" "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac" - "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/mp3" - "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus" - "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/tta" + "git.gammaspectra.live/S.O.N.G/Kirika/hasher" "os" "path" "testing" @@ -16,7 +17,9 @@ var TestSampleLocations = []string{ "resources/samples/Babbe Music - RADIANT DANCEFLOOR/", } -const BlockSize = 1024 * 64 +const TestSingleSample24 = "resources/samples/cYsmix - Haunted House/11. The Great Rigid Desert.flac" + +const TestSingleSample16 = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.flac" func doTest(format format.Format, ext string, t *testing.T) { for _, location := range TestSampleLocations { @@ -36,14 +39,14 @@ func doTest(format format.Format, ext string, t *testing.T) { return } defer fp.Close() - stream, err := format.Open(fp, BlockSize) + source, err := format.Open(fp) if err != nil { t.Error(err) return } //Decode - for range stream.GetAsBlockChannel() { + for range source.Blocks { } }) @@ -52,6 +55,7 @@ func doTest(format format.Format, ext string, t *testing.T) { } } +/* func TestFLACDecode(t *testing.T) { doTest(flac.NewFormat(), ".flac", t) } @@ -64,3 +68,130 @@ func TestOpusDecode(t *testing.T) { func TestMP3Decode(t *testing.T) { doTest(mp3.NewFormat(), ".mp3", t) } +*/ + +func TestHasher24(t *testing.T) { + + fp, err := os.Open(TestSingleSample24) + if err != nil { + t.Error(err) + return + } + defer fp.Close() + source, analyzerChannel, err := flac.NewFormat().OpenAnalyzer(fp) + if err != nil { + t.Error(err) + return + } + + chans := analyzerChannel.Split(2) + + crc32 := hasher.NewHasher(chans[0], hasher.HashtypeCrc32) + sha256 := hasher.NewHasher(chans[1], hasher.HashtypeSha256) + //Decode + for range source.Blocks { + + } + crc32.Wait() + sha256.Wait() + + expectCrc32, _ := hex.DecodeString("A54636CD") + if bytes.Compare(crc32.GetResult(), expectCrc32) != 0 { + t.Errorf("Wrong CRC32 %08X != %08X", crc32.GetResult(), expectCrc32) + } + + expectSha256, _ := hex.DecodeString("9B6715ED75B6C8074B749C630AC9C626994080DEACBEA363976391366DA4E4FA") + if bytes.Compare(sha256.GetResult(), expectSha256) != 0 { + t.Errorf("Wrong SHA256 %08X != %08X", sha256.GetResult(), expectSha256) + } + +} +func TestHasher16(t *testing.T) { + + fp, err := os.Open(TestSingleSample16) + if err != nil { + t.Error(err) + return + } + defer fp.Close() + source, analyzerChannel, err := flac.NewFormat().OpenAnalyzer(fp) + if err != nil { + t.Error(err) + return + } + + channels := analyzerChannel.Split(4) + cueToolsCrc32 := hasher.NewHasher(channels[0].SkipStartSamples(hasher.Int16SamplesPerSector*10), hasher.HashtypeCrc32) + arChannels := channels[1].SkipStartSamples(hasher.Int16SamplesPerSector*5 - 1).Split(2) + accurateRipV1 := hasher.NewHasher(arChannels[0], hasher.HashtypeAccurateRipV1Start) + accurateRipV2 := hasher.NewHasher(arChannels[1], hasher.HashtypeAccurateRipV2Start) + crc32 := hasher.NewHasher(channels[2], hasher.HashtypeCrc32) + sha256 := hasher.NewHasher(channels[3], hasher.HashtypeSha256) + + //Decode + for range source.Blocks { + + } + cueToolsCrc32.Wait() + accurateRipV1.Wait() + accurateRipV2.Wait() + crc32.Wait() + sha256.Wait() + + expectCueToolsCrc32, _ := hex.DecodeString("18701E02") + if bytes.Compare(cueToolsCrc32.GetResult(), expectCueToolsCrc32) != 0 { + t.Errorf("Wrong CTDB CRC32 %08X != %08X", cueToolsCrc32.GetResult(), expectCueToolsCrc32) + } + + expectAccurateRipV1, _ := hex.DecodeString("5593DA89") + if bytes.Compare(accurateRipV1.GetResult(), expectAccurateRipV1) != 0 { + t.Errorf("Wrong AccurateRip V1 %08X != %08X", accurateRipV1.GetResult(), expectAccurateRipV1) + } + + expectAccurateRipV2, _ := hex.DecodeString("DAA40E75") + if bytes.Compare(accurateRipV2.GetResult(), expectAccurateRipV2) != 0 { + t.Errorf("Wrong AccurateRip V2 %08X != %08X", accurateRipV2.GetResult(), expectAccurateRipV2) + } + + expectCrc32, _ := hex.DecodeString("50CE5057") + if bytes.Compare(crc32.GetResult(), expectCrc32) != 0 { + t.Errorf("Wrong CRC32 %08X != %08X", crc32.GetResult(), expectCrc32) + } + + expectSha256, _ := hex.DecodeString("FEDF080D500D1A49DF8366BE619918D2A5D00413B7C7613A39DC00659FA25AC6") + if bytes.Compare(sha256.GetResult(), expectSha256) != 0 { + t.Errorf("Wrong SHA256 %X != %X", sha256.GetResult(), expectSha256) + } + +} + +func TestFilterChain(t *testing.T) { + fp, err := os.Open(TestSingleSample24) + if err != nil { + t.Error(err) + return + } + defer fp.Close() + source, err := flac.NewFormat().Open(fp) + if err != nil { + t.Error(err) + return + } + + const sampleRate = 16000 + + result := audio.NewFilterChain(source, audio.MonoFilter{}, audio.NewResampleFilter(sampleRate, audio.BandlimitedFastest, 0), audio.StereoFilter{}) + + sink := audio.NewForwardSink(audio.NewNullSink()) + sink.Process(result) + + if result.SampleRate != sampleRate { + t.Errorf("Wrong SampleRate %d != %d", result.SampleRate, sampleRate) + } + if result.Channels != 2 { + t.Errorf("Wrong Channel Count %d != %d", result.SampleRate, 2) + } + if sink.SamplesRead != 6284999 { + t.Errorf("Wrong Sample Count %d != %d", sink.SamplesRead, 6284999) + } +} diff --git a/README.md b/README.md index f4e948c..b37840d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # [![](resources/kirikas.png)](resources/kirika.png) Kirika Collection of audio utilities for decoding/encoding files and streams. -* channel-based audio consumption chain +* Channel-based audio consumption/filter chain +* Raw sample analyzer channels +* AnalyzerChannel channels / mergers / splitters / trimmers * Audio resampler +* Audio downmixing to stereo/mono * FLAC stream decoder and encoder * TTA stream decoder * MP3 stream decoder diff --git a/audio/filter.go b/audio/filter.go new file mode 100644 index 0000000..58e1722 --- /dev/null +++ b/audio/filter.go @@ -0,0 +1,190 @@ +package audio + +/* +#cgo CFLAGS: -I"${SRCDIR}/../cgo" -march=native -Ofast -std=c99 +#include "audio.h" +*/ +import "C" +import ( + "github.com/dh1tw/gosamplerate" + "log" + "time" +) + +type Filter interface { + Process(source Source) Source +} + +func NewFilterChain(source Source, filters ...Filter) Source { + for _, filter := range filters { + source = filter.Process(source) + } + return source +} + +type RealTimeFilter struct { +} + +func (f RealTimeFilter) Process(source Source) Source { + outBlocks := make(chan []float32) + const blocksPerSecond = 10 + if source.SampleRate%blocksPerSecond != 0 { + log.Panicf("%d %% %d != 0", source.SampleRate, blocksPerSecond) + } + blockSize := (source.SampleRate / blocksPerSecond) * source.Channels + throttler := time.Tick(time.Second / blocksPerSecond) + + go func() { + defer close(outBlocks) + var buf []float32 + for block := range source.Blocks { + buf = append(buf, block...) + for len(buf) >= blockSize { + outBlocks <- buf[0:blockSize] + buf = buf[blockSize:] + <-throttler + } + } + + outBlocks <- buf + }() + + return Source{ + Channels: source.Channels, + SampleRate: source.SampleRate, + Blocks: outBlocks, + } +} + +type StereoFilter struct { +} + +func (f StereoFilter) Process(source Source) Source { + if source.Channels == 2 { //no change + return source + } + outBlocks := make(chan []float32) + + go func() { + defer close(outBlocks) + for block := range source.Blocks { + //bring any number of channels to stereo, using downmix formulas when necessary + buf := make([]float32, (len(block)/source.Channels)*2) + C.audio_multiple_channels_to_stereo((*C.float)(&block[0]), C.size_t(len(block)), (*C.float)(&buf[0]), C.int(source.Channels)) + outBlocks <- buf + } + }() + + return Source{ + Channels: 2, + SampleRate: source.SampleRate, + Blocks: outBlocks, + } +} + +type MonoFilter struct { +} + +func (f MonoFilter) Process(source Source) Source { + if source.Channels == 1 { //no change + return source + } + outBlocks := make(chan []float32) + + go func() { + defer close(outBlocks) + for block := range source.Blocks { + //bring any number of channels to mono, equally weighted, reusing buffer backwards + C.audio_multiple_channels_to_mono((*C.float)(&block[0]), C.size_t(len(block)), C.int(source.Channels)) + outBlocks <- block[0:(len(block) / source.Channels)] + } + }() + + return Source{ + Channels: 1, + SampleRate: source.SampleRate, + Blocks: outBlocks, + } +} + +type ResampleFilter struct { + sampleRate int + quality ResampleQuality + blockSize int +} + +type ResampleQuality int + +const ( + BandlimitedBest ResampleQuality = gosamplerate.SRC_SINC_BEST_QUALITY + BandlimitedMedium ResampleQuality = gosamplerate.SRC_SINC_MEDIUM_QUALITY + BandlimitedFastest ResampleQuality = gosamplerate.SRC_SINC_FASTEST + ZeroOrderHold ResampleQuality = gosamplerate.SRC_ZERO_ORDER_HOLD + Linear ResampleQuality = gosamplerate.SRC_LINEAR +) + +func NewResampleFilter(sampleRate int, quality ResampleQuality, blockSize int) ResampleFilter { + if blockSize == 0 { + blockSize = 1024 * 64 + } + return ResampleFilter{ + sampleRate: sampleRate, + quality: quality, + blockSize: blockSize, + } +} + +func (f ResampleFilter) Process(source Source) Source { + if source.SampleRate == f.sampleRate { //no change + return source + } + + outBlocks := make(chan []float32) + + go func() { + defer close(outBlocks) + + blockSize := f.blockSize * source.Channels + samplerateConverter, err := gosamplerate.New(int(f.quality), source.Channels, blockSize) + + if err != nil { + log.Panic(err) + } + defer gosamplerate.Delete(samplerateConverter) + + ratio := float64(f.sampleRate) / float64(source.SampleRate) + + for block := range source.Blocks { + for len(block) >= blockSize { + b, err := samplerateConverter.Process(block[0:blockSize], ratio, false) + if err != nil { + log.Panic(err) + } + if len(b) > 0 { + outBlocks <- b + } + block = block[0:blockSize] + } + b, err := samplerateConverter.Process(block, ratio, false) + if err != nil { + log.Panic(err) + } + if len(b) > 0 { + outBlocks <- b + } + } + b, err := samplerateConverter.Process([]float32{}, ratio, true) + if err != nil { + log.Panic(err) + } + if len(b) > 0 { + outBlocks <- b + } + }() + + return Source{ + Channels: source.Channels, + SampleRate: f.sampleRate, + Blocks: outBlocks, + } +} diff --git a/audio/format/analyzer.go b/audio/format/analyzer.go new file mode 100644 index 0000000..0becbfe --- /dev/null +++ b/audio/format/analyzer.go @@ -0,0 +1,208 @@ +package format + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "io" + "sync" + "sync/atomic" +) + +const chanBuf = 16 + +type AnalyzerChannel chan *AnalyzerPacket + +type AnalyzerFormat interface { + Format + // OpenAnalyzer Opens a stream and decodes it into an audio.Source, and additionally copy AnalyzerPacket back + OpenAnalyzer(r io.ReadSeekCloser) (audio.Source, AnalyzerChannel, error) +} + +func (c AnalyzerChannel) Split(n int) (channels []AnalyzerChannel) { + channels = make([]AnalyzerChannel, n) + for i := range channels { + channels[i] = make(AnalyzerChannel, chanBuf) + } + + go func() { + defer func() { + for _, channel := range channels { + close(channel) + } + }() + + for packet := range c { + for _, channel := range channels { + channel <- packet + } + } + }() + + return +} + +func (c AnalyzerChannel) PrependGap(samples, sampleRate, channels, bitDepth int) (channel AnalyzerChannel) { + return MergeHasherChannels(NewHasherAudioGap(samples, sampleRate, channels, bitDepth), c) +} + +func (c AnalyzerChannel) AppendGap(samples, sampleRate, channels, bitDepth int) (channel AnalyzerChannel) { + return MergeHasherChannels(c, NewHasherAudioGap(samples, sampleRate, channels, bitDepth)) +} + +func (c AnalyzerChannel) SkipStartSamples(samples int) (channel AnalyzerChannel) { + channel = make(AnalyzerChannel, chanBuf) + go func() { + defer close(channel) + + for samples > 0 { + packet, ok := <-c + if !ok { + return + } + + if len(packet.Samples)/packet.Channels > samples { + startIndex := samples * packet.Channels + channel <- &AnalyzerPacket{ + Samples: packet.Samples[startIndex:], + Channels: packet.Channels, + SampleRate: packet.SampleRate, + BitDepth: packet.BitDepth, + } + samples = 0 + break + } else { + samples -= len(packet.Samples) / packet.Channels + continue + } + } + + for packet := range c { + channel <- packet + } + }() + + return +} + +func (c AnalyzerChannel) SkipEndSamples(samples int) (channel AnalyzerChannel) { + channel = make(AnalyzerChannel, chanBuf) + go func() { + defer close(channel) + + var buffer []*AnalyzerPacket + bufferSamples := 0 + + for packet := range c { + for len(buffer) > 0 && (bufferSamples-len(buffer[0].Samples)/buffer[0].Channels) > samples { + channel <- buffer[0] + bufferSamples -= len(buffer[0].Samples) / buffer[0].Channels + buffer = buffer[1:] + } + + bufferSamples += len(packet.Samples) / packet.Channels + buffer = append(buffer, packet) + } + + for _, packet := range buffer { + //TODO: check this + leftSamples := bufferSamples - len(packet.Samples)/packet.Channels + + if leftSamples <= samples { + endIndex := len(packet.Samples) - (samples-leftSamples)*packet.Channels + channel <- &AnalyzerPacket{ + Samples: packet.Samples[:endIndex], + Channels: packet.Channels, + SampleRate: packet.SampleRate, + BitDepth: packet.BitDepth, + } + samples = 0 + break + } else { + channel <- packet + bufferSamples -= len(packet.Samples) / packet.Channels + } + } + }() + + return +} + +func (c AnalyzerChannel) SkipEndSamplesMultiple(wg *sync.WaitGroup, offset *uint32, samples int) (channel AnalyzerChannel) { + channel = make(AnalyzerChannel, chanBuf) + go func() { + defer close(channel) + + var buffer []*AnalyzerPacket + bufferSamples := 0 + + maxSamples := samples * 2 + + samplesRead := 0 + for packet := range c { + for len(buffer) > 0 && (bufferSamples-len(buffer[0].Samples)/buffer[0].Channels) > maxSamples { + channel <- buffer[0] + samplesRead += len(buffer[0].Samples) / buffer[0].Channels + bufferSamples -= len(buffer[0].Samples) / buffer[0].Channels + buffer = buffer[1:] + } + + bufferSamples += len(packet.Samples) / packet.Channels + buffer = append(buffer, packet) + } + + wg.Wait() + totalSampleOffset := samplesRead + int(atomic.LoadUint32(offset)) + + if len(buffer) > 0 { + p := &AnalyzerPacket{ + Channels: buffer[0].Channels, + SampleRate: buffer[0].SampleRate, + BitDepth: buffer[0].BitDepth, + } + for _, packet := range buffer { + p.Samples = append(p.Samples, packet.Samples...) + } + nsamples := samples + (((len(p.Samples) / p.Channels) + totalSampleOffset) % samples) + + if len(p.Samples)/p.Channels > nsamples { + endIndex := len(p.Samples) - nsamples*p.Channels + channel <- &AnalyzerPacket{ + Samples: p.Samples[:endIndex], + Channels: p.Channels, + SampleRate: p.SampleRate, + BitDepth: p.BitDepth, + } + } + } + }() + + return +} + +func NewHasherAudioGap(samples, sampleRate, channels, bitDepth int) (channel AnalyzerChannel) { + channel = make(AnalyzerChannel, 1) + channel <- &AnalyzerPacket{ + Samples: make([]int32, samples*channels), + Channels: channels, + SampleRate: sampleRate, + BitDepth: bitDepth, + } + close(channel) + + return +} + +func MergeHasherChannels(channels ...AnalyzerChannel) (channel AnalyzerChannel) { + channel = make(AnalyzerChannel, chanBuf) + + go func() { + defer close(channel) + + for _, c := range channels { + for packet := range c { + channel <- packet + } + } + }() + + return +} diff --git a/audio/format/flac/flac.go b/audio/format/flac/flac.go index 2641b29..f2f5845 100644 --- a/audio/format/flac/flac.go +++ b/audio/format/flac/flac.go @@ -22,10 +22,10 @@ func NewFormat() Format { return Format{} } -func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { +func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { decoder, err := libflac.NewDecoderReader(r) if err != nil { - return nil, err + return audio.Source{}, err } newChannel := make(chan []float32) @@ -58,16 +58,21 @@ func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) } }() - return audio.NewStream(newChannel, decoder.Channels, float64(decoder.Rate), blockSize), nil + return audio.Source{ + Channels: decoder.Channels, + SampleRate: decoder.Rate, + Blocks: newChannel, + }, nil } -func (f Format) OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, chan *format.AnalyzerPacket, error) { + +func (f Format) OpenAnalyzer(r io.ReadSeekCloser) (audio.Source, format.AnalyzerChannel, error) { decoder, err := libflac.NewDecoderReader(r) if err != nil { - return nil, nil, err + return audio.Source{}, nil, err } newChannel := make(chan []float32) - analyzerChannel := make(chan *format.AnalyzerPacket) + analyzerChannel := make(format.AnalyzerChannel) go func() { defer close(newChannel) @@ -108,10 +113,14 @@ func (f Format) OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, } }() - return audio.NewStream(newChannel, decoder.Channels, float64(decoder.Rate), blockSize), analyzerChannel, nil + return audio.Source{ + Channels: decoder.Channels, + SampleRate: decoder.Rate, + Blocks: newChannel, + }, analyzerChannel, nil } -func (f Format) Encode(stream *audio.Stream, writer format.WriteSeekCloser, options map[string]interface{}) error { +func (f Format) Encode(source audio.Source, writer format.WriteSeekCloser, options map[string]interface{}) error { var bitsPerSample = 16 if options != nil { @@ -125,21 +134,21 @@ func (f Format) Encode(stream *audio.Stream, writer format.WriteSeekCloser, opti } } - encoder, err := libflac.NewEncoderWriter(writer, stream.GetChannels(), bitsPerSample, int(stream.GetSampleRate())) + encoder, err := libflac.NewEncoderWriter(writer, source.Channels, bitsPerSample, source.SampleRate) if err != nil { return err } defer encoder.Close() - for block := range stream.GetAsBlockChannel() { + for block := range source.Blocks { samples := make([]int32, len(block)) C.audio_float32_to_int32((*C.float)(&block[0]), C.size_t(len(block)), (*C.int32_t)(&samples[0]), C.int(bitsPerSample)) err = encoder.WriteFrame(libflac.Frame{ - Rate: int(stream.GetSampleRate()), - Channels: stream.GetChannels(), + Rate: source.SampleRate, + Channels: source.Channels, Depth: bitsPerSample, Buffer: samples, }) diff --git a/audio/format/format.go b/audio/format/format.go index 30f624a..29e9f54 100644 --- a/audio/format/format.go +++ b/audio/format/format.go @@ -9,14 +9,8 @@ type Format interface { // Identify checks whether a format is of a type. peek includes a few first bytes, extension is the lowercase file extension without a dot. Identify(peek []byte, extension string) bool - // Open Opens a stream and decodes it into an audio.Stream - Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) -} - -type AnalyzerFormat interface { - Format - // OpenAnalyzer Opens a stream and decodes it into an audio.Stream, and additionally copy AnalyzerPacket back - OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, chan *AnalyzerPacket, error) + // Open Opens a stream and decodes it into an audio.Source + Open(r io.ReadSeekCloser) (audio.Source, error) } type WriteSeekCloser interface { @@ -27,8 +21,8 @@ type WriteSeekCloser interface { type Encoder interface { Format - // Encode Receives a stream and encodes it into a writer - Encode(stream *audio.Stream, writer WriteSeekCloser, options map[string]interface{}) error + // Encode Receives an audio.Source and encodes it into a writer + Encode(source audio.Source, writer WriteSeekCloser, options map[string]interface{}) error } type AnalyzerPacket struct { diff --git a/audio/format/mp3/mp3.go b/audio/format/mp3/mp3.go index d9f33f1..8331c97 100644 --- a/audio/format/mp3/mp3.go +++ b/audio/format/mp3/mp3.go @@ -13,6 +13,8 @@ import ( "unsafe" ) +const BlockSize = 1024 * 128 + type Format struct { format.Format } @@ -21,19 +23,19 @@ func NewFormat() Format { return Format{} } -func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { +func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { decoder := mp3Lib.NewDecoder(r) _, err := decoder.Read([]byte{}) if err != nil { - return nil, err + return audio.Source{}, err } newChannel := make(chan []float32) go func() { defer close(newChannel) - samples := make([]int16, blockSize*2) + samples := make([]int16, BlockSize*2) const SizeofInt16 = int(unsafe.Sizeof(int16(0))) byteSlice := unsafe.Slice((*byte)(unsafe.Pointer(&samples[0])), len(samples)*SizeofInt16) for { @@ -52,7 +54,11 @@ func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) } }() - return audio.NewStream(newChannel, decoder.Channels(), float64(decoder.SampleRate()), blockSize), nil + return audio.Source{ + Channels: decoder.Channels(), + SampleRate: decoder.SampleRate(), + Blocks: newChannel, + }, nil } func (f Format) Identify(peek []byte, extension string) bool { diff --git a/audio/format/opus/opus.go b/audio/format/opus/opus.go index c7cd26b..7739db4 100644 --- a/audio/format/opus/opus.go +++ b/audio/format/opus/opus.go @@ -8,7 +8,8 @@ import ( "io" ) -const OPUS_SAMPLE_RATE int = 48000 +const FixedSampleRate int = 48000 +const BlockSize = 1024 * 128 type Format struct { format.Format @@ -18,10 +19,10 @@ func NewFormat() Format { return Format{} } -func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { +func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { stream, err := libopus.NewStream(r) if err != nil { - return nil, err + return audio.Source{}, err } newChannel := make(chan []float32) @@ -32,7 +33,7 @@ func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) for { - buf := make([]float32, blockSize*2) + buf := make([]float32, BlockSize*2) n, err := stream.ReadStereoFloat32(buf) if err != nil { @@ -46,7 +47,11 @@ func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) }() //We always get two channels via stereo api - return audio.NewStream(newChannel, 2, float64(OPUS_SAMPLE_RATE), blockSize), nil + return audio.Source{ + Channels: 2, + SampleRate: FixedSampleRate, + Blocks: newChannel, + }, nil } func (f Format) Identify(peek []byte, extension string) bool { diff --git a/audio/format/tta/tta.go b/audio/format/tta/tta.go index b65e511..0a7a823 100644 --- a/audio/format/tta/tta.go +++ b/audio/format/tta/tta.go @@ -15,6 +15,8 @@ import ( "unsafe" ) +const BlockSize = 1024 * 128 + type Format struct { format.Format format.Encoder @@ -58,24 +60,24 @@ func (i fakeReadWriteSeeker2) Write(p []byte) (n int, err error) { return i.w.Write(p) } -func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { +func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { decoder := libtta.MakeDecoder(fakeReadWriteSeeker{r}) if decoder == nil { - return nil, errors.New("invalid decoder") + return audio.Source{}, errors.New("invalid decoder") } newChannel := make(chan []float32) info := &libtta.Info{} if err := decoder.GetInfo(info, 0); err != nil { - return nil, err + return audio.Source{}, err } go func() { defer close(newChannel) smpSize := int(info.Nch) * ((int(info.Bps) + 7) / 8) - buffer := make([]byte, blockSize*smpSize) + buffer := make([]byte, BlockSize*smpSize) var writeLen int for { @@ -105,27 +107,31 @@ func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) } }() - return audio.NewStream(newChannel, int(info.Nch), float64(info.Sps), blockSize), nil + return audio.Source{ + Channels: int(info.Nch), + SampleRate: int(info.Sps), + Blocks: newChannel, + }, nil } -func (f Format) OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, chan *format.AnalyzerPacket, error) { +func (f Format) OpenAnalyzer(r io.ReadSeekCloser) (audio.Source, format.AnalyzerChannel, error) { decoder := libtta.MakeDecoder(fakeReadWriteSeeker{r}) if decoder == nil { - return nil, nil, errors.New("invalid decoder") + return audio.Source{}, nil, errors.New("invalid decoder") } newChannel := make(chan []float32) - analyzerChannel := make(chan *format.AnalyzerPacket) + analyzerChannel := make(format.AnalyzerChannel) info := &libtta.Info{} if err := decoder.GetInfo(info, 0); err != nil { - return nil, nil, err + return audio.Source{}, nil, err } go func() { defer close(newChannel) smpSize := int(info.Nch) * ((int(info.Bps) + 7) / 8) - buffer := make([]byte, blockSize*smpSize) + buffer := make([]byte, BlockSize*smpSize) var writeLen int for { @@ -154,7 +160,11 @@ func (f Format) OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, } }() - return audio.NewStream(newChannel, int(info.Nch), float64(info.Sps), blockSize), analyzerChannel, nil + return audio.Source{ + Channels: int(info.Nch), + SampleRate: int(info.Sps), + Blocks: newChannel, + }, analyzerChannel, nil } func (f Format) Identify(peek []byte, extension string) bool { diff --git a/audio/sink.go b/audio/sink.go new file mode 100644 index 0000000..3e16e1f --- /dev/null +++ b/audio/sink.go @@ -0,0 +1,63 @@ +package audio + +import ( + "sync/atomic" + "time" +) + +type Sink interface { + Process(source Source) +} + +type NullSink struct { + Sink +} + +func NewNullSink() *NullSink { + return &NullSink{} +} + +func (n *NullSink) Process(source Source) { + for range source.Blocks { + + } +} + +type ForwardSink struct { + Sink + Target Sink + SamplesRead int64 + Duration time.Duration +} + +func NewForwardSink(target Sink) *ForwardSink { + return &ForwardSink{ + Target: target, + } +} + +func (n *ForwardSink) GetDuration() time.Duration { + return time.Duration(atomic.LoadInt64((*int64)(&n.Duration))) +} + +func (n *ForwardSink) GetSamplesRead() int64 { + return atomic.LoadInt64(&n.SamplesRead) +} + +func (n *ForwardSink) Process(source Source) { + processor := Source{ + Channels: source.Channels, + SampleRate: source.SampleRate, + Blocks: make(chan []float32), + } + go func() { + defer close(processor.Blocks) + for block := range source.Blocks { + atomic.AddInt64((*int64)(&n.Duration), int64((time.Second*time.Duration(len(block)/source.Channels))/time.Duration(source.SampleRate))) + atomic.AddInt64(&n.SamplesRead, int64(len(block)/source.Channels)) + processor.Blocks <- block + } + }() + + n.Target.Process(processor) +} diff --git a/audio/source.go b/audio/source.go new file mode 100644 index 0000000..7370f3f --- /dev/null +++ b/audio/source.go @@ -0,0 +1,7 @@ +package audio + +type Source struct { + SampleRate int + Channels int + Blocks chan []float32 +} diff --git a/audio/stream.go b/audio/stream.go index 44e0839..d2bb9d5 100644 --- a/audio/stream.go +++ b/audio/stream.go @@ -1,40 +1,25 @@ package audio -/* -#cgo CFLAGS: -I"${SRCDIR}/../cgo" -march=native -Ofast -std=c99 -#include "audio.h" -*/ -import "C" -import ( - "fmt" - "github.com/dh1tw/gosamplerate" - "log" -) - type Stream struct { - source chan []float32 - channels int - sampleRate float64 + source Source samplesProcessed int buffer []float32 blockSize int } -func NewStream(source chan []float32, channels int, sampleRate float64, blockSize int) *Stream { +func NewStream(source Source, blockSize int) *Stream { return &Stream{ - source: source, - channels: channels, - sampleRate: sampleRate, - blockSize: blockSize, + source: source, + blockSize: blockSize, } } func (s *Stream) GetChannels() int { - return s.channels + return s.source.Channels } -func (s *Stream) GetSampleRate() float64 { - return s.sampleRate +func (s *Stream) GetSampleRate() int { + return s.source.SampleRate } func (s *Stream) GetSamplesProcessed() int { @@ -62,12 +47,12 @@ func (s *Stream) GetAsBlockChannel() chan []float32 { go func() { defer close(newChannel) - for buf := range s.source { + for buf := range s.source.Blocks { s.buffer = append(s.buffer, buf...) - for len(s.buffer) >= s.blockSize*s.channels { - newChannel <- s.buffer[0 : s.blockSize*s.channels] - s.samplesProcessed += s.blockSize * s.channels - s.buffer = s.buffer[s.blockSize*s.channels:] + for len(s.buffer) >= s.blockSize*s.source.Channels { + newChannel <- s.buffer[0 : s.blockSize*s.source.Channels] + s.samplesProcessed += s.blockSize * s.source.Channels + s.buffer = s.buffer[s.blockSize*s.source.Channels:] } } @@ -83,7 +68,7 @@ func (s *Stream) GetAsBlockChannel() chan []float32 { func (s *Stream) Get() (float32, bool) { var more bool if len(s.buffer) == 0 { - s.buffer, more = <-s.source + s.buffer, more = <-s.source.Blocks if !more { return 0, false } @@ -106,76 +91,5 @@ func (s *Stream) AdvanceSeconds(seconds float64) bool { } func (s *Stream) secondsIndex(seconds float64) int { - return int(seconds * s.sampleRate) -} - -const ( - RESAMPLER_QUALITY_BANDLIMITED_BEST = gosamplerate.SRC_SINC_BEST_QUALITY - RESAMPLER_QUALITY_BANDLIMITED_MEDIUM = gosamplerate.SRC_SINC_MEDIUM_QUALITY - RESAMPLER_QUALITY_BANDLIMITED_FASTEST = gosamplerate.SRC_SINC_FASTEST - RESAMPLER_QUALITY_HOLD = gosamplerate.SRC_ZERO_ORDER_HOLD - RESAMPLER_QUALITY_LINEAR = gosamplerate.SRC_LINEAR -) - -func (s *Stream) DoResample(channels int, sampleRate float64, quality int) (*Stream, error) { - if channels != 1 && channels != 2 && s.channels != channels { - return nil, fmt.Errorf("cannot convert from %d channels to %d", s.channels, channels) - } - - if channels == s.channels && sampleRate == s.sampleRate { - return s, nil - } - - samplerateConverter, err := gosamplerate.New(quality, channels, s.blockSize*s.channels) - if err != nil { - return nil, err - } - - newChannel := make(chan []float32) - - go func() { - defer gosamplerate.Delete(samplerateConverter) - defer close(newChannel) - ratio := sampleRate / s.sampleRate - - for buf := range s.GetAsBlockChannel() { - if channels != s.channels && channels == 1 { - if channels == 1 { - //bring any number of channels to mono, equally weighted, reusing buffer backwards - C.audio_multiple_channels_to_mono((*C.float)(&buf[0]), C.size_t(len(buf)), C.int(s.channels)) - buf = buf[0:(len(buf) / s.channels)] - } else if channels == 2 { - //bring any number of channels to stereo, using downmix formulas when necessary - out := make([]float32, (len(buf)/s.channels)*2) - C.audio_multiple_channels_to_stereo((*C.float)(&buf[0]), C.size_t(len(buf)), (*C.float)(&out[0]), C.int(s.channels)) - buf = out - } - } - - for len(buf) > 0 { - n := len(buf) - if n > s.blockSize*channels { - n = s.blockSize * channels - } - b, err := samplerateConverter.Process(buf[0:n], ratio, false) - if err != nil { - log.Panic(err) - } - if len(b) > 0 { - newChannel <- b - } - buf = buf[n:] - } - } - - b, err := samplerateConverter.Process([]float32{}, ratio, true) - if err != nil { - log.Panic(err) - } - if len(b) > 0 { - newChannel <- b - } - }() - - return NewStream(newChannel, channels, sampleRate, s.blockSize), nil + return int(seconds * float64(s.source.SampleRate)) } diff --git a/hasher/accuraterip.go b/hasher/accuraterip.go new file mode 100644 index 0000000..b0dd938 --- /dev/null +++ b/hasher/accuraterip.go @@ -0,0 +1,82 @@ +package hasher + +import ( + "hash" + "unsafe" +) + +type accurateRipDigestV1 struct { + crc uint32 + pos uint32 + offset uint32 +} + +func NewAccurateRipV1(offset uint32) hash.Hash32 { + return &accurateRipDigestV1{0, offset + 1, offset} +} + +func (d *accurateRipDigestV1) Size() int { return 4 } + +func (d *accurateRipDigestV1) BlockSize() int { return 1 } + +func (d *accurateRipDigestV1) Reset() { d.crc = 0; d.pos = d.offset + 1 } + +func (d *accurateRipDigestV1) Sum32() uint32 { return d.crc } + +func (d *accurateRipDigestV1) Sum(in []byte) []byte { + s := d.Sum32() + return append(in, byte(s>>24), byte(s>>16), byte(s>>8), byte(s)) +} + +func (d *accurateRipDigestV1) Write(p []byte) (n int, err error) { + numWords := uintptr(len(p)) * unsafe.Sizeof(p[0]) / unsafe.Sizeof(uint32(0)) + words := unsafe.Slice((*uint32)(unsafe.Pointer(&p[0])), numWords) + + for _, w := range words { + //this can wrap + d.crc += d.pos * w + d.pos++ + } + + return len(p), nil +} + +type accurateRipDigestV2 struct { + crc uint32 + multiplier uint32 + offset uint32 +} + +func NewAccurateRipV2(offset uint32) hash.Hash32 { + return &accurateRipDigestV2{0, offset + 1, offset} +} + +func (d *accurateRipDigestV2) Size() int { return 4 } + +func (d *accurateRipDigestV2) BlockSize() int { return 1 } + +func (d *accurateRipDigestV2) Reset() { d.crc = 0; d.multiplier = d.offset + 1 } + +func (d *accurateRipDigestV2) Sum32() uint32 { return d.crc } + +func (d *accurateRipDigestV2) Sum(in []byte) []byte { + s := d.Sum32() + return append(in, byte(s>>24), byte(s>>16), byte(s>>8), byte(s)) +} + +func (d *accurateRipDigestV2) Write(p []byte) (n int, err error) { + numWords := uintptr(len(p)) * unsafe.Sizeof(p[0]) / unsafe.Sizeof(uint32(0)) + words := unsafe.Slice((*uint32)(unsafe.Pointer(&p[0])), numWords) + + for _, w := range words { + crcNew := uint64(w) * uint64(d.multiplier) + LO := crcNew & 0xFFFFFFFF + HI := crcNew / 0x100000000 + //this can wrap + d.crc += uint32(HI) + d.crc += uint32(LO) + d.multiplier++ + } + + return len(p), nil +} diff --git a/hasher/hasher.go b/hasher/hasher.go new file mode 100644 index 0000000..e36cb22 --- /dev/null +++ b/hasher/hasher.go @@ -0,0 +1,183 @@ +package hasher + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/binary" + "git.gammaspectra.live/S.O.N.G/Kirika/audio/format" + "hash" + "hash/crc32" + "sync" + "time" +) + +const TocPregap = 150 +const SectorsPerSecond = 75 +const DataTrackGap = 11400 +const BytesPerSector = 2352 +const CDChannels = 2 +const Int16SamplesPerSector = BytesPerSector / (2 * CDChannels) +const CDSampleRate = Int16SamplesPerSector * SectorsPerSecond + +type HashType int + +const ( + HashtypeCrc32 = HashType(iota) + HashtypeSha256 + HashtypeSha1 + HashtypeMd5 + HashtypeAccurateRipV1 + HashtypeAccurateRipV1Start + HashtypeAccurateRipV2 + HashtypeAccurateRipV2Start +) + +type Hasher struct { + hash HashType + hasher hash.Hash + result []byte + channel format.AnalyzerChannel + wg sync.WaitGroup + samples int + duration float64 + sampleRate int + bitDepth int + channels int + buffer [][]int32 +} + +func NewHasher(channel format.AnalyzerChannel, hashType HashType) (h *Hasher) { + h = &Hasher{ + hash: hashType, + channel: channel, + } + + switch hashType { + case HashtypeCrc32: + h.hasher = crc32.NewIEEE() + case HashtypeSha256: + h.hasher = sha256.New() + case HashtypeSha1: + h.hasher = sha1.New() + case HashtypeMd5: + h.hasher = md5.New() + case HashtypeAccurateRipV1: + h.hasher = NewAccurateRipV1(0) + case HashtypeAccurateRipV1Start: + h.hasher = NewAccurateRipV1(Int16SamplesPerSector*5 - 1) + case HashtypeAccurateRipV2: + h.hasher = NewAccurateRipV2(0) + case HashtypeAccurateRipV2Start: + h.hasher = NewAccurateRipV2(Int16SamplesPerSector*5 - 1) + + } + + h.startRoutine() + + return +} + +func (h *Hasher) startRoutine() { + h.wg.Add(1) + go func() { + defer h.wg.Done() + + for packet := range h.channel { + h.handlePacket(packet) + } + + h.result = h.hasher.Sum([]byte{}) + + }() +} + +func (h *Hasher) handlePacket(packet *format.AnalyzerPacket) { + samples := len(packet.Samples) / packet.Channels + + h.samples += samples + + if h.sampleRate == 0 { + h.sampleRate = packet.SampleRate + } else if h.sampleRate != packet.SampleRate { + h.sampleRate = -1 + } + if h.bitDepth == 0 { + h.bitDepth = packet.BitDepth + } else if h.bitDepth != packet.BitDepth { + h.bitDepth = -1 + } + if h.channels == 0 { + h.channels = packet.Channels + } else if h.channels != packet.Channels { + h.channels = -1 + } + + h.duration += float64(samples) / float64(packet.SampleRate) + + var buf []byte + switch packet.BitDepth { + case 8: + buf = make([]byte, len(packet.Samples)) + for i := range packet.Samples { + buf[i] = byte(packet.Samples[i]) + } + case 16: + buf = make([]byte, len(packet.Samples)*2) + for i := range packet.Samples { + binary.LittleEndian.PutUint16(buf[i*2:], uint16(int16(packet.Samples[i]))) + } + case 24: + buf = make([]byte, len(packet.Samples)*3) + for i := range packet.Samples { + buf[i*3] = byte((packet.Samples[i]) & 0xFF) + buf[i*3+1] = byte((packet.Samples[i] >> 8) & 0xFF) + buf[i*3+2] = byte((packet.Samples[i] >> 16) & 0xFF) + } + default: + buf = make([]byte, len(packet.Samples)*4) + for i := range packet.Samples { + binary.LittleEndian.PutUint32(buf[i*4:], uint32(packet.Samples[i])) + } + + } + + h.hasher.Write(buf) +} + +func (h *Hasher) GetSampleCount() int { + return h.samples +} + +func (h *Hasher) GetChannels() int { + return h.channels +} + +func (h *Hasher) GetSampleRate() int { + return h.sampleRate +} + +func (h *Hasher) GetHashType() HashType { + return h.hash +} + +func (h *Hasher) GetResult() []byte { + return h.result +} + +func (h *Hasher) GetDuration() time.Duration { + if h.sampleRate > 0 { + return time.Duration(float64(time.Second) * (float64(h.samples) / float64(h.sampleRate))) + } + + //Fallback calculated duration + return time.Duration(float64(time.Second) * h.duration) +} + +func (h *Hasher) GetWaitGroup() *sync.WaitGroup { + return &h.wg +} + +func (h *Hasher) Wait() { + h.wg.Wait() +}