rodio icon indicating copy to clipboard operation
rodio copied to clipboard

Draft of an optimized pausing architecture

Open yara-blue opened this issue 3 months ago • 3 comments

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?

yara-blue avatar Aug 30 '25 17:08 yara-blue

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.

yara-blue avatar Aug 30 '25 19:08 yara-blue

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?

roderickvd avatar Aug 30 '25 20:08 roderickvd

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.

yara-blue avatar Aug 31 '25 09:08 yara-blue