nativeaudio

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

audio.go (5456B)


      1 // Package nativeaudio decodes compressed audio into PCM using the
      2 // decoder each operating system already ships, falling back to ffmpeg
      3 // where there is no native API to call.
      4 //
      5 //	Windows: Media Foundation
      6 //	  macOS: AudioToolbox
      7 //	  Linux: ffmpeg's libraries, linked
      8 //
      9 // Built without cgo, and on any other operating system, the ffmpeg
     10 // binary is run as a subprocess instead.
     11 //
     12 // Output is always signed 16-bit little-endian PCM, which is directly
     13 // playable and is what the common Go audio stacks expect. The play
     14 // subpackage is a thin convenience over that for callers who just want
     15 // to hear a file.
     16 package nativeaudio
     17 
     18 import (
     19 	"errors"
     20 	"fmt"
     21 	"io"
     22 	"sync"
     23 )
     24 
     25 // ErrClosed is returned when a Decoder is used after Close.
     26 var ErrClosed = errors.New("nativeaudio: decoder is closed")
     27 
     28 // Decoder decodes compressed audio into PCM.
     29 //
     30 // Create one with New and release it with Close. A Decoder owns
     31 // whatever platform state the backend requires, which is why it is a
     32 // value rather than a set of package functions: two independent parts
     33 // of a program can hold their own without one tearing down the other.
     34 //
     35 // A Decoder is safe for concurrent use. Decodes may run in parallel,
     36 // and Close waits for those in flight, and for any open Stream to be
     37 // closed, before releasing platform state.
     38 type Decoder struct {
     39 	mu      sync.RWMutex
     40 	closed  bool
     41 	streams sync.WaitGroup
     42 	limits  Limits
     43 }
     44 
     45 // Option configures a Decoder at construction.
     46 type Option func(*Decoder)
     47 
     48 // WithLimits bounds what a single decode may consume. See [Limits].
     49 func WithLimits(l Limits) Option {
     50 	return func(d *Decoder) { d.limits = l }
     51 }
     52 
     53 // New creates a Decoder, initialising any platform state the backend
     54 // needs. Call Close when you are finished with it.
     55 func New(opts ...Option) (*Decoder, error) {
     56 	if err := start(); err != nil {
     57 		return nil, fmt.Errorf("initialising platform decoder: %w", err)
     58 	}
     59 	d := &Decoder{limits: DefaultLimits()}
     60 	for _, opt := range opts {
     61 		opt(d)
     62 	}
     63 	return d, nil
     64 }
     65 
     66 // Close releases the platform state held by the Decoder. It is
     67 // idempotent, and any further use of the Decoder returns ErrClosed.
     68 func (d *Decoder) Close() error {
     69 	d.mu.Lock()
     70 	defer d.mu.Unlock()
     71 	if d.closed {
     72 		return nil
     73 	}
     74 	d.closed = true
     75 	// Wait for open streams before tearing down platform state, which
     76 	// their decoder objects are still using.
     77 	d.streams.Wait()
     78 	if err := end(); err != nil {
     79 		return fmt.Errorf("shutting down platform decoder: %w", err)
     80 	}
     81 	return nil
     82 }
     83 
     84 // DecodeFile decodes the audio file at path, returning s16le PCM and
     85 // the format needed to play it back correctly.
     86 func (d *Decoder) DecodeFile(path string) (pcm []byte, format Format, err error) {
     87 	d.mu.RLock()
     88 	defer d.mu.RUnlock()
     89 	if d.closed {
     90 		return nil, format, ErrClosed
     91 	}
     92 	return d.drain(openStreamFile(path))
     93 }
     94 
     95 // Decode decodes compressed audio held in memory, returning s16le PCM
     96 // and the format needed to play it back correctly.
     97 func (d *Decoder) Decode(compressed []byte) (pcm []byte, format Format, err error) {
     98 	d.mu.RLock()
     99 	defer d.mu.RUnlock()
    100 	if d.closed {
    101 		return nil, format, ErrClosed
    102 	}
    103 	return d.drain(openStream(compressed))
    104 }
    105 
    106 // drain reads a stream to completion under the decoder's limits, always
    107 // closing it. Routing the buffered API through the streaming one is what
    108 // lets limits apply to both.
    109 func (d *Decoder) drain(s *Stream, err error) ([]byte, Format, error) {
    110 	if err != nil {
    111 		return nil, Format{}, err
    112 	}
    113 	defer s.Close()
    114 	pcm, err := io.ReadAll(newLimited(s.r, d.limits))
    115 	if err != nil {
    116 		return nil, s.format, err
    117 	}
    118 	return pcm, s.format, nil
    119 }
    120 
    121 // Format describes the PCM a decode produced, and is everything needed
    122 // to play it back correctly.
    123 type Format struct {
    124 	SampleRate     int // samples per second.
    125 	Channels       int // number of channels.
    126 	BytesPerSample int // bytes per sample; always 2, for the s16le output this package produces.
    127 }
    128 
    129 // Stream decodes compressed audio held in memory, returning PCM through
    130 // an [io.Reader] rather than a single buffer.
    131 //
    132 // The returned Stream must be closed. Every native backend decodes
    133 // incrementally; only the subprocess fallback decodes up front and
    134 // serves from memory, because ffmpeg cannot read every container this
    135 // package supports from a pipe.
    136 func (d *Decoder) Stream(compressed []byte) (*Stream, error) {
    137 	d.mu.RLock()
    138 	defer d.mu.RUnlock()
    139 	if d.closed {
    140 		return nil, ErrClosed
    141 	}
    142 	s, err := openStream(compressed)
    143 	if err != nil {
    144 		return nil, err
    145 	}
    146 	s.r = newLimited(s.r, d.limits)
    147 	d.track(s)
    148 	return s, nil
    149 }
    150 
    151 // StreamFile decodes the audio file at path, returning PCM through an
    152 // [io.Reader] rather than a single buffer.
    153 //
    154 // The returned Stream must be closed. The Linux backend and the
    155 // subprocess fallback read the file directly, so the PCM is never held
    156 // whole; Windows and macOS read the compressed file into memory first,
    157 // which is small next to the PCM it avoids buffering.
    158 func (d *Decoder) StreamFile(path string) (*Stream, error) {
    159 	d.mu.RLock()
    160 	defer d.mu.RUnlock()
    161 	if d.closed {
    162 		return nil, ErrClosed
    163 	}
    164 	s, err := openStreamFile(path)
    165 	if err != nil {
    166 		return nil, err
    167 	}
    168 	s.r = newLimited(s.r, d.limits)
    169 	d.track(s)
    170 	return s, nil
    171 }
    172 
    173 // track registers an open Stream so Close can wait for it. The caller
    174 // holds at least a read lock, which is what keeps this from racing the
    175 // Wait in Close.
    176 func (d *Decoder) track(s *Stream) {
    177 	d.streams.Add(1)
    178 	s.done = d.streams.Done
    179 }