Skip to content
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

Feature: ChannelRouter #656

Open
wants to merge 30 commits into
base: master
Choose a base branch
from

Conversation

iluvcapra
Copy link
Contributor

This is an implementation of a multichannel router/mixer source. See #653.

@iluvcapra iluvcapra changed the title ChannelRouter feature Feature: ChannelRouter Dec 8, 2024
@iluvcapra iluvcapra marked this pull request as ready for review December 10, 2024 04:04
0 => println!("Left speaker ramp up"),
1..10 => {
_ = controller.map(0, 0, n as f32 / 10.0);
_ = controller.map(0, 1, 0f32);
Copy link
Collaborator

@dvdsk dvdsk Dec 11, 2024

Choose a reason for hiding this comment

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

this is not very readable right? I can guess its channel from, channel too, gain? But we might be able to do better. I see a number of options, there might well be more:
1️⃣ make map take a struct like: controller.map(Mapping { from: 0, to: 1, gain: 0f32 })

2️⃣ make map a builder: controller.map().from(0).to(1).with_gain(0.f32).appy(). Here you could make the gain optional and do a default of 1.0

3️⃣ rename it so the argument names are in the name: controller.map_from_to_with_gain(0, 1, 0f32).

I think 2️⃣ is my favorite, though its a little more code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the builder too.

@iluvcapra
Copy link
Contributor Author

BTW I'm working on a separate project right now to try to profile all these different approaches, once I have that I'll post what I find and then we can move forward with this.


self.current_channel = 0;

for change in self.receiver.try_iter() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, it may do as first version, but polling channel is relatively expensive. In other places an additional flag is used to skip the loop altogether when there are no changes (e.g. mixer::Mixer::has_pending). See also #658.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This only checks the receiver once if there are no changes in it right? Is a single channel poll slower then checking an atomic flag?

edit: Nevermind I checked the source for mpsc and its kinda complex. Still I would like to have a benchmark for this source before making such a change. If only to enjoy the speedup, but it might also show there to be no effect. Maybe that complexity for mpsc compiles down to something real lean?

let gain = input_gains[self.current_channel as usize];
in_sample.amplify(gain)
})
.reduce(|a, b| a.saturating_add(b));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is real-time I'd prefer this to be more explicit. Maybe the matrix should be transposed to streamline this. This way instead of zip it would select a gain vector and then do the dot product of coefficients by input_buffer (it can be even optimized with SIMD).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I do not want to overburden @iluvcapra, this is already quite a big effort! So we can always leave optimizations for later and merge this first. Someone else can work on them too (maybe @PetrGlad). Let me know what you prefer @iluvcapra.

from: u16,
to: u16,
gain: f32,
) -> Result<(), ChannelRouterControllerError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ If the routing has to change one will have to remember to clear all the gains that are not needed anymore.
Maybe a "reset" function that sets all the gains to zero would be helpful.

❓ I wonder if we should allow specifying matrix at the initialization time, or let users to pass a list of coefficients.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably that is why I wanted to set the whole gain set at once: users may have the matrix on their side process and inspect it a they prefer and then use its to update the router.

/// - The first dimension is respective to the input channels
/// - The second is respective to the output channels
///
/// Thus, if a value at `map[1,1]` is 0.2, this signifies that the signal on
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is still confusing, I'd picked different numbers for input and output, or maybe just use names instead of constants (e.g. map[in,out])

//
// Doing it as a HashMap<(u16,u16), f32> is an option too but there's a penalty hashing these
// values, there's ways to speed that up though. It'd be great if the object upgraded its
// implementation if it got sufficiently big.
Copy link
Collaborator

Choose a reason for hiding this comment

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

No we do not need complex containers, if a sparse matrix is preferable I have an example how that can be implemented.


// This might be too fussy, a source should never break a frame in the middle of an
// audio frame.
let samples_to_take = min(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I'd prefer to require all the sources to produce only complete frames. But I think it is not actually enforced anywhere so far. This can be an assertion, though.

channel_router::channel_router(self, 2, mapping).1
}

/// Creates a [`ChannelRouter`] that mixes a 5.1 source in SMPTE channel order (L, R, C, Lfe, Ls,
Copy link
Collaborator

Choose a reason for hiding this comment

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

very nice example use-case 👍

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 12, 2024

BTW I'm working on a separate project right now to try to profile all these different approaches, once I have that I'll post what I find and then we can move forward with this.

very cool, I'm curious to see if what ends up as best lines up with our suspicions

@PetrGlad
Copy link
Collaborator

For mixing several input streams we can use similar class that uses the same algorithm but accepts several Sources at once. Our existing mixer only sums all inputs without any volume adjustments,

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 20, 2024

For mixing several input streams we can use similar class that uses the same algorithm but accepts several Sources at once. Our existing mixer only sums all inputs without any volume adjustments,

Isn't the better way to do this to have users wrap the streams they are mixing in a Volume struct?

@PetrGlad
Copy link
Collaborator

Yes, it is a simpler solution. Maybe we could add and example or make some kind of cookbook.

I was just pondering whether an über-mixer might be useful (the one like in this PR but allowing multiple inputs). For sparse matrix representation this would be straightforward to adapt (or have another more advances version).

By he way, the router in this PR should also handle situation when number of input channels change. At least for current API this is required.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 20, 2024

make some kind of cookbook.

I really like that idea! Sink/Player will be easy to use but the power of rodio lies in connecting the sources in an audio pipeline. We could have a bunch of examples but a cookbook is way nicer!

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.

3 participants