diff --git a/flac/decode.go b/flac/decode.go index 886d761..8e86433 100644 --- a/flac/decode.go +++ b/flac/decode.go @@ -1,10 +1,10 @@ package flac import ( - "fmt" "io" "github.com/mewkiz/flac" + "github.com/mewkiz/flac/frame" "github.com/pkg/errors" "github.com/gopxl/beep/v2" @@ -32,10 +32,17 @@ 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") } + d.hasFixedBlockSize = d.frame.HasFixedBlockSize + format = beep.Format{ SampleRate: beep.SampleRate(d.stream.Info.SampleRate), NumChannels: int(d.stream.Info.NChannels), @@ -47,96 +54,70 @@ 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 + + hasFixedBlockSize 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 == 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 == 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 == 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 == 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 + + if numChannels == 1 { + samples1 := frame.Subframes[0].Samples[start:] + for i := 0; i < num; i++ { + v := float64(samples1[i]) * q + into[i][0] = v + into[i][1] = v } - 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 + } else { + samples1 := frame.Subframes[0].Samples[start:] + samples2 := frame.Subframes[1].Samples[start:] + for i := 0; i < num; i++ { + into[i][0] = float64(samples1[i]) * q + into[i][1] = float64(samples2[i]) * q } - default: - panic(fmt.Errorf("support for %d bits-per-sample and %d channels combination not yet implemented", bps, nchannels)) } - return nil } func (d *decoder) Err() error { @@ -148,18 +129,68 @@ func (d *decoder) Len() int { } func (d *decoder) Position() int { - return d.pos + 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 } -// p represents flac sample num perhaps? func (d *decoder) Seek(p int) error { if !d.seekEnabled { 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. pos, err := d.stream.Seek(uint64(p)) - d.pos = int(pos) - return err + if err != nil { + return errors.Wrap(err, "flac") + } + d.posInFrame = p - int(pos) + + d.frame, err = d.stream.ParseNext() + if err != nil { + return errors.Wrap(err, "flac") + } + + return nil } func (d *decoder) Close() error { diff --git a/flac/decode_test.go b/flac/decode_test.go index f34bfe7..b447d4c 100644 --- a/flac/decode_test.go +++ b/flac/decode_test.go @@ -1,6 +1,9 @@ package flac_test import ( + "bytes" + "io" + "log" "os" "testing" @@ -8,10 +11,13 @@ 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) { - 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() @@ -21,3 +27,163 @@ 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, 22050, wavStream.Len()) + assert.Equal(t, 22050, 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_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() + + // 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()) + + wavSamples := testtools.CollectNum(100, wavStream) + flacSamples := testtools.CollectNum(100, flacStream) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) + + // 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()) + + wavSamples = testtools.CollectNum(100, wavStream) + flacSamples = testtools.CollectNum(100, flacStream) + testtools.AssertSamplesEqual(t, wavSamples, flacSamples) + + // 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()) + + 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) { + 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 +} + +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) + }) +} diff --git a/internal/testdata/valid_44100hz_22050_samples.flac b/internal/testdata/valid_44100hz_22050_samples.flac deleted file mode 100644 index b6dd431..0000000 Binary files a/internal/testdata/valid_44100hz_22050_samples.flac and /dev/null differ diff --git a/internal/testdata/valid_44100hz_22050_samples_ffmpeg.flac b/internal/testdata/valid_44100hz_22050_samples_ffmpeg.flac new file mode 100644 index 0000000..18fee28 Binary files /dev/null and b/internal/testdata/valid_44100hz_22050_samples_ffmpeg.flac differ diff --git a/internal/testtools/asserts.go b/internal/testtools/asserts.go index 033adc7..60b77de 100644 --- a/internal/testtools/asserts.go +++ b/internal/testtools/asserts.go @@ -54,3 +54,25 @@ func AssertStreamerHasCorrectReturnBehaviour(t *testing.T, s beep.Streamer, expe assert.Equal(t, 0, n) assert.NoError(t, s.Err()) } + +func AssertSamplesEqual(t *testing.T, expected, actual [][2]float64) { + t.Helper() + + if len(expected) != len(actual) { + t.Errorf("expected sample data length to be %d, got %d", len(expected), len(actual)) + return + } + + const epsilon = 1e-9 + equals := true + for i := range expected { + if actual[i][0] < expected[i][0]-epsilon || actual[i][0] > 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") + } +}