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

Fix(484) - ChannelVolume misbehaving #493

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

Conversation

DivineGod
Copy link
Contributor

This PR will probably collect a few fixes that relate to #484 as I go through rodio to find all the issues that seem to prevent proper use of ChannelVolume.

I think I've identified a few areas that need attention, first being the ChannelCountConverter which is started with an Empty source when Sink::try_new is called. This would cause an issue where the channel converter had the wrong to count, as the actual source input channel count might not necessarily be 1 as the empty source was specifying. By setting Empty channels to 0 and handling the cases where they show up, we can more gracefully handle these issues.

Next issue is an issue with the sample rate converter where if there's a mismatch the ChannelVolume once again seem to be messed up and not produce the right results. This might also have been identified in other issues; I've not triaged them all yet.

@DivineGod DivineGod force-pushed the fix/channel-volume branch 2 times, most recently from 0f059bd to 50b6f40 Compare April 15, 2023 08:59
@DivineGod DivineGod marked this pull request as ready for review April 16, 2023 23:18
@DivineGod DivineGod force-pushed the fix/channel-volume branch from 50b6f40 to 1167092 Compare May 1, 2023 00:50
Copy link
Collaborator

@dvdsk dvdsk left a comment

Choose a reason for hiding this comment

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

First thanks a lot for going after this issue. Does the example your PR adds reproduce the issue? I would love to have a test that reproduces it.

If I understand correctly your approach modifies the channel converter and sample rate converter to recognize the Empty source. They do so by checking for its now nonsensical channel count and sample rate of zero.

An alternative would be removing the use of empty in queue (used by Sink::try_new) here:

current: Box<dyn Source<Item = S> + Send>,

We could use a Option<()> there and make the None branch play 'empty' sounds.

On the other hand, Empty probably has other valid uses....

Quite a difficult one this. I do not have any more time right now, ill come back to this.

@@ -36,17 +39,17 @@ where
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
None
Some(0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure about this change. None means the frame length will not change going forward. I do not know what Some(0) means. Filters/wrappers around the Empty source will treat every new sample as a new frame and run code paths to handle that.

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 don't recall why I had that in there. I don't think it needs to be there. So I'll happily revert that change

@DivineGod
Copy link
Contributor Author

[...] Does the example your PR adds reproduce the issue? I would love to have a test that reproduces it.

Yes, it does, running the example on master produces a nasty clipped sound on all channels and then only output on channel 0. Note for reproducing is that the sample rate of the output device seems to have to be 48kHz on my setup. If it's not then I run into the sample rate issue that I mentioned.

@dvdsk
Copy link
Collaborator

dvdsk commented Apr 21, 2024

Yes, it does, running the example on master produces a nasty clipped sound on all channels and then only output on channel 0.

Nice, then I can try it out and play around a little with the PR

@dvdsk
Copy link
Collaborator

dvdsk commented Apr 21, 2024

I do not yet understand why ChannelVolume is misbehaving. I am gonna have to play with it a bit to find that out and to understand it better.

I can not merge this until I understand everything around it.

Unfortunately I do not have a lot of time allocated for this, so it could a bit. I'll keep you updated.

@DivineGod
Copy link
Contributor Author

That’s totally understandable. I am happy to help out getting it to a state where it fixes things and there’s an understanding of why it all works out

@dvdsk dvdsk force-pushed the master branch 3 times, most recently from b10961c to a7f67b3 Compare October 10, 2024 00:14
@dvdsk
Copy link
Collaborator

dvdsk commented Oct 17, 2024

I have some time now :)

If you still have time to help out, could you rebase or merge master to resolve the conflict?

As I understand the PR sample-rate & channel-count zero function as a sort of flag/placeholder. Then the behavior of various rodio parts change when they see it. Specifically the converters pick the output instead of the input as target count/rate. Would Option not work better for that role then zero?

@dvdsk
Copy link
Collaborator

dvdsk commented Oct 17, 2024

Oh and the changelog needs an update marking this is fixed now

@DivineGod
Copy link
Contributor Author

If you still have time to help out, could you rebase or merge master to resolve the conflict?

Rebased to sort merge conflict

@DivineGod
Copy link
Contributor Author

Just rested the example after rebase; it has regressed to not sorting out the channel volumes correctly. I'll try to investigate and come up with some more in-depth unit tests as well to see if I can better isolate the issue.

A preliminary test in source/channel_volume is showing that that part is iterating correctly. But running a 6 channel output with the example distributes the first two provided volumes over the 6 total outputs.

@DivineGod

This comment was marked as resolved.

@DivineGod
Copy link
Contributor Author

I've added a test to sink.rs which reproduces my issue with the following assert:

---- sink::tests::test_channel_volume stdout ----
thread 'sink::tests::test_channel_volume' panicked at src/sink.rs:462:9:
assertion `left == right` failed
  left: Some(3.0517578e-5)
 right: Some(1.0)

I thought it repro'd but it didn't. I've pushed a working test for now.

@dvdsk dvdsk added this to the 0.20 milestone Oct 23, 2024
@dvdsk
Copy link
Collaborator

dvdsk commented Oct 23, 2024

I thought it repro'd but it didn't. I've pushed a working test for now.

thanks! and thank you very much for hunting for the bug!

@dvdsk dvdsk removed this from the 0.20 milestone Nov 2, 2024
@dvdsk dvdsk force-pushed the master branch 4 times, most recently from 0ee0413 to 073fdb1 Compare November 8, 2024 00:55
@will3942
Copy link

Any update on this bug? I fear we're about to run into this issue as we try to use a 10 channel interface with our app that uses ChannelVolume.

@DivineGod
Copy link
Contributor Author

Not yet, I’m afraid.

I’ve been busy with life events. I’ll have time to look into it more next week, hopefully

src/sink.rs Outdated
let v = vec![100.0f32];
let input = SamplesBuffer::new(1, 48000, v.clone());

// High rate to avoid immediate control.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you explain this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Honestly, no idea. I'll be spending the day today going through this throughly to actually try to understand what I did.

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'm going to remove the test here. I don't think it's actually helpful to have there

@PetrGlad
Copy link
Collaborator

PetrGlad commented Dec 1, 2024

@dvdsk BTW, what is the use case for ChannelVolume? It does simple mono conversion (with saturating_add) and only then splits it into output channels applying volume to each output channel. Including mono conversion looks odd to me. And this mono conversion also means there is no way to adjust channel volumes independently while keeping the stereo scene,

I would split ChannelVolume into a Mono converter and a per channel volume converter.

Looks like rodio does not have a limiter? I'd want one to muffle saturation distortions and make sound more "analog".

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 2, 2024

@dvdsk BTW, what is the use case for ChannelVolume?

its only used in the spatial source (and indirectly in spatial sink):

/// A simple spatial audio source. The underlying source is transformed to Mono
/// and then played in stereo. The left and right channel's volume are amplified
/// differently depending on the distance of the left and right ear to the source.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 2, 2024

Looks like rodio does not have a limiter? I'd want one to muffle saturation distortions and make sound more "analog".

If a limiter is different then a low pass/high pass/band pass filter then we do not have one atm. Might be a good add.

@will3942
Copy link

will3942 commented Dec 2, 2024

@dvdsk BTW, what is the use case for ChannelVolume? It does simple mono conversion (with saturating_add) and only then splits it into output channels applying volume to each output channel. Including mono conversion looks odd to me. And this mono conversion also means there is no way to adjust channel volumes independently while keeping the stereo scene,

I would split ChannelVolume into a Mono converter and a per channel volume converter.

Looks like rodio does not have a limiter? I'd want one to muffle saturation distortions and make sound more "analog".

We're currently using this to be able to output different sources onto different channels on a device, we have a scenario where we want to be able to do the following using a 10 channel USB audio interface:

  • Output Track A onto Channel 1, 4, 7
  • Output Track B onto Channel 2, 3, 5, 6
  • Output Track C Onto Channel 8

with channels 9 and 10 silent.

@DivineGod
Copy link
Contributor Author

Having some time to look at this. I've rebased the branch and re-run my channel_volume example on both the master branch and this branch. It looks to me that my branch is fixing the issue I'm seeing compared to what happens on master

@DivineGod
Copy link
Contributor Author

I've been testing this by using Loopback on macOS to setup a virtual interface with 6 output channels and setting it as the default output device and noting that the volume meters are displaying correctly the output levels needed.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 4, 2024

I've been testing this by using Loopback on macOS to setup a virtual interface with 6 output channels and setting it as the default output device and noting that the volume meters are displaying correctly the output levels needed.

very nice! Thanks for putting in all this effort (reporting, finding fixing & testing!). I left a few comments on the source. It mainly comes down to this:

I am afraid we might screw up ChannelVolume again in the future because the match { ... 0 => to } on their own seem puzzeling. We might remove them in the future without realizing. An integration test would catch it, alternatively a short comment at everyone of those matches might prevent such a thing too. We are playing with the idea of outer/later sources being able to instruct their inner sources. They can then pass them a preferred sample_rate. Would this get fixed if empty could get instructed to copy the sample_rate from an outer source? Or does that do nothing?

@DivineGod
Copy link
Contributor Author

I've implemented Option for channels and sample_rate and fixed all the files. I've left a bunch of FIX-ME comments in places where I wasn't too sure what the correct path forward might be

@DivineGod DivineGod force-pushed the fix/channel-volume branch 3 times, most recently from 9ea54c1 to 9f95a58 Compare December 5, 2024 02:38
This is a major refactor touching all the parts.
The idea behind it is that for the exceptional case of `Empty` we can supply None to either and otherwise we either have supplied values or some "sane" defaults available to use
@dvdsk
Copy link
Collaborator

dvdsk commented Dec 5, 2024

I was afraid adding all the options might reduce performance, but I am measuring a slight increase.. thats pretty neat.

@PetrGlad
Copy link
Collaborator

PetrGlad commented Dec 5, 2024

Folks, this change worries me because I do not understand where the actual problem is. Number of channels in Empty should not affect anything if downstream sinks are implemented properly. If reducing Empty channels to 0 helps, that means the actual problem is elsewhere downstream and this change only masks it.

Or did I miss something?

I do not see any code that handles even number of channels differently, except Spatial. If I had to guess, I still think that that maybe chunks are not handled correctly somewhere so after a change (chunk end) the input number of channels change but the resulting stream is shifted by some samples, which in turn confuses output channels.

@PetrGlad
Copy link
Collaborator

PetrGlad commented Dec 5, 2024

// Side-notes:

Also, I could not see any tests that check conversion to mono in ChannelVolume (I still find this down-mixing confusing, but that is an other issue). Tests for ChannelVolume always use only single channel input.

I think Spatial should also adjust relative phases for left and right ear, not only loudness to properly simulate spatial position.

@PetrGlad
Copy link
Collaborator

PetrGlad commented Dec 5, 2024

And this is just theoretical, I am wondering if it makes sense to have typed streams, that are sequences of fixed size slices each containing all channel samples for a particular moment. This will make confusion of channels in a stream unlikely.

@DivineGod
Copy link
Contributor Author

Folks, this change worries me because I do not understand where the actual problem is. Number of channels in Empty should not affect anything if downstream sinks are implemented properly. If reducing Empty channels to 0 helps, that means the actual problem is elsewhere downstream and this change only masks it.

Or did I miss something?

I do not see any code that handles even number of channels differently, except Spatial. If I had to guess, I still think that that maybe chunks are not handled correctly somewhere so after a change (chunk end) the input number of channels change but the resulting stream is shifted by some samples, which in turn confuses output channels.

So looking through the actual meat of the change here again. It looks like source/uniform.rs is the place where the channel count fix is crucial. And having an option makes it a little more clear.

I am on the same page as you with concerns about the fragility of this.

@DivineGod
Copy link
Contributor Author

// Side-notes:

Also, I could not see any tests that check conversion to mono in ChannelVolume (I still find this down-mixing confusing, but that is an other issue). Tests for ChannelVolume always use only single channel input.

I think Spatial should also adjust relative phases for left and right ear, not only loudness to properly simulate spatial position.

FWIW, the use case I was using channel volume for was having multiple mono-sources spatially located using distance-based amplitude panning (DBAP) which doesn't require phase-shifting. So I would setup an output with n-channels and have m-sources each with a channel volume backed DBAP Source based on the same principle as SpatialSink

@PetrGlad
Copy link
Collaborator

PetrGlad commented Dec 8, 2024

@DivineGod Feel free to file a ticket if you have particular suggestions on how to improve spatial.

I think a test that demonstrates the problem is necessary for this issue. Do you use ChannelCountConverter? It is only piece that I am aware of that does have specific logic for stereo channels.

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.

4 participants