nativeaudio

audio playback for Go
Log | Files | Refs | README | LICENSE

commit 29feeef5d399bb0613c8094f07d63a8e8cdeb8ec
parent a4d54b52ff25999a2e0a71a39b8fd2ec135a353c
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Sat, 19 Sep 2026 14:56:54 -0300

test: compare decodes without the padding that varies by ffmpeg

The reference disagreed with current ffmpeg by 767 trailing frames of
silence, and by no audio at all.

Diffstat:
Minternal/test/audio_test.go | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 102 insertions(+), 5 deletions(-)

diff --git a/internal/test/audio_test.go b/internal/test/audio_test.go @@ -57,7 +57,7 @@ func TestLoad(t *testing.T) { if bytes.Equal(by, getUncompressed()) { return } - if !equal(t, by, getUncompressed()) { + if !equal(t, by, getUncompressed(), f.Channels*f.BytesPerSample) { t.Fatalf("native output does not match ffmpeg output") } } @@ -84,7 +84,7 @@ func TestDecode(t *testing.T) { if bytes.Equal(by, getUncompressed()) { return } - if !equal(t, by, getUncompressed()) { + if !equal(t, by, getUncompressed(), f.Channels*f.BytesPerSample) { t.Fatalf("native output does not match ffmpeg output") } } @@ -151,16 +151,35 @@ func TestMemoryLeak(t *testing.T) { // equal decodes the PCM samples and tests if they are "close enough" // using a heuristic tolerance. // -// The two decoders must agree on length exactly. Sample values are +// Silence at either end is trimmed first. How much padding surrounds the +// audio is not a property of the audio: AAC carries encoder delay, and +// how many priming and trailing frames survive the round trip differs +// between decoder implementations and between ffmpeg releases. The +// reference here was generated by one ffmpeg; a later one decodes the +// same file to 767 fewer trailing frames of silence, and the audio +// between the silent ends is unchanged. Comparing raw lengths turns that +// into a failure and dates the fixture to whichever ffmpeg produced it. +// +// After trimming the two must agree on length exactly. Sample values are // compared as signed integers by mean absolute difference, which must // stay under one quantisation step (1 LSB). Different AAC decoders // legitimately differ by rounding, so bit-exact output is not expected. -func equal(t *testing.T, left, right []byte) bool { +// +// Trimming cannot hide a bad decode. Only frames that are entirely zero +// are removed, so lost audio still reaches the comparison, and any +// misalignment introduced would put the mean far above 1 rather than +// passing quietly. +func equal(t *testing.T, left, right []byte, align int) bool { if len(left) == 0 || len(right) == 0 { return false } + left, right = trimSilence(left, align), trimSilence(right, align) + if len(left) == 0 && len(right) == 0 { + // Both sides decoded to nothing but silence, which is agreement. + return true + } if len(left) != len(right) { - t.Logf("length mismatch: native %d bytes, reference %d bytes", len(left), len(right)) + t.Logf("length mismatch after trimming silence: native %d bytes, reference %d bytes", len(left), len(right)) return false } var ( @@ -189,6 +208,84 @@ func equal(t *testing.T, left, right []byte) bool { return mean < 1.0 } +// TestEqual guards the comparison itself. Trimming silence is what lets +// the reference PCM outlive the ffmpeg that produced it, and the failure +// mode of getting it wrong is a comparison that passes for good, so the +// cases it must still reject are worth pinning down. +func TestEqual(t *testing.T) { + reference := getUncompressed() + const align = 4 // the fixture is 16-bit stereo. + + reject := func(name string, mutate func([]byte) []byte) { + t.Run(name, func(t *testing.T) { + if equal(t, mutate(append([]byte(nil), reference...)), reference, align) { + t.Errorf("expected the comparison to reject %s", name) + } + }) + } + + // Every sample louder by one step, which is the tolerance exactly. + reject("amplitude", func(b []byte) []byte { + for i := 0; i+1 < len(b); i += 2 { + v := int16(uint16(b[i]) | uint16(b[i+1])<<8) + if v < 32767 { + v++ + } + b[i], b[i+1] = byte(v), byte(uint16(v)>>8) + } + return b + }) + + // Audio dropped from the middle, where no amount of trimming helps. + reject("truncated", func(b []byte) []byte { + mid := len(b) / 2 + return append(b[:mid], b[mid+1000*align:]...) + }) + + // Shifted by a frame, so the channels land on each other. + reject("shifted", func(b []byte) []byte { + lead := 0 + for lead+align <= len(b) && isZero(b[lead:lead+align]) { + lead += align + } + return append(b[:lead:lead], b[lead+align:]...) + }) + + t.Run("identity", func(t *testing.T) { + if !equal(t, append([]byte(nil), reference...), reference, align) { + t.Errorf("expected the comparison to accept identical input") + } + }) +} + +// trimSilence removes whole silent frames from both ends of the PCM. +// +// A frame at a time, rather than a byte at a time: the low byte of a +// sample is zero whenever its value is a multiple of 256, so trimming +// individual zero bytes would leave the buffer straddling a frame +// boundary and compare the left channel against the right. +func trimSilence(b []byte, align int) []byte { + if align <= 0 { + return b + } + for len(b) >= align && isZero(b[:align]) { + b = b[align:] + } + for len(b) >= align && isZero(b[len(b)-align:]) { + b = b[:len(b)-align] + } + return b +} + +func isZero(b []byte) bool { + for _, c := range b { + if c != 0 { + return false + } + } + return true +} + func abs(n int) int { if n < 0 { return -n