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 }