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 }