pcm.go (4612B)
1 // Package pcm adapts the PCM this module produces to the shapes other Go 2 // audio libraries consume. 3 // 4 // Stacks that take an [io.Reader] of signed 16-bit little-endian samples, 5 // such as oto and Ebitengine's audio package, need no adapter: a 6 // [nativeaudio.Stream] is already one, and buffered output can be wrapped 7 // in a [bytes.Reader]. 8 // 9 // Libraries built around composable streams want something else. They 10 // deal in pairs of float64 samples, so [Streamer] converts. It satisfies 11 // beep's Streamer and StreamCloser interfaces without importing beep, 12 // because Go interfaces are structural and neither side needs to know 13 // about the other. 14 package pcm 15 16 import ( 17 "encoding/binary" 18 "errors" 19 "fmt" 20 "io" 21 22 "git.sr.ht/~jackmordaunt/nativeaudio" 23 ) 24 25 // sampleScale converts a signed 16-bit sample to the range [-1, 1). 26 const sampleScale = 1 << 15 27 28 // Streamer presents s16le PCM as pairs of float64 samples. 29 // 30 // Mono input is written to both channels. Input with more than two 31 // channels keeps the first two, because the interface this serves is 32 // stereo and mixing down is a decision the caller should make rather 33 // than inherit. 34 type Streamer struct { 35 r io.Reader 36 format nativeaudio.Format 37 38 // partial holds the bytes of a frame split across two reads, since 39 // the underlying reader is free to stop mid-frame. 40 partial []byte 41 42 buf []byte 43 err error 44 done bool 45 } 46 47 // NewStreamer adapts s16le PCM read from r. 48 // 49 // Pass the [nativeaudio.Format] that came back with the audio. Taking a 50 // reader rather than a stream means the buffered API works too, through 51 // a [bytes.Reader] over the decoded PCM. 52 func NewStreamer(r io.Reader, format nativeaudio.Format) *Streamer { 53 return &Streamer{r: r, format: format} 54 } 55 56 // Stream fills samples with as many frames as it can, returning the 57 // number written and whether streaming should continue. 58 // 59 // It reports false once the audio is exhausted or an error has occurred, 60 // which is the convention the consuming interface expects; the error 61 // itself comes from [Streamer.Err]. 62 func (s *Streamer) Stream(samples [][2]float64) (int, bool) { 63 if s.done || len(samples) == 0 { 64 return 0, false 65 } 66 if s.format.BytesPerSample != 2 { 67 s.fail(fmt.Errorf("pcm: unsupported sample size %d bytes, want 2", s.format.BytesPerSample)) 68 return 0, false 69 } 70 if s.format.Channels < 1 { 71 s.fail(fmt.Errorf("pcm: unsupported channel count %d", s.format.Channels)) 72 return 0, false 73 } 74 75 var ( 76 frame = s.format.Channels * s.format.BytesPerSample 77 want = len(samples) * frame 78 ) 79 if cap(s.buf) < want { 80 s.buf = make([]byte, want) 81 } 82 s.buf = s.buf[:want] 83 84 // Start from whatever was left over last time. 85 n := copy(s.buf, s.partial) 86 s.partial = s.partial[:0] 87 88 read, err := io.ReadFull(s.r, s.buf[n:]) 89 n += read 90 switch { 91 case err == nil, errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF): 92 // A short read is normal: it means the audio ended. 93 default: 94 s.fail(err) 95 if n == 0 { 96 return 0, false 97 } 98 } 99 100 frames := n / frame 101 if rest := n % frame; rest != 0 { 102 // Hold the trailing bytes for the next call. At the end of the 103 // audio they are a truncated frame and stay unplayed, which is 104 // the only sane reading of a file that ends mid-frame. 105 s.partial = append(s.partial[:0], s.buf[n-rest:n]...) 106 } 107 108 for i := 0; i < frames; i++ { 109 base := i * frame 110 left := sample(s.buf[base:]) 111 right := left 112 if s.format.Channels > 1 { 113 right = sample(s.buf[base+2:]) 114 } 115 samples[i] = [2]float64{left, right} 116 } 117 118 if frames == 0 { 119 s.done = true 120 return 0, false 121 } 122 // The reader is exhausted, so this is the last batch. 123 if err != nil { 124 s.done = true 125 } 126 return frames, true 127 } 128 129 // Err returns the first error encountered, if any. Reaching the end of 130 // the audio is not an error. 131 func (s *Streamer) Err() error { 132 return s.err 133 } 134 135 // Close closes the underlying reader when it is an [io.Closer], which is 136 // the case for a [nativeaudio.Stream]. Otherwise it does nothing. 137 func (s *Streamer) Close() error { 138 s.done = true 139 if c, ok := s.r.(io.Closer); ok { 140 return c.Close() 141 } 142 return nil 143 } 144 145 // Format describes the audio being streamed, as sample rate and channel 146 // count. It is the format passed to [NewStreamer], unchanged. 147 func (s *Streamer) Format() nativeaudio.Format { 148 return s.format 149 } 150 151 // fail records the first error and stops streaming. 152 func (s *Streamer) fail(err error) { 153 if s.err == nil { 154 s.err = err 155 } 156 s.done = true 157 } 158 159 // sample reads one little-endian 16-bit sample as a float in [-1, 1). 160 func sample(b []byte) float64 { 161 return float64(int16(binary.LittleEndian.Uint16(b))) / sampleScale 162 }