feat(quick-15): insert BufferedStreamer into player pipeline and increase speaker buffer

- BufferedStreamer wraps resampled streamer with 2s read-ahead buffer
- Speaker buffer increased from 100ms to 200ms for secondary protection
- Close old BufferedStreamer on track change and unload to prevent goroutine leaks
- Streamer chain: decode -> resample -> BufferedStreamer -> ctrl -> volume -> speaker
This commit is contained in:
2026-03-05 15:28:28 -05:00
parent 85b23acb24
commit 8a0b16a4ec
+24 -2
View File
@@ -48,6 +48,7 @@ type Player struct {
baseStreamer beep.Streamer
seeker beep.StreamSeeker
resampled beep.Streamer
buffered *BufferedStreamer
control *beep.Ctrl
volume *effects.Volume
speakerStreamer beep.Streamer
@@ -125,9 +126,12 @@ func (p *Player) InitSpeaker() error {
defer profiling.TimeOp(p.logger, "player.InitSpeaker")()
// TODO: allow user to change buffer size and speaker sample rate.
// Speaker buffer is 200ms (~8820 samples at 44100 Hz), providing
// secondary protection against underruns behind the read-ahead
// BufferedStreamer.
err := speaker.Init(
p.format.SampleRate,
p.format.SampleRate.N(time.Second/10),
p.format.SampleRate.N(time.Second/5),
)
if err != nil {
return fmt.Errorf(
@@ -307,8 +311,15 @@ func (p *Player) updateStreamers(
4, sr, speakerSampleRate, p.baseStreamer,
)
// Buffer resampled audio to decouple disk I/O from speaker
// timing. 2 seconds of read-ahead at speaker sample rate
// absorbs I/O stalls and GC pauses without audible glitches.
p.buffered = NewBufferedStreamer(
p.resampled, int(speakerSampleRate)*2,
)
// wrap in ctrl streamer to allow play/pause
p.control = &beep.Ctrl{Streamer: p.resampled}
p.control = &beep.Ctrl{Streamer: p.buffered}
// Preserve existing volume settings across track changes.
prevVolume := 0.0
@@ -432,6 +443,11 @@ func (p *Player) loadFileLocked(filePath string) error {
p.state = Stopped
speaker.Unlock()
// Stop the read-ahead goroutine for the previous track.
if p.buffered != nil {
p.buffered.Close()
}
if p.currentFile != nil {
if closeErr := p.currentFile.Close(); closeErr != nil {
p.logger.Warn(
@@ -604,11 +620,17 @@ func (p *Player) UnloadTrack() {
p.currentFile = nil
}
// Stop the read-ahead goroutine before releasing the chain.
if p.buffered != nil {
p.buffered.Close()
}
// Release streamer chain. Volume is intentionally kept so the
// user's volume setting persists across tracks.
p.baseStreamer = nil
p.seeker = nil
p.resampled = nil
p.buffered = nil
p.control = nil
p.speakerStreamer = nil
p.trackLengthMs = 0