nativeaudio

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

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 }