Skip to content

rodio backend: pause drains the queued buffer, delaying the audio and PlayerEvent::Paused by up to ~0.5s #1835

Open

Description

@Tako88

What happens

With rodio-backend, Player::pause() takes up to about half a second to take
effect: the audio keeps playing for that long, and PlayerEvent::Paused is only
emitted afterwards. Resuming is immediate, so pausing feels noticeably slower than
playing.

Why

src/audio_backend/rodio.rs:

fn stop(&mut self) -> SinkResult<()> {
    self.rodio_sink.sleep_until_end();
    self.rodio_sink.pause();
    Ok(())
}

sleep_until_end() waits until everything already appended to the sink has
played. handle_pause calls ensure_sink_stopped(false) before it sends the
event:

PlayerState::Playing { .. } => {
    self.state.playing_to_paused();
    self.ensure_sink_stopped(false);        // the whole drain happens here
    self.send_event(PlayerEvent::Paused { .. });
}

handle_play is the other way round — it sends Playing before it touches
the sink — which is why resuming does not pay the same cost.

The upper bound is the queue depth, which RodioSink::write caps:

// Chunk sizes seem to be about 256 to 3000 ish items long.
// Assuming they're on average 1628 then a half second buffer is:
// 44100 elements --> about 27 chunks
while self.rodio_sink.len() > 26 {
    thread::sleep(Duration::from_millis(10));
}

So sleep_until_end() waits for up to ~27 chunks, roughly half a second of
already-decoded audio.

Why it matters

Any client that reflects pause state from PlayerEvent::Paused shows it late, and
the listener hears up to half a second of audio after pressing pause. The other
backends do not have this shape: alsa::stop and pulseaudio::stop call
drain(), which is bounded by the device buffer rather than by everything the
decoder has queued ahead.

Suggested direction

A pause does not need the sink closed. rodio_sink.pause() returns immediately and
keeps the queue, and a later start() (play()) continues from where it stopped.
Either:

  • give the sink a suspended state that handle_pause uses instead of
    ensure_sink_stopped, or
  • have RodioSink::stop stop the audio first and not block on the queue.

Draining plays out what is already buffered so that nothing is cut mid-packet,
which is presumably the intent — but pause() does not lose those samples either,
it keeps them queued. So pausing first, and not draining, is both immediate and
lossless.

Reproducing

cargo run --features rodio-backend --example play -- <track uri>

Press pause while a track is playing: the audio stops and Paused arrives about
half a second later. Resume is immediate.

Version: librespot 0.8.0, rodio-backend, Linux, cpal 0.16 / ALSA backend.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions