Skip to content

bug(audio): 500ms pop on seek — SPSC ring buffer not purged #173

@InstaZDLL

Description

@InstaZDLL

Symptom

When seeking within a playing track (e.g. dragging the position bar from 1:00 to 1:30), the user hears ~500 ms of the pre-seek audio drain before the new position kicks in, producing an audible glitch / brief overlap. Reproduced on Windows, cpal shared mode, FLAC and AAC sources.

Root cause

The seek path in src-tauri/crates/app/src/audio/decoder.rs calls stream.seek_ms(ms) to reposition the symphonia reader, but the rtrb::Producer → cpal callback ring buffer keeps every sample already pushed before the seek. With the typical ring sized for several hundred ms of headroom, the user hears:

  1. ~200-500 ms of samples from the pre-seek position drain from the ring
  2. A discontinuity at the boundary (audible pop/click)
  3. Playback continues at the new position

A glance at the seek handlers confirms no ring drain happens:

ControlFlow::Seek(ms) => {
    // …
    stream.seek_ms(ms);   // only the symphonia reader moves
    // …
}

The resampler (rubato::FftFixedIn) also keeps internal history; without a reset, ~10-30 ms of interpolated samples are computed from the old position and bleed into the new one.

Fix sketch

In every Seek branch of the decoder loop (ControlFlow::Seek, PushOutcome::Seek, A-B loop wrap):

  1. Pause the cpal callback consumption (e.g. set a seek_in_progress atomic the callback checks and zeroes its output during)
  2. Drain the rtrb ring — either pop until empty client-side, or rebuild the producer/consumer pair and swap the producer through AudioCmd::SwapProducer
  3. Reset the resampler state (Resampler::reset() if exposed, else rebuild it with the same target rate)
  4. Apply a 5-10 ms fade-in on the first post-seek block to hide any residual discontinuity (cheap insurance even after the drain)
  5. Resume the callback

Acceptance criteria

  • Seek from any position to any position on a FLAC, MP3, AAC, OGG source produces no audible artifact
  • Seek during a smart crossfade (next-track prefetch in flight) doesn't desync
  • No regression on the A-B loop wrap path (it uses the same seek_ms helper)
  • Position update on the frontend matches the new audio position within one frame (verified by ear + log timestamps)

Notes

Phase 1.a waveflow-core extraction did not touch the audio engine — this bug is pre-existing on main. Surfaced via a user report on the v1.4.0 candidate.

Related callsites in decoder.rs: ControlFlow::Seek(ms), PushOutcome::Seek(ms), A-B loop wrap branch around loop_a_ms.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingrustPull requests that update rust codescope: backendRust/Tauri backend (src-tauri/)

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions