commit 32313045de260b778f0f6b76bbacfa1154136599
parent 6a28e1fb185cdd2edf9a9cef7c6200d031a4d3cc
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 16 Sep 2026 21:28:25 -0400
audio: [Windows] surface source reader errors instead of looping
ReadSample, ConvertToContiguousBuffer and Lock results were discarded.
A mid-stream decode failure leaves the sample nil and sets only
MF_SOURCE_READERF_ERROR, which the loop did not test, so next() spun on
ReadSample forever. A failed Lock would have handed a nil pointer to
unsafe.Slice. Errors now propagate through Read, a media type change is
reported rather than silently truncating, and samples that are skipped
for repeating a timestamp or that arrive with a terminal flag are
released instead of leaked. The timestamp bookkeeping drops the
prevTimestamp-1 trick for an explicit hasPrev flag.
Diffstat:
| M | audio_windows.go | | | 84 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- |
1 file changed, 60 insertions(+), 24 deletions(-)
diff --git a/audio_windows.go b/audio_windows.go
@@ -194,8 +194,8 @@ type SourceReader struct {
chunk *byte // start of chunk of audio data
chunkSz uint32 // size of chunk
- prevTimestamp int64
- currentTimestamp int64
+ prevTimestamp int64 // timestamp of the last emitted sample.
+ hasPrev bool // whether any sample has been emitted yet.
data []byte // Go view of the audio data, backed by native memory.
}
@@ -216,49 +216,83 @@ func (s *SourceReader) Read(p []byte) (int, error) {
return s.copyInto(p), nil
}
- if !s.next() {
+ ok, err := s.next()
+ if err != nil {
+ return 0, err
+ }
+ if !ok {
return 0, io.EOF
}
return s.copyInto(p), nil
}
-// next reads the next audio sample, returning true if found, or false if EOF.
-func (s *SourceReader) next() bool {
+// next reads the next audio sample into s, returning false at end of
+// stream. Any error from the source reader ends the stream.
+func (s *SourceReader) next() (bool, error) {
for {
- var flags uint32
-
- // Read the next sample; skipping samples with matching time stamps.
- // For some reason ReadSample can produce more than one sample at time 0.
- // Emitting all of them produces largers files and audio artifacts.
- s.source.ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nil, &flags, &s.currentTimestamp, &s.sample)
-
+ var (
+ flags uint32
+ timestamp int64
+ sample *IMFSample
+ )
+
+ err := s.source.ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nil, &flags, ×tamp, &sample)
+
+ // A sample can accompany a flag we treat as terminal, so release
+ // it before deciding whether to stop.
+ if err != nil || flags&(MF_SOURCE_READERF_ERROR|MF_SOURCE_READERF_ENDOFSTREAM|MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) != 0 {
+ sample.Release()
+ }
+ if err != nil {
+ return false, fmt.Errorf("reading sample: %w", err)
+ }
+ if flags&MF_SOURCE_READERF_ERROR != 0 {
+ return false, fmt.Errorf("reading sample: source reader reported an error")
+ }
if flags&MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED != 0 {
- return false
+ return false, fmt.Errorf("reading sample: media type changed mid-stream")
}
-
if flags&MF_SOURCE_READERF_ENDOFSTREAM != 0 {
- return false
+ return false, nil
}
- if s.sample == nil {
+ // The reader can legitimately return no sample and no terminal
+ // flag (for example, while a decoder is buffering). Ask again.
+ if sample == nil {
continue
}
- if s.currentTimestamp != s.prevTimestamp-1 {
- break
+ // Skip samples that repeat the previous timestamp. ReadSample can
+ // produce more than one sample at time 0; emitting all of them
+ // produces larger output and audible artifacts.
+ if s.hasPrev && timestamp == s.prevTimestamp {
+ sample.Release()
+ continue
}
- }
- s.prevTimestamp = s.currentTimestamp + 1
+ s.sample = sample
+ s.prevTimestamp = timestamp
+ s.hasPrev = true
+ break
+ }
- s.sample.ConvertToContiguousBuffer(&s.buffer)
- s.buffer.Lock(&s.chunk, nil, &s.chunkSz)
+ if err := s.sample.ConvertToContiguousBuffer(&s.buffer); err != nil {
+ s.sample.Release()
+ s.sample = nil
+ return false, fmt.Errorf("converting sample to contiguous buffer: %w", err)
+ }
+ if err := s.buffer.Lock(&s.chunk, nil, &s.chunkSz); err != nil {
+ s.buffer.Release()
+ s.sample.Release()
+ s.buffer, s.sample = nil, nil
+ return false, fmt.Errorf("locking sample buffer: %w", err)
+ }
// Make a Go slice view of the data for easy consumption.
- s.data = unsafe.Slice((*byte)(s.chunk), s.chunkSz)
+ s.data = unsafe.Slice(s.chunk, s.chunkSz)
- return true
+ return true, nil
}
// copyInto copies audio data into p, unlocking the memory once fully copied.
@@ -271,6 +305,7 @@ func (s *SourceReader) copyInto(p []byte) int {
s.buffer.Unlock()
s.buffer.Release()
s.sample.Release()
+ s.buffer, s.sample = nil, nil
s.data = nil
}
@@ -299,6 +334,7 @@ const (
MF_VERSION = 0x20070
MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED = 0x20
MF_SOURCE_READERF_ENDOFSTREAM = 0x2
+ MF_SOURCE_READERF_ERROR = 0x1
MF_SOURCE_READER_ALL_STREAMS = 0xfffffffe
MF_SOURCE_READER_FIRST_AUDIO_STREAM = 0xfffffffd
)