Skip to content

Conversation

dvdsk
Copy link
Member

@dvdsk dvdsk commented Aug 30, 2025

Sources in rodio get queried by the OutputStream. That means that the stream calls the next method on a Source. Only then can the Source do something, like call next on a Source it wraps.

For pausing we want to turn this flow around, we want to affect the OutputStream from a Source (possibly wrapped by many others).

To do so we need to get access to the OutputStream in a Source. We do so by making the OutputStream call set_pause_handle(<some handle>) on each Source added to it. During that call of set_pause_handle each of those sources calls set_pause_handle on which ever source the wrap. This goes on all through the Sources tree.

A Pausable source keeps a copy of the pause handle passed to it when set_pause_handle got called on it. When it needs to pause the stream it can simply call pause() on its stored pause handle.

There will be multiple types of pause handles:

  • One which simply (un)pauses the cpal::Stream. It is passed to all the sources through set_pause_handle on OutputStream creation.
  • One made for Mixer. It is passed to all streams added to the Mixer. This wraps whichever pause handle was set on the mixer. It only calls pause on that handle when all streams in the mixer have called pause on it.
  • Maybe more I did not think off?

`Source`s in rodio get queried by the `OutputStream`. That means that the
stream calls the next method on a `Source`. Only then can the `Source` do
something, like call next on a `Source` it wraps.

For pausing we want to turn this flow around, we want to affect the
`OutputStream` from a `Source` (possibly wrapped by many others).

To do so we need to get access to the `OutputStream` in a `Source`. We
do so by making the `OutputStream` call `set_pause_handle(<some handle>)`
on each `Source` added to it. During that call of `set_pause_handle`
each of those sources calls `set_pause_handle` on which ever source the
wrap. This goes on all through the `Source`s tree.

A `Pausable` source keeps a copy of the pause handle passed to it when
`set_pause_handle` got called on it. When it needs to pause the stream
it can simply call `pause()` on its stored pause handle.

There will be multiple types of pause handles:
- One which simply (un)pauses the `cpal::Stream`. It is passed to all
  the sources through `set_pause_handle` on `OutputStream` creation.
- One made for `Mixer`. It is passed to all streams added to the `Mixer`.
  This wraps whichever pause handle was set on the mixer. It only calls
  pause on that handle when all streams in the mixer have called pause
  on it.
- Maybe more I did not think off?
@dvdsk
Copy link
Member Author

dvdsk commented Aug 30, 2025

Note: another thing to think about. What should a paused source do when next is called while its paused?

  • return None
  • return Some(0.0)
  • panic

I'm leaning towards panic. We need to think about the possibility for race conditions here though. Especially if we start moving the pause handle out of the sources themselves (like a Pausable getting a PausHandle that is Clone+Send+Sync and can be moved all over the users application.

@roderickvd
Copy link
Member

Heh, I understand this took you a few hours. Architecture looks extensible and the code is well laid out.

As a mental model I'm thinking how much sense it makes that a Source is Pausable. The sink / output stream can be pausable, when it should stop consuming audio to play, but a source isn't really pausable. Well, some real-time network streams may be, but in any other case it's weird to say that a set of samples is paused.

Note: another thing to think about. What should a paused source do when next is called while its paused?

  • return None
  • return Some(0.0)
  • panic

I'm leaning towards panic. We need to think about the possibility for race conditions here though. Especially if we start moving the pause handle out of the sources themselves (like a Pausable getting a PausHandle that is Clone+Send+Sync and can be moved all over the users application.

I think that latter use case would definitely be something that users would be expecting. Heck, I know that this point about CPU usage came up in a similar context in librespot when the users pause their app and not the source.

Also, users may not want auto-pausing, because it may trigger behavior downstream the sink that they don't want. Like a DAC switching off its outputs or dropping signal lock.

Have you considered doing it the other way around? Having the output stream (optionally) switching into "paused" mode when its last source was exhausted? Switching it on again when a mixer or queue signals it has appended new inputs?


fn set_pause_handle(&mut self, pause_handle: crate::source::PauseHandle) {
// TODO create MixerController in pausable. It will only set paused on this
// pause handle when all sources in the mixer have been paused
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have been paused or exhausted.

@dvdsk
Copy link
Member Author

dvdsk commented Aug 31, 2025

As a mental model I'm thinking how much sense it makes that a Source is Pausable. The sink / output stream can be pausable, when it should stop consuming audio to play, but a source isn't really pausable. Well, some real-time network streams may be, but in any other case it's weird to say that a set of samples is paused.

We already have the Pausable source and it is widely used. But we should discourage/forbid users from using the pause_handle directly. Ideally we make the whole thing private but will users then still be able to extend rodio....

Also, users may not want auto-pausing, because it may trigger behavior downstream the sink that they don't want. Like a DAC switching off its outputs or dropping signal lock.

Good point, outputs switching off would be annoying. Maybe we could present some options:

  • enable it for everything
  • just affect the rodio parts (mixers/queue etc), never call stream.pause()

Have you considered doing it the other way around? Having the output stream (optionally) switching into "paused" mode when its last source was exhausted? Switching it on again when a mixer or queue signals it has appended new inputs?

I failed to write it out but that is part of what I planned for the pause_handle. I've added a comment to the code skeleton noting that for mixer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants