From a6134dfd3e9d40f31866f3649563fb59851d5afc Mon Sep 17 00:00:00 2001 From: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Fri, 20 May 2022 17:23:50 +0200 Subject: [PATCH] Added support for building using CGO_ENABLED=0 with limited codec and performance --- .drone.yml | 6 +- README.md | 30 ++- audio/filter/filter.go | 85 -------- audio/filter/filter_test.go | 13 +- audio/filter/resample_filter.go | 20 ++ audio/filter/resample_filter_cgo.go | 73 +++++++ audio/filter/resample_filter_cgo_test.go | 43 ++++ audio/filter/resample_filter_nocgo.go | 72 +++++++ audio/filter/resample_filter_nocgo_test.go | 43 ++++ audio/format/aac/libfdk-aac.go | 3 +- audio/format/aac/libfdk-aac_test.go | 3 +- audio/format/aac/mp4.go | 1 - audio/format/aac/vo-aacenc.go | 3 +- audio/format/aac/vo-aacenc_test.go | 3 +- audio/format/alac/libalac.go | 3 +- audio/format/alac/libalac_test.go | 3 +- audio/format/alac/mp4.go | 1 - audio/format/flac/goflac.go | 5 +- audio/format/flac/libflac.go | 3 +- audio/format/flac/libflac_test.go | 3 +- audio/format/guess/flac_decode_test.go | 3 +- audio/format/guess/init_aac.go | 3 +- audio/format/guess/init_alac.go | 3 +- audio/format/guess/init_flac.go | 1 - audio/format/guess/init_mp3.go | 1 - audio/format/guess/init_opus.go | 3 +- audio/format/guess/init_tta.go | 1 - audio/format/guess/init_vorbis.go | 1 - audio/format/guess/mp3_decode_test.go | 1 - audio/format/guess/opus_decode_test.go | 3 +- audio/format/guess/tta_decode_test.go | 1 - audio/format/guess/vorbis_decode_test.go | 1 - audio/format/mp3/mp3.go | 3 +- audio/format/mp3/mp3_lame.go | 3 +- audio/format/mp3/mp3_lame_test.go | 3 +- audio/format/mp3/mp3_nocgo.go | 65 ++++++ audio/format/mp3/mp3_nolame.go | 3 +- audio/format/opus/opus.go | 5 +- audio/format/opus/opus_test.go | 3 +- audio/format/tta/tta.go | 1 - audio/format/tta/tta_test.go | 1 - audio/format/vorbis/vorbis.go | 1 - audio/packetizer/adts_test.go | 3 +- audio/packetizer/flac_test.go | 3 +- audio/packetizer/mp3_test.go | 3 +- audio/packetizer/ogg_test.go | 3 +- audio/queue/queue.go | 4 +- audio/replaygain/filter.go | 109 ---------- audio/replaygain/filter_normalization.go | 116 ++++++++++ audio/replaygain/normalization.go | 2 + audio/replaygain/normalization_test.go | 2 + cgo/audio.c | 11 +- cgo/audio.go | 2 + cgo/audio_nocgo.go | 236 +++++++++++++++++++++ go.mod | 2 + go.sum | 10 + 56 files changed, 754 insertions(+), 278 deletions(-) create mode 100644 audio/filter/resample_filter.go create mode 100644 audio/filter/resample_filter_cgo.go create mode 100644 audio/filter/resample_filter_cgo_test.go create mode 100644 audio/filter/resample_filter_nocgo.go create mode 100644 audio/filter/resample_filter_nocgo_test.go create mode 100644 audio/format/mp3/mp3_nocgo.go create mode 100644 audio/replaygain/filter_normalization.go create mode 100644 cgo/audio_nocgo.go diff --git a/.drone.yml b/.drone.yml index 514a274..6823b0e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -19,8 +19,9 @@ steps: - git clone --depth 1 https://gitlab.xiph.org/xiph/libopusenc.git && cd libopusenc && ./autogen.sh && ./configure --prefix /usr && make && make install && cd .. - git clone --depth 1 https://git.gammaspectra.live/S.O.N.G/alac.git && cd alac && autoreconf -fi && ./configure --prefix /usr && make && make install && cd .. - go test -cover -v ./... - - go test -cover -v -tags=disable_codec_libfdk-aac,disable_codec_lame,disable_codec_tta,disable_codec_libflac,enable_codec_libalac ./... + - go test -cover -v -tags=disable_codec_libfdk-aac,disable_codec_lame,disable_codec_tta,disable_codec_libflac ./... - go test -cover -v -tags=disable_format_aac,disable_format_alac,disable_format_mp3,disable_format_opus,disable_format_tta,disable_format_vorbis ./... + - CGO_ENABLED=0 GOOS=linux go test -cover -v ./... --- kind: pipeline type: docker @@ -42,6 +43,7 @@ steps: - git clone --depth 1 https://github.com/mstorsjo/fdk-aac.git && cd fdk-aac && ./autogen.sh && ./configure --prefix /usr && make -j$(nproc) && make install && cd .. - git clone --depth 1 https://git.gammaspectra.live/S.O.N.G/alac.git && cd alac && autoreconf -fi && ./configure --prefix /usr && make && make install && cd .. - go test -cover -v ./... - - go test -cover -v -tags=disable_codec_libfdk-aac,disable_codec_lame,disable_codec_tta,enable_codec_libalac ./... + - go test -cover -v -tags=disable_codec_libfdk-aac,disable_codec_lame,disable_codec_tta,disable_codec_libflac ./... - go test -cover -v -tags=disable_format_aac,disable_format_alac,disable_format_mp3,disable_format_opus,disable_format_tta,disable_format_vorbis ./... + - CGO_ENABLED=0 GOOS=linux go test -cover -v ./... ... diff --git a/README.md b/README.md index 2687091..cacd9a4 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,26 @@ sudo apt install libebur128-dev ## Build tags Several Golang build tags exist to change which features are included in the project. +### CGO_ENABLED=0 +This tag disables CGO support, and disables any formats/codecs that require it. Some other features will degrade or be less performant, or alternative decoders be used. + +Additionally, libebur128 (used for normalization) will be disabled. + +Some tests cannot be completed in this mode. + +#### Codecs supported, CGO_ENABLED=0 + +| Codec | Containers | Decoder | Analyzer | Encoder | Notes | +|:----------:|:------------------------------------------------------------:|:-------:|:--------:|:-------:|:---------------------------------------------------------------------------------------------------------------------------------------| +| **FLAC** | [FLAC](https://xiph.org/flac/format.html) | ✅ | ✅ | ❌ | Decoding by [mewkiz/flac](https://github.com/mewkiz/flac). | +| **TTA** | [TTA](https://www.tausoft.org/en/true_audio_codec_format/) | ✅ | ✅ | ✅ | Decoding/encoding via [go-tta](https://git.gammaspectra.live/S.O.N.G/go-tta). | +| **MP3** | [MP3](http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html) | ✅ | - | ❌ | Decoding via [go-mp3](https://github.com/hajimehoshi/go-mp3). | +| **Opus** | - | ❌ | - | ❌ | | +| **Vorbis** | [Ogg](https://www.xiph.org/ogg/doc/framing.html) | ✅ | - | ❌ | Decoding by [jfreymuth/vorbis](https://github.com/jfreymuth/vorbis) via [jfreymuth/oggvorbis](https://github.com/jfreymuth/oggvorbis). | +| **AAC** | - | ❌ | - | ❌ | | +| **ALAC** | - | ❌ | - | ❌ | | + + ### disable_format_[format] This tag disables support for the specified format/codec. This does not affect packetizers. @@ -161,7 +181,7 @@ Subdependencies that are not cgo-based are denoted in cursive. | [S.O.N.G/alac](https://git.gammaspectra.live/S.O.N.G/alac) | C++/C | [Apache 2.0](https://git.gammaspectra.live/S.O.N.G/alac/src/branch/master/LICENSE) | Can be linked by cgo as a shared library. | | [S.O.N.G/go-ebur128](https://git.gammaspectra.live/S.O.N.G/go-ebur128) | Go | [BSD 2-Clause](https://git.gammaspectra.live/S.O.N.G/go-ebur128/src/branch/master/LICENSE) | | | [jiixyj/libebur128](https://github.com/jiixyj/libebur128) | C | [MIT](https://github.com/jiixyj/libebur128/blob/master/COPYING) | Can be linked by cgo as a shared library. | -| [S.O.N.G/go-fdkaac](https://git.gammaspectra.live/S.O.N.G/go-fdkaac) | Go | | [MIT](https://git.gammaspectra.live/S.O.N.G/go-fdkaac/src/branch/master/LICENSE) | | +| [S.O.N.G/go-fdkaac](https://git.gammaspectra.live/S.O.N.G/go-fdkaac) | Go | [MIT](https://git.gammaspectra.live/S.O.N.G/go-fdkaac/src/branch/master/LICENSE) | | | [mstorsjo/fdk-aac](https://github.com/mstorsjo/fdk-aac) | C++/C | [FDK License, BSD-like](https://github.com/mstorsjo/fdk-aac/blob/master/NOTICE) | Does not include patent grants.
Considered free by [Red hat](https://bugzilla.redhat.com/show_bug.cgi?id=1501522), [Fedora](https://fedoraproject.org/wiki/Licensing/FDK-AAC), [GNU](https://www.gnu.org/licenses/license-list.html#fdk), [FSF](https://www.fsf.org/blogs/licensing/recent-licensing-updates#:~:text=maintain%20free%20versions.-,The%20Fraunhofer%20FDK%20AAC%20license,-We%20recently%20added); and non-free by [Debian](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=694257).
Can be linked by cgo as a shared library. | | [S.O.N.G/go-pus](https://git.gammaspectra.live/S.O.N.G/go-pus) | Go | [MIT](https://git.gammaspectra.live/S.O.N.G/go-pus/src/branch/v2/LICENSE) | | | [xiph/opus](https://gitlab.xiph.org/xiph/opus) | C | [BSD 3-Clause](https://gitlab.xiph.org/xiph/opus/-/blob/master/COPYING) | Read extra license details on the [official site](https://opus-codec.org/license/).
Can be linked by cgo as a shared library. | @@ -176,11 +196,13 @@ Subdependencies that are not cgo-based are denoted in cursive. | [edgeware/mp4ff](https://github.com/edgeware/mp4ff) | Go | [MIT](https://github.com/edgeware/mp4ff/blob/master/LICENSE.md) | | | [gen2brain/aac-go](https://github.com/gen2brain/aac-go) | Go | [Apache 2.0](https://github.com/gen2brain/aac-go/blob/master/COPYING) | | | [mstorsjo/vo-aacenc](https://github.com/mstorsjo/vo-aacenc) | C | [Apache 2.0](https://github.com/mstorsjo/vo-aacenc/blob/master/COPYING) | Subdependency and included as part of _gen2brain/aac-go_. | -| [jfreymuth/oggvorbis](https://github.com/jfreymuth/oggvorbis) | Go | [MIT](https://github.com/jfreymuth/oggvorbis/blob/master/LICENSE) | | | -| _[jfreymuth/vorbis](https://github.com/jfreymuth/vorbis)_ | Go | [MIT](https://github.com/jfreymuth/vorbis/blob/master/LICENSE) | Subdependency of _jfreymuth/oggvorbis_. | | -| [kvark128/minimp3](https://github.com/kvark128/minimp3) | Go | [MIT](https://github.com/kvark128/minimp3/blob/master/LICENSE.txt) | | | +| [hajimehoshi/go-mp3](https://github.com/hajimehoshi/go-mp3) | Go | [Apache 2.0](https://github.com/hajimehoshi/go-mp3/blob/main/LICENSE) | | +| [jfreymuth/oggvorbis](https://github.com/jfreymuth/oggvorbis) | Go | [MIT](https://github.com/jfreymuth/oggvorbis/blob/master/LICENSE) | | +| _[jfreymuth/vorbis](https://github.com/jfreymuth/vorbis)_ | Go | [MIT](https://github.com/jfreymuth/vorbis/blob/master/LICENSE) | Subdependency of _jfreymuth/oggvorbis_. | +| [kvark128/minimp3](https://github.com/kvark128/minimp3) | Go | [MIT](https://github.com/kvark128/minimp3/blob/master/LICENSE.txt) | | | [lieff/minimp3](https://github.com/lieff/minimp3) | C | [CC0 1.0](https://github.com/lieff/minimp3/blob/master/LICENSE) | Subdependency and included as part of _lieff/minimp3_. | | [mewkiz/flac](https://github.com/mewkiz/flac) | Go | [The Unlicense](https://github.com/mewkiz/flac/blob/master/LICENSE) | | +| [oov/audio](https://github.com/oov/audio) | Go | [MIT](https://github.com/oov/audio/blob/master/LICENSE) | Used only for its audio resampler. | | _[go-audio/audio](https://github.com/go-audio/audio)_ | Go | [Apache 2.0](https://github.com/go-audio/audio/blob/master/LICENSE) | Subdependency of _mewkiz/flac_. Only used on tests there. | | _[go-audio/wav](https://github.com/go-audio/wav)_ | Go | [Apache 2.0](https://github.com/go-audio/wav/blob/master/LICENSE) | Subdependency of _mewkiz/flac_. Only used on tests there. | | _[go-audio/riff](https://github.com/go-audio/riff)_ | Go | [Apache 2.0](https://github.com/go-audio/riff/blob/master/LICENSE) | Subdependency of _go-audio/wav_. Only used on tests there. | diff --git a/audio/filter/filter.go b/audio/filter/filter.go index 21087d6..30ea64c 100644 --- a/audio/filter/filter.go +++ b/audio/filter/filter.go @@ -3,7 +3,6 @@ package filter import ( "git.gammaspectra.live/S.O.N.G/Kirika/audio" "git.gammaspectra.live/S.O.N.G/Kirika/cgo" - "github.com/dh1tw/gosamplerate" "log" "time" ) @@ -162,87 +161,3 @@ func (f MonoFilter) Process(source audio.Source) audio.Source { 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 audio.Source) audio.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[blockSize:] - } - if len(block) > 0 { - 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 audio.Source{ - Channels: source.Channels, - SampleRate: f.sampleRate, - Blocks: outBlocks, - } -} diff --git a/audio/filter/filter_test.go b/audio/filter/filter_test.go index 882e619..4560de6 100644 --- a/audio/filter/filter_test.go +++ b/audio/filter/filter_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -func TestFilterChain(t *testing.T) { +func TestFilterChainNoResample(t *testing.T) { t.Parallel() fp, err := os.Open(test.TestSingleSample24) if err != nil { @@ -22,20 +22,15 @@ func TestFilterChain(t *testing.T) { return } - const sampleRate = 16000 - - result := NewFilterChain(source, MonoFilter{}, NewResampleFilter(sampleRate, BandlimitedFastest, 0), StereoFilter{}) + result := NewFilterChain(source, MonoFilter{}, 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) + if sink.SamplesRead != 17323031 { + t.Errorf("Wrong Sample Count %d != %d", sink.SamplesRead, 17323031) } } diff --git a/audio/filter/resample_filter.go b/audio/filter/resample_filter.go new file mode 100644 index 0000000..e0f96ee --- /dev/null +++ b/audio/filter/resample_filter.go @@ -0,0 +1,20 @@ +package filter + +type ResampleFilter struct { + sampleRate int + quality ResampleQuality + blockSize int +} + +type ResampleQuality int + +func NewResampleFilter(sampleRate int, quality ResampleQuality, blockSize int) ResampleFilter { + if blockSize == 0 { + blockSize = 1024 * 64 + } + return ResampleFilter{ + sampleRate: sampleRate, + quality: quality, + blockSize: blockSize, + } +} diff --git a/audio/filter/resample_filter_cgo.go b/audio/filter/resample_filter_cgo.go new file mode 100644 index 0000000..6071c12 --- /dev/null +++ b/audio/filter/resample_filter_cgo.go @@ -0,0 +1,73 @@ +//go:build cgo + +package filter + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "github.com/dh1tw/gosamplerate" + "log" +) + +const ( + QualityBest ResampleQuality = gosamplerate.SRC_SINC_BEST_QUALITY + QualityGood ResampleQuality = gosamplerate.SRC_SINC_MEDIUM_QUALITY + QualityFast ResampleQuality = gosamplerate.SRC_SINC_FASTEST + QualityFastest ResampleQuality = gosamplerate.SRC_LINEAR +) + +func (f ResampleFilter) Process(source audio.Source) audio.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[blockSize:] + } + if len(block) > 0 { + 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 audio.Source{ + Channels: source.Channels, + SampleRate: f.sampleRate, + Blocks: outBlocks, + } +} diff --git a/audio/filter/resample_filter_cgo_test.go b/audio/filter/resample_filter_cgo_test.go new file mode 100644 index 0000000..6510077 --- /dev/null +++ b/audio/filter/resample_filter_cgo_test.go @@ -0,0 +1,43 @@ +//go:build cgo + +package filter + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac" + "git.gammaspectra.live/S.O.N.G/Kirika/test" + "os" + "testing" +) + +func TestFilterChainResample(t *testing.T) { + t.Parallel() + fp, err := os.Open(test.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 := NewFilterChain(source, MonoFilter{}, NewResampleFilter(sampleRate, QualityFast, 0), 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/audio/filter/resample_filter_nocgo.go b/audio/filter/resample_filter_nocgo.go new file mode 100644 index 0000000..59fe73c --- /dev/null +++ b/audio/filter/resample_filter_nocgo.go @@ -0,0 +1,72 @@ +//go:build !cgo + +package filter + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "github.com/oov/audio/resampler" + "log" +) + +const ( + QualityBest ResampleQuality = 10 + QualityGood ResampleQuality = 8 + QualityFast ResampleQuality = 5 + QualityFastest ResampleQuality = 0 +) + +func (f ResampleFilter) Process(source audio.Source) audio.Source { + if source.SampleRate == f.sampleRate { //no change + return source + } + + outBlocks := make(chan []float32) + + go func() { + defer close(outBlocks) + + samplerateConverter := resampler.New(source.Channels, source.SampleRate, f.sampleRate, int(f.quality)) + + for block := range source.Blocks { + output := make([][]float32, source.Channels) + input := make([][]float32, source.Channels) + + outputBufferLength := 0 + for i := 0; i < source.Channels; i++ { + input[i] = make([]float32, len(block)/source.Channels) + + ix := 0 + for j := i; j < len(block); j += source.Channels { + input[i][ix] = block[j] + ix++ + } + + output[i] = make([]float32, int(float64(len(block)/source.Channels)*(float64(f.sampleRate)/float64(source.SampleRate))*1.1)) //resize to match expected output, with a fuzz factor of 10% + + read, written := samplerateConverter.ProcessFloat32(i, input[i], output[i]) + if read != len(input[i]) { + log.Panicf("could not read whole input: %d vs %d", len(input[i]), read) + } + output[i] = output[i][:written] + + outputBufferLength += written + } + + buffer := make([]float32, outputBufferLength) + for i, b := range output { + for j := range b { + buffer[i+j*len(output)] = b[j] + } + } + + outBlocks <- buffer + } + + }() + + return audio.Source{ + Channels: source.Channels, + SampleRate: f.sampleRate, + Blocks: outBlocks, + } +} diff --git a/audio/filter/resample_filter_nocgo_test.go b/audio/filter/resample_filter_nocgo_test.go new file mode 100644 index 0000000..e163adb --- /dev/null +++ b/audio/filter/resample_filter_nocgo_test.go @@ -0,0 +1,43 @@ +//go:build !cgo + +package filter + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac" + "git.gammaspectra.live/S.O.N.G/Kirika/test" + "os" + "testing" +) + +func TestFilterChainResample(t *testing.T) { + t.Parallel() + fp, err := os.Open(test.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 := NewFilterChain(source, MonoFilter{}, NewResampleFilter(sampleRate, QualityFast, 0), 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 != 6285000 { + t.Errorf("Wrong Sample Count %d != %d", sink.SamplesRead, 6285000) + } +} diff --git a/audio/format/aac/libfdk-aac.go b/audio/format/aac/libfdk-aac.go index 4130f18..df64082 100644 --- a/audio/format/aac/libfdk-aac.go +++ b/audio/format/aac/libfdk-aac.go @@ -1,5 +1,4 @@ -//go:build !disable_format_aac && !disable_codec_libfdk_aac -// +build !disable_format_aac,!disable_codec_libfdk_aac +//go:build !disable_format_aac && !disable_codec_libfdk_aac && cgo package aac diff --git a/audio/format/aac/libfdk-aac_test.go b/audio/format/aac/libfdk-aac_test.go index 5517eb0..45d0bcf 100644 --- a/audio/format/aac/libfdk-aac_test.go +++ b/audio/format/aac/libfdk-aac_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_aac && !disable_codec_libfdk_aac -// +build !disable_format_aac,!disable_codec_libfdk_aac +//go:build !disable_format_aac && !disable_codec_libfdk_aac && cgo package aac diff --git a/audio/format/aac/mp4.go b/audio/format/aac/mp4.go index 2f11e64..0cd8517 100644 --- a/audio/format/aac/mp4.go +++ b/audio/format/aac/mp4.go @@ -1,5 +1,4 @@ //go:build !disable_format_mp4 -// +build !disable_format_mp4 package aac diff --git a/audio/format/aac/vo-aacenc.go b/audio/format/aac/vo-aacenc.go index 713de73..8ebe92e 100644 --- a/audio/format/aac/vo-aacenc.go +++ b/audio/format/aac/vo-aacenc.go @@ -1,5 +1,4 @@ -//go:build !disable_format_aac && disable_codec_libfdk_aac -// +build !disable_format_aac,disable_codec_libfdk_aac +//go:build !disable_format_aac && disable_codec_libfdk_aac && cgo package aac diff --git a/audio/format/aac/vo-aacenc_test.go b/audio/format/aac/vo-aacenc_test.go index 1961844..be1bfa3 100644 --- a/audio/format/aac/vo-aacenc_test.go +++ b/audio/format/aac/vo-aacenc_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_aac && disable_codec_libfdk_aac -// +build !disable_format_aac,disable_codec_libfdk_aac +//go:build !disable_format_aac && disable_codec_libfdk_aac && cgo package aac diff --git a/audio/format/alac/libalac.go b/audio/format/alac/libalac.go index b18aa4c..a11dcd0 100644 --- a/audio/format/alac/libalac.go +++ b/audio/format/alac/libalac.go @@ -1,5 +1,4 @@ -//go:build !disable_format_alac && enable_codec_libalac -// +build !disable_format_alac,enable_codec_libalac +//go:build !disable_format_alac && enable_codec_libalac && cgo package alac diff --git a/audio/format/alac/libalac_test.go b/audio/format/alac/libalac_test.go index 2f373f8..c9dfe67 100644 --- a/audio/format/alac/libalac_test.go +++ b/audio/format/alac/libalac_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_alac && enable_codec_libalac -// +build !disable_format_alac,enable_codec_libalac +//go:build !disable_format_alac && enable_codec_libalac && cgo package alac diff --git a/audio/format/alac/mp4.go b/audio/format/alac/mp4.go index 71bebbf..45e5ace 100644 --- a/audio/format/alac/mp4.go +++ b/audio/format/alac/mp4.go @@ -1,5 +1,4 @@ //go:build !disable_format_alac -// +build !disable_format_alac package alac diff --git a/audio/format/flac/goflac.go b/audio/format/flac/goflac.go index f8dcbe5..b682b85 100644 --- a/audio/format/flac/goflac.go +++ b/audio/format/flac/goflac.go @@ -1,5 +1,4 @@ -//go:build !disable_format_flac && disable_codec_libflac -// +build !disable_format_flac,disable_codec_libflac +//go:build !disable_format_flac && (disable_codec_libflac || !cgo) package flac @@ -24,7 +23,7 @@ func (f Format) Name() string { } func (f Format) Description() string { - return "github.com/mewkiz/flac" + return "mewkiz/flac" } func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { diff --git a/audio/format/flac/libflac.go b/audio/format/flac/libflac.go index 50dfee7..b447c23 100644 --- a/audio/format/flac/libflac.go +++ b/audio/format/flac/libflac.go @@ -1,5 +1,4 @@ -//go:build !disable_format_flac && !disable_codec_libflac -// +build !disable_format_flac,!disable_codec_libflac +//go:build !disable_format_flac && !disable_codec_libflac && cgo package flac diff --git a/audio/format/flac/libflac_test.go b/audio/format/flac/libflac_test.go index 3508e28..c3e4b36 100644 --- a/audio/format/flac/libflac_test.go +++ b/audio/format/flac/libflac_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_flac && !disable_codec_libflac -// +build !disable_format_flac,!disable_codec_libflac +//go:build !disable_format_flac && !disable_codec_libflac && cgo package flac diff --git a/audio/format/guess/flac_decode_test.go b/audio/format/guess/flac_decode_test.go index 7d9d498..ee8e0b3 100644 --- a/audio/format/guess/flac_decode_test.go +++ b/audio/format/guess/flac_decode_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_alac -// +build !disable_format_alac +//go:build !disable_format_flac package guess diff --git a/audio/format/guess/init_aac.go b/audio/format/guess/init_aac.go index fe5d980..978f89f 100644 --- a/audio/format/guess/init_aac.go +++ b/audio/format/guess/init_aac.go @@ -1,5 +1,4 @@ -//go:build !disable_format_aac -// +build !disable_format_aac +//go:build !disable_format_aac && cgo package guess diff --git a/audio/format/guess/init_alac.go b/audio/format/guess/init_alac.go index 699fcae..858ac36 100644 --- a/audio/format/guess/init_alac.go +++ b/audio/format/guess/init_alac.go @@ -1,5 +1,4 @@ -//go:build !disable_format_alac && enable_codec_libalac -// +build !disable_format_alac,enable_codec_libalac +//go:build !disable_format_alac && enable_codec_libalac && cgo package guess diff --git a/audio/format/guess/init_flac.go b/audio/format/guess/init_flac.go index ae3a3d3..306c5fb 100644 --- a/audio/format/guess/init_flac.go +++ b/audio/format/guess/init_flac.go @@ -1,5 +1,4 @@ //go:build !disable_format_flac -// +build !disable_format_flac package guess diff --git a/audio/format/guess/init_mp3.go b/audio/format/guess/init_mp3.go index cc33755..2009df2 100644 --- a/audio/format/guess/init_mp3.go +++ b/audio/format/guess/init_mp3.go @@ -1,5 +1,4 @@ //go:build !disable_format_mp3 -// +build !disable_format_mp3 package guess diff --git a/audio/format/guess/init_opus.go b/audio/format/guess/init_opus.go index 7185df2..4e9dfc0 100644 --- a/audio/format/guess/init_opus.go +++ b/audio/format/guess/init_opus.go @@ -1,5 +1,4 @@ -//go:build !disable_format_opus -// +build !disable_format_opus +//go:build !disable_format_opus && cgo package guess diff --git a/audio/format/guess/init_tta.go b/audio/format/guess/init_tta.go index 9ba446a..09c3521 100644 --- a/audio/format/guess/init_tta.go +++ b/audio/format/guess/init_tta.go @@ -1,5 +1,4 @@ //go:build !disable_format_tta && !disable_codec_tta -// +build !disable_format_tta,!disable_codec_tta package guess diff --git a/audio/format/guess/init_vorbis.go b/audio/format/guess/init_vorbis.go index e9f4ea8..6a9b5b0 100644 --- a/audio/format/guess/init_vorbis.go +++ b/audio/format/guess/init_vorbis.go @@ -1,5 +1,4 @@ //go:build !disable_format_vorbis -// +build !disable_format_vorbis package guess diff --git a/audio/format/guess/mp3_decode_test.go b/audio/format/guess/mp3_decode_test.go index 3e214f5..c4c5ced 100644 --- a/audio/format/guess/mp3_decode_test.go +++ b/audio/format/guess/mp3_decode_test.go @@ -1,5 +1,4 @@ //go:build !disable_format_mp3 -// +build !disable_format_mp3 package guess diff --git a/audio/format/guess/opus_decode_test.go b/audio/format/guess/opus_decode_test.go index 108e90b..fc2c1de 100644 --- a/audio/format/guess/opus_decode_test.go +++ b/audio/format/guess/opus_decode_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_opus -// +build !disable_format_opus +//go:build !disable_format_opus && cgo package guess diff --git a/audio/format/guess/tta_decode_test.go b/audio/format/guess/tta_decode_test.go index 30309c1..3257994 100644 --- a/audio/format/guess/tta_decode_test.go +++ b/audio/format/guess/tta_decode_test.go @@ -1,5 +1,4 @@ //go:build !disable_format_tta && !disable_codec_tta -// +build !disable_format_tta,!disable_codec_tta package guess diff --git a/audio/format/guess/vorbis_decode_test.go b/audio/format/guess/vorbis_decode_test.go index 9206181..61246d3 100644 --- a/audio/format/guess/vorbis_decode_test.go +++ b/audio/format/guess/vorbis_decode_test.go @@ -1,5 +1,4 @@ //go:build !disable_format_vorbis -// +build !disable_format_vorbis package guess diff --git a/audio/format/mp3/mp3.go b/audio/format/mp3/mp3.go index 234ae93..06f69ba 100644 --- a/audio/format/mp3/mp3.go +++ b/audio/format/mp3/mp3.go @@ -1,5 +1,4 @@ -//go:build !disable_format_mp3 -// +build !disable_format_mp3 +//go:build !disable_format_mp3 && cgo package mp3 diff --git a/audio/format/mp3/mp3_lame.go b/audio/format/mp3/mp3_lame.go index 39c07ed..104c43a 100644 --- a/audio/format/mp3/mp3_lame.go +++ b/audio/format/mp3/mp3_lame.go @@ -1,5 +1,4 @@ -//go:build !disable_format_mp3 && !disable_codec_lame -// +build !disable_format_mp3,!disable_codec_lame +//go:build !disable_format_mp3 && !disable_codec_lame && cgo package mp3 diff --git a/audio/format/mp3/mp3_lame_test.go b/audio/format/mp3/mp3_lame_test.go index d29108c..d8c4e2c 100644 --- a/audio/format/mp3/mp3_lame_test.go +++ b/audio/format/mp3/mp3_lame_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_mp3 && !disable_codec_lame -// +build !disable_format_mp3,!disable_codec_lame +//go:build !disable_format_mp3 && !disable_codec_lame && cgo package mp3 diff --git a/audio/format/mp3/mp3_nocgo.go b/audio/format/mp3/mp3_nocgo.go new file mode 100644 index 0000000..ed7995d --- /dev/null +++ b/audio/format/mp3/mp3_nocgo.go @@ -0,0 +1,65 @@ +//go:build !disable_format_mp3 && !cgo + +package mp3 + +import ( + "bytes" + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "git.gammaspectra.live/S.O.N.G/Kirika/cgo" + mp3Lib "github.com/hajimehoshi/go-mp3" + "io" + "unsafe" +) + +const BlockSize = 1024 * 128 + +type Format struct { +} + +func NewFormat() Format { + return Format{} +} + +func (f Format) Name() string { + return "mp3" +} +func (f Format) Description() string { + return "hajimehoshi/go-mp3" +} + +func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { + decoder, err := mp3Lib.NewDecoder(r) + if err != nil { + return audio.Source{}, 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 + + //convert 16-bit to f32 samples + newChannel <- cgo.Int16ToFloat32(samples[:n], 16) + } + }() + + return audio.Source{ + Channels: 2, + SampleRate: decoder.SampleRate(), + Blocks: newChannel, + }, nil +} + +func (f Format) Identify(peek []byte, extension string) bool { + //match ID3 / sync header + return peek[0] == 0xff && (peek[1]>>5) == 0b111 || (bytes.Compare(peek[:3], []byte{'I', 'D', '3'}) == 0 && extension != "flac") || extension == "mp3" +} diff --git a/audio/format/mp3/mp3_nolame.go b/audio/format/mp3/mp3_nolame.go index eba34b4..582e752 100644 --- a/audio/format/mp3/mp3_nolame.go +++ b/audio/format/mp3/mp3_nolame.go @@ -1,5 +1,4 @@ -//go:build !disable_format_mp3 && disable_codec_lame -// +build !disable_format_mp3,disable_codec_lame +//go:build !disable_format_mp3 && disable_codec_lame && cgo package mp3 diff --git a/audio/format/opus/opus.go b/audio/format/opus/opus.go index 544cc19..b9af8f2 100644 --- a/audio/format/opus/opus.go +++ b/audio/format/opus/opus.go @@ -1,5 +1,4 @@ -//go:build !disable_format_opus -// +build !disable_format_opus +//go:build !disable_format_opus && cgo package opus @@ -68,7 +67,7 @@ func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) { func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error { var bitrate = 0 - var resampleQuality = filter.Linear + var resampleQuality = filter.QualityFastest if options != nil { var val interface{} diff --git a/audio/format/opus/opus_test.go b/audio/format/opus/opus_test.go index de3c763..1f2e8a3 100644 --- a/audio/format/opus/opus_test.go +++ b/audio/format/opus/opus_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_opus -// +build !disable_format_opus +//go:build !disable_format_opus && cgo package opus diff --git a/audio/format/tta/tta.go b/audio/format/tta/tta.go index 7020a99..1bbe073 100644 --- a/audio/format/tta/tta.go +++ b/audio/format/tta/tta.go @@ -1,5 +1,4 @@ //go:build !disable_format_tta && !disable_codec_tta -// +build !disable_format_tta,!disable_codec_tta package tta diff --git a/audio/format/tta/tta_test.go b/audio/format/tta/tta_test.go index 6188a66..b57d4c3 100644 --- a/audio/format/tta/tta_test.go +++ b/audio/format/tta/tta_test.go @@ -1,5 +1,4 @@ //go:build !disable_format_tta && !disable_codec_tta -// +build !disable_format_tta,!disable_codec_tta package tta diff --git a/audio/format/vorbis/vorbis.go b/audio/format/vorbis/vorbis.go index 089a4f1..f80b4a1 100644 --- a/audio/format/vorbis/vorbis.go +++ b/audio/format/vorbis/vorbis.go @@ -1,5 +1,4 @@ //go:build !disable_format_vorbis -// +build !disable_format_vorbis package vorbis diff --git a/audio/packetizer/adts_test.go b/audio/packetizer/adts_test.go index 7bc5e42..ce05587 100644 --- a/audio/packetizer/adts_test.go +++ b/audio/packetizer/adts_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_aac -// +build !disable_format_aac +//go:build !disable_format_aac && cgo package packetizer diff --git a/audio/packetizer/flac_test.go b/audio/packetizer/flac_test.go index a93536e..7a6331d 100644 --- a/audio/packetizer/flac_test.go +++ b/audio/packetizer/flac_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_flac && !disable_codec_libflac -// +build !disable_format_flac,!disable_codec_libflac +//go:build !disable_format_flac && !disable_codec_libflac && cgo package packetizer diff --git a/audio/packetizer/mp3_test.go b/audio/packetizer/mp3_test.go index 6add1ff..1543ee6 100644 --- a/audio/packetizer/mp3_test.go +++ b/audio/packetizer/mp3_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_mp3 && !disable_codec_lame -// +build !disable_format_mp3,!disable_codec_lame +//go:build !disable_format_mp3 && !disable_codec_lame && cgo package packetizer diff --git a/audio/packetizer/ogg_test.go b/audio/packetizer/ogg_test.go index 62acb9a..9707811 100644 --- a/audio/packetizer/ogg_test.go +++ b/audio/packetizer/ogg_test.go @@ -1,5 +1,4 @@ -//go:build !disable_format_opus -// +build !disable_format_opus +//go:build !disable_format_opus && cgo package packetizer diff --git a/audio/queue/queue.go b/audio/queue/queue.go index f3e31d2..3c832a0 100644 --- a/audio/queue/queue.go +++ b/audio/queue/queue.go @@ -133,9 +133,9 @@ func (q *Queue) start() { } func (q *Queue) getFilterChain(source audio.Source) audio.Source { if q.GetChannels() == 1 { - return filter.NewFilterChain(source, filter.MonoFilter{}, filter.NewResampleFilter(q.GetSampleRate(), filter.BandlimitedFastest, 0)) + return filter.NewFilterChain(source, filter.MonoFilter{}, filter.NewResampleFilter(q.GetSampleRate(), filter.QualityFastest, 0)) } else { - return filter.NewFilterChain(source, filter.StereoFilter{}, filter.NewResampleFilter(q.GetSampleRate(), filter.BandlimitedFastest, 0)) + return filter.NewFilterChain(source, filter.StereoFilter{}, filter.NewResampleFilter(q.GetSampleRate(), filter.QualityFastest, 0)) } } diff --git a/audio/replaygain/filter.go b/audio/replaygain/filter.go index c97e132..7ceeffb 100644 --- a/audio/replaygain/filter.go +++ b/audio/replaygain/filter.go @@ -1,11 +1,8 @@ package replaygain import ( - "git.gammaspectra.live/S.O.N.G/Kirika/audio" "git.gammaspectra.live/S.O.N.G/Kirika/audio/filter" - libebur128 "git.gammaspectra.live/S.O.N.G/go-ebur128" "math" - "time" ) //NewReplayGainFilter Creates a VolumeFilter applying calculated ReplayGain values, pre amplifying by preAmp. Values are in dB @@ -16,109 +13,3 @@ func NewReplayGainFilter(gain, peak, preAmp float64) filter.VolumeFilter { return filter.NewVolumeFilter(float32(volume)) } - -// NormalizationFilter Normalizes running audio source -type NormalizationFilter struct { - delay int -} - -func NewNormalizationFilter(delayInSeconds int) NormalizationFilter { - return NormalizationFilter{ - delay: delayInSeconds, - } -} - -func (f NormalizationFilter) Process(source audio.Source) audio.Source { - outBlocks := make(chan []float32) - go func() { - defer close(outBlocks) - - state := libebur128.NewState(source.Channels, source.SampleRate, libebur128.LoudnessShortTerm|libebur128.SamplePeak) - if state == nil { - return - } - defer state.Close() - - if state.SetMaxWindow(time.Second*time.Duration(f.delay)) != nil { - return - } - - var sampleBuffer []float32 - var adjustment float32 = 1.0 - - for block := range source.Blocks { - if state.AddFloat(block) != nil { - return - } - - sampleBuffer = append(sampleBuffer, block...) - - loudness, _ := state.GetLoudnessWindow(time.Second * time.Duration(f.delay)) - peakSlice, _ := state.GetPreviousSamplePeak() - - var peak float64 - for _, p := range peakSlice { - if p > peak { - peak = p - } - } - - gain := referenceLevel - loudness - if gain > 52 { - gain = 52 - } else if gain < -52 { - gain = -52 - } - - volume := math.Pow(10, (gain)/20) - - nsamples := source.SampleRate * source.Channels * f.delay - - ratio := float32(math.Min(1, float64(len(block))/float64(nsamples))) - - adjustment = adjustment*(1-ratio) + float32(volume)*ratio - - if adjustment > float32(1/peak) { - adjustment = float32(1 / peak) - } - - if len(sampleBuffer) > nsamples { - adjustment = adjustment*(1-ratio) + float32(volume)*ratio - - if adjustment > float32(1/peak) { - adjustment = float32(1 / peak) - } - - size := len(sampleBuffer) - nsamples - out := make([]float32, size) - for i, e := range sampleBuffer[:size] { - out[i] = e * adjustment - } - outBlocks <- out - sampleBuffer = sampleBuffer[size:] - } else { - adjustment = float32(volume) - - if adjustment > float32(1/peak) { - adjustment = float32(1 / peak) - } - } - } - - //flush - if len(sampleBuffer) > 0 { - out := make([]float32, len(sampleBuffer)) - for i := range sampleBuffer { - out[i] = sampleBuffer[i] * adjustment - } - outBlocks <- out - } - - }() - - return audio.Source{ - Channels: source.Channels, - SampleRate: source.SampleRate, - Blocks: outBlocks, - } -} diff --git a/audio/replaygain/filter_normalization.go b/audio/replaygain/filter_normalization.go new file mode 100644 index 0000000..6cd4cd4 --- /dev/null +++ b/audio/replaygain/filter_normalization.go @@ -0,0 +1,116 @@ +//go:build cgo + +package replaygain + +import ( + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + libebur128 "git.gammaspectra.live/S.O.N.G/go-ebur128" + "math" + "time" +) + +// NormalizationFilter Normalizes running audio source +type NormalizationFilter struct { + delay int +} + +func NewNormalizationFilter(delayInSeconds int) NormalizationFilter { + return NormalizationFilter{ + delay: delayInSeconds, + } +} + +func (f NormalizationFilter) Process(source audio.Source) audio.Source { + outBlocks := make(chan []float32) + go func() { + defer close(outBlocks) + + state := libebur128.NewState(source.Channels, source.SampleRate, libebur128.LoudnessShortTerm|libebur128.SamplePeak) + if state == nil { + return + } + defer state.Close() + + if state.SetMaxWindow(time.Second*time.Duration(f.delay)) != nil { + return + } + + var sampleBuffer []float32 + var adjustment float32 = 1.0 + + for block := range source.Blocks { + if state.AddFloat(block) != nil { + return + } + + sampleBuffer = append(sampleBuffer, block...) + + loudness, _ := state.GetLoudnessWindow(time.Second * time.Duration(f.delay)) + peakSlice, _ := state.GetPreviousSamplePeak() + + var peak float64 + for _, p := range peakSlice { + if p > peak { + peak = p + } + } + + gain := referenceLevel - loudness + if gain > 52 { + gain = 52 + } else if gain < -52 { + gain = -52 + } + + volume := math.Pow(10, (gain)/20) + + nsamples := source.SampleRate * source.Channels * f.delay + + ratio := float32(math.Min(1, float64(len(block))/float64(nsamples))) + + adjustment = adjustment*(1-ratio) + float32(volume)*ratio + + if adjustment > float32(1/peak) { + adjustment = float32(1 / peak) + } + + if len(sampleBuffer) > nsamples { + adjustment = adjustment*(1-ratio) + float32(volume)*ratio + + if adjustment > float32(1/peak) { + adjustment = float32(1 / peak) + } + + size := len(sampleBuffer) - nsamples + out := make([]float32, size) + for i, e := range sampleBuffer[:size] { + out[i] = e * adjustment + } + outBlocks <- out + sampleBuffer = sampleBuffer[size:] + } else { + adjustment = float32(volume) + + if adjustment > float32(1/peak) { + adjustment = float32(1 / peak) + } + } + } + + //flush + if len(sampleBuffer) > 0 { + out := make([]float32, len(sampleBuffer)) + for i := range sampleBuffer { + out[i] = sampleBuffer[i] * adjustment + } + outBlocks <- out + } + + }() + + return audio.Source{ + Channels: source.Channels, + SampleRate: source.SampleRate, + Blocks: outBlocks, + } +} diff --git a/audio/replaygain/normalization.go b/audio/replaygain/normalization.go index 6ce17bf..516027a 100644 --- a/audio/replaygain/normalization.go +++ b/audio/replaygain/normalization.go @@ -1,3 +1,5 @@ +//go:build cgo + package replaygain import ( diff --git a/audio/replaygain/normalization_test.go b/audio/replaygain/normalization_test.go index a79edc1..f6ef55d 100644 --- a/audio/replaygain/normalization_test.go +++ b/audio/replaygain/normalization_test.go @@ -1,3 +1,5 @@ +//go:build cgo + package replaygain import ( diff --git a/cgo/audio.c b/cgo/audio.c index 371248f..91eed54 100644 --- a/cgo/audio.c +++ b/cgo/audio.c @@ -5,10 +5,11 @@ void audio_multiple_channels_to_mono(float* buffer, size_t buffer_len, float* ou float f; switch(channels) { case 1: + memcpy(out, buffer, sizeof(float) * buffer_len); break; case 2: for (int i = 0; i < buffer_len; i += 2){ - out[i/2] = buffer[i] + buffer[i+1]; + out[i/2] = (buffer[i] + buffer[i+1]) / 2; } break; default: @@ -52,8 +53,8 @@ void audio_multiple_channels_to_stereo(float* buffer, size_t buffer_len, float* 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; + out[samples*2] = buffer[FL] + surroundMix * buffer[C] + surroundMix * buffer[RL]; + out[samples*2+1] = buffer[FR] + surroundMix * buffer[C] + surroundMix * buffer[RR]; ++samples; } case 6: //5.1, FL, FR, FC, LFE, RL, RR @@ -64,8 +65,8 @@ void audio_multiple_channels_to_stereo(float* buffer, size_t buffer_len, float* 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; + out[samples*2] = buffer[FL] + surroundMix * buffer[C] + surroundMix * buffer[RL]; + out[samples*2+1] = buffer[FR] + surroundMix * buffer[C] + surroundMix * buffer[RR]; ++samples; } break; diff --git a/cgo/audio.go b/cgo/audio.go index 697549b..0395d09 100644 --- a/cgo/audio.go +++ b/cgo/audio.go @@ -1,3 +1,5 @@ +//go:build cgo + package cgo /* diff --git a/cgo/audio_nocgo.go b/cgo/audio_nocgo.go new file mode 100644 index 0000000..a2b3253 --- /dev/null +++ b/cgo/audio_nocgo.go @@ -0,0 +1,236 @@ +//go:build !cgo + +package cgo + +import ( + "encoding/binary" + "math" +) + +func bitsToDiv(b int) float32 { + return float32((int(1) << (b - 1)) - 1) +} + +//MultipleChannelsToMono bring any number of channels to mono, equally weighted, reusing buffer backwards +func MultipleChannelsToMono(buffer []float32, channels int) (buf []float32) { + buf = make([]float32, len(buffer)/channels) + for i := 0; i < len(buffer); i += channels { + f := buffer[i] + for j := 1; j < channels; j++ { + f += buffer[i+j] + } + buf[i/channels] = f / float32(channels) + } + + return +} + +//MultipleChannelsToStereo bring any number of channels to stereo, using downmix formulas when necessary +func MultipleChannelsToStereo(buffer []float32, channels int) (buf []float32) { + buf = make([]float32, (len(buffer)/channels)*2) + + var samples int + surroundMix := 1 / float32(math.Sqrt(2)) + + switch channels { + case 1: //mono, duplicate channels + for i := 0; i < len(buffer); i++ { + buf[i*2] = buffer[i] + buf[i*2+1] = buffer[i] + } + break + case 2: //copy + copy(buf, buffer) + break + case 3: //2.1, FL, FR, LFE + for i := 0; i < len(buffer); i += 3 { + FL := i + FR := i + 1 + //LFE := i + 2 + buf[samples*2] = buffer[FL] + buf[samples*2+1] = buffer[FR] + samples++ + } + case 5: //5.0, FL, FR, FC, RL, RR + for i := 0; i < len(buffer); i += 5 { + FL := i + FR := i + 1 + C := i + 2 + RL := i + 3 + RR := i + 4 + buf[samples*2] = buffer[FL] + surroundMix*buffer[C] + surroundMix*buffer[RL] + buf[samples*2+1] = buffer[FR] + surroundMix*buffer[C] + surroundMix*buffer[RR] + samples++ + } + case 6: //5.1, FL, FR, FC, LFE, RL, RR + for i := 0; i < len(buffer); i += 6 { + FL := i + FR := i + 1 + C := i + 2 + //LFE := i + 3 + RL := i + 4 + RR := i + 5 + buf[samples*2] = buffer[FL] + surroundMix*buffer[C] + surroundMix*buffer[RL] + buf[samples*2+1] = buffer[FR] + surroundMix*buffer[C] + surroundMix*buffer[RR] + samples++ + } + break + default: //no known formula, just take stereo out of it + for i := 0; i < len(buffer); i += channels { + buf[samples*2] = buffer[i] + buf[samples*2+1] = buffer[i+1] + samples++ + } + } + + return +} + +func BytesToInt32(data []byte, bitDepth int) (buf []int32) { + buf = make([]int32, len(data)/(bitDepth/8)) + + switch bitDepth { + case 8: + + for i := 0; i < len(data); i++ { + buf[i] = int32(data[i]) + } + break + case 16: + for i := 0; i < len(data); i += 2 { + buf[i/2] = int32(int16(binary.LittleEndian.Uint16(data[i : i+2]))) + } + break + case 24: + { + var sample uint32 + + for i := 0; i < len(data); i += 3 { + sample = uint32(data[i]) + sample += uint32(data[i+1]) << 8 + sample += uint32(data[i+2]) << 16 + + /*if sample > (1 << 23) { + sample = -((1 << 24) - sample) + }*/ + //sign extend + buf[i/3] = int32(sample<<8) >> 8 + } + } + break + case 32: + for i := 0; i < len(data)/4; i++ { + buf[i] = int32(binary.LittleEndian.Uint32(data[i : i+4])) + } + break + } + + return +} + +func Int32ToFloat32(data []int32, bitDepth int) (buf []float32) { + buf = make([]float32, len(data)) + for i := 0; i < len(data); i++ { + buf[i] = float32(data[i]) / bitsToDiv(bitDepth) + } + return +} + +//Int24ToFloat32 special case +func Int24ToFloat32(data []byte, bitDepth int) (buf []float32) { + buf = make([]float32, len(data)/3) + var sample uint32 + + for i := 0; i < len(data); i += 3 { + sample = uint32(data[i]) + sample += uint32(data[i+1]) << 8 + sample += uint32(data[i+2]) << 16 + + //sign extend + buf[i/3] = (float32(int32(sample<<8) >> 8)) / bitsToDiv(bitDepth) + } + return +} + +func Int16ToFloat32(data []int16, bitDepth int) (buf []float32) { + buf = make([]float32, len(data)) + for i := 0; i < len(data); i++ { + buf[i] = float32(data[i]) / bitsToDiv(bitDepth) + } + return +} + +func Int8ToFloat32(data []int8, bitDepth int) (buf []float32) { + buf = make([]float32, len(data)) + + for i := 0; i < len(data); i++ { + buf[i] = float32(data[i]) / bitsToDiv(bitDepth) + } + + return +} + +func Float32ToInt32(data []float32, bitDepth int) (buf []int32) { + buf = make([]int32, len(data)) + for i := 0; i < len(data); i++ { + f := data[i] + if f < -1.0 { + f = -1.0 + } + if f > 1.0 { + f = 1.0 + } + buf[i] = int32(f * bitsToDiv(bitDepth)) + } + return +} + +func Float32ToInt24(data []float32) (buf []byte) { + buf = make([]byte, len(data)*3) + + for i := 0; i < len(data); i++ { + f := data[i] + if f < -1.0 { + f = -1.0 + } + if f > 1.0 { + f = 1.0 + } + + value := int32(f * bitsToDiv(24)) + buf[i*3] = byte(value & 0xFF) + buf[i*3+1] = byte((value >> 8) & 0xFF) + buf[i*3+2] = byte((value >> 16) & 0xFF) + } + return +} + +func Float32ToInt16(data []float32) (buf []int16) { + buf = make([]int16, len(data)) + for i := 0; i < len(data); i++ { + f := data[i] + if f < -1.0 { + f = -1.0 + } + if f > 1.0 { + f = 1.0 + } + buf[i] = int16(f * bitsToDiv(16)) + } + return +} + +func Float32ToInt8(data []float32) (buf []int8) { + buf = make([]int8, len(data)) + for i := 0; i < len(data); i++ { + f := data[i] + if f < -1.0 { + f = -1.0 + } + if f > 1.0 { + f = 1.0 + } + buf[i] = int8(f * bitsToDiv(8)) + } + return +} diff --git a/go.mod b/go.mod index cd5a053..28a9359 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,11 @@ require ( github.com/dh1tw/gosamplerate v0.1.2 github.com/edgeware/mp4ff v0.28.0 github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565 + github.com/hajimehoshi/go-mp3 v0.3.3 github.com/jfreymuth/oggvorbis v1.0.3 github.com/kvark128/minimp3 v0.0.0-20220408223524-dd428325fce7 github.com/mewkiz/flac v1.0.7 + github.com/oov/audio v0.0.0-20171004131523-88a2be6dbe38 github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081 github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d ) diff --git a/go.sum b/go.sum index 2032ac5..c35338a 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,9 @@ github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hajimehoshi/go-mp3 v0.3.3 h1:cWnfRdpye2m9ElSoVqneYRcpt/l3ijttgjMeQh+r+FE= +github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0= github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= @@ -44,6 +47,8 @@ github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 h1:DDyKVkTkrFmd9lR84QW3EIfkkoHlurlpgW+DYuAUJn8= github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77/go.mod h1:J/rDzvIiwiVpv72OEP8aJFxLXjGpUdviIIeqJPLIctA= +github.com/oov/audio v0.0.0-20171004131523-88a2be6dbe38 h1:4Upfs5rLQdx7KwBct3bmPYAhWsDDJdx660gYb7Lv9TQ= +github.com/oov/audio v0.0.0-20171004131523-88a2be6dbe38/go.mod h1:Xj06yMta9R1RSKiHmxL0Bo2TB8wiKVnMgA0KVopHHkk= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -59,8 +64,13 @@ github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU= github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50= github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634= github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=