From 8d1dcf17212b8b9010d82df2dd43a4ae8400b892 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 17:10:15 +0200 Subject: [PATCH 1/8] Add test for flac.Seek() --- flac/decode_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/flac/decode_test.go b/flac/decode_test.go index f34bfe7..848a5bb 100644 --- a/flac/decode_test.go +++ b/flac/decode_test.go @@ -1,6 +1,8 @@ package flac_test import ( + "io" + "log" "os" "testing" @@ -8,6 +10,9 @@ import ( "github.com/gopxl/beep/v2/flac" "github.com/gopxl/beep/v2/internal/testtools" + "github.com/gopxl/beep/v2/wav" + + mewkiz_flac "github.com/mewkiz/flac" ) func TestDecoder_ReturnBehaviour(t *testing.T) { @@ -21,3 +26,78 @@ func TestDecoder_ReturnBehaviour(t *testing.T) { testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len()) } + +func TestDecoder_Seek(t *testing.T) { + flacFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.flac")) + assert.NoError(t, err) + defer flacFile.Close() + + // Use WAV file as reference + wavFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.wav")) + assert.NoError(t, err) + defer wavFile.Close() + + // Get the frame numbers from the FLAC files manually, so that we + // can explicitly test difficult Seek positions. + frameStarts, err := getFlacFrameStartPositions(flacFile) + assert.NoError(t, err) + _, err = flacFile.Seek(0, io.SeekStart) + assert.NoError(t, err) + + flacStream, _, err := flac.Decode(flacFile) + assert.NoError(t, err) + + wavStream, _, err := wav.Decode(wavFile) + assert.NoError(t, err) + + assert.Equal(t, wavStream.Len(), flacStream.Len()) + + // Test start of 2nd frame + seekPos := int(frameStarts[1]) + err = wavStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, wavStream.Position()) + err = flacStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, flacStream.Position()) + + // Test middle of 2nd frame + seekPos = (int(frameStarts[1]) + int(frameStarts[2])) / 2 + err = wavStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, wavStream.Position()) + err = flacStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, flacStream.Position()) + + // Test end of 2nd frame + seekPos = int(frameStarts[2]) - 1 + err = wavStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, wavStream.Position()) + err = flacStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, flacStream.Position()) +} + +func getFlacFrameStartPositions(r io.Reader) ([]uint64, error) { + stream, err := mewkiz_flac.New(r) + if err != nil { + log.Fatal(err) + } + defer stream.Close() + + var frameStarts []uint64 + for { + frame, err := stream.ParseNext() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + frameStarts = append(frameStarts, frame.SampleNumber()) + } + + return frameStarts, nil +} From 667a3ec897f9659d0a87fb23afcdb9acfbeb4344 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 17:10:36 +0200 Subject: [PATCH 2/8] Remove old comment --- flac/decode.go | 1 - 1 file changed, 1 deletion(-) diff --git a/flac/decode.go b/flac/decode.go index 886d761..a0f2bfb 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -151,7 +151,6 @@ func (d *decoder) Position() int { return d.pos } -// p represents flac sample num perhaps? func (d *decoder) Seek(p int) error { if !d.seekEnabled { return errors.New("flac.decoder.Seek: not enabled") From 37712ee0acdc9e1a051dcf91f3614f6f14c18157 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 17:15:21 +0200 Subject: [PATCH 3/8] Test the retrieved samples are correct after flac.Seek() --- flac/decode_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/flac/decode_test.go b/flac/decode_test.go index 848a5bb..848d581 100644 --- a/flac/decode_test.go +++ b/flac/decode_test.go @@ -61,6 +61,10 @@ func TestDecoder_Seek(t *testing.T) { assert.NoError(t, err) assert.Equal(t, seekPos, flacStream.Position()) + wavSamples := testtools.CollectNum(100, wavStream) + flacSamples := testtools.CollectNum(100, flacStream) + assert.Equal(t, wavSamples, flacSamples) + // Test middle of 2nd frame seekPos = (int(frameStarts[1]) + int(frameStarts[2])) / 2 err = wavStream.Seek(seekPos) @@ -70,6 +74,10 @@ func TestDecoder_Seek(t *testing.T) { assert.NoError(t, err) assert.Equal(t, seekPos, flacStream.Position()) + wavSamples = testtools.CollectNum(100, wavStream) + flacSamples = testtools.CollectNum(100, flacStream) + assert.Equal(t, wavSamples, flacSamples) + // Test end of 2nd frame seekPos = int(frameStarts[2]) - 1 err = wavStream.Seek(seekPos) @@ -78,6 +86,10 @@ func TestDecoder_Seek(t *testing.T) { err = flacStream.Seek(seekPos) assert.NoError(t, err) assert.Equal(t, seekPos, flacStream.Position()) + + wavSamples = testtools.CollectNum(100, wavStream) + flacSamples = testtools.CollectNum(100, flacStream) + assert.Equal(t, wavSamples, flacSamples) } func getFlacFrameStartPositions(r io.Reader) ([]uint64, error) { From 1a95ae10c14178b2b9b9aa2bc19909eabd63f539 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 18:16:28 +0200 Subject: [PATCH 4/8] Fix flac.Seek() --- flac/decode.go | 14 ++++++- flac/decode_test.go | 38 +++++++++++++++--- .../testdata/valid_44100hz_22050_samples.flac | Bin 17081 -> 0 bytes .../valid_44100hz_22050_samples_ffmpeg.flac | Bin 0 -> 19968 bytes internal/testtools/asserts.go | 22 ++++++++++ 5 files changed, 67 insertions(+), 7 deletions(-) delete mode 100644 internal/testdata/valid_44100hz_22050_samples.flac create mode 100644 internal/testdata/valid_44100hz_22050_samples_ffmpeg.flac diff --git a/flac/decode.go b/flac/decode.go index a0f2bfb..f65aeea 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -157,7 +157,19 @@ func (d *decoder) Seek(p int) error { } pos, err := d.stream.Seek(uint64(p)) - d.pos = int(pos) + if err != nil { + return errors.Wrap(err, "flac") + } + + toDiscard := p - int(pos) + err = d.refill() + if err != nil { + return err + } + d.buf = d.buf[toDiscard:] + + d.pos = p + return err } diff --git a/flac/decode_test.go b/flac/decode_test.go index 848d581..16a1699 100644 --- a/flac/decode_test.go +++ b/flac/decode_test.go @@ -16,7 +16,7 @@ import ( ) func TestDecoder_ReturnBehaviour(t *testing.T) { - f, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.flac")) + f, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) assert.NoError(t, err) defer f.Close() @@ -27,12 +27,38 @@ func TestDecoder_ReturnBehaviour(t *testing.T) { testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len()) } +func TestDecoder_Stream(t *testing.T) { + flacFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) + assert.NoError(t, err) + defer flacFile.Close() + + // Use WAV file as reference. Since both FLAC and WAV are lossless, comparing + // the samples should be possible (allowing for some floating point errors). + wavFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.wav")) + assert.NoError(t, err) + defer wavFile.Close() + + flacStream, _, err := flac.Decode(flacFile) + assert.NoError(t, err) + + wavStream, _, err := wav.Decode(wavFile) + assert.NoError(t, err) + + assert.Equal(t, wavStream.Len(), flacStream.Len()) + + wavSamples := testtools.Collect(wavStream) + flacSamples := testtools.Collect(flacStream) + + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) +} + func TestDecoder_Seek(t *testing.T) { - flacFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.flac")) + flacFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) assert.NoError(t, err) defer flacFile.Close() - // Use WAV file as reference + // Use WAV file as reference. Since both FLAC and WAV are lossless, comparing + // the samples should be possible (allowing for some floating point errors). wavFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.wav")) assert.NoError(t, err) defer wavFile.Close() @@ -63,7 +89,7 @@ func TestDecoder_Seek(t *testing.T) { wavSamples := testtools.CollectNum(100, wavStream) flacSamples := testtools.CollectNum(100, flacStream) - assert.Equal(t, wavSamples, flacSamples) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) // Test middle of 2nd frame seekPos = (int(frameStarts[1]) + int(frameStarts[2])) / 2 @@ -76,7 +102,7 @@ func TestDecoder_Seek(t *testing.T) { wavSamples = testtools.CollectNum(100, wavStream) flacSamples = testtools.CollectNum(100, flacStream) - assert.Equal(t, wavSamples, flacSamples) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) // Test end of 2nd frame seekPos = int(frameStarts[2]) - 1 @@ -89,7 +115,7 @@ func TestDecoder_Seek(t *testing.T) { wavSamples = testtools.CollectNum(100, wavStream) flacSamples = testtools.CollectNum(100, flacStream) - assert.Equal(t, wavSamples, flacSamples) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) } func getFlacFrameStartPositions(r io.Reader) ([]uint64, error) { diff --git a/internal/testdata/valid_44100hz_22050_samples.flac b/internal/testdata/valid_44100hz_22050_samples.flac deleted file mode 100644 index b6dd4315c1cfea0fad3fac8eb9e006ba804dd7d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17081 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

Kdiu58P{R&1c!8lJf<+8G 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 expected[i][0]+epsilon || + actual[i][1] < expected[i][1]-epsilon || actual[i][1] > expected[i][1]+epsilon { + equals = false + break + } + } + if !equals { + t.Errorf("the sample data isn't equal to the expected data") + } +} From d2771da28390c9fdf305720bc0024de554f23f52 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 19:45:26 +0200 Subject: [PATCH 5/8] Rewrite flac.Stream() to load the samples directly from the frame --- flac/decode.go | 133 ++++++++++++++++++++++---------------------- flac/decode_test.go | 24 +++++++- 2 files changed, 88 insertions(+), 69 deletions(-) diff --git a/flac/decode.go b/flac/decode.go index f65aeea..2c82619 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -5,6 +5,7 @@ import ( "io" "github.com/mewkiz/flac" + "github.com/mewkiz/flac/frame" "github.com/pkg/errors" "github.com/gopxl/beep/v2" @@ -32,10 +33,16 @@ func Decode(r io.Reader) (s beep.StreamSeekCloser, format beep.Format, err error } else { d.stream, err = flac.New(r) } + if err != nil { + return nil, beep.Format{}, errors.Wrap(err, "flac") + } + // Read the first frame + d.frame, err = d.stream.ParseNext() if err != nil { return nil, beep.Format{}, errors.Wrap(err, "flac") } + format = beep.Format{ SampleRate: beep.SampleRate(d.stream.Info.SampleRate), NumChannels: int(d.stream.Info.NChannels), @@ -47,96 +54,86 @@ func Decode(r io.Reader) (s beep.StreamSeekCloser, format beep.Format, err error type decoder struct { r io.Reader stream *flac.Stream - buf [][2]float64 - pos int + frame *frame.Frame + posInFrame int err error seekEnabled bool } func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { - if d.err != nil { + if d.err != nil || d.frame == nil { return 0, false } - // Copy samples from buffer. - j := 0 - for i := range samples { - if j >= len(d.buf) { - // refill buffer. - if err := d.refill(); err != nil { - d.pos += n + + for len(samples) > 0 { + samplesLeft := int(d.frame.BlockSize) - d.posInFrame + if samplesLeft <= 0 { + // Read next frame + var err error + d.frame, err = d.stream.ParseNext() + if err != nil { + d.frame = nil if err == io.EOF { return n, n > 0 } - d.err = err + d.err = errors.Wrap(err, "flac") return 0, false } - j = 0 + d.posInFrame = 0 + continue } - samples[i] = d.buf[j] - j++ - n++ + + toFill := min(samplesLeft, len(samples)) + d.decodeFrameRangeInto(d.frame, d.posInFrame, toFill, samples) + d.posInFrame += toFill + n += toFill + samples = samples[toFill:] } - d.buf = d.buf[j:] - d.pos += n + return n, true } -// refill decodes audio samples to fill the decode buffer. -func (d *decoder) refill() error { - // Empty buffer. - d.buf = d.buf[:0] - // Parse audio frame. - frame, err := d.stream.ParseNext() - if err != nil { - return err - } - // Expand buffer size if needed. - n := len(frame.Subframes[0].Samples) - if cap(d.buf) < n { - d.buf = make([][2]float64, n) - } else { - d.buf = d.buf[:n] - } - // Decode audio samples. +// decodeFrameRangeInto decodes the samples frame from the position `start` up to `start + num` +// and stores them in Beep's format into the provided slice `into`. +func (d *decoder) decodeFrameRangeInto(frame *frame.Frame, start, num int, into [][2]float64) { bps := d.stream.Info.BitsPerSample - nchannels := d.stream.Info.NChannels + numChannels := d.stream.Info.NChannels s := 1 << (bps - 1) q := 1 / float64(s) switch { - case bps == 8 && nchannels == 1: - for i := 0; i < n; i++ { - d.buf[i][0] = float64(int8(frame.Subframes[0].Samples[i])) * q - d.buf[i][1] = float64(int8(frame.Subframes[0].Samples[i])) * q + case bps == 8 && numChannels == 1: + for i := 0; i < num; i++ { + into[i][0] = float64(int8(frame.Subframes[0].Samples[start+i])) * q + into[i][1] = into[i][0] } - case bps == 16 && nchannels == 1: - for i := 0; i < n; i++ { - d.buf[i][0] = float64(int16(frame.Subframes[0].Samples[i])) * q - d.buf[i][1] = float64(int16(frame.Subframes[0].Samples[i])) * q + case bps == 16 && numChannels == 1: + for i := 0; i < num; i++ { + into[i][0] = float64(int16(frame.Subframes[0].Samples[start+i])) * q + into[i][1] = into[i][0] } - case bps == 24 && nchannels == 1: - for i := 0; i < n; i++ { - d.buf[i][0] = float64(int32(frame.Subframes[0].Samples[i])) * q - d.buf[i][1] = float64(int32(frame.Subframes[0].Samples[i])) * q + case bps == 24 && numChannels == 1: + for i := 0; i < num; i++ { + into[i][0] = float64(int32(frame.Subframes[0].Samples[start+i])) * q + into[i][1] = into[i][0] } - case bps == 8 && nchannels >= 2: - for i := 0; i < n; i++ { - d.buf[i][0] = float64(int8(frame.Subframes[0].Samples[i])) * q - d.buf[i][1] = float64(int8(frame.Subframes[1].Samples[i])) * q + case bps == 8 && numChannels >= 2: + for i := 0; i < num; i++ { + into[i][0] = float64(int8(frame.Subframes[0].Samples[start+i])) * q + into[i][1] = float64(int8(frame.Subframes[1].Samples[start+i])) * q } - case bps == 16 && nchannels >= 2: - for i := 0; i < n; i++ { - d.buf[i][0] = float64(int16(frame.Subframes[0].Samples[i])) * q - d.buf[i][1] = float64(int16(frame.Subframes[1].Samples[i])) * q + case bps == 16 && numChannels >= 2: + for i := 0; i < num; i++ { + into[i][0] = float64(int16(frame.Subframes[0].Samples[start+i])) * q + into[i][1] = float64(int16(frame.Subframes[1].Samples[start+i])) * q } - case bps == 24 && nchannels >= 2: - for i := 0; i < n; i++ { - d.buf[i][0] = float64(frame.Subframes[0].Samples[i]) * q - d.buf[i][1] = float64(frame.Subframes[1].Samples[i]) * q + case bps == 24 && numChannels >= 2: + for i := 0; i < num; i++ { + into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q + into[i][1] = float64(frame.Subframes[1].Samples[start+i]) * q } default: - panic(fmt.Errorf("support for %d bits-per-sample and %d channels combination not yet implemented", bps, nchannels)) + panic(fmt.Errorf("flac: support for %d bits-per-sample and %d channels combination not yet implemented", bps, numChannels)) } - return nil } func (d *decoder) Err() error { @@ -148,7 +145,7 @@ func (d *decoder) Len() int { } func (d *decoder) Position() int { - return d.pos + return int(d.frame.SampleNumber()) + d.posInFrame } func (d *decoder) Seek(p int) error { @@ -156,19 +153,19 @@ func (d *decoder) Seek(p int) error { return errors.New("flac.decoder.Seek: not enabled") } + // d.stream.Seek() doesn't seek to the exact position p, instead + // it seeks to the start of the frame p is in. The frame position + // is returned and stored in pos. pos, err := d.stream.Seek(uint64(p)) if err != nil { return errors.Wrap(err, "flac") } + d.posInFrame = p - int(pos) - toDiscard := p - int(pos) - err = d.refill() + d.frame, err = d.stream.ParseNext() if err != nil { - return err + return errors.Wrap(err, "flac") } - d.buf = d.buf[toDiscard:] - - d.pos = p return err } diff --git a/flac/decode_test.go b/flac/decode_test.go index 16a1699..c98c16f 100644 --- a/flac/decode_test.go +++ b/flac/decode_test.go @@ -1,6 +1,7 @@ package flac_test import ( + "bytes" "io" "log" "os" @@ -44,7 +45,8 @@ func TestDecoder_Stream(t *testing.T) { wavStream, _, err := wav.Decode(wavFile) assert.NoError(t, err) - assert.Equal(t, wavStream.Len(), flacStream.Len()) + assert.Equal(t, 22050, wavStream.Len()) + assert.Equal(t, 22050, flacStream.Len()) wavSamples := testtools.Collect(wavStream) flacSamples := testtools.Collect(flacStream) @@ -139,3 +141,23 @@ func getFlacFrameStartPositions(r io.Reader) ([]uint64, error) { return frameStarts, nil } + +func BenchmarkDecoder_Stream(b *testing.B) { + // Load the file into memory, so the disk performance doesn't impact the benchmark. + data, err := os.ReadFile(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) + assert.NoError(b, err) + + r := bytes.NewReader(data) + + b.Run("test", func(b *testing.B) { + s, _, err := flac.Decode(r) + assert.NoError(b, err) + + samples := testtools.Collect(s) + assert.Equal(b, 22050, len(samples)) + + // Reset for next run. + _, err = r.Seek(0, io.SeekStart) + assert.NoError(b, err) + }) +} From dd2c6893a0c6c730b45cceaf3bc204530fb9dc98 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 19:47:58 +0200 Subject: [PATCH 6/8] Remove unnecessary casts --- flac/decode.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flac/decode.go b/flac/decode.go index 2c82619..339f2e0 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -103,28 +103,28 @@ func (d *decoder) decodeFrameRangeInto(frame *frame.Frame, start, num int, into switch { case bps == 8 && numChannels == 1: for i := 0; i < num; i++ { - into[i][0] = float64(int8(frame.Subframes[0].Samples[start+i])) * q + into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q into[i][1] = into[i][0] } case bps == 16 && numChannels == 1: for i := 0; i < num; i++ { - into[i][0] = float64(int16(frame.Subframes[0].Samples[start+i])) * q + into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q into[i][1] = into[i][0] } case bps == 24 && numChannels == 1: for i := 0; i < num; i++ { - into[i][0] = float64(int32(frame.Subframes[0].Samples[start+i])) * q + into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q into[i][1] = into[i][0] } case bps == 8 && numChannels >= 2: for i := 0; i < num; i++ { - into[i][0] = float64(int8(frame.Subframes[0].Samples[start+i])) * q - into[i][1] = float64(int8(frame.Subframes[1].Samples[start+i])) * q + into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q + into[i][1] = float64(frame.Subframes[1].Samples[start+i]) * q } case bps == 16 && numChannels >= 2: for i := 0; i < num; i++ { - into[i][0] = float64(int16(frame.Subframes[0].Samples[start+i])) * q - into[i][1] = float64(int16(frame.Subframes[1].Samples[start+i])) * q + into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q + into[i][1] = float64(frame.Subframes[1].Samples[start+i]) * q } case bps == 24 && numChannels >= 2: for i := 0; i < num; i++ { From e66b2861a9576f748f4bbd33b2ae8be098644871 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sat, 3 Aug 2024 19:56:45 +0200 Subject: [PATCH 7/8] Simplify flac decoding --- flac/decode.go | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/flac/decode.go b/flac/decode.go index 339f2e0..5bdc5ca 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -1,7 +1,6 @@ package flac import ( - "fmt" "io" "github.com/mewkiz/flac" @@ -100,39 +99,21 @@ func (d *decoder) decodeFrameRangeInto(frame *frame.Frame, start, num int, into numChannels := d.stream.Info.NChannels s := 1 << (bps - 1) q := 1 / float64(s) - switch { - case bps == 8 && numChannels == 1: - for i := 0; i < num; i++ { - into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q - into[i][1] = into[i][0] - } - case bps == 16 && numChannels == 1: - for i := 0; i < num; i++ { - into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q - into[i][1] = into[i][0] - } - case bps == 24 && numChannels == 1: - for i := 0; i < num; i++ { - into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q - into[i][1] = into[i][0] - } - case bps == 8 && numChannels >= 2: - for i := 0; i < num; i++ { - into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q - into[i][1] = float64(frame.Subframes[1].Samples[start+i]) * q - } - case bps == 16 && numChannels >= 2: + + if numChannels == 1 { + samples1 := frame.Subframes[0].Samples[start:] for i := 0; i < num; i++ { - into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q - into[i][1] = float64(frame.Subframes[1].Samples[start+i]) * q + v := float64(samples1[i]) * q + into[i][0] = v + into[i][1] = v } - case bps == 24 && numChannels >= 2: + } else { + samples1 := frame.Subframes[0].Samples[start:] + samples2 := frame.Subframes[1].Samples[start:] for i := 0; i < num; i++ { - into[i][0] = float64(frame.Subframes[0].Samples[start+i]) * q - into[i][1] = float64(frame.Subframes[1].Samples[start+i]) * q + into[i][0] = float64(samples1[i]) * q + into[i][1] = float64(samples2[i]) * q } - default: - panic(fmt.Errorf("flac: support for %d bits-per-sample and %d channels combination not yet implemented", bps, numChannels)) } } From f595a79cf476b606b6460231746fb8bf36e94c84 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sun, 11 Aug 2024 19:14:00 +0200 Subject: [PATCH 8/8] Implement temporary fix for mewkiz/flac frame.Position() bug See https://github.com/mewkiz/flac/pull/73 --- flac/decode.go | 44 +++++++++++++++++++++++++++++++++++++++++++- flac/decode_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/flac/decode.go b/flac/decode.go index 5bdc5ca..8e86433 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -41,6 +41,7 @@ func Decode(r io.Reader) (s beep.StreamSeekCloser, format beep.Format, err error if err != nil { return nil, beep.Format{}, errors.Wrap(err, "flac") } + d.hasFixedBlockSize = d.frame.HasFixedBlockSize format = beep.Format{ SampleRate: beep.SampleRate(d.stream.Info.SampleRate), @@ -57,6 +58,8 @@ type decoder struct { posInFrame int err error seekEnabled bool + + hasFixedBlockSize bool } func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { @@ -126,6 +129,15 @@ func (d *decoder) Len() int { } func (d *decoder) Position() int { + if d.frame == nil { + return d.Len() + } + + // Temporary workaround until https://github.com/mewkiz/flac/pull/73 is resolved. + if d.hasFixedBlockSize { + return int(d.frame.Num)*int(d.stream.Info.BlockSizeMax) + d.posInFrame + } + return int(d.frame.SampleNumber()) + d.posInFrame } @@ -134,6 +146,36 @@ func (d *decoder) Seek(p int) error { return errors.New("flac.decoder.Seek: not enabled") } + // Temporary workaround until https://github.com/mewkiz/flac/pull/73 is resolved. + // frame.SampleNumber() doesn't work for the last frame of a fixed block size stream + // with the result that seeking to that frame doesn't work either. Therefore, if such + // a seek is requested, we seek to one of the frames before it and consume until the + // desired position is reached. + if d.hasFixedBlockSize { + lastFrameStartLowerBound := d.Len() - int(d.stream.Info.BlockSizeMax) + if p >= lastFrameStartLowerBound { + // Seek to & consume an earlier frame. + _, err := d.stream.Seek(uint64(lastFrameStartLowerBound - 1)) + if err != nil { + return errors.Wrap(err, "flac") + } + for { + d.frame, err = d.stream.ParseNext() + if err != nil { + return errors.Wrap(err, "flac") + } + // Calculate the frame start position manually, because this doesn't + // work for the last frame. + frameStart := d.frame.Num * uint64(d.stream.Info.BlockSizeMax) + if frameStart+uint64(d.frame.BlockSize) >= d.stream.Info.NSamples { + // Found the desired frame. + d.posInFrame = p - int(frameStart) + return nil + } + } + } + } + // d.stream.Seek() doesn't seek to the exact position p, instead // it seeks to the start of the frame p is in. The frame position // is returned and stored in pos. @@ -148,7 +190,7 @@ func (d *decoder) Seek(p int) error { return errors.Wrap(err, "flac") } - return err + return nil } func (d *decoder) Close() error { diff --git a/flac/decode_test.go b/flac/decode_test.go index c98c16f..b447d4c 100644 --- a/flac/decode_test.go +++ b/flac/decode_test.go @@ -118,6 +118,32 @@ func TestDecoder_Seek(t *testing.T) { wavSamples = testtools.CollectNum(100, wavStream) flacSamples = testtools.CollectNum(100, flacStream) testtools.AssertSamplesEqual(t, wavSamples, flacSamples) + + // Test end of stream. + seekPos = wavStream.Len() - 1 + err = wavStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, wavStream.Position()) + err = flacStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, flacStream.Position()) + + wavSamples = testtools.CollectNum(100, wavStream) + flacSamples = testtools.CollectNum(100, flacStream) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) + + // Test after end of stream. + seekPos = wavStream.Len() + err = wavStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, wavStream.Position()) + err = flacStream.Seek(seekPos) + assert.NoError(t, err) + assert.Equal(t, seekPos, flacStream.Position()) + + wavSamples = testtools.CollectNum(100, wavStream) + flacSamples = testtools.CollectNum(100, flacStream) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) } func getFlacFrameStartPositions(r io.Reader) ([]uint64, error) {