commit 00585aac138a82590dc0930a8e376c682bfaf01c Author: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Tue Feb 22 10:24:49 2022 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..757fee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/Kirika.go b/Kirika.go new file mode 100644 index 0000000..f05df49 --- /dev/null +++ b/Kirika.go @@ -0,0 +1 @@ +package Kirika diff --git a/Kirika_test.go b/Kirika_test.go new file mode 100644 index 0000000..6702830 --- /dev/null +++ b/Kirika_test.go @@ -0,0 +1,62 @@ +package Kirika + +import ( + "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" + "os" + "path" + "testing" +) + +var TestSampleLocations = []string{ + "resources/samples/cYsmix - Haunted House/", + "resources/samples/Babbe Music - RADIANT DANCEFLOOR/", +} + +const BlockSize = 1024 * 64 + +func doTest(format format.Format, ext string, t *testing.T) { + for _, location := range TestSampleLocations { + entries, err := os.ReadDir(location) + if err != nil { + t.Error(err) + return + } + for _, f := range entries { + if path.Ext(f.Name()) == ext { + fullPath := path.Join(location, f.Name()) + t.Run(f.Name(), func(t *testing.T) { + t.Parallel() + fp, err := os.Open(fullPath) + if err != nil { + t.Error(err) + return + } + defer fp.Close() + stream, err := format.Open(fp, BlockSize) + if err != nil { + t.Error(err) + return + } + + //Decode + for range stream.GetAsBlockChannel() { + + } + }) + } + } + } +} + +func TestFLACDecode(t *testing.T) { + doTest(flac.NewFormat(), ".flac", t) +} +func TestOpusDecode(t *testing.T) { + doTest(opus.NewFormat(), ".opus", t) +} +func TestMP3Decode(t *testing.T) { + doTest(mp3.NewFormat(), ".mp3", t) +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..005484c --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2022 Kirika Contributors All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ba9196 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# [![](resources/kirikas.png)](resources/kirika.png) Kirika + +Collection of audio utilities for decoding/encoding files and streams. +* channel-based audio consumption chain +* Audio resampler +* FLAC stream decoder and encoder +* MP3 stream decoder +* Opus stream decoder + +## Dependencies +### Go >= 1.18 + +### [libFLAC](https://github.com/xiph/flac) (required by [goflac](https://github.com/cocoonlife/goflac)) +```shell +sudo apt install libflac-dev +``` + +### [libopus](https://github.com/xiph/opus) and [libopusfile](https://github.com/xiph/opusfile) (required by [go-pus](https://git.gammaspectra.live/S.O.N.G/go-pus)) +```shell +sudo apt install libopus-dev libopusfile-dev +``` + +### [libsamplerate](https://github.com/libsndfile/libsamplerate) (required by [gosamplerate](https://github.com/dh1tw/gosamplerate)) +```shell +sudo apt install libsamplerate0-dev +``` \ No newline at end of file diff --git a/audio/format/flac/flac.go b/audio/format/flac/flac.go new file mode 100644 index 0000000..ddfdfc3 --- /dev/null +++ b/audio/format/flac/flac.go @@ -0,0 +1,144 @@ +package flac + +/* +#cgo CFLAGS: -I"${SRCDIR}/../../../cgo" -march=native -Ofast -std=c99 +#include "audio.h" +*/ +import "C" +import ( + "bytes" + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "git.gammaspectra.live/S.O.N.G/Kirika/audio/format" + libflac "github.com/cocoonlife/goflac" + "io" +) + +type Format struct { +} + +func NewFormat() Format { + return Format{} +} + +func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { + decoder, err := libflac.NewDecoderReader(r) + if err != nil { + return nil, err + } + + newChannel := make(chan []float32) + + go func() { + defer close(newChannel) + defer decoder.Close() + + frameNumber := 0 + + for { + currentFrame, err := decoder.ReadFrame() + if err != nil { + return + } + + bitDepth := decoder.Depth + + if currentFrame.Depth != 0 { + bitDepth = currentFrame.Depth + } + + buf := make([]float32, len(currentFrame.Buffer)) + + C.audio_int32_to_float32((*C.int32_t)(¤tFrame.Buffer[0]), C.size_t(len(currentFrame.Buffer)), (*C.float)(&buf[0]), C.int(bitDepth)) + + newChannel <- buf + + frameNumber++ + } + }() + + return audio.NewStream(newChannel, decoder.Channels, float64(decoder.Rate), blockSize), nil +} +func (f Format) OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, chan *format.AnalyzerPacket, error) { + decoder, err := libflac.NewDecoderReader(r) + if err != nil { + return nil, nil, err + } + + newChannel := make(chan []float32) + analyzerChannel := make(chan *format.AnalyzerPacket) + + go func() { + defer close(newChannel) + defer close(analyzerChannel) + defer decoder.Close() + + frameNumber := 0 + + for { + currentFrame, err := decoder.ReadFrame() + if err != nil { + return + } + + bitDepth := decoder.Depth + channels := decoder.Channels + + if currentFrame.Depth != 0 { + bitDepth = currentFrame.Depth + } + if currentFrame.Channels != 0 { + channels = currentFrame.Channels + } + + buf := make([]float32, len(currentFrame.Buffer)) + + C.audio_int32_to_float32((*C.int32_t)(¤tFrame.Buffer[0]), C.size_t(len(currentFrame.Buffer)), (*C.float)(&buf[0]), C.int(bitDepth)) + + newChannel <- buf + analyzerChannel <- &format.AnalyzerPacket{ + Samples: currentFrame.Buffer, + Channels: channels, + BitDepth: bitDepth, + SampleRate: decoder.Rate, + } + + frameNumber++ + } + }() + + return audio.NewStream(newChannel, decoder.Channels, float64(decoder.Rate), blockSize), analyzerChannel, nil +} + +func (f Format) Encode(stream *audio.Stream, writer format.WriteSeekCloser) error { + const bitsPerSample = 16 + + encoder, err := libflac.NewEncoderWriter(writer, stream.GetChannels(), bitsPerSample, int(stream.GetSampleRate())) + if err != nil { + return err + } + + defer encoder.Close() + + for block := range stream.GetAsBlockChannel() { + 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(), + Depth: bitsPerSample, + Buffer: samples, + }) + + if err != nil { + return err + } + } + + return nil +} + +func (f Format) Identify(peek []byte, extension string) bool { + return bytes.Compare(peek[:4], []byte{'f', 'L', 'a', 'C'}) == 0 || extension == "flac" +} diff --git a/audio/format/format.go b/audio/format/format.go new file mode 100644 index 0000000..744d179 --- /dev/null +++ b/audio/format/format.go @@ -0,0 +1,39 @@ +package format + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "io" +) + +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) +} + +type WriteSeekCloser interface { + io.Writer + io.Closer + io.Seeker +} + +type Encoder interface { + // Encode Receives a stream and encodes it into a writer + Encode(stream *audio.Stream, writer WriteSeekCloser) error +} + +type AnalyzerPacket struct { + //Samples interleaved samples + Samples []int32 + Channels int + SampleRate int + BitDepth int +} diff --git a/audio/format/mp3/mp3.go b/audio/format/mp3/mp3.go new file mode 100644 index 0000000..0ce5483 --- /dev/null +++ b/audio/format/mp3/mp3.go @@ -0,0 +1,58 @@ +package mp3 + +/* +#cgo CFLAGS: -I"${SRCDIR}/../../../cgo" -march=native -Ofast -std=c99 +#include "audio.h" +*/ +import "C" +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + mp3Lib "github.com/kvark128/minimp3" + "io" + "unsafe" +) + +type Format struct { +} + +func NewFormat() Format { + return Format{} +} + +func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { + decoder := mp3Lib.NewDecoder(r) + + _, err := decoder.Read([]byte{}) + if err != nil { + return nil, err + } + + newChannel := make(chan []float32) + + go func() { + defer close(newChannel) + samples := make([]int16, blockSize*2) + const SizeofInt16 = int(unsafe.Sizeof(int16(0))) + byteSlice := unsafe.Slice((*byte)(unsafe.Pointer(&samples[0])), len(samples)*SizeofInt16) + for { + n, err := decoder.Read(byteSlice) + if err != nil { + return + } + n /= SizeofInt16 + + buf := make([]float32, n) + + //convert 16-bit to f32 samples + C.audio_int16_to_float32((*C.int16_t)(&samples[0]), C.size_t(n), (*C.float)(&buf[0]), C.int(16)) + + newChannel <- buf + } + }() + + return audio.NewStream(newChannel, decoder.Channels(), float64(decoder.SampleRate()), blockSize), nil +} + +func (f Format) Identify(peek []byte, extension string) bool { + return /*bytes.Compare(peek[:4], []byte{'f', 'L', 'a', 'C'}) == 0 || */ extension == "mp3" +} diff --git a/audio/format/opus/opus.go b/audio/format/opus/opus.go new file mode 100644 index 0000000..e35fadf --- /dev/null +++ b/audio/format/opus/opus.go @@ -0,0 +1,52 @@ +package opus + +import ( + "bytes" + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + libopus "git.gammaspectra.live/S.O.N.G/go-pus" + "io" +) + +const OPUS_SAMPLE_RATE int = 48000 + +type Format struct { +} + +func NewFormat() Format { + return Format{} +} + +func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) { + stream, err := libopus.NewStream(r) + if err != nil { + return nil, err + } + + newChannel := make(chan []float32) + + go func() { + defer stream.Close() + defer close(newChannel) + + for { + + buf := make([]float32, blockSize*2) + + n, err := stream.ReadStereoFloat32(buf) + if err != nil { + return + } + + if n > 0 { + newChannel <- buf[:n*2] + } + } + }() + + //We always get two channels via stereo api + return audio.NewStream(newChannel, 2, float64(OPUS_SAMPLE_RATE), blockSize), nil +} + +func (f Format) Identify(peek []byte, extension string) bool { + return bytes.Compare(peek[:4], []byte{'O', 'g', 'g', 'S'}) == 0 || extension == "opus" +} diff --git a/audio/stream.go b/audio/stream.go new file mode 100644 index 0000000..44e0839 --- /dev/null +++ b/audio/stream.go @@ -0,0 +1,181 @@ +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 + samplesProcessed int + buffer []float32 + blockSize int +} + +func NewStream(source chan []float32, channels int, sampleRate float64, blockSize int) *Stream { + return &Stream{ + source: source, + channels: channels, + sampleRate: sampleRate, + blockSize: blockSize, + } +} + +func (s *Stream) GetChannels() int { + return s.channels +} + +func (s *Stream) GetSampleRate() float64 { + return s.sampleRate +} + +func (s *Stream) GetSamplesProcessed() int { + return s.samplesProcessed +} + +func (s *Stream) GetAsChannel() chan float32 { + newChannel := make(chan float32) + go func() { + defer close(newChannel) + for { + v, more := s.Get() + if !more { + return + } + newChannel <- v + s.samplesProcessed++ + } + }() + return newChannel +} + +func (s *Stream) GetAsBlockChannel() chan []float32 { + newChannel := make(chan []float32) + + go func() { + defer close(newChannel) + for buf := range s.source { + 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:] + } + } + + if len(s.buffer) > 0 { + newChannel <- s.buffer + s.samplesProcessed += len(s.buffer) + } + s.buffer = nil + }() + return newChannel +} + +func (s *Stream) Get() (float32, bool) { + var more bool + if len(s.buffer) == 0 { + s.buffer, more = <-s.source + if !more { + return 0, false + } + } + f := s.buffer[0] + s.buffer = s.buffer[1:] + s.samplesProcessed++ + return f, true +} + +func (s *Stream) AdvanceSeconds(seconds float64) bool { + stopAt := s.secondsIndex(seconds) + for i := 0; i < stopAt; i++ { + _, more := s.Get() + if !more { + return false + } + } + return true +} + +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 +} diff --git a/cgo/audio.h b/cgo/audio.h new file mode 100644 index 0000000..dd224ca --- /dev/null +++ b/cgo/audio.h @@ -0,0 +1,121 @@ +#include +#include +#include + +#define BITS_TO_DIV(b) (float)((1 << (b - 1)) - 1) + +__attribute__((weak)) void audio_multiple_channels_to_mono(float* buffer, size_t buffer_len, int channels) { + float f; + switch(channels) { + case 1: + break; + case 2: + for (int i = 0; i < buffer_len; i += 2){ + buffer[i/2] = buffer[i] + buffer[i+1]; + } + break; + default: + for (int i = 0; i < buffer_len; i += channels){ + f = buffer[i]; + for (int j = 1; j < channels; ++j) { + f += buffer[i + j]; + } + buffer[i/channels] = f / (float)channels; + } + } +} + +__attribute__((weak)) void audio_multiple_channels_to_stereo(float* buffer, size_t buffer_len, float* out, int channels) { + float f; + int samples = 0; + float surroundMix = 1 / sqrt(2); + switch(channels) { + case 1: //mono, duplicate channels + for (int i = 0; i < buffer_len; i++){ + out[i*2] = buffer[i]; + out[i*2+1] = buffer[i]; + } + break; + case 2: //copy + memcpy(out, buffer, sizeof(float) * buffer_len); + break; + case 3: //2.1, FL, FR, LFE + for (int i = 0; i < buffer_len; i += 3){ + int FL = i; + int FR = i+1; + int LFE = i+2; + out[samples*2] = buffer[FL]; + out[samples*2+1] = buffer[FR]; + ++samples; + } + case 5: //5.0, FL, FR, FC, RL, RR + for (int i = 0; i < buffer_len; i += 5){ + int FL = i; + int FR = i+1; + int C = i+2; + int RL = i+3; + int RR = i+4; + out[samples*2] = buffer[FL] + surroundMix * buffer[C] + surroundMix * RL; + out[samples*2+1] = buffer[FR] + surroundMix * buffer[C] + surroundMix * RR; + ++samples; + } + case 6: //5.1, FL, FR, FC, LFE, RL, RR + for (int i = 0; i < buffer_len; i += 6){ + int FL = i; + int FR = i+1; + int C = i+2; + int LFE = i+3; + int RL = i+4; + int RR = i+5; + out[samples*2] = buffer[FL] + surroundMix * buffer[C] + surroundMix * RL; + out[samples*2+1] = buffer[FR] + surroundMix * buffer[C] + surroundMix * RR; + ++samples; + } + break; + default: //no known formula, just take stereo out of it + for (int i = 0; i < buffer_len; i += channels){ + out[samples*2] = buffer[i]; + out[samples*2+1] = buffer[i+1]; + ++samples; + } + } +} + +__attribute__((weak)) void audio_int32_to_float32(int32_t* restrict data, size_t data_len, float* restrict buffer, int bitDepth){ + switch(bitDepth) { + case 16: + for (int i = 0; i < data_len; ++i){ + buffer[i] = ((float)data[i]) / BITS_TO_DIV(16); + } + break; + case 24: + for (int i = 0; i < data_len; ++i){ + buffer[i] = ((float)data[i]) / BITS_TO_DIV(24); + } + break; + default: + for (int i = 0; i < data_len; ++i){ + buffer[i] = ((float)data[i]) / BITS_TO_DIV(bitDepth); + } + } +} + +__attribute__((weak)) void audio_int16_to_float32(int16_t* restrict data, size_t data_len, float* restrict buffer, int bitDepth){ + switch(bitDepth) { + case 16: + for (int i = 0; i < data_len; ++i){ + buffer[i] = ((float)data[i]) / BITS_TO_DIV(16); + } + break; + default: + for (int i = 0; i < data_len; ++i){ + buffer[i] = ((float)data[i]) / BITS_TO_DIV(bitDepth); + } + } +} + +__attribute__((weak)) void audio_float32_to_int32(float* restrict data, size_t data_len, int32_t* restrict buffer, int bitDepth){ + for (int i = 0; i < data_len; ++i){ + buffer[i] = (int32_t)(data[i] * BITS_TO_DIV(bitDepth)); + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c92f90 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.gammaspectra.live/S.O.N.G/Kirika + +go 1.18 + +require ( + git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a + github.com/cocoonlife/goflac v0.0.0-20170210142907-50ea06ed5a9d + github.com/dh1tw/gosamplerate v0.1.2 + github.com/kvark128/minimp3 v0.0.0-20211109174940-101188771a65 +) + +require github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..caab84d --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a h1:LxrTp9gf4w5KnFHRPFLXYfoxC58GCSEmZrHI6Ogtrm0= +git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a/go.mod h1:vkoHSHVM9p6vAUmXAik0gvaLcIfiQYrD6bQqVpOulUk= +github.com/cocoonlife/goflac v0.0.0-20170210142907-50ea06ed5a9d h1:utj98F6D5jVv2tHYMsYzM6Z5sG71/W12Ivkd/SnFiN0= +github.com/cocoonlife/goflac v0.0.0-20170210142907-50ea06ed5a9d/go.mod h1:swNVb00X8NOH/qeHuqnqiyfecAnWlThLX+NbH8r6yHw= +github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 h1:LjPYdzoFSAJ5Tr/ElL8kzTJghXgpnOjJVbgd1UvZB1o= +github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64/go.mod h1:LoCAz53rbPcqs8Da2BjB/yDy4gxMtiSQmqnYI/DGH+U= +github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI= +github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM= +github.com/kvark128/minimp3 v0.0.0-20211109174940-101188771a65 h1:8qfVQv7MSACDXadEwl1yjUKJ68yC9B7nR4cioEoCfH0= +github.com/kvark128/minimp3 v0.0.0-20211109174940-101188771a65/go.mod h1:hIq9nAqNcwTySvnFhCe1C8xC/STIr2Fe5vJ52zk1jkE=