nativeaudio

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

stream.go (4064B)


      1 package nativeaudio
      2 
      3 import (
      4 	"bytes"
      5 	"errors"
      6 	"fmt"
      7 	"io"
      8 	"time"
      9 )
     10 
     11 // Stream decodes audio incrementally, yielding signed 16-bit
     12 // little-endian PCM through [io.Reader].
     13 //
     14 // It exists because the buffered API holds the whole decode in memory,
     15 // and PCM is roughly an order of magnitude larger than the compressed
     16 // input it came from. A few minutes of stereo audio is tens of
     17 // megabytes, which is fine for a sound effect and wasteful for a track.
     18 //
     19 // A Stream must be closed. Closing the Decoder that produced it waits
     20 // for every open Stream to be closed first.
     21 type Stream struct {
     22 	r      io.ReadCloser
     23 	format Format
     24 	done   func()
     25 }
     26 
     27 // Format describes the PCM this Stream produces. It is known as soon as
     28 // the Stream is opened, before any audio is read.
     29 func (s *Stream) Format() Format {
     30 	return s.format
     31 }
     32 
     33 // Read fills p with decoded PCM, returning [io.EOF] when the audio is
     34 // exhausted.
     35 func (s *Stream) Read(p []byte) (int, error) {
     36 	return s.r.Read(p)
     37 }
     38 
     39 // Close releases the decoder state behind the Stream. It is safe to
     40 // call more than once, and abandoning a Stream before [io.EOF] is fine
     41 // as long as it is closed.
     42 func (s *Stream) Close() error {
     43 	err := s.r.Close()
     44 	if s.done != nil {
     45 		s.done()
     46 		s.done = nil
     47 	}
     48 	return err
     49 }
     50 
     51 // newBufferedStream serves PCM that has already been fully decoded.
     52 //
     53 // Backends that cannot yet decode incrementally use this so that the
     54 // streaming API behaves correctly everywhere, at the cost of the memory
     55 // saving on those platforms.
     56 func newBufferedStream(pcm []byte, format Format) *Stream {
     57 	return &Stream{
     58 		r:      io.NopCloser(bytes.NewReader(pcm)),
     59 		format: format,
     60 	}
     61 }
     62 
     63 // Limits bound a single decode. A zero field means no limit.
     64 //
     65 // Malformed audio can make a decoder grind: fuzzing found a half-megabyte
     66 // file that Media Foundation decodes indefinitely, a few hundred
     67 // kilobytes of PCM at a time, never finishing and never failing. No
     68 // per-read timeout catches that, because every individual read completes.
     69 // Limits are the backstop, and matter whenever the input is untrusted.
     70 type Limits struct {
     71 	// MaxBytes caps the PCM a decode may produce.
     72 	MaxBytes int64
     73 
     74 	// MaxDuration caps how long a decode may run.
     75 	MaxDuration time.Duration
     76 }
     77 
     78 // DefaultLimits is applied to a Decoder created without WithLimits.
     79 //
     80 // The duration is generous by orders of magnitude: a healthy backend
     81 // decodes half an hour of audio in well under a second, so anything
     82 // still running after this is not making meaningful progress. No byte
     83 // limit is imposed by default, since a legitimately long recording is
     84 // legitimately large.
     85 func DefaultLimits() Limits {
     86 	return Limits{MaxDuration: 2 * time.Minute}
     87 }
     88 
     89 // ErrLimitExceeded reports that a decode hit its [Limits].
     90 var ErrLimitExceeded = errors.New("nativeaudio: decode exceeded its limits")
     91 
     92 // limited enforces Limits over a stream.
     93 type limited struct {
     94 	r        io.ReadCloser
     95 	limits   Limits
     96 	read     int64
     97 	deadline time.Time
     98 }
     99 
    100 func newLimited(r io.ReadCloser, l Limits) io.ReadCloser {
    101 	if l.MaxBytes <= 0 && l.MaxDuration <= 0 {
    102 		return r
    103 	}
    104 	lr := &limited{r: r, limits: l}
    105 	if l.MaxDuration > 0 {
    106 		lr.deadline = time.Now().Add(l.MaxDuration)
    107 		// Hand the deadline to the backend too, so a read already
    108 		// waiting on the decoder gives up with everything else rather
    109 		// than running on to its own backstop.
    110 		if d, ok := r.(interface{ SetDeadline(time.Time) }); ok {
    111 			d.SetDeadline(lr.deadline)
    112 		}
    113 	}
    114 	return lr
    115 }
    116 
    117 func (l *limited) Read(p []byte) (int, error) {
    118 	if !l.deadline.IsZero() && time.Now().After(l.deadline) {
    119 		return 0, fmt.Errorf("%w: still decoding after %s", ErrLimitExceeded, l.limits.MaxDuration)
    120 	}
    121 	if l.limits.MaxBytes > 0 {
    122 		if remaining := l.limits.MaxBytes - l.read; remaining <= 0 {
    123 			return 0, fmt.Errorf("%w: produced more than %d bytes", ErrLimitExceeded, l.limits.MaxBytes)
    124 		} else if int64(len(p)) > remaining {
    125 			p = p[:remaining]
    126 		}
    127 	}
    128 	n, err := l.r.Read(p)
    129 	l.read += int64(n)
    130 	return n, err
    131 }
    132 
    133 func (l *limited) Close() error {
    134 	return l.r.Close()
    135 }