nativeaudio

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

decode_test.go (5300B)


      1 package test
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/binary"
      6 	"errors"
      7 	"os"
      8 	"path/filepath"
      9 	"testing"
     10 	"time"
     11 
     12 	"git.sr.ht/~jackmordaunt/nativeaudio"
     13 )
     14 
     15 // silentWAV builds a canonical 16-bit PCM WAV of digital silence in
     16 // memory, so format tests need no fixtures and playback tests make no
     17 // sound.
     18 func silentWAV(rate, channels, seconds int) []byte {
     19 	n := rate * channels * 2 * seconds
     20 	var b bytes.Buffer
     21 	b.WriteString("RIFF")
     22 	binary.Write(&b, binary.LittleEndian, uint32(36+n))
     23 	b.WriteString("WAVEfmt ")
     24 	binary.Write(&b, binary.LittleEndian, uint32(16))              // fmt chunk size
     25 	binary.Write(&b, binary.LittleEndian, uint16(1))               // PCM
     26 	binary.Write(&b, binary.LittleEndian, uint16(channels))        // channels
     27 	binary.Write(&b, binary.LittleEndian, uint32(rate))            // sample rate
     28 	binary.Write(&b, binary.LittleEndian, uint32(rate*channels*2)) // byte rate
     29 	binary.Write(&b, binary.LittleEndian, uint16(channels*2))      // block align
     30 	binary.Write(&b, binary.LittleEndian, uint16(16))              // bits per sample
     31 	b.WriteString("data")
     32 	binary.Write(&b, binary.LittleEndian, uint32(n))
     33 	b.Write(make([]byte, n))
     34 	return b.Bytes()
     35 }
     36 
     37 // writeTemp writes data to a file in the test's temp dir and returns its path.
     38 func writeTemp(t *testing.T, name string, data []byte) string {
     39 	t.Helper()
     40 	path := filepath.Join(t.TempDir(), name)
     41 	if err := os.WriteFile(path, data, 0o644); err != nil {
     42 		t.Fatalf("writing %s: %v", name, err)
     43 	}
     44 	return path
     45 }
     46 
     47 // TestDecodeFormats checks that Format reports the source's sample rate
     48 // and channel count for inputs other than the 44.1 kHz stereo fixture,
     49 // and that the PCM length matches.
     50 func TestDecodeFormats(t *testing.T) {
     51 	cases := []struct{ rate, channels int }{
     52 		{44100, 2},
     53 		{22050, 1},
     54 		{48000, 1},
     55 		{48000, 2},
     56 	}
     57 	for _, c := range cases {
     58 		by, f, err := newDecoder(t).Decode(silentWAV(c.rate, c.channels, 1))
     59 		if err != nil {
     60 			t.Errorf("%d Hz %d ch: unexpected error: %v", c.rate, c.channels, err)
     61 			continue
     62 		}
     63 		want := nativeaudio.Format{SampleRate: c.rate, Channels: c.channels, BytesPerSample: 2}
     64 		if f != want {
     65 			t.Errorf("%d Hz %d ch: format: want %+v, got %+v", c.rate, c.channels, want, f)
     66 		}
     67 		if wantLen := c.rate * c.channels * 2; len(by) != wantLen {
     68 			t.Errorf("%d Hz %d ch: pcm length: want %d, got %d", c.rate, c.channels, wantLen, len(by))
     69 		}
     70 	}
     71 }
     72 
     73 // TestLoadMissingFile ensures a bad path is reported as an error.
     74 func TestLoadMissingFile(t *testing.T) {
     75 	if _, _, err := newDecoder(t).DecodeFile(filepath.Join(t.TempDir(), "does-not-exist.m4a")); err == nil {
     76 		t.Fatal("expected error for missing file, got nil")
     77 	}
     78 }
     79 
     80 // TestStartEndCycle ensures the platform can be torn down and brought
     81 // back up repeatedly, with a decode in between to prove each Start took.
     82 func TestDecoderLifecycle(t *testing.T) {
     83 	// Several create-and-close cycles, with a decode in between to prove
     84 	// each New actually initialised the platform.
     85 	for i := 0; i < 3; i++ {
     86 		d, err := nativeaudio.New()
     87 		if err != nil {
     88 			t.Fatalf("cycle %d: New: %v", i, err)
     89 		}
     90 		if _, _, err := d.Decode(compressed); err != nil {
     91 			t.Fatalf("cycle %d: Decode: %v", i, err)
     92 		}
     93 		if err := d.Close(); err != nil {
     94 			t.Fatalf("cycle %d: Close: %v", i, err)
     95 		}
     96 		if err := d.Close(); err != nil {
     97 			t.Fatalf("cycle %d: second Close should be a no-op: %v", i, err)
     98 		}
     99 		if _, _, err := d.Decode(compressed); !errors.Is(err, nativeaudio.ErrClosed) {
    100 			t.Fatalf("cycle %d: decode after close: want ErrClosed, got %v", i, err)
    101 		}
    102 	}
    103 }
    104 
    105 // TestDecodersAreIndependent ensures closing one Decoder does not tear
    106 // the platform out from under another. This is the whole reason the API
    107 // is a value rather than a set of package functions.
    108 func TestDecodersAreIndependent(t *testing.T) {
    109 	a, err := nativeaudio.New()
    110 	if err != nil {
    111 		t.Fatalf("first New: %v", err)
    112 	}
    113 	b := newDecoder(t)
    114 	if err := a.Close(); err != nil {
    115 		t.Fatalf("closing first: %v", err)
    116 	}
    117 	if _, _, err := b.Decode(compressed); err != nil {
    118 		t.Fatalf("second decoder broke when the first closed: %v", err)
    119 	}
    120 }
    121 
    122 // TestDecodeCorruptMidStream overwrites a stretch of sample data in the
    123 // middle of the fixture. Decoders differ in whether they conceal the
    124 // damage or fail, so either a result or an error is acceptable. What is
    125 // not acceptable is a hang: a decoder that returns no sample and no
    126 // end-of-stream flag on error must be treated as terminal.
    127 func TestDecodeCorruptMidStream(t *testing.T) {
    128 	bad := append([]byte(nil), compressed...)
    129 	mid := len(bad) / 2
    130 	for i := mid; i < mid+64*1024 && i < len(bad); i++ {
    131 		bad[i] = 0xff
    132 	}
    133 	type result struct {
    134 		n   int
    135 		err error
    136 	}
    137 	done := make(chan result, 1)
    138 	go func() {
    139 		by, _, err := newDecoder(t).Decode(bad)
    140 		done <- result{len(by), err}
    141 	}()
    142 	select {
    143 	case r := <-done:
    144 		t.Logf("mid-stream corruption: %d bytes, err=%v", r.n, r.err)
    145 	case <-time.After(30 * time.Second):
    146 		t.Fatal("decode did not return within 30s")
    147 	}
    148 }
    149 
    150 // newDecoder returns a Decoder that is closed when the test ends.
    151 func newDecoder(t testing.TB) *nativeaudio.Decoder {
    152 	t.Helper()
    153 	d, err := nativeaudio.New()
    154 	if err != nil {
    155 		t.Fatalf("creating decoder: %v", err)
    156 	}
    157 	t.Cleanup(func() {
    158 		if err := d.Close(); err != nil {
    159 			t.Errorf("closing decoder: %v", err)
    160 		}
    161 	})
    162 	return d
    163 }