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

Closed
wants to merge 9 commits into 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
Contributor

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

@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.

@will3942
Copy link
Contributor

@PetrGlad you're right in that this should work correctly and that the issue is downstream.

I believe the issue is in

const THRESHOLD: usize = 512;
/ the THRESHOLD value.

For Empty source we use the THRESHOLD value, as Empty declares a current_span_len of None and also returns 0 to size_hint() which leaves us in (to quote the queue code) "the worst case scenario".

If this value is not divisible by the number of channels in the current Source Input then the bug occurs (this is the case for none power of 2 numbers as THRESHOLD is 512).

I'm still tracing exactly why but I believe it is as you say where we have shifted the input by a number of samples (those left over from the Empty source).

I'm trying to understand this more and understand a fix but as I'm not very familiar with the Rodio source code this may take a while so any suggestions would be really helpful.

@will3942
Copy link
Contributor

I can't craft a test here to make it work but I believe the issue is shared/across the Queue/SourcesQueueOutput and UniformSourceIterator when used with a mixer.

I believe a test along these lines should uncover the issue but I don't understand Mixer well enough to do this. @dvdsk can you input at all in light of these findings?

#[test]
    fn mixer_different_channels() {
        use crate::mixer;

        println!("mixer_different_channels");

        let (mixer_tx, mut mixer_rx) = mixer::mixer(6, 44100);
        let (queue_tx, queue_rx) = queue::queue(true);

        mixer_tx.add(queue_rx);

        assert_eq!(mixer_rx.next(), Some(0));

        queue_tx.append(SamplesBuffer::new(6, 44100, vec![10i16, 10, 0, 0, 0, 0]));

        assert_eq!(mixer_rx.channels(), 6);
        assert_eq!(mixer_rx.sample_rate(), 1);
        assert_eq!(mixer_rx.next(), Some(10));
        assert_eq!(mixer_rx.next(), Some(10));
        assert_eq!(mixer_rx.next(), Some(0));
        assert_eq!(mixer_rx.next(), Some(0));
        assert_eq!(mixer_rx.next(), Some(0));
        assert_eq!(mixer_rx.next(), Some(0));
    }

@dvdsk
Copy link
Collaborator

dvdsk commented Jan 18, 2025

I believe a test along these lines should uncover the issue but I don't understand Mixer well enough to do this. @dvdsk can you input at all in light of these findings?

I do not have a lot of time the next few days but sure, I'll take a look next week, if I dont report feel free to remind me. You have all done a lot of work here, it feels like we are close now. It would be very nice to close fix this bug, it has plagued is for a while now.

@PetrGlad
Copy link
Collaborator

PetrGlad commented Jan 19, 2025

@will3942 rodio's mixer should sum respective channels from each source. So it should handle all the cases (different span lengths and channel counts) properly.
I think we will have to simplify this (I would like to not use spans except on sources that do need to produce them).

Now I think it was a mistake to define span length in samples, not frames... Yes, it looks like ideally THRESHOLD should define number of output frames not number of samples.

I will have a look at this once more.

@will3942
Copy link
Contributor

This test is showing some bizarre behaviour where if Empty has 1 channel the mixer (according to the test) outputs on the correct channel - whereas when actually running Rodio this is not the case. When Empty has 0 channels the mixer outputs on the wrong channel (3 and 4).

This could just be a bad test or perhaps the issue is further downstream but maybe this will help tracing the root cause.

#[test]
    fn mixer_different_channels() {
        use crate::mixer;

        let (mixer_tx, mut mixer_rx) = mixer::mixer(6, 48000);
        let mut current_channel: usize = 1;

        let inc_current_channel = |current_channel: &mut usize| {
            *current_channel = (*current_channel + 1) % 6;
        };

        let mixer_next = |mixer_rx: &mut MixerSource<i16>, current_channel: &mut usize| {
            let next = mixer_rx.next();
            inc_current_channel(current_channel);
            next
        };

        assert_eq!(current_channel, 1);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), None);     
        assert_eq!(current_channel, 2);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), None);
        assert_eq!(current_channel, 3);

        let (queue_tx, queue_rx) = queue::queue(true);

        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), None);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), None);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), None);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), None);

        mixer_tx.add(queue_rx);

        assert_eq!(current_channel, 1);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));

        queue_tx.append(SamplesBuffer::new(6, 48000, vec![10i16, 10, 0, 0, 0, 0]));

        // Empty with 1 channels - this is seemingly 6 * (512-1)
        for _ in 0..3071 {
            assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        }

        // // // Empty with 0 channels
        // for _ in 0..511 {
        //     assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        // }

        assert_eq!(mixer_rx.channels(), 6);
        assert_eq!(mixer_rx.sample_rate(), 48000);
        assert_eq!(current_channel, 1);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(10));
        assert_eq!(current_channel, 2);
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(10));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(10));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(10));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
        assert_eq!(mixer_next(&mut mixer_rx, &mut current_channel), Some(0));
    }

@dvdsk
Copy link
Collaborator

dvdsk commented Jan 21, 2025

This test is showing some bizarre behaviour where if Empty has 1 channel the mixer (according to the test) outputs on the correct channel - whereas when actually running Rodio this is not the case. When Empty has 0 channels the mixer outputs on the wrong channel (3 and 4).

This could just be a bad test or perhaps the issue is further downstream but maybe this will help tracing the root cause.

I do not really understand what this test is actually testing. The use of the two closures makes it pretty hard to understand for me. The tests also passes without problem on my system, that seems intentional? Maybe a test that fails without the callbacks would be clearer. One note, the first channel will be channel zero, your counter starts at one?

@will3942
Copy link
Contributor

will3942 commented Jan 21, 2025

@dvdsk apologies for the bad test - it was designed to check if the output was on the correct channel. The test does indeed pass even though in reality the audio comes out on the wrong channel.

I was intrigued that the fix in this pull makes the test fail as it comes out on the wrong channel although in reality comes out on the correct channel.

@dvdsk
Copy link
Collaborator

dvdsk commented Jan 21, 2025

@dvdsk apologies for the bad test - it was designed to check if the output was on the correct channel. The test does indeed pass even though in reality the audio comes out on the wrong channel.

No worries, I took a close look at queue and I think I have a fix, you can check it out here: #684

@DivineGod
Copy link
Contributor Author

Looks like there's been some good progress in figuring out the core underlying issue here and this PR is going to be obsolete? I am happy to close this if the new PR is superceding this.

@dvdsk
Copy link
Collaborator

dvdsk commented Jan 22, 2025

Looks like there's been some good progress in figuring out the core underlying issue here and this PR is going to be obsolete? I am happy to close this if the new PR is superceding this.

progress but we are not done yet 😢 , its a difficult one this. But seeing the way we go this pr is going to be obsolete. Thanks for the valuable discussion that helped a lot!

@dvdsk dvdsk closed this Jan 22, 2025
@DivineGod DivineGod deleted the fix/channel-volume branch January 27, 2025 06:07
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