Improved guess package, split tests into individual files, added alternate aac encoder
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
5fc88e3a49
commit
c2d2a52614
28
.drone.yml
28
.drone.yml
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
name: build-from-repo
|
||||
|
||||
steps:
|
||||
- name: submodules/LFS
|
||||
|
@ -17,15 +17,29 @@ steps:
|
|||
- DEBIAN_FRONTEND=noninteractive apt install -y git build-essential autoconf automake libtool libflac-dev libopus-dev libopusfile-dev libsamplerate0-dev libmp3lame-dev libebur128-dev libfdk-aac-dev
|
||||
- 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 -c -v
|
||||
- name: build-test-full
|
||||
- go test -cover -v ./...
|
||||
- go test -cover -v -tags=disable_codec_libfdk-aac,disable_codec_lame,disable_codec_tta ./...
|
||||
- go test -cover -v -tags=disable_format_aac,disable_format_alac,disable_format_mp3,disable_format_opus,disable_format_tta,disable_format_vorbis ./...
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-from-source
|
||||
|
||||
steps:
|
||||
- name: submodules/LFS
|
||||
image: alpine/git
|
||||
commands:
|
||||
- apk update && apk add --no-cache git-lfs && git lfs install
|
||||
- git submodule update --init --recursive
|
||||
- name: build-repo
|
||||
image: golang:1.18-bullseye
|
||||
commands:
|
||||
- DEBIAN_FRONTEND=noninteractive apt update
|
||||
- DEBIAN_FRONTEND=noninteractive apt install -y git build-essential autoconf automake libtool libflac-dev libopus-dev libopusfile-dev libsamplerate0-dev libmp3lame-dev libebur128-dev
|
||||
- cd libopusenc && ./autogen.sh && ./configure --prefix /usr && make && make install && cd ..
|
||||
- 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://github.com/mstorsjo/fdk-aac.git && cd fdk-aac && ./autogen.sh && ./configure --prefix /usr && make -j$(nproc) && make install && cd ..
|
||||
- cd alac && autoreconf -fi && ./configure --prefix /usr && make && make install && cd ..
|
||||
- go test -cover -coverpkg=git.gammaspectra.live/S.O.N.G/Kirika/hasher,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/aac,git.gammaspectra.live/S.O.N.G/Kirika/audio/format/alac,git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus,git.gammaspectra.live/S.O.N.G/Kirika/audio/format/tta -v
|
||||
|
||||
- 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 ./...
|
||||
- go test -cover -v -tags=disable_format_aac,disable_format_alac,disable_format_mp3,disable_format_opus,disable_format_tta,disable_format_vorbis ./...
|
||||
...
|
||||
|
|
990
Kirika_test.go
990
Kirika_test.go
|
@ -1,990 +0,0 @@
|
|||
package Kirika
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/aac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/alac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||
"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/audio/packetizer"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/replaygain"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/hasher"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var TestSampleLocations = []string{
|
||||
"resources/samples/cYsmix - Haunted House/",
|
||||
"resources/samples/Babbe Music - RADIANT DANCEFLOOR/",
|
||||
}
|
||||
|
||||
const TestSingleSample24 = "resources/samples/cYsmix - Haunted House/11. The Great Rigid Desert.flac"
|
||||
|
||||
const TestSingleSample16 = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.flac"
|
||||
const TestSingleSample16TTA = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.tta"
|
||||
|
||||
func doTest(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()
|
||||
decoders, err := guess.GetDecoders(fp, fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
source, err := guess.Open(fp, decoders)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACDecode(t *testing.T) {
|
||||
doTest(".flac", t)
|
||||
}
|
||||
func TestTTADecode(t *testing.T) {
|
||||
doTest(".tta", t)
|
||||
}
|
||||
func TestOpusDecode(t *testing.T) {
|
||||
doTest(".opus", t)
|
||||
}
|
||||
func TestMP3Decode(t *testing.T) {
|
||||
doTest(".mp3", t)
|
||||
}
|
||||
func TestVorbisDecode(t *testing.T) {
|
||||
doTest(".vorbis", t)
|
||||
}
|
||||
|
||||
func TestReplayGainNormalization24(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
source = replaygain.NewNormalizationFilter(5).Process(source)
|
||||
|
||||
gain, peak, err := replaygain.GetTrackReplayGain(source)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-0.033242 dB, 0.627992"
|
||||
result := fmt.Sprintf("%f dB, %f", gain, peak)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayGain24(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
gain, peak, err := replaygain.GetTrackReplayGain(source)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-11.590850 dB, 1.000000"
|
||||
result := fmt.Sprintf("%f dB, %f", gain, peak)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayGain16(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(TestSingleSample16)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, err := flac.NewFormat().Open(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
gain, peak, err := replaygain.GetTrackReplayGain(source)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-7.748202 dB, 0.942137"
|
||||
result := fmt.Sprintf("%f dB, %f", gain, peak)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbumReplayGain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
location := TestSampleLocations[1]
|
||||
entries, err := os.ReadDir(location)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var sources []audio.Source
|
||||
for _, f := range entries {
|
||||
if path.Ext(f.Name()) == ".flac" {
|
||||
fullPath := path.Join(location, f.Name())
|
||||
|
||||
fp, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
decoders, err := guess.GetDecoders(fp, fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
source, err := guess.Open(fp, decoders)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
albumGain, albumPeak, trackGains, trackPeaks, err := replaygain.GetAlbumReplayGain(sources)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-8.539780 dB, 0.970916, [-7.748202 -9.020742 -9.192557 -8.015611 -8.303189 -9.071835 -9.356090 -8.114885 -7.760444 -7.317174 -8.339798 -8.906274 -8.001073], [0.942137 0.970916 0.970916 0.970916 0.942106 0.970916 0.970916 0.965606 0.970916 0.970916 0.942137 0.970916 0.942137]"
|
||||
result := fmt.Sprintf("%f dB, %f, %f, %f", albumGain, albumPeak, trackGains, trackPeaks)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasher24(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
sha256Result := hasher.NewHasher(channels[3], hasher.HashtypeSha256)
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
cueToolsCrc32.Wait()
|
||||
accurateRipV1.Wait()
|
||||
accurateRipV2.Wait()
|
||||
crc32.Wait()
|
||||
sha256Result.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(sha256Result.GetResult(), expectSha256) != 0 {
|
||||
t.Errorf("Wrong SHA256 %X != %X", sha256Result.GetResult(), expectSha256)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHasher16TTA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(TestSingleSample16TTA)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, analyzerChannel, err := tta.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)
|
||||
sha256Result := hasher.NewHasher(channels[3], hasher.HashtypeSha256)
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
cueToolsCrc32.Wait()
|
||||
accurateRipV1.Wait()
|
||||
accurateRipV2.Wait()
|
||||
crc32.Wait()
|
||||
sha256Result.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(sha256Result.GetResult(), expectSha256) != 0 {
|
||||
t.Errorf("Wrong SHA256 %X != %X", sha256Result.GetResult(), expectSha256)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFilterChain(t *testing.T) {
|
||||
t.Parallel()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeFLAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.flac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
err = flac.NewFormat().Encode(source, target, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeFLACOgg(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.ogg")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["ogg"] = true
|
||||
|
||||
err = flac.NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketizeFLAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
shaHasher := sha256.New()
|
||||
shaPacketHasher := sha256.New()
|
||||
encodeReader := io.TeeReader(reader, shaHasher)
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = flac.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := packetizer.NewFLACPacketizer(encodeReader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
shaPacketHasher.Write(packet.GetData())
|
||||
}
|
||||
if packetCount != 4231 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 4231)
|
||||
}
|
||||
if packetBytes != 51513533 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 51513533)
|
||||
}
|
||||
if bytes.Compare(shaHasher.Sum([]byte{}), shaPacketHasher.Sum([]byte{})) != 0 {
|
||||
t.Errorf("Mismatch on byte output %X != %X", shaPacketHasher.Sum([]byte{}), shaHasher.Sum([]byte{}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeTTA(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.tta")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
err = tta.NewFormat().Encode(source, target, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeALAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.m4a")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitdepth"] = 24
|
||||
|
||||
err = alac.NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeMP3(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.mp3")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
err = mp3.NewFormat().Encode(source, target, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketizeMP3(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = mp3.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := packetizer.NewMp3Packetizer(reader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
}
|
||||
if packetCount != 15040 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 15040)
|
||||
}
|
||||
if packetBytes != 13901254 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 13901254)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeOpus(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.opus")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
|
||||
err = opus.NewFormat().Encode(audio.NewResampleFilter(opus.FixedSampleRate, audio.Linear, 0).Process(source), target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketizeOgg(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = opus.NewFormat().Encode(audio.NewResampleFilter(opus.FixedSampleRate, audio.Linear, 0).Process(source), writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := packetizer.NewOggPacketizer(reader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
}
|
||||
if packetCount != 395 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 395)
|
||||
}
|
||||
if packetBytes != 5962688 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 5962688)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
|
||||
err = aac.NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeAAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "decode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
|
||||
err = aac.NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
target.Seek(0, io.SeekStart)
|
||||
|
||||
source, err = aac.NewFormat().Open(target)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketizeADTS(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = aac.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := packetizer.NewAdtsPacketizer(reader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
}
|
||||
if packetCount != 16920 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 16920)
|
||||
}
|
||||
if packetBytes != 6436973 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 6436973)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAACHE(t *testing.T) {
|
||||
t.Parallel()
|
||||
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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
options["mode"] = "hev2"
|
||||
|
||||
err = aac.NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const sampleRate = 44100
|
||||
|
||||
q := audio.NewQueue(sampleRate, 2)
|
||||
flacFormat := flac.NewFormat()
|
||||
|
||||
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()) == ".flac" {
|
||||
fullPath := path.Join(location, f.Name())
|
||||
fp, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
source, err := flacFormat.Open(fp)
|
||||
if err != nil {
|
||||
fp.Close()
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
q.AddTail(source, func(q *audio.Queue, entry *audio.QueueEntry) {
|
||||
t.Logf("Started playback of %d %s\n", entry.Identifier, fullPath)
|
||||
}, func(q *audio.Queue, entry *audio.QueueEntry) {
|
||||
t.Logf("Finished playback of %d %s: output %d samples\n", entry.Identifier, fullPath, entry.ReadSamples)
|
||||
}, func(q *audio.Queue, entry *audio.QueueEntry) {
|
||||
fp.Close()
|
||||
if q.GetQueueSize() == 0 {
|
||||
t.Log("Finished playback, closing\n")
|
||||
q.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Decode
|
||||
result := q.GetSource()
|
||||
|
||||
sink := audio.NewForwardSink(audio.NewNullSink())
|
||||
sink.Process(result)
|
||||
|
||||
q.Wait()
|
||||
|
||||
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 != 470828559 {
|
||||
t.Errorf("Wrong Sample Count %d != %d", sink.SamplesRead, 470828559)
|
||||
}
|
||||
}
|
87
README.md
87
README.md
|
@ -98,6 +98,29 @@ sudo apt install libsamplerate0-dev
|
|||
sudo apt install libebur128-dev
|
||||
```
|
||||
|
||||
## Build tags
|
||||
Several Golang build tags exist to change which features are included in the project.
|
||||
|
||||
### disable_format_[format]
|
||||
This tag disables support for the specified format/codec. This does not affect packetizers.
|
||||
|
||||
Current implemented [format]: `aac, alac, flac, mp3, opus, tta, vorbis`. Disabling `flac` will break tests.
|
||||
|
||||
### disable_codec_libfdk-aac
|
||||
This tag disables the [libfdk-aac](https://git.gammaspectra.live/S.O.N.G/go-fdkaac) support for decoding/encoding AAC. This is available for specific problems with the FDK license that conflicts with your needs or policy.
|
||||
|
||||
If this tag is enabled, yet `aac` support remains enabled, an AAC encoder (but not decoder) will be built with the [VisualOn AAC encoder](https://github.com/gen2brain/aac-go) bindings, which are vastly worse.
|
||||
|
||||
### disable_codec_lame
|
||||
This tag disables the [LAME](https://github.com/viert/go-lame) support for encoding MP3. This is available for specific problems with the LGPL v2 license that conflicts with your needs or policy.
|
||||
|
||||
MP3 decoding is not affected by this tag.
|
||||
|
||||
### disable_codec_tta
|
||||
This tag disables the [TTA](https://git.gammaspectra.live/S.O.N.G/go-tta) support for decoding/encoding TTA. This is available for specific problems with the LGPL v3 license that conflicts with your needs or policy.
|
||||
|
||||
This is equivalent to `disable_format_tta`.
|
||||
|
||||
## Licenses
|
||||
|
||||
### Kirika
|
||||
|
@ -119,34 +142,36 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|||
|
||||
Subdependencies that are not cgo-based are denoted in cursive.
|
||||
|
||||
| Dependency | License | Notes |
|
||||
|:-----------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [S.O.N.G/go-alac](https://git.gammaspectra.live/S.O.N.G/go-alac) | [BSD 2-Clause](https://git.gammaspectra.live/S.O.N.G/go-alac/src/branch/master/LICENSE) | |
|
||||
| [S.O.N.G/alac](https://git.gammaspectra.live/S.O.N.G/alac) | [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) | [BSD 2-Clause](https://git.gammaspectra.live/S.O.N.G/go-ebur128/src/branch/master/LICENSE) | |
|
||||
| [jiixyj/libebur128](https://github.com/jiixyj/libebur128) | [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) | [MIT](https://git.gammaspectra.live/S.O.N.G/go-fdkaac/src/branch/master/LICENSE) | |
|
||||
| [mstorsjo/fdk-aac](https://github.com/mstorsjo/fdk-aac) | [FDK License, BSD-like](https://github.com/mstorsjo/fdk-aac/blob/master/NOTICE) | Does not include patent grants.<br/>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) | [MIT](https://git.gammaspectra.live/S.O.N.G/go-pus/src/branch/v2/LICENSE) | |
|
||||
| [xiph/opus](https://gitlab.xiph.org/xiph/opus) | [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/).<br/>Can be linked by cgo as a shared library. |
|
||||
| [xiph/opusfile](https://gitlab.xiph.org/xiph/opusfile) | [BSD 3-Clause](https://gitlab.xiph.org/xiph/opusfile/-/blob/master/COPYINGG) | Can be linked by cgo as a shared library. |
|
||||
| [xiph/ogg](https://gitlab.xiph.org/xiph/ogg) | [BSD 3-Clause](https://gitlab.xiph.org/xiph/ogg/-/blob/master/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| [xiph/libopusenc](https://gitlab.xiph.org/xiph/libopusenc) | [BSD 3-Clause](https://gitlab.xiph.org/xiph/libopusenc/-/blob/master/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| [S.O.N.G/go-tta](https://git.gammaspectra.live/S.O.N.G/go-tta) | [LGPL v3](https://git.gammaspectra.live/S.O.N.G/go-tta/src/branch/master/LICENSE) | |
|
||||
| [S.O.N.G/goflac](https://git.gammaspectra.live/S.O.N.G/goflac) | [BSD 3-Clause](https://git.gammaspectra.live/S.O.N.G/goflac/src/branch/master/LICENSE) | |
|
||||
| [xiph/flac](https://gitlab.xiph.org/xiph/flac) | [BSD 3-Clause](https://gitlab.xiph.org/xiph/flac/-/blob/master/COPYING.Xiph) | Read extra license details on the [official site](https://xiph.org/flac/license.html).<br/>Can be linked by cgo as a shared library. |
|
||||
| [dh1tw/gosamplerate](https://github.com/dh1tw/gosamplerate) | [BSD 2-Clause](https://github.com/dh1tw/gosamplerate/blob/master/LICENSE) | |
|
||||
| [libsndfile/libsamplerate](https://github.com/libsndfile/libsamplerate) | [BSD 2-Clause](https://github.com/libsndfile/libsamplerate/blob/master/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| [edgeware/mp4ff](https://github.com/edgeware/mp4ff) | [MIT](https://github.com/edgeware/mp4ff/blob/master/LICENSE.md) | |
|
||||
| [jfreymuth/oggvorbis](https://github.com/jfreymuth/oggvorbis) | [MIT](https://github.com/jfreymuth/oggvorbis/blob/master/LICENSE) | | |
|
||||
| _[jfreymuth/vorbis](https://github.com/jfreymuth/vorbis)_ | [MIT](https://github.com/jfreymuth/vorbis/blob/master/LICENSE) | Subdependency of _jfreymuth/oggvorbis_. | |
|
||||
| [kvark128/minimp3](https://github.com/kvark128/minimp3) | [MIT](https://github.com/kvark128/minimp3/blob/master/LICENSE.txt) | | |
|
||||
| [lieff/minimp3](https://github.com/lieff/minimp3) | [CC0 1.0](https://github.com/lieff/minimp3/blob/master/LICENSE) | Subdependency of _lieff/minimp3_. |
|
||||
| [mewkiz/flac](https://github.com/mewkiz/flac) | [The Unlicense](https://github.com/mewkiz/flac/blob/master/LICENSE) | |
|
||||
| _[go-audio/audio](https://github.com/go-audio/audio)_ | [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)_ | [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)_ | [Apache 2.0](https://github.com/go-audio/riff/blob/master/LICENSE) | Subdependency of _go-audio/wav_. Only used on tests there. |
|
||||
| _[icza/bitio](https://github.com/icza/bitio)_ | [Apache 2.0](https://github.com/icza/bitio/blob/master/LICENSE-APACHE) or [LGPL v2.1](https://github.com/icza/bitio/blob/master/LICENSE-LGPL-v2.1) | Subdependency of _mewkiz/flac_. |
|
||||
| [sssgun/mp3](https://github.com/sssgun/mp3) | [MIT](https://github.com/sssgun/mp3/blob/master/LICENSE) | |
|
||||
| [viert/go-lame](https://github.com/viert/go-lame) | [MIT](https://github.com/viert/go-lame/blob/master/LICENSE) | |
|
||||
| [LAME](https://lame.sourceforge.io/) | [LGPL v2](https://sourceforge.net/p/lame/svn/HEAD/tree/trunk/lame/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| Dependency | Language | License | Notes |
|
||||
|:-----------------------------------------------------------------------:|:--------:|:--------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [S.O.N.G/go-alac](https://git.gammaspectra.live/S.O.N.G/go-alac) | Go | [BSD 2-Clause](https://git.gammaspectra.live/S.O.N.G/go-alac/src/branch/master/LICENSE) | |
|
||||
| [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) | |
|
||||
| [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.<br/>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).<br/>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/).<br/>Can be linked by cgo as a shared library. |
|
||||
| [xiph/opusfile](https://gitlab.xiph.org/xiph/opusfile) | C | [BSD 3-Clause](https://gitlab.xiph.org/xiph/opusfile/-/blob/master/COPYINGG) | Can be linked by cgo as a shared library. |
|
||||
| [xiph/ogg](https://gitlab.xiph.org/xiph/ogg) | C | [BSD 3-Clause](https://gitlab.xiph.org/xiph/ogg/-/blob/master/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| [xiph/libopusenc](https://gitlab.xiph.org/xiph/libopusenc) | C | [BSD 3-Clause](https://gitlab.xiph.org/xiph/libopusenc/-/blob/master/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| [S.O.N.G/go-tta](https://git.gammaspectra.live/S.O.N.G/go-tta) | Go | [LGPL v3](https://git.gammaspectra.live/S.O.N.G/go-tta/src/branch/master/LICENSE) | |
|
||||
| [S.O.N.G/goflac](https://git.gammaspectra.live/S.O.N.G/goflac) | Go | [BSD 3-Clause](https://git.gammaspectra.live/S.O.N.G/goflac/src/branch/master/LICENSE) | |
|
||||
| [xiph/flac](https://gitlab.xiph.org/xiph/flac) | C | [BSD 3-Clause](https://gitlab.xiph.org/xiph/flac/-/blob/master/COPYING.Xiph) | Read extra license details on the [official site](https://xiph.org/flac/license.html).<br/>Can be linked by cgo as a shared library. |
|
||||
| [dh1tw/gosamplerate](https://github.com/dh1tw/gosamplerate) | Go | [BSD 2-Clause](https://github.com/dh1tw/gosamplerate/blob/master/LICENSE) | |
|
||||
| [libsndfile/libsamplerate](https://github.com/libsndfile/libsamplerate) | C | [BSD 2-Clause](https://github.com/libsndfile/libsamplerate/blob/master/COPYING) | Can be linked by cgo as a shared library. |
|
||||
| [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) | | |
|
||||
| [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) | |
|
||||
| _[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. |
|
||||
| _[icza/bitio](https://github.com/icza/bitio)_ | Go | [Apache 2.0](https://github.com/icza/bitio/blob/master/LICENSE-APACHE) or [LGPL v2.1](https://github.com/icza/bitio/blob/master/LICENSE-LGPL-v2.1) | Subdependency of _mewkiz/flac_. |
|
||||
| [sssgun/mp3](https://github.com/sssgun/mp3) | Go | [MIT](https://github.com/sssgun/mp3/blob/master/LICENSE) | |
|
||||
| [viert/go-lame](https://github.com/viert/go-lame) | Go | [MIT](https://github.com/viert/go-lame/blob/master/LICENSE) | |
|
||||
| [LAME](https://lame.sourceforge.io/) | C | [LGPL v2](https://sourceforge.net/p/lame/svn/HEAD/tree/trunk/lame/COPYING) | Can be linked by cgo as a shared library. |
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package audio
|
||||
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"
|
||||
|
@ -8,10 +9,10 @@ import (
|
|||
)
|
||||
|
||||
type Filter interface {
|
||||
Process(source Source) Source
|
||||
Process(source audio.Source) audio.Source
|
||||
}
|
||||
|
||||
func NewFilterChain(source Source, filters ...Filter) Source {
|
||||
func NewFilterChain(source audio.Source, filters ...Filter) audio.Source {
|
||||
for _, filter := range filters {
|
||||
source = filter.Process(source)
|
||||
}
|
||||
|
@ -28,7 +29,7 @@ func NewBufferFilter(blockBufferSize int) BufferFilter {
|
|||
}
|
||||
}
|
||||
|
||||
func (f BufferFilter) Process(source Source) Source {
|
||||
func (f BufferFilter) Process(source audio.Source) audio.Source {
|
||||
outBlocks := make(chan []float32, f.blockBufferSize)
|
||||
go func() {
|
||||
defer close(outBlocks)
|
||||
|
@ -37,7 +38,7 @@ func (f BufferFilter) Process(source Source) Source {
|
|||
}
|
||||
}()
|
||||
|
||||
return Source{
|
||||
return audio.Source{
|
||||
Channels: source.Channels,
|
||||
SampleRate: source.SampleRate,
|
||||
Blocks: outBlocks,
|
||||
|
@ -54,7 +55,7 @@ func NewRealTimeFilter(blocksPerSecond int) RealTimeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
func (f RealTimeFilter) Process(source Source) Source {
|
||||
func (f RealTimeFilter) Process(source audio.Source) audio.Source {
|
||||
outBlocks := make(chan []float32)
|
||||
if source.SampleRate%f.blocksPerSecond != 0 {
|
||||
log.Panicf("%d %% %d != 0", source.SampleRate, f.blocksPerSecond)
|
||||
|
@ -77,7 +78,7 @@ func (f RealTimeFilter) Process(source Source) Source {
|
|||
outBlocks <- buf
|
||||
}()
|
||||
|
||||
return Source{
|
||||
return audio.Source{
|
||||
Channels: source.Channels,
|
||||
SampleRate: source.SampleRate,
|
||||
Blocks: outBlocks,
|
||||
|
@ -94,7 +95,7 @@ func NewVolumeFilter(adjustment float32) VolumeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
func (f VolumeFilter) Process(source Source) Source {
|
||||
func (f VolumeFilter) Process(source audio.Source) audio.Source {
|
||||
outBlocks := make(chan []float32)
|
||||
go func() {
|
||||
defer close(outBlocks)
|
||||
|
@ -109,7 +110,7 @@ func (f VolumeFilter) Process(source Source) Source {
|
|||
|
||||
}()
|
||||
|
||||
return Source{
|
||||
return audio.Source{
|
||||
Channels: source.Channels,
|
||||
SampleRate: source.SampleRate,
|
||||
Blocks: outBlocks,
|
||||
|
@ -119,7 +120,7 @@ func (f VolumeFilter) Process(source Source) Source {
|
|||
type StereoFilter struct {
|
||||
}
|
||||
|
||||
func (f StereoFilter) Process(source Source) Source {
|
||||
func (f StereoFilter) Process(source audio.Source) audio.Source {
|
||||
if source.Channels == 2 { //no change
|
||||
return source
|
||||
}
|
||||
|
@ -132,7 +133,7 @@ func (f StereoFilter) Process(source Source) Source {
|
|||
}
|
||||
}()
|
||||
|
||||
return Source{
|
||||
return audio.Source{
|
||||
Channels: 2,
|
||||
SampleRate: source.SampleRate,
|
||||
Blocks: outBlocks,
|
||||
|
@ -142,7 +143,7 @@ func (f StereoFilter) Process(source Source) Source {
|
|||
type MonoFilter struct {
|
||||
}
|
||||
|
||||
func (f MonoFilter) Process(source Source) Source {
|
||||
func (f MonoFilter) Process(source audio.Source) audio.Source {
|
||||
if source.Channels == 1 { //no change
|
||||
return source
|
||||
}
|
||||
|
@ -155,7 +156,7 @@ func (f MonoFilter) Process(source Source) Source {
|
|||
}
|
||||
}()
|
||||
|
||||
return Source{
|
||||
return audio.Source{
|
||||
Channels: 1,
|
||||
SampleRate: source.SampleRate,
|
||||
Blocks: outBlocks,
|
||||
|
@ -189,7 +190,7 @@ func NewResampleFilter(sampleRate int, quality ResampleQuality, blockSize int) R
|
|||
}
|
||||
}
|
||||
|
||||
func (f ResampleFilter) Process(source Source) Source {
|
||||
func (f ResampleFilter) Process(source audio.Source) audio.Source {
|
||||
if source.SampleRate == f.sampleRate { //no change
|
||||
return source
|
||||
}
|
||||
|
@ -239,7 +240,7 @@ func (f ResampleFilter) Process(source Source) Source {
|
|||
}
|
||||
}()
|
||||
|
||||
return Source{
|
||||
return audio.Source{
|
||||
Channels: source.Channels,
|
||||
SampleRate: f.sampleRate,
|
||||
Blocks: outBlocks,
|
41
audio/filter/filter_test.go
Normal file
41
audio/filter/filter_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
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 TestFilterChain(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, BandlimitedFastest, 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)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
//go:build !disable_format_aac && !disable_codec_libfdk_aac
|
||||
// +build !disable_format_aac,!disable_codec_libfdk_aac
|
||||
|
||||
package aac
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/cgo"
|
||||
"git.gammaspectra.live/S.O.N.G/go-fdkaac/fdkaac"
|
||||
aac_adts "github.com/edgeware/mp4ff/aac"
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
@ -18,7 +20,15 @@ func NewFormat() Format {
|
|||
return Format{}
|
||||
}
|
||||
|
||||
func decodeFrame(decoder *fdkaac.AacDecoder, p *packetizer.AdtsPacketizer) ([]float32, error) {
|
||||
func (f Format) Name() string {
|
||||
return "aac"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "libfdk-aac (go-fdkaac)"
|
||||
}
|
||||
|
||||
func decodeFrame(decoder *fdkaac.AacDecoder, r io.Reader) ([]float32, error) {
|
||||
pcm, err := decoder.Decode()
|
||||
|
||||
if err != nil {
|
||||
|
@ -29,18 +39,25 @@ func decodeFrame(decoder *fdkaac.AacDecoder, p *packetizer.AdtsPacketizer) ([]fl
|
|||
return cgo.Int32ToFloat32(cgo.BytesToInt32(pcm, 16), 16), nil
|
||||
}
|
||||
|
||||
packet := p.GetPacket()
|
||||
if packet == nil {
|
||||
return nil, errors.New("error decoding")
|
||||
}
|
||||
|
||||
_, err = decoder.Fill(packet.GetData())
|
||||
header, _, err := aac_adts.DecodeADTSHeader(r)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decodeFrame(decoder, p)
|
||||
data := make([]byte, header.PayloadLength)
|
||||
|
||||
if _, err = io.ReadFull(r, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = decoder.Fill(append(header.Encode(), data...))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decodeFrame(decoder, r)
|
||||
}
|
||||
|
||||
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
||||
|
@ -51,9 +68,7 @@ func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
|||
return audio.Source{}, err
|
||||
}
|
||||
|
||||
p := packetizer.NewAdtsPacketizer(r)
|
||||
|
||||
buf, err := decodeFrame(decoder, p)
|
||||
buf, err := decodeFrame(decoder, r)
|
||||
|
||||
if err != nil {
|
||||
decoder.Close()
|
||||
|
@ -71,7 +86,7 @@ func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
|||
}
|
||||
|
||||
for {
|
||||
buf, err = decodeFrame(decoder, p)
|
||||
buf, err = decodeFrame(decoder, r)
|
||||
|
||||
if err != nil {
|
||||
return
|
134
audio/format/aac/libfdk-aac_test.go
Normal file
134
audio/format/aac/libfdk-aac_test.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
//go:build !disable_format_aac && !disable_codec_libfdk_aac
|
||||
// +build !disable_format_aac,!disable_codec_libfdk_aac
|
||||
|
||||
package aac
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeAAC(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeAAC(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "decode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
target.Seek(0, io.SeekStart)
|
||||
|
||||
source, err = NewFormat().Open(target)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAACHE(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
options["mode"] = "hev2"
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
104
audio/format/aac/vo-aacenc.go
Normal file
104
audio/format/aac/vo-aacenc.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
//go:build !disable_format_aac && disable_codec_libfdk_aac
|
||||
// +build !disable_format_aac,disable_codec_libfdk_aac
|
||||
|
||||
package aac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/cgo"
|
||||
"github.com/gen2brain/aac-go"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Format struct {
|
||||
}
|
||||
|
||||
func NewFormat() Format {
|
||||
return Format{}
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "aac"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "vo-aacenc (aac-go)"
|
||||
}
|
||||
|
||||
func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error {
|
||||
var bitrate = 128
|
||||
var isHEv2 bool
|
||||
|
||||
if options != nil {
|
||||
var val interface{}
|
||||
var ok bool
|
||||
var intVal int
|
||||
var strVal string
|
||||
if val, ok = options["bitrate"]; ok {
|
||||
if strVal, ok = val.(string); ok {
|
||||
switch strVal {
|
||||
case "320k":
|
||||
bitrate = 320
|
||||
case "256k":
|
||||
bitrate = 256
|
||||
case "192k":
|
||||
bitrate = 192
|
||||
case "128k":
|
||||
bitrate = 128
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown setting bitrate=%s", strVal)
|
||||
}
|
||||
} else if intVal, ok = val.(int); ok {
|
||||
bitrate = intVal
|
||||
}
|
||||
}
|
||||
if val, ok = options["mode"]; ok {
|
||||
if strVal, ok = val.(string); ok {
|
||||
switch strVal {
|
||||
case "lc":
|
||||
isHEv2 = false
|
||||
case "hev2":
|
||||
isHEv2 = true
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown setting mode=%s", strVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encoder, err := aac.NewEncoder(writer, &aac.Options{
|
||||
BitRate: bitrate * 1024,
|
||||
NumChannels: source.Channels,
|
||||
SampleRate: source.SampleRate,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
pReader, pWriter := io.Pipe()
|
||||
defer pWriter.Close()
|
||||
|
||||
go func() {
|
||||
defer pWriter.Close()
|
||||
for block := range source.Blocks {
|
||||
if _, decodeErr := pWriter.Write(cgo.Float32ToInt16(block)); decodeErr != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = encoder.Encode(pReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Format) Identify(peek []byte, extension string) bool {
|
||||
return extension == "aac" || extension == "adts"
|
||||
}
|
47
audio/format/aac/vo-aacenc_test.go
Normal file
47
audio/format/aac/vo-aacenc_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
//go:build !disable_format_aac && disable_codec_libfdk_aac
|
||||
// +build !disable_format_aac,disable_codec_libfdk_aac
|
||||
|
||||
package aac
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeAAC(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.aac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "64k"
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//go:build !disable_format_alac
|
||||
// +build !disable_format_alac
|
||||
|
||||
package alac
|
||||
|
||||
import (
|
||||
|
@ -18,6 +21,14 @@ func NewFormat() Format {
|
|||
return Format{}
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "alac"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "libalac (go-alac)"
|
||||
}
|
||||
|
||||
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
||||
decoder := go_alac.NewFormatDecoder(r)
|
||||
if decoder == nil {
|
||||
|
|
47
audio/format/alac/alac_test.go
Normal file
47
audio/format/alac/alac_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
//go:build !disable_format_alac
|
||||
// +build !disable_format_alac
|
||||
|
||||
package alac
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeALAC(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.m4a")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitdepth"] = 24
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//go:build !disable_format_flac
|
||||
// +build !disable_format_flac
|
||||
|
||||
package flac
|
||||
|
||||
import (
|
||||
|
@ -16,6 +19,14 @@ func NewFormat() Format {
|
|||
return Format{}
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "flac"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "libFLAC (goflac)"
|
||||
}
|
||||
|
||||
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
||||
currentPosition, _ := r.Seek(0, io.SeekCurrent)
|
||||
decoder, err := libflac.NewDecoderReader(r)
|
||||
|
|
79
audio/format/flac/flac_test.go
Normal file
79
audio/format/flac/flac_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
//go:build !disable_format_alac
|
||||
// +build !disable_format_alac
|
||||
|
||||
package flac
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeFLAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
fp, err := os.Open(test.TestSingleSample24)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, err := NewFormat().Open(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.flac")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
err = NewFormat().Encode(source, target, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeFLACOgg(t *testing.T) {
|
||||
t.Parallel()
|
||||
fp, err := os.Open(test.TestSingleSample24)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, err := NewFormat().Open(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.ogg")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["ogg"] = true
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -8,6 +8,10 @@ import (
|
|||
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
|
||||
// Name returns the name of the codec or format
|
||||
Name() string
|
||||
// Description returns a longer description of the backing libraries or versions
|
||||
Description() string
|
||||
}
|
||||
|
||||
type WriteSeekCloser interface {
|
||||
|
|
10
audio/format/guess/flac_decode_test.go
Normal file
10
audio/format/guess/flac_decode_test.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
//go:build !disable_format_alac
|
||||
// +build !disable_format_alac
|
||||
|
||||
package guess
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFLACDecode(t *testing.T) {
|
||||
DoTest(".flac", t)
|
||||
}
|
|
@ -4,26 +4,91 @@ import (
|
|||
"errors"
|
||||
"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/aac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/alac"
|
||||
"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/audio/format/vorbis"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var knownFormats = []format.Format{
|
||||
aac.NewFormat(),
|
||||
flac.NewFormat(),
|
||||
mp3.NewFormat(),
|
||||
opus.NewFormat(),
|
||||
tta.NewFormat(),
|
||||
vorbis.NewFormat(),
|
||||
alac.NewFormat(),
|
||||
type NullFormat struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (f NullFormat) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f NullFormat) Description() string {
|
||||
return "null"
|
||||
}
|
||||
|
||||
func (f NullFormat) Open(io.ReadSeekCloser) (audio.Source, error) {
|
||||
return audio.Source{}, errors.New("null format")
|
||||
}
|
||||
|
||||
func (f NullFormat) OpenAnalyzer(io.ReadSeekCloser) (audio.Source, format.AnalyzerChannel, error) {
|
||||
return audio.Source{}, nil, errors.New("null format")
|
||||
}
|
||||
|
||||
func (f NullFormat) Encode(audio.Source, io.WriteCloser, map[string]interface{}) error {
|
||||
return errors.New("null format")
|
||||
}
|
||||
|
||||
func (f NullFormat) Identify([]byte, string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var knownFormats []format.Format
|
||||
|
||||
func AddFormat(format format.Format) {
|
||||
knownFormats = append(knownFormats, format)
|
||||
}
|
||||
|
||||
func GetFormat(name string) format.Format {
|
||||
for _, formatEntry := range knownFormats {
|
||||
if formatEntry.Name() == name {
|
||||
return formatEntry
|
||||
}
|
||||
}
|
||||
|
||||
return NullFormat{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDecoder(name string) format.Decoder {
|
||||
for _, formatEntry := range knownFormats {
|
||||
if decoder, ok := formatEntry.(format.Decoder); ok && formatEntry.Name() == name {
|
||||
return decoder
|
||||
}
|
||||
}
|
||||
|
||||
return NullFormat{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func GetAnalyzerDecoder(name string) format.AnalyzerDecoder {
|
||||
for _, formatEntry := range knownFormats {
|
||||
if decoder, ok := formatEntry.(format.AnalyzerDecoder); ok && formatEntry.Name() == name {
|
||||
return decoder
|
||||
}
|
||||
}
|
||||
|
||||
return NullFormat{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func GetEncoder(name string) format.Encoder {
|
||||
for _, formatEntry := range knownFormats {
|
||||
if encoder, ok := formatEntry.(format.Encoder); ok && formatEntry.Name() == name {
|
||||
return encoder
|
||||
}
|
||||
}
|
||||
|
||||
return NullFormat{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDecoders(r io.ReadSeekCloser, pathName string) (decoders []format.Decoder, err error) {
|
||||
|
|
47
audio/format/guess/guess_test_utils.go
Normal file
47
audio/format/guess/guess_test_utils.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func DoTest(ext string, t *testing.T) {
|
||||
for _, location := range test.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()
|
||||
decoders, err := GetDecoders(fp, fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
source, err := Open(fp, decoders)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
audio/format/guess/init_aac.go
Normal file
12
audio/format/guess/init_aac.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !disable_format_aac
|
||||
// +build !disable_format_aac
|
||||
|
||||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/aac"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddFormat(aac.NewFormat())
|
||||
}
|
10
audio/format/guess/init_alac.go
Normal file
10
audio/format/guess/init_alac.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
//go:build !disable_format_alac
|
||||
// +build !disable_format_alac
|
||||
|
||||
package guess
|
||||
|
||||
import "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/alac"
|
||||
|
||||
func init() {
|
||||
AddFormat(alac.NewFormat())
|
||||
}
|
12
audio/format/guess/init_flac.go
Normal file
12
audio/format/guess/init_flac.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !disable_format_flac
|
||||
// +build !disable_format_flac
|
||||
|
||||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddFormat(flac.NewFormat())
|
||||
}
|
12
audio/format/guess/init_mp3.go
Normal file
12
audio/format/guess/init_mp3.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !disable_format_mp3
|
||||
// +build !disable_format_mp3
|
||||
|
||||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/mp3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddFormat(mp3.NewFormat())
|
||||
}
|
12
audio/format/guess/init_opus.go
Normal file
12
audio/format/guess/init_opus.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !disable_format_opus
|
||||
// +build !disable_format_opus
|
||||
|
||||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddFormat(opus.NewFormat())
|
||||
}
|
12
audio/format/guess/init_tta.go
Normal file
12
audio/format/guess/init_tta.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !disable_format_tta && !disable_codec_tta
|
||||
// +build !disable_format_tta,!disable_codec_tta
|
||||
|
||||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/tta"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddFormat(tta.NewFormat())
|
||||
}
|
12
audio/format/guess/init_vorbis.go
Normal file
12
audio/format/guess/init_vorbis.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
//go:build !disable_format_vorbis
|
||||
// +build !disable_format_vorbis
|
||||
|
||||
package guess
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/vorbis"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddFormat(vorbis.NewFormat())
|
||||
}
|
10
audio/format/guess/mp3_decode_test.go
Normal file
10
audio/format/guess/mp3_decode_test.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
//go:build !disable_format_mp3
|
||||
// +build !disable_format_mp3
|
||||
|
||||
package guess
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMP3Decode(t *testing.T) {
|
||||
DoTest(".mp3", t)
|
||||
}
|
10
audio/format/guess/opus_decode_test.go
Normal file
10
audio/format/guess/opus_decode_test.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
//go:build !disable_format_opus
|
||||
// +build !disable_format_opus
|
||||
|
||||
package guess
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOpusDecode(t *testing.T) {
|
||||
DoTest(".opus", t)
|
||||
}
|
10
audio/format/guess/tta_decode_test.go
Normal file
10
audio/format/guess/tta_decode_test.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
//go:build !disable_format_tta && !disable_codec_tta
|
||||
// +build !disable_format_tta,!disable_codec_tta
|
||||
|
||||
package guess
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTTADecode(t *testing.T) {
|
||||
DoTest(".tta", t)
|
||||
}
|
10
audio/format/guess/vorbis_decode_test.go
Normal file
10
audio/format/guess/vorbis_decode_test.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
//go:build !disable_format_vorbis
|
||||
// +build !disable_format_vorbis
|
||||
|
||||
package guess
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVorbisDecode(t *testing.T) {
|
||||
DoTest(".vorbis", t)
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
//go:build !disable_format_mp3
|
||||
// +build !disable_format_mp3
|
||||
|
||||
package mp3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/cgo"
|
||||
mp3Lib "github.com/kvark128/minimp3"
|
||||
"github.com/viert/go-lame"
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
@ -21,6 +21,10 @@ func NewFormat() Format {
|
|||
return Format{}
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
||||
decoder := mp3Lib.NewDecoder(r)
|
||||
|
||||
|
@ -55,102 +59,6 @@ func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error {
|
||||
var vbr = true
|
||||
var bitrate = 0
|
||||
|
||||
if options != nil {
|
||||
var val interface{}
|
||||
var ok bool
|
||||
var intVal int
|
||||
var int64Val int64
|
||||
var strVal string
|
||||
if val, ok = options["bitrate"]; ok {
|
||||
if strVal, ok = val.(string); ok {
|
||||
switch strVal {
|
||||
case "v0":
|
||||
vbr = true
|
||||
bitrate = 0
|
||||
case "v1":
|
||||
vbr = true
|
||||
bitrate = 1
|
||||
case "v2":
|
||||
vbr = true
|
||||
bitrate = 2
|
||||
case "v3":
|
||||
vbr = true
|
||||
bitrate = 3
|
||||
case "320k":
|
||||
vbr = false
|
||||
bitrate = 320
|
||||
case "256k":
|
||||
vbr = false
|
||||
bitrate = 256
|
||||
case "192k":
|
||||
vbr = false
|
||||
bitrate = 192
|
||||
case "128k":
|
||||
vbr = false
|
||||
bitrate = 128
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown setting bitrate=%s", strVal)
|
||||
}
|
||||
} else if intVal, ok = val.(int); ok {
|
||||
vbr = false
|
||||
bitrate = intVal
|
||||
} else if int64Val, ok = val.(int64); ok {
|
||||
vbr = false
|
||||
bitrate = int(int64Val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encoder := lame.NewEncoder(writer)
|
||||
|
||||
if encoder == nil {
|
||||
return errors.New("could not create encoder")
|
||||
}
|
||||
defer encoder.Close()
|
||||
if err := encoder.SetNumChannels(source.Channels); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.SetInSamplerate(source.SampleRate); err != nil {
|
||||
return err
|
||||
}
|
||||
encoder.SetWriteID3TagAutomatic(false)
|
||||
|
||||
if vbr {
|
||||
if err := encoder.SetVBR(lame.VBRDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.SetVBRQuality(float64(bitrate)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := encoder.SetVBR(lame.VBROff); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.SetBrate(bitrate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for block := range source.Blocks {
|
||||
samples := cgo.Float32ToInt16(block)
|
||||
|
||||
_, err := encoder.Write(unsafe.Slice((*byte)(unsafe.Pointer(&samples[0])), len(samples)*2))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
|
||||
return 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"
|
||||
|
|
114
audio/format/mp3/mp3_lame.go
Normal file
114
audio/format/mp3/mp3_lame.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
//go:build !disable_format_mp3 && !disable_codec_lame
|
||||
// +build !disable_format_mp3,!disable_codec_lame
|
||||
|
||||
package mp3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/cgo"
|
||||
"github.com/viert/go-lame"
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "minimp3 / LAME (go-lame)"
|
||||
}
|
||||
|
||||
func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error {
|
||||
var vbr = true
|
||||
var bitrate = 0
|
||||
|
||||
if options != nil {
|
||||
var val interface{}
|
||||
var ok bool
|
||||
var intVal int
|
||||
var int64Val int64
|
||||
var strVal string
|
||||
if val, ok = options["bitrate"]; ok {
|
||||
if strVal, ok = val.(string); ok {
|
||||
switch strVal {
|
||||
case "v0":
|
||||
vbr = true
|
||||
bitrate = 0
|
||||
case "v1":
|
||||
vbr = true
|
||||
bitrate = 1
|
||||
case "v2":
|
||||
vbr = true
|
||||
bitrate = 2
|
||||
case "v3":
|
||||
vbr = true
|
||||
bitrate = 3
|
||||
case "320k":
|
||||
vbr = false
|
||||
bitrate = 320
|
||||
case "256k":
|
||||
vbr = false
|
||||
bitrate = 256
|
||||
case "192k":
|
||||
vbr = false
|
||||
bitrate = 192
|
||||
case "128k":
|
||||
vbr = false
|
||||
bitrate = 128
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown setting bitrate=%s", strVal)
|
||||
}
|
||||
} else if intVal, ok = val.(int); ok {
|
||||
vbr = false
|
||||
bitrate = intVal
|
||||
} else if int64Val, ok = val.(int64); ok {
|
||||
vbr = false
|
||||
bitrate = int(int64Val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encoder := lame.NewEncoder(writer)
|
||||
|
||||
if encoder == nil {
|
||||
return errors.New("could not create encoder")
|
||||
}
|
||||
defer encoder.Close()
|
||||
if err := encoder.SetNumChannels(source.Channels); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.SetInSamplerate(source.SampleRate); err != nil {
|
||||
return err
|
||||
}
|
||||
encoder.SetWriteID3TagAutomatic(false)
|
||||
|
||||
if vbr {
|
||||
if err := encoder.SetVBR(lame.VBRDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.SetVBRQuality(float64(bitrate)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := encoder.SetVBR(lame.VBROff); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.SetBrate(bitrate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for block := range source.Blocks {
|
||||
samples := cgo.Float32ToInt16(block)
|
||||
|
||||
_, err := encoder.Write(unsafe.Slice((*byte)(unsafe.Pointer(&samples[0])), len(samples)*2))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
encoder.Flush()
|
||||
|
||||
return nil
|
||||
}
|
44
audio/format/mp3/mp3_lame_test.go
Normal file
44
audio/format/mp3/mp3_lame_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
//go:build !disable_format_mp3 && !disable_codec_lame
|
||||
// +build !disable_format_mp3,!disable_codec_lame
|
||||
|
||||
package mp3
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeMP3(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.mp3")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
err = NewFormat().Encode(source, target, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
8
audio/format/mp3/mp3_nolame.go
Normal file
8
audio/format/mp3/mp3_nolame.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
//go:build !disable_format_mp3 && disable_codec_lame
|
||||
// +build !disable_format_mp3,disable_codec_lame
|
||||
|
||||
package mp3
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "minimp3 / LAME"
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
//go:build !disable_format_opus
|
||||
// +build !disable_format_opus
|
||||
|
||||
package opus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/filter"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/cgo"
|
||||
libopus "git.gammaspectra.live/S.O.N.G/go-pus"
|
||||
"io"
|
||||
|
@ -18,6 +22,14 @@ func NewFormat() Format {
|
|||
return Format{}
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "opus"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "libopus (go-pus)"
|
||||
}
|
||||
|
||||
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
||||
stream, err := libopus.NewStream(r)
|
||||
if err != nil {
|
||||
|
@ -54,12 +66,9 @@ 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 {
|
||||
if source.SampleRate != FixedSampleRate {
|
||||
//TODO: maybe not needed
|
||||
return fmt.Errorf("invalid source SampleRate: expected %d, got %d", FixedSampleRate, source.SampleRate)
|
||||
}
|
||||
|
||||
var bitrate = 0
|
||||
var resampleQuality = filter.Linear
|
||||
|
||||
if options != nil {
|
||||
var val interface{}
|
||||
|
@ -88,8 +97,19 @@ func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[s
|
|||
bitrate = int(int64Val)
|
||||
}
|
||||
}
|
||||
if val, ok = options["resampleQuality"]; ok {
|
||||
if typeVal, ok := val.(filter.ResampleQuality); ok {
|
||||
resampleQuality = typeVal
|
||||
} else if intVal, ok = val.(int); ok {
|
||||
resampleQuality = filter.ResampleQuality(intVal)
|
||||
} else if int64Val, ok = val.(int64); ok {
|
||||
resampleQuality = filter.ResampleQuality(int64Val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source = filter.NewResampleFilter(FixedSampleRate, resampleQuality, 0).Process(source)
|
||||
|
||||
encoder, err := libopus.NewEncoder(FixedSampleRate, source.Channels, libopus.AppAudio, writer)
|
||||
|
||||
if err != nil {
|
||||
|
|
47
audio/format/opus/opus_test.go
Normal file
47
audio/format/opus/opus_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
//go:build !disable_format_opus
|
||||
// +build !disable_format_opus
|
||||
|
||||
package opus
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeOpus(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.opus")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
options := make(map[string]interface{})
|
||||
options["bitrate"] = "256k"
|
||||
|
||||
err = NewFormat().Encode(source, target, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//go:build !disable_format_tta && !disable_codec_tta
|
||||
// +build !disable_format_tta,!disable_codec_tta
|
||||
|
||||
package tta
|
||||
|
||||
import (
|
||||
|
@ -17,6 +20,14 @@ const BlockSize = 1024 * 128
|
|||
type Format struct {
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "tta"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "go-tta"
|
||||
}
|
||||
|
||||
func NewFormat() Format {
|
||||
return Format{}
|
||||
}
|
||||
|
|
44
audio/format/tta/tta_test.go
Normal file
44
audio/format/tta/tta_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
//go:build !disable_format_tta && !disable_codec_tta
|
||||
// +build !disable_format_tta,!disable_codec_tta
|
||||
|
||||
package tta
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeTTA(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
|
||||
}
|
||||
|
||||
target, err := os.CreateTemp("/tmp", "encode_test_*.tta")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
name := target.Name()
|
||||
target.Close()
|
||||
os.Remove(name)
|
||||
}()
|
||||
|
||||
err = NewFormat().Encode(source, target, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//go:build !disable_format_vorbis
|
||||
// +build !disable_format_vorbis
|
||||
|
||||
package vorbis
|
||||
|
||||
import (
|
||||
|
@ -14,6 +17,14 @@ func NewFormat() Format {
|
|||
return Format{}
|
||||
}
|
||||
|
||||
func (f Format) Name() string {
|
||||
return "vorbis"
|
||||
}
|
||||
|
||||
func (f Format) Description() string {
|
||||
return "oggvorvis"
|
||||
}
|
||||
|
||||
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
|
||||
reader, err := libvorbis.NewReader(r)
|
||||
if err != nil {
|
||||
|
|
57
audio/packetizer/adts_test.go
Normal file
57
audio/packetizer/adts_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
//go:build !disable_format_aac
|
||||
// +build !disable_format_aac
|
||||
|
||||
package packetizer
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/aac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketizeADTS(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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = aac.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := NewAdtsPacketizer(reader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
}
|
||||
if packetCount != 16920 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 16920)
|
||||
}
|
||||
if packetBytes != 6436973 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 6436973)
|
||||
}
|
||||
}
|
66
audio/packetizer/flac_test.go
Normal file
66
audio/packetizer/flac_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
//go:build !disable_format_flac
|
||||
// +build !disable_format_flac
|
||||
|
||||
package packetizer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketizeFLAC(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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
shaHasher := sha256.New()
|
||||
shaPacketHasher := sha256.New()
|
||||
encodeReader := io.TeeReader(reader, shaHasher)
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = flac.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := NewFLACPacketizer(encodeReader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
shaPacketHasher.Write(packet.GetData())
|
||||
}
|
||||
if packetCount != 4231 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 4231)
|
||||
}
|
||||
if packetBytes != 51513533 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 51513533)
|
||||
}
|
||||
if bytes.Compare(shaHasher.Sum([]byte{}), shaPacketHasher.Sum([]byte{})) != 0 {
|
||||
t.Errorf("Mismatch on byte output %X != %X", shaPacketHasher.Sum([]byte{}), shaHasher.Sum([]byte{}))
|
||||
}
|
||||
}
|
57
audio/packetizer/mp3_test.go
Normal file
57
audio/packetizer/mp3_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
//go:build !disable_format_mp3 && !disable_codec_lame
|
||||
// +build !disable_format_mp3,!disable_codec_lame
|
||||
|
||||
package packetizer
|
||||
|
||||
import (
|
||||
"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/test"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketizeMP3(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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = mp3.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := NewMp3Packetizer(reader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
}
|
||||
if packetCount != 15040 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 15040)
|
||||
}
|
||||
if packetBytes != 13901254 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 13901254)
|
||||
}
|
||||
}
|
57
audio/packetizer/ogg_test.go
Normal file
57
audio/packetizer/ogg_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
//go:build !disable_format_opus
|
||||
// +build !disable_format_opus
|
||||
|
||||
package packetizer
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketizeOgg(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
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err = opus.NewFormat().Encode(source, writer, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
pipePacketizer := NewOggPacketizer(reader)
|
||||
packetCount := 0
|
||||
packetBytes := 0
|
||||
for {
|
||||
packet := pipePacketizer.GetPacket()
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
packetCount++
|
||||
packetBytes += len(packet.GetData())
|
||||
}
|
||||
if packetCount != 395 {
|
||||
t.Errorf("Wrong Packet Count %d != %d", packetCount, 395)
|
||||
}
|
||||
if packetBytes != 5962688 {
|
||||
t.Errorf("Wrong Packet Bytes %d != %d", packetBytes, 5962688)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package audio
|
||||
package queue
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/filter"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -10,7 +12,7 @@ type QueueIdentifier int
|
|||
|
||||
type QueueEntry struct {
|
||||
Identifier QueueIdentifier
|
||||
Source Source
|
||||
Source audio.Source
|
||||
ReadSamples int
|
||||
cancel chan bool
|
||||
StartCallback func(q *Queue, entry *QueueEntry)
|
||||
|
@ -20,7 +22,7 @@ type QueueEntry struct {
|
|||
|
||||
type Queue struct {
|
||||
queue []*QueueEntry
|
||||
output Source
|
||||
output audio.Source
|
||||
interrupt chan bool
|
||||
interruptDepth int64
|
||||
closed bool
|
||||
|
@ -36,7 +38,7 @@ func NewQueue(sampleRate, channels int) *Queue {
|
|||
|
||||
q := &Queue{
|
||||
interrupt: make(chan bool, 1),
|
||||
output: Source{
|
||||
output: audio.Source{
|
||||
SampleRate: sampleRate,
|
||||
Channels: channels,
|
||||
Blocks: make(chan []float32),
|
||||
|
@ -46,9 +48,9 @@ func NewQueue(sampleRate, channels int) *Queue {
|
|||
return q
|
||||
}
|
||||
|
||||
func (q *Queue) spliceSources(input Source) (output Source, cancel chan bool) {
|
||||
func (q *Queue) spliceSources(input audio.Source) (output audio.Source, cancel chan bool) {
|
||||
cancel = make(chan bool, 1)
|
||||
output = Source{
|
||||
output = audio.Source{
|
||||
Channels: input.Channels,
|
||||
SampleRate: input.SampleRate,
|
||||
Blocks: make(chan []float32),
|
||||
|
@ -71,7 +73,7 @@ func (q *Queue) spliceSources(input Source) (output Source, cancel chan bool) {
|
|||
}
|
||||
|
||||
//sink remaining
|
||||
go NewNullSink().Process(input)
|
||||
go audio.NewNullSink().Process(input)
|
||||
}()
|
||||
|
||||
return
|
||||
|
@ -129,15 +131,15 @@ func (q *Queue) start() {
|
|||
}()
|
||||
|
||||
}
|
||||
func (q *Queue) getFilterChain(source Source) Source {
|
||||
func (q *Queue) getFilterChain(source audio.Source) audio.Source {
|
||||
if q.GetChannels() == 1 {
|
||||
return NewFilterChain(source, MonoFilter{}, NewResampleFilter(q.GetSampleRate(), BandlimitedFastest, 0))
|
||||
return filter.NewFilterChain(source, filter.MonoFilter{}, filter.NewResampleFilter(q.GetSampleRate(), filter.BandlimitedFastest, 0))
|
||||
} else {
|
||||
return NewFilterChain(source, StereoFilter{}, NewResampleFilter(q.GetSampleRate(), BandlimitedFastest, 0))
|
||||
return filter.NewFilterChain(source, filter.StereoFilter{}, filter.NewResampleFilter(q.GetSampleRate(), filter.BandlimitedFastest, 0))
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) AddHead(source Source, startCallback func(q *Queue, entry *QueueEntry), endCallback func(q *Queue, entry *QueueEntry), removeCallback func(q *Queue, entry *QueueEntry)) (identifier QueueIdentifier) {
|
||||
func (q *Queue) AddHead(source audio.Source, startCallback func(q *Queue, entry *QueueEntry), endCallback func(q *Queue, entry *QueueEntry), removeCallback func(q *Queue, entry *QueueEntry)) (identifier QueueIdentifier) {
|
||||
q.lock.Lock()
|
||||
|
||||
splicedOutput, cancel := q.spliceSources(source)
|
||||
|
@ -168,7 +170,7 @@ func (q *Queue) AddHead(source Source, startCallback func(q *Queue, entry *Queue
|
|||
return
|
||||
}
|
||||
|
||||
func (q *Queue) AddTail(source Source, startCallback func(q *Queue, entry *QueueEntry), endCallback func(q *Queue, entry *QueueEntry), removeCallback func(q *Queue, entry *QueueEntry)) (identifier QueueIdentifier) {
|
||||
func (q *Queue) AddTail(source audio.Source, startCallback func(q *Queue, entry *QueueEntry), endCallback func(q *Queue, entry *QueueEntry), removeCallback func(q *Queue, entry *QueueEntry)) (identifier QueueIdentifier) {
|
||||
q.lock.Lock()
|
||||
|
||||
splicedOutput, cancel := q.spliceSources(source)
|
||||
|
@ -201,7 +203,7 @@ func (q *Queue) Remove(identifier QueueIdentifier) bool {
|
|||
if e.Identifier == identifier {
|
||||
q.sendInterrupt()
|
||||
e.cancel <- true
|
||||
go NewNullSink().Process(e.Source)
|
||||
go audio.NewNullSink().Process(e.Source)
|
||||
//delete entry
|
||||
q.queue = append(q.queue[:i], q.queue[i+1:]...)
|
||||
q.lock.Unlock()
|
||||
|
@ -272,7 +274,7 @@ func (q *Queue) GetQueue() (entries []*QueueEntry) {
|
|||
return
|
||||
}
|
||||
|
||||
func (q *Queue) GetSource() Source {
|
||||
func (q *Queue) GetSource() audio.Source {
|
||||
return q.output
|
||||
}
|
||||
|
74
audio/queue/queue_test.go
Normal file
74
audio/queue/queue_test.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package queue
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const sampleRate = 44100
|
||||
|
||||
q := NewQueue(sampleRate, 2)
|
||||
flacFormat := guess.GetDecoder("flac")
|
||||
|
||||
for _, location := range test.TestSampleLocations {
|
||||
entries, err := os.ReadDir(location)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
for _, f := range entries {
|
||||
if path.Ext(f.Name()) == ".flac" {
|
||||
fullPath := path.Join(location, f.Name())
|
||||
fp, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
source, err := flacFormat.Open(fp)
|
||||
if err != nil {
|
||||
fp.Close()
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
q.AddTail(source, func(q *Queue, entry *QueueEntry) {
|
||||
t.Logf("Started playback of %d %s\n", entry.Identifier, fullPath)
|
||||
}, func(q *Queue, entry *QueueEntry) {
|
||||
t.Logf("Finished playback of %d %s: output %d samples\n", entry.Identifier, fullPath, entry.ReadSamples)
|
||||
}, func(q *Queue, entry *QueueEntry) {
|
||||
fp.Close()
|
||||
if q.GetQueueSize() == 0 {
|
||||
t.Log("Finished playback, closing\n")
|
||||
q.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Decode
|
||||
result := q.GetSource()
|
||||
|
||||
sink := audio.NewForwardSink(audio.NewNullSink())
|
||||
sink.Process(result)
|
||||
|
||||
q.Wait()
|
||||
|
||||
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 != 470828559 {
|
||||
t.Errorf("Wrong Sample Count %d != %d", sink.SamplesRead, 470828559)
|
||||
}
|
||||
}
|
|
@ -2,18 +2,19 @@ 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
|
||||
func NewReplayGainFilter(gain, peak, preAmp float64) audio.VolumeFilter {
|
||||
func NewReplayGainFilter(gain, peak, preAmp float64) filter.VolumeFilter {
|
||||
volume := math.Pow(10, (gain+preAmp)/20)
|
||||
//prevent clipping
|
||||
volume = math.Min(volume, 1/peak)
|
||||
|
||||
return audio.NewVolumeFilter(float32(volume))
|
||||
return filter.NewVolumeFilter(float32(volume))
|
||||
}
|
||||
|
||||
// NormalizationFilter Normalizes running audio source
|
||||
|
|
146
audio/replaygain/normalization_test.go
Normal file
146
audio/replaygain/normalization_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package replaygain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplayGainNormalization24(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(test.TestSingleSample24)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, err := guess.GetDecoder("flac").Open(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
source = NewNormalizationFilter(5).Process(source)
|
||||
|
||||
gain, peak, err := GetTrackReplayGain(source)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-0.033242 dB, 0.627992"
|
||||
result := fmt.Sprintf("%f dB, %f", gain, peak)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayGain24(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(test.TestSingleSample24)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, err := guess.GetDecoder("flac").Open(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
gain, peak, err := GetTrackReplayGain(source)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-11.590850 dB, 1.000000"
|
||||
result := fmt.Sprintf("%f dB, %f", gain, peak)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayGain16(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(test.TestSingleSample16)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, err := guess.GetDecoder("flac").Open(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
gain, peak, err := GetTrackReplayGain(source)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-7.748202 dB, 0.942137"
|
||||
result := fmt.Sprintf("%f dB, %f", gain, peak)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbumReplayGain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
location := test.TestSampleLocations[1]
|
||||
entries, err := os.ReadDir(location)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var sources []audio.Source
|
||||
for _, f := range entries {
|
||||
if path.Ext(f.Name()) == ".flac" {
|
||||
fullPath := path.Join(location, f.Name())
|
||||
|
||||
fp, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
decoders, err := guess.GetDecoders(fp, fullPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
source, err := guess.Open(fp, decoders)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
albumGain, albumPeak, trackGains, trackPeaks, err := GetAlbumReplayGain(sources)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expect := "-8.539780 dB, 0.970916, [-7.748202 -9.020742 -9.192557 -8.015611 -8.303189 -9.071835 -9.356090 -8.114885 -7.760444 -7.317174 -8.339798 -8.906274 -8.001073], [0.942137 0.970916 0.970916 0.970916 0.942106 0.970916 0.970916 0.965606 0.970916 0.970916 0.942137 0.970916 0.942137]"
|
||||
result := fmt.Sprintf("%f dB, %f, %f, %f", albumGain, albumPeak, trackGains, trackPeaks)
|
||||
if result != expect {
|
||||
t.Errorf("Wrong ReplayGain %s != %s", result, expect)
|
||||
}
|
||||
}
|
4
go.mod
4
go.mod
|
@ -3,7 +3,7 @@ module git.gammaspectra.live/S.O.N.G/Kirika
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f
|
||||
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94
|
||||
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba
|
||||
|
@ -11,6 +11,7 @@ require (
|
|||
git.gammaspectra.live/S.O.N.G/goflac v0.0.0-20220417181802-3057bde44c07
|
||||
github.com/dh1tw/gosamplerate v0.1.2
|
||||
github.com/edgeware/mp4ff v0.27.0
|
||||
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565
|
||||
github.com/jfreymuth/oggvorbis v1.0.3
|
||||
github.com/kvark128/minimp3 v0.0.0-20220408223524-dd428325fce7
|
||||
github.com/mewkiz/flac v1.0.7
|
||||
|
@ -23,4 +24,5 @@ require (
|
|||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
github.com/klauspost/cpuid v1.3.1 // indirect
|
||||
github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect
|
||||
github.com/youpy/go-wav v0.3.2 // indirect
|
||||
)
|
||||
|
|
20
go.sum
20
go.sum
|
@ -1,5 +1,7 @@
|
|||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1 h1:D1VyacBGUBfvFD4Fq2eO6RQ5eZPUdC2YsIv9gXun9aQ=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f h1:CxN7zlk5FdAieyRKQSbwBGBsvQ2cDF8JVCODZpzcRkA=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
|
||||
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255 h1:BWRx2ZFyhp5+rsXhdDZtk5Gld+L44lxlN9ASqB9Oj0M=
|
||||
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255/go.mod h1:5H4eVW9uknpn8REFr+C3ejhvXdncgm/pbGqKGC43gFY=
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94 h1:gD6lOyQwwuyJilwbLC8lAEWpUSZvndJsUGwxxDatCAE=
|
||||
|
@ -12,15 +14,19 @@ git.gammaspectra.live/S.O.N.G/goflac v0.0.0-20220417181802-3057bde44c07 h1:7YxnU
|
|||
git.gammaspectra.live/S.O.N.G/goflac v0.0.0-20220417181802-3057bde44c07/go.mod h1:/po1QgOh3xynbvi4sxdY6Iw8m5WPJfGGmry2boZD8fs=
|
||||
github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 h1:LjPYdzoFSAJ5Tr/ElL8kzTJghXgpnOjJVbgd1UvZB1o=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/edgeware/mp4ff v0.27.0 h1:Lv773H6c4pUt3Zj9z44TOICv6fjd6gKzi1sVl8GbcYE=
|
||||
github.com/edgeware/mp4ff v0.27.0/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
|
||||
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565 h1:5cGj3eU0uEzpHDFwqmcKc6zM1+HNvzPIR6ZY7T1/MsA=
|
||||
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565/go.mod h1:Tymn6PFuf0SYIYLOWIZWtatGOmi/5FLUNawJt9OgxJw=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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=
|
||||
|
@ -41,10 +47,24 @@ github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8
|
|||
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/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=
|
||||
github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081 h1:Qo/HswJzVywl0podyXMD62HIohsj/Ij2oXbD26aUIxM=
|
||||
github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081/go.mod h1:ExwZtltybPz8zLO8c2lKRfpPk1HAxhrkp038QIBs+yg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d h1:LptdD7GTUZeklomtW5vZ1AHwBvDBUCZ2Ftpaz7uEI7g=
|
||||
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d/go.mod h1:EqTcYM7y4JlSfeTI47pmNu3EZQuCuLQefsQyg1Imlz8=
|
||||
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
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/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
|
|
108
hasher/hasher_test.go
Normal file
108
hasher/hasher_test.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package hasher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHasher24(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(test.TestSingleSample24)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, analyzerChannel, err := guess.GetAnalyzerDecoder("flac").OpenAnalyzer(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
chans := analyzerChannel.Split(2)
|
||||
|
||||
crc32 := NewHasher(chans[0], HashtypeCrc32)
|
||||
sha256 := NewHasher(chans[1], 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) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(test.TestSingleSample16)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, analyzerChannel, err := guess.GetAnalyzerDecoder("flac").OpenAnalyzer(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
channels := analyzerChannel.Split(4)
|
||||
cueToolsCrc32 := NewHasher(channels[0].SkipStartSamples(Int16SamplesPerSector*10), HashtypeCrc32)
|
||||
arChannels := channels[1].SkipStartSamples(Int16SamplesPerSector*5 - 1).Split(2)
|
||||
accurateRipV1 := NewHasher(arChannels[0], HashtypeAccurateRipV1Start)
|
||||
accurateRipV2 := NewHasher(arChannels[1], HashtypeAccurateRipV2Start)
|
||||
crc32 := NewHasher(channels[2], HashtypeCrc32)
|
||||
sha256Result := NewHasher(channels[3], HashtypeSha256)
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
cueToolsCrc32.Wait()
|
||||
accurateRipV1.Wait()
|
||||
accurateRipV2.Wait()
|
||||
crc32.Wait()
|
||||
sha256Result.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(sha256Result.GetResult(), expectSha256) != 0 {
|
||||
t.Errorf("Wrong SHA256 %X != %X", sha256Result.GetResult(), expectSha256)
|
||||
}
|
||||
|
||||
}
|
73
hasher/hasher_tta_test.go
Normal file
73
hasher/hasher_tta_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
//go:build !disable_format_tta && !disable_codec_tta
|
||||
// +build !disable_format_tta,!disable_codec_tta
|
||||
|
||||
package hasher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/test"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHasher16TTA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fp, err := os.Open(test.TestSingleSample16TTA)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
source, analyzerChannel, err := guess.GetAnalyzerDecoder("tta").OpenAnalyzer(fp)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
channels := analyzerChannel.Split(4)
|
||||
cueToolsCrc32 := NewHasher(channels[0].SkipStartSamples(Int16SamplesPerSector*10), HashtypeCrc32)
|
||||
arChannels := channels[1].SkipStartSamples(Int16SamplesPerSector*5 - 1).Split(2)
|
||||
accurateRipV1 := NewHasher(arChannels[0], HashtypeAccurateRipV1Start)
|
||||
accurateRipV2 := NewHasher(arChannels[1], HashtypeAccurateRipV2Start)
|
||||
crc32 := NewHasher(channels[2], HashtypeCrc32)
|
||||
sha256Result := NewHasher(channels[3], HashtypeSha256)
|
||||
|
||||
//Decode
|
||||
for range source.Blocks {
|
||||
|
||||
}
|
||||
cueToolsCrc32.Wait()
|
||||
accurateRipV1.Wait()
|
||||
accurateRipV2.Wait()
|
||||
crc32.Wait()
|
||||
sha256Result.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(sha256Result.GetResult(), expectSha256) != 0 {
|
||||
t.Errorf("Wrong SHA256 %X != %X", sha256Result.GetResult(), expectSha256)
|
||||
}
|
||||
|
||||
}
|
27
test/constants.go
Normal file
27
test/constants.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var TestSampleLocations = []string{
|
||||
"resources/samples/cYsmix - Haunted House/",
|
||||
"resources/samples/Babbe Music - RADIANT DANCEFLOOR/",
|
||||
}
|
||||
|
||||
func init() {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
// The ".." may change depending on you folder structure
|
||||
dir := path.Join(path.Dir(filename), "..")
|
||||
err := os.Chdir(dir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const TestSingleSample24 = "resources/samples/cYsmix - Haunted House/11. The Great Rigid Desert.flac"
|
||||
|
||||
const TestSingleSample16 = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.flac"
|
||||
const TestSingleSample16TTA = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.tta"
|
Loading…
Reference in a new issue