nativeaudio

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

commit 9ffd7724e3c2f8bec605d5b958910a57f6bb930d
parent a55f0ac25fe2b1ee2ca99ad323d1e9370e999801
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Thu, 17 Sep 2026 11:57:34 -0400

test: cover more formats, Start/End cycles and mid-stream corruption

The suite decoded one 44.1 kHz stereo AAC file and nothing else, so End
was never called and Format was only ever checked for that one shape.
A generated silent WAV gives mono and other sample rates without
fixtures. The mid-stream corruption test is a hang guard: the sample
reader used to spin forever when the decoder returned neither a sample
nor an end-of-stream flag.

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

diff --git a/internal/test/decode_test.go b/internal/test/decode_test.go @@ -0,0 +1,125 @@ +package test + +import ( + "bytes" + "encoding/binary" + "os" + "path/filepath" + "testing" + "time" + + "git.sr.ht/~jackmordaunt/nativeaudio" +) + +// silentWAV builds a canonical 16-bit PCM WAV of digital silence in +// memory, so format tests need no fixtures and playback tests make no +// sound. +func silentWAV(rate, channels, seconds int) []byte { + n := rate * channels * 2 * seconds + var b bytes.Buffer + b.WriteString("RIFF") + binary.Write(&b, binary.LittleEndian, uint32(36+n)) + b.WriteString("WAVEfmt ") + binary.Write(&b, binary.LittleEndian, uint32(16)) // fmt chunk size + binary.Write(&b, binary.LittleEndian, uint16(1)) // PCM + binary.Write(&b, binary.LittleEndian, uint16(channels)) // channels + binary.Write(&b, binary.LittleEndian, uint32(rate)) // sample rate + binary.Write(&b, binary.LittleEndian, uint32(rate*channels*2)) // byte rate + binary.Write(&b, binary.LittleEndian, uint16(channels*2)) // block align + binary.Write(&b, binary.LittleEndian, uint16(16)) // bits per sample + b.WriteString("data") + binary.Write(&b, binary.LittleEndian, uint32(n)) + b.Write(make([]byte, n)) + return b.Bytes() +} + +// writeTemp writes data to a file in the test's temp dir and returns its path. +func writeTemp(t *testing.T, name string, data []byte) string { + t.Helper() + path := filepath.Join(t.TempDir(), name) + if err := os.WriteFile(path, data, 0o644); err != nil { + t.Fatalf("writing %s: %v", name, err) + } + return path +} + +// TestDecodeFormats checks that Format reports the source's sample rate +// and channel count for inputs other than the 44.1 kHz stereo fixture, +// and that the PCM length matches. +func TestDecodeFormats(t *testing.T) { + cases := []struct{ rate, channels int }{ + {44100, 2}, + {22050, 1}, + {48000, 1}, + {48000, 2}, + } + for _, c := range cases { + by, f, err := nativeaudio.Decode(silentWAV(c.rate, c.channels, 1)) + if err != nil { + t.Errorf("%d Hz %d ch: unexpected error: %v", c.rate, c.channels, err) + continue + } + want := nativeaudio.Format{SampleRate: c.rate, Channels: c.channels, BytesPerSample: 2} + if f != want { + t.Errorf("%d Hz %d ch: format: want %+v, got %+v", c.rate, c.channels, want, f) + } + if wantLen := c.rate * c.channels * 2; len(by) != wantLen { + t.Errorf("%d Hz %d ch: pcm length: want %d, got %d", c.rate, c.channels, wantLen, len(by)) + } + } +} + +// TestLoadMissingFile ensures a bad path is reported as an error. +func TestLoadMissingFile(t *testing.T) { + if _, _, err := nativeaudio.Load(filepath.Join(t.TempDir(), "does-not-exist.m4a")); err == nil { + t.Fatal("expected error for missing file, got nil") + } +} + +// TestStartEndCycle ensures the platform can be torn down and brought +// back up repeatedly, with a decode in between to prove each Start took. +func TestStartEndCycle(t *testing.T) { + for i := 0; i < 3; i++ { + if err := nativeaudio.Start(); err != nil { + t.Fatalf("cycle %d: Start: %v", i, err) + } + if _, _, err := nativeaudio.Decode(compressed); err != nil { + t.Fatalf("cycle %d: Decode: %v", i, err) + } + if err := nativeaudio.End(); err != nil { + t.Fatalf("cycle %d: End: %v", i, err) + } + } + // Leave the platform started for the other tests, as they expect. + if err := nativeaudio.Start(); err != nil { + t.Fatalf("final Start: %v", err) + } +} + +// TestDecodeCorruptMidStream overwrites a stretch of sample data in the +// middle of the fixture. Decoders differ in whether they conceal the +// damage or fail, so either a result or an error is acceptable. What is +// not acceptable is a hang: a decoder that returns no sample and no +// end-of-stream flag on error must be treated as terminal. +func TestDecodeCorruptMidStream(t *testing.T) { + bad := append([]byte(nil), compressed...) + mid := len(bad) / 2 + for i := mid; i < mid+64*1024 && i < len(bad); i++ { + bad[i] = 0xff + } + type result struct { + n int + err error + } + done := make(chan result, 1) + go func() { + by, _, err := nativeaudio.Decode(bad) + done <- result{len(by), err} + }() + select { + case r := <-done: + t.Logf("mid-stream corruption: %d bytes, err=%v", r.n, r.err) + case <-time.After(30 * time.Second): + t.Fatal("decode did not return within 30s") + } +}