fix: flush BufferedStreamer ring buffer on seek to prevent stale audio
When seeking, the underlying decoder position was updated but the BufferedStreamer's ring buffer still contained up to 2 seconds of pre-seek audio data. The speaker would drain this stale buffer before playing audio from the new position, causing an audible delay where the old position's audio continued playing. Add a Flush() method to BufferedStreamer that resets the ring buffer pointers, and call it in seekLocked() immediately after a successful seek. This ensures the speaker starts playing from the seeked position without any stale audio artifact.
This commit is contained in:
@@ -177,6 +177,19 @@ func (bs *BufferedStreamer) Err() error {
|
||||
return bs.err
|
||||
}
|
||||
|
||||
// Flush discards all buffered samples so the next Stream call
|
||||
// returns freshly-read data from the source. This must be called
|
||||
// after seeking the underlying source to prevent stale pre-seek
|
||||
// audio from being played back.
|
||||
func (bs *BufferedStreamer) Flush() {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
|
||||
bs.readPos = 0
|
||||
bs.writPos = 0
|
||||
bs.count = 0
|
||||
}
|
||||
|
||||
// Close signals the read-ahead goroutine to stop. It is safe to
|
||||
// call multiple times.
|
||||
func (bs *BufferedStreamer) Close() {
|
||||
|
||||
@@ -260,6 +260,100 @@ func TestBufferedStreamer_EmptyBufferReturnsSilence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedStreamer_Flush(t *testing.T) {
|
||||
// Use a slow source so the read-ahead goroutine cannot fully
|
||||
// drain it before we flush. Each chunk sleeps 5ms, giving us
|
||||
// time to flush while data is still being produced.
|
||||
src := &slowStreamer{
|
||||
inner: finiteStreamer(5000),
|
||||
delay: 5 * time.Millisecond,
|
||||
}
|
||||
bs := NewBufferedStreamer(src, 2048)
|
||||
|
||||
defer bs.Close()
|
||||
|
||||
// Let read-ahead fill some data.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Read a few samples to confirm data is buffered.
|
||||
buf := make([][2]float64, 32)
|
||||
n, ok := bs.Stream(buf)
|
||||
|
||||
if !ok || n == 0 {
|
||||
t.Fatal("expected buffered data before flush")
|
||||
}
|
||||
|
||||
// Record the last sample value we saw.
|
||||
lastBefore := buf[n-1][0]
|
||||
|
||||
// Verify the buffer had more data than we consumed (i.e.
|
||||
// there's stale data in the ring that Flush should discard).
|
||||
bs.mu.Lock()
|
||||
countBeforeFlush := bs.count
|
||||
bs.mu.Unlock()
|
||||
|
||||
if countBeforeFlush == 0 {
|
||||
t.Fatal("expected non-empty ring buffer before flush")
|
||||
}
|
||||
|
||||
// Flush discards all buffered data.
|
||||
bs.Flush()
|
||||
|
||||
// Verify the ring buffer is empty after flush.
|
||||
bs.mu.Lock()
|
||||
countAfterFlush := bs.count
|
||||
bs.mu.Unlock()
|
||||
|
||||
if countAfterFlush != 0 {
|
||||
t.Fatalf(
|
||||
"expected 0 samples after flush, got %d",
|
||||
countAfterFlush,
|
||||
)
|
||||
}
|
||||
|
||||
// Wait for read-ahead to refill with fresh data.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// The next non-zero sample must come from AFTER the
|
||||
// pre-flush position in the source (i.e. its value must be
|
||||
// greater than lastBefore + countBeforeFlush, since those
|
||||
// samples were discarded).
|
||||
foundNonZero := false
|
||||
|
||||
for range 200 {
|
||||
n, ok = bs.Stream(buf)
|
||||
|
||||
for i := range n {
|
||||
if buf[i][0] != 0 {
|
||||
// The sample must be strictly greater than what
|
||||
// was buffered before flush.
|
||||
if buf[i][0] <= lastBefore {
|
||||
t.Fatalf(
|
||||
"after flush, got sample value %f "+
|
||||
"which is <= pre-flush value %f "+
|
||||
"(stale data not discarded)",
|
||||
buf[i][0], lastBefore,
|
||||
)
|
||||
}
|
||||
|
||||
foundNonZero = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundNonZero || !ok {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !foundNonZero {
|
||||
t.Fatal("expected non-zero samples after flush")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedStreamer_Close(t *testing.T) {
|
||||
// Use a source that never drains.
|
||||
infinite := beep.StreamerFunc(func(samples [][2]float64) (int, bool) {
|
||||
|
||||
@@ -818,6 +818,13 @@ func (p *Player) seekLocked(targetSeconds int) error {
|
||||
|
||||
speaker.Unlock()
|
||||
|
||||
// Flush the read-ahead buffer so the speaker immediately
|
||||
// plays audio from the new position instead of draining
|
||||
// up to 2 seconds of stale pre-seek samples.
|
||||
if p.buffered != nil {
|
||||
p.buffered.Flush()
|
||||
}
|
||||
|
||||
if p.mediaControls != nil {
|
||||
p.mediaControls.NotifySeek(targetSeconds)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user