nativeaudio

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

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 }