pcm_test.go (5438B)
1 package pcm_test 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "io" 7 "math" 8 "testing" 9 10 "git.sr.ht/~jackmordaunt/nativeaudio" 11 "git.sr.ht/~jackmordaunt/nativeaudio/pcm" 12 ) 13 14 // The interfaces beep defines, restated here. Asserting against a local 15 // copy pins the signatures the adapter has to match without taking on the 16 // dependency, so a change to either side shows up as a build failure. 17 type ( 18 beepStreamer interface { 19 Stream(samples [][2]float64) (n int, ok bool) 20 Err() error 21 } 22 beepStreamCloser interface { 23 beepStreamer 24 Close() error 25 } 26 ) 27 28 var _ beepStreamCloser = (*pcm.Streamer)(nil) 29 30 // s16 encodes samples as little-endian 16-bit PCM. 31 func s16(samples ...int16) []byte { 32 var b bytes.Buffer 33 for _, s := range samples { 34 binary.Write(&b, binary.LittleEndian, s) 35 } 36 return b.Bytes() 37 } 38 39 func stereo(rate int) nativeaudio.Format { 40 return nativeaudio.Format{SampleRate: rate, Channels: 2, BytesPerSample: 2} 41 } 42 43 func mono(rate int) nativeaudio.Format { 44 return nativeaudio.Format{SampleRate: rate, Channels: 1, BytesPerSample: 2} 45 } 46 47 // near reports whether two samples match within rounding. 48 func near(t *testing.T, a, b float64) bool { 49 t.Helper() 50 return math.Abs(a-b) < 1e-9 51 } 52 53 // TestStereoConversion checks the sample scaling and channel order. 54 func TestStereoConversion(t *testing.T) { 55 in := s16(0, 32767, -32768, 16384) 56 s := pcm.NewStreamer(bytes.NewReader(in), stereo(44100)) 57 58 got := make([][2]float64, 4) 59 n, ok := s.Stream(got) 60 if !ok || n != 2 { 61 t.Fatalf("Stream: got n=%d ok=%v, want 2 frames", n, ok) 62 } 63 if err := s.Err(); err != nil { 64 t.Fatalf("Err: %v", err) 65 } 66 67 want := [][2]float64{ 68 {0, 32767.0 / 32768.0}, 69 {-1, 0.5}, 70 } 71 for i := range want { 72 if !near(t, got[i][0], want[i][0]) || !near(t, got[i][1], want[i][1]) { 73 t.Errorf("frame %d: got %v, want %v", i, got[i], want[i]) 74 } 75 } 76 } 77 78 // TestMonoIsDuplicated checks that one channel reaches both ears rather 79 // than being silently dropped or halving the frame count. 80 func TestMonoIsDuplicated(t *testing.T) { 81 in := s16(16384, -16384) 82 s := pcm.NewStreamer(bytes.NewReader(in), mono(22050)) 83 84 got := make([][2]float64, 4) 85 n, ok := s.Stream(got) 86 if !ok || n != 2 { 87 t.Fatalf("Stream: got n=%d ok=%v, want 2 frames", n, ok) 88 } 89 for i, want := range []float64{0.5, -0.5} { 90 if !near(t, got[i][0], want) || !near(t, got[i][1], want) { 91 t.Errorf("frame %d: got %v, want both channels %v", i, got[i], want) 92 } 93 } 94 } 95 96 // dribble returns at most n bytes per Read, so frames land split across 97 // calls. The underlying decoder is free to do this, and the earlier 98 // small-read test showed it does. 99 type dribble struct { 100 r io.Reader 101 n int 102 } 103 104 func (d dribble) Read(p []byte) (int, error) { 105 if len(p) > d.n { 106 p = p[:d.n] 107 } 108 return d.r.Read(p) 109 } 110 111 // TestPartialFramesAcrossReads streams through a reader that splits 112 // frames, and checks nothing is lost or misaligned. 113 func TestPartialFramesAcrossReads(t *testing.T) { 114 const frames = 100 115 samples := make([]int16, 0, frames*2) 116 for i := 0; i < frames; i++ { 117 samples = append(samples, int16(i), int16(-i)) 118 } 119 in := s16(samples...) 120 121 // Three bytes at a time never aligns with a four byte frame. 122 s := pcm.NewStreamer(dribble{r: bytes.NewReader(in), n: 3}, stereo(44100)) 123 124 var got [][2]float64 125 buf := make([][2]float64, 7) 126 for { 127 n, ok := s.Stream(buf) 128 got = append(got, buf[:n]...) 129 if !ok { 130 break 131 } 132 } 133 if err := s.Err(); err != nil { 134 t.Fatalf("Err: %v", err) 135 } 136 if len(got) != frames { 137 t.Fatalf("got %d frames, want %d", len(got), frames) 138 } 139 for i := range got { 140 wantL := float64(int16(i)) / 32768 141 wantR := float64(int16(-i)) / 32768 142 if !near(t, got[i][0], wantL) || !near(t, got[i][1], wantR) { 143 t.Fatalf("frame %d: got %v, want {%v %v}", i, got[i], wantL, wantR) 144 } 145 } 146 } 147 148 // TestRejectsUnsupportedFormat ensures a format the adapter cannot honour 149 // is reported rather than producing quiet nonsense. 150 func TestRejectsUnsupportedFormat(t *testing.T) { 151 s := pcm.NewStreamer(bytes.NewReader(s16(1, 2, 3, 4)), nativeaudio.Format{ 152 SampleRate: 44100, Channels: 2, BytesPerSample: 3, 153 }) 154 if n, ok := s.Stream(make([][2]float64, 4)); ok || n != 0 { 155 t.Fatalf("Stream: got n=%d ok=%v, want it to refuse", n, ok) 156 } 157 if s.Err() == nil { 158 t.Fatal("Err: want an error for a 3 byte sample size") 159 } 160 } 161 162 // closeSpy records whether the adapter closed what it was given. 163 type closeSpy struct { 164 io.Reader 165 closed bool 166 } 167 168 func (c *closeSpy) Close() error { 169 c.closed = true 170 return nil 171 } 172 173 // TestCloseDelegates covers the streaming case, where the reader owns 174 // decoder state that has to be released. 175 func TestCloseDelegates(t *testing.T) { 176 spy := &closeSpy{Reader: bytes.NewReader(s16(1, 2))} 177 s := pcm.NewStreamer(spy, stereo(44100)) 178 if err := s.Close(); err != nil { 179 t.Fatalf("Close: %v", err) 180 } 181 if !spy.closed { 182 t.Error("Close did not close the underlying reader") 183 } 184 185 // A plain reader is not a closer, and that must not be an error. 186 if err := pcm.NewStreamer(bytes.NewReader(nil), stereo(44100)).Close(); err != nil { 187 t.Errorf("Close on a non-closer: %v", err) 188 } 189 } 190 191 // TestEmptyInput checks the degenerate case reports no frames rather than 192 // blocking or panicking. 193 func TestEmptyInput(t *testing.T) { 194 s := pcm.NewStreamer(bytes.NewReader(nil), stereo(44100)) 195 if n, ok := s.Stream(make([][2]float64, 4)); ok || n != 0 { 196 t.Fatalf("Stream: got n=%d ok=%v, want no frames", n, ok) 197 } 198 if err := s.Err(); err != nil { 199 t.Fatalf("Err: %v", err) 200 } 201 }