-
Notifications
You must be signed in to change notification settings - Fork 240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Runtime controlling sources in rodio, can we do better? #658
Comments
I was thinking couldn't there just be a trait like |
Also I think If a client needed a source to update itself at precise sample times, what you could do is a do a ETA- In fact you could do something like how complications work on the Apple Watch, where you provide a clousure that you can use to construct future events for a scheduled source, the API contract promises this might be called once a second or so in advance of the relevant samples, you return a set of changes and when they should happen, and the machinery does them when the samples come up. But client code never runs on the audio thread. |
So each source gets an associated type State which is a controller of that source (and maybe Clone + Send + Sync for ease of use)? I do have ideas for an When you use let (source, amplify_controller) = some_source.amplify().controller();
let (source, pausable_controller) = source.pausable().controller();
play(source);
amplify_controller.set_gain(0.5);
pausable_controller.set_paused(true);
The idea is that the user does the minimal amount of work in the closure, for example check if a queue has received a message. That requires the user to be efficient and usually it will boil down to something like this: some_source.periodic_callback(move |src| match rx.recv() {
Cmd::Pause => src.inner_mut().set_paused(true),
Cmd::Resume => src.inner_mut().set_paused(false),
Cmd::Amplify(va) => src.inner_mut().inner_mut().set_amplify(val)
}
) But by allowing the user to run on the audio thread we eliminate a ton of synchronization overhead. None of the source parameters need to be atomic, neither do we need any mutexes at all. All parameters live on the stack and because the final source is one big type the optimizer can go crazy. That should lead to really good performance, I say should since it has never been bench-marked.
We could do something like |
periodic callback has a big issue when one of the stages in the audio pipelines is I think we can address the most common mix cases by having a example: let source_a: Sine = ..
let source_a = source_a.pausable();
let source_b: Square = ..
let (rx, tx) = mpsc::channel();
let source: Mix2<Sine, Square> = source_a.mix2(source_b);
let source = source.with_periodic_callback(move |mix| match rx.recv() {
Cmd::SetSineFreq(new) => mix.get_1th_mut().freq = new,
Cmd::SetSquareFreq(new) => mix.get_2th_mut().freq = new,
Cmd::PauseSine => mix.get_1th_mut().into_inner().set_paused(true),
})
play(source)
// from UI thread or somewhere else in the program (tx can be cloned)
tx.send(SetSineFreq(9001);
// do something else then
tx.send(Cmd::PauseSine); A similar problem seems to exist with |
Brittle chain navigation in Source is unnecessary. It is just a consequence of having all the controls in the same place. I even wanted to rewrite that. let external source = open_mp3();
let (pausable, pause_ctl) = pausable(external_source);
let (amplified, volume_ctl) = amplify(pausable); This way there is always only one periodic callback right next to its source. And the source's constructor knows what to change in there. So only pausable implementation needs to know how to pause itself. API-wise we'll need constructors for static (the existing ones) and controllable versions of a source. Or alternatively in addition to normal constructors add also wrapping functions in form If we still want to keep default source with all the gadgets included, then it could do the above and keep all the This also lets users to construct only filters they need. |
I like the returned-controller approach as well, I could take a swing at writing generics that have this pattern. |
Its slightly more nuanced in my opinion. I would say its a consequence of wanting all parameters on the stack in the audio thread with minimal overhead. I do not see another way of doing that.
I would keep the current API for that, that way we do not lose the unique performance advantage (I am pretty sure) rodio has. I like this builder like pattern I proposed earlier: The The controller should be really easy to use, it should implement : Send, Sync and Clone in addition to Debug ofc. |
Regarding current source implementation, yes it looks indeed that the current implementation of What I had in mind for the callback version of let source = source
.speed(1.0)
.periodic_access(Duration::from_millis(5), move |src| {
// Update speed via src here
}).
.track_position()
.periodic_access(Duration::from_millis(5), move |src| {
// Update position via src here
}).
.pausable(false)
.periodic_access(Duration::from_millis(5), move |src| {
// Update pausable via src here
}).
.amplify(1.0)
.periodic_access(Duration::from_millis(5), move |src| {
// Update amplify via src here
}).
.skippable()
.periodic_access(Duration::from_millis(5), move |src| {
// Update skippable via src here
}).
.stoppable()
.periodic_access(Duration::from_millis(5), move |src| {
// Control stoppable via src here
}) instead of let source = source
.speed(1.0)
.track_position()
.pausable(false)
.amplify(1.0)
.skippable()
.stoppable()
.periodic_access(Duration::from_millis(5), move |src| {
// src.inner().inner().inner() ...
}) |
could you explain the advantage of this approach? Is it for code readability/maintainability? Or is the 'callback version' something else entirely? |
The advantage is that this approach is composable, and easier to read. Although I admit that if all existing Sink functionality is required, the existing implementation is probably the most efficient (at least if updates are very frequent, at 5ms poll it may not be the case). That is because no indirection or |
Just in case |
Chains of rodio
Source
's are currently only controllable using periodic callback. It has a few great advantages namely:It does have its downsides it require users to either: have lots of periodic callbacks which negates the advantages, for example:
Or to use lots of
inner_mut
to navigate through the pipeline/chain. This feels akward and is less readable.Users also have to set up their own method of moving intentions/commands into the callbacks. Two solutions are: checking a queue receiver or always updating inner sources from loaded atomic's. This can be hard for newer rust users or those not skilled at concurrency.
Over the last few months a few alternatives have been proposed such as:
There are also other audio projects we can take inspiration from, such as:
hodaun and
kira.
Not all of these solutions are optimal for every source. Where atomics might
work well for simple few paramater sources like
amplify
andpausable
they couldreally hamper performance for things like channel_router.
Lets discuss our options highlighting advantages and potential drawbacks then
evaluate those.
The text was updated successfully, but these errors were encountered: