Improved guess package, split tests into individual files, added alternate aac encoder
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
DataHoarder 2022-04-22 14:06:01 +02:00
parent 5fc88e3a49
commit c2d2a52614
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
51 changed files with 1876 additions and 1193 deletions

View file

@ -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 ./...
...

View file

@ -1 +0,0 @@
package Kirika

View file

@ -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)
}
}

View file

@ -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. |

View file

@ -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,

View 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)
}
}

View file

@ -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

View 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
}
}

View 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"
}

View 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
}
}

View file

@ -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 {

View 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
}
}

View file

@ -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)

View 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
}
}

View file

@ -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 {

View 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)
}

View file

@ -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) {

View 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 {
}
})
}
}
}
}

View 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())
}

View 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())
}

View 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())
}

View 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())
}

View 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())
}

View 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())
}

View 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())
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View file

@ -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"

View 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
}

View 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
}
}

View 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"
}

View file

@ -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 {

View 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
}
}

View file

@ -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{}
}

View 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
}
}

View file

@ -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 {

View 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)
}
}

View 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{}))
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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
View 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)
}
}

View file

@ -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

View 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
View file

@ -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
View file

@ -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
View 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
View 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
View 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"