nativeaudio

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

commit 3bdbe9d9e5d2e907bf64e2d33492e9a4e5648f9a
parent 87ea90df507740bf93d4cf2840953b0b38977098
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Thu, 17 Sep 2026 11:58:20 -0400

test: cover the ffmpeg fallbacks when ffmpeg is installed

FFmpegLoad, FFmpegDecode and FFmpegPlay had no tests, which is how a
mistyped ffplay flag shipped. They now run wherever ffmpeg is on PATH,
including Windows, so the fallback is checked without a Linux machine.
Load and Decode must agree exactly with each other and match the
ffmpeg-produced reference; FFmpegPlay must block for the file's duration.

Diffstat:
Ainternal/test/ffmpeg_test.go | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 70 insertions(+), 0 deletions(-)

diff --git a/internal/test/ffmpeg_test.go b/internal/test/ffmpeg_test.go @@ -0,0 +1,70 @@ +package test + +import ( + "bytes" + "os/exec" + "testing" + "time" + + "git.sr.ht/~jackmordaunt/nativeaudio" +) + +// requireTools skips the test unless every named program is on PATH. +func requireTools(t *testing.T, names ...string) { + t.Helper() + for _, n := range names { + if _, err := exec.LookPath(n); err != nil { + t.Skipf("%s not installed", n) + } + } +} + +// TestFFmpegLoadAndDecode exercises the ffmpeg fallback on every +// platform that has ffmpeg, regardless of which native decoder is in use. +// The reference PCM was produced by ffmpeg, so output should match within +// the same tolerance as the native decoders, and Load and Decode must +// agree with each other exactly. +func TestFFmpegLoadAndDecode(t *testing.T) { + requireTools(t, "ffmpeg", "ffprobe") + want := nativeaudio.Format{SampleRate: 44100, Channels: 2, BytesPerSample: 2} + + loaded, f, err := nativeaudio.FFmpegLoad("compressed.m4a") + if err != nil { + t.Fatalf("FFmpegLoad: %v", err) + } + if f != want { + t.Fatalf("FFmpegLoad format: want %+v, got %+v", want, f) + } + if !bytes.Equal(loaded, uncompressed) && !equal(t, loaded, uncompressed) { + t.Fatal("FFmpegLoad output does not match reference") + } + + decoded, f, err := nativeaudio.FFmpegDecode(compressed) + if err != nil { + t.Fatalf("FFmpegDecode: %v", err) + } + if f != want { + t.Fatalf("FFmpegDecode format: want %+v, got %+v", want, f) + } + if !bytes.Equal(decoded, loaded) { + t.Fatal("FFmpegDecode output differs from FFmpegLoad output for the same data") + } +} + +// TestFFmpegPlay plays one second of silence through ffplay and checks +// the call blocks for at least that long. A mistyped flag used to make +// ffplay exit immediately with an error. +func TestFFmpegPlay(t *testing.T) { + requireTools(t, "ffplay") + const seconds = 1 + path := writeTemp(t, "silence.wav", silentWAV(44100, 2, seconds)) + start := time.Now() + if err := nativeaudio.FFmpegPlay(path); err != nil { + t.Fatalf("FFmpegPlay: %v", err) + } + elapsed := time.Since(start) + t.Logf("ffplay took %v", elapsed.Round(time.Millisecond)) + if min := time.Duration(seconds) * time.Second * 9 / 10; elapsed < min { + t.Errorf("FFmpegPlay returned after %v, want at least %v", elapsed, min) + } +}