Skip to content

Fix auto fallback for --alsa-mixer-device and --alsa-mixer-index#910

Merged
roderickvd merged 1 commit intolibrespot-org:devfrom
JasonLG1979:fix-alsa-mixer-device
Dec 22, 2021
Merged

Fix auto fallback for --alsa-mixer-device and --alsa-mixer-index#910
roderickvd merged 1 commit intolibrespot-org:devfrom
JasonLG1979:fix-alsa-mixer-device

Conversation

@JasonLG1979
Copy link
Copy Markdown
Contributor

@JasonLG1979 JasonLG1979 commented Dec 17, 2021

As mentioned in #898 (comment)

--alsa-mixer-device and --alsa-mixer-index now fallback to the card and index specified in --device.

@roderickvd
Copy link
Copy Markdown
Member

Would be nice if @coderootme and yourself could report back on various combinations, also in the long form of hdmi:CARD=HDMI,DEV=0.

@ghost
Copy link
Copy Markdown

ghost commented Dec 17, 2021

Yes, I can test it no problem!

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

Would be nice if @coderootme and yourself could report back on various combinations, also in the long form of hdmi:CARD=HDMI,DEV=0.

It works for me. Where my --device is hw:CARD=Generic_1,DEV=0 or hw:1,0 I get:

[2021-12-17T08:47:44Z INFO  librespot_playback::mixer::alsamixer] Mixing with Alsa and volume control: Log(0.0) for device: hw:CARD=Generic_1 with mixer control: PCM,0

And:

[2021-12-17T08:43:14Z INFO  librespot_playback::mixer::alsamixer] Mixing with Alsa and volume control: Log(0.0) for device: hw:1 with mixer control: PCM,0

Everything works as intended.

Note that hw:CARD=Generic_1,DEV=0 and hw:1,0 are the same exact output just different ways to express them. It's preferable though to use the hw:CARD=Generic_1,DEV=0 form as it's possible for the index of a card to change boot to boot, so it's more stable than hw:1,0.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

After this quick fix is merged I will go though and de-panic the mixer(s).

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 17, 2021

As a future improvement --alsa-mixer-index could also auto fallback to the dev index specified in --device if at all. So in that case for example a --device of either hw:CARD=Generic_1,DEV=1 or hw:1,1 becomes the equivalent of --alsa-mixer-index 1. None of my devices actually have a mixer on hw:x,1 so I can't really test it but the logic is sound.

--alsa-mixer-control could also default to trying both PCM and Master if not specified instead of just PCM.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

--device really should be --card or maybe --audio-output(?) since --device conflicts with ALSA naming conventions where a ALSA device is an input or an output on a card. I kinda like --audio-output since it can be either hardware or software based and --device and --card both sound like they might be hardware only.

But that's just my opinion.

@roderickvd
Copy link
Copy Markdown
Member

What do the other backends call it? Particularly rodio, PulseAudio and GStreamer as all of those seem to be used pretty commonly.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

PulseAudio and Gstreamer both call outputs Sinks. I don't know about rodio?

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 17, 2021

In Gstreamer an element has an input called a sink and an output called a src. Elements connect their src to the next element's sink. A sink element like an audio output has no src as it's the end of the audio pipeline. A src element has no sink since it is the beginning of the audio pipeline.

https://gstreamer.freedesktop.org/documentation/application-development/basics/elements.html?gi-language=c

PulseAudio works about the same way.

https://gavv.github.io/articles/pulseaudio-under-the-hood/

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 17, 2021

rodio seems to work about the same way:

https://docs.rs/rodio/latest/rodio/struct.Sink.html

So basically everyone calls it a sink except ALSA. Which I can honestly understand since they all represent audio signal flow more abstractly while ALSA is lower level. Where all of the different "sink" abstractions will eventually hit the "real" ALSA "card,device" (on Linux ofc)

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 17, 2021

So I guess --sink is a better name than --device since the concept of a sink translates across different backends better than device.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 19, 2021

@roderickvd that last commit fixes the auto fallback for --alsa-mixer-index. So instead of always falling back to 0 if not explicitly set it will fallback to the device index part of hw:x,x or hw:CARD=x,DEV=x if it's present in --device.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 19, 2021

Looking for testing from you @coderootme and a review and possibly merge from you @roderickvd.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 19, 2021

On related side note I looked into unpanicking the mixer and there are 2 things I see as temporary blockers for me:

  1. The mixer(s) touch network code so I'd rather wait for you to finish your work on the new API @roderickvd.

  2. Much like the backends open can not return an error. Either the mixers will have to be refactored to be able to return an error from open or the alsa mixer will need to be refactored to move all panicky code out of open.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

Well hold up on the merge. auto fallback doesn't work with things like plughw:0,0 because the mixer must be hw:

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

The last commit should fix fallbacks for devices that don't start with hw:. Basically it drops the prefix before the : and adds hw so for example plughw:CARD=Generic_1 becomes hw:CARD=Generic_1 because although plughw:CARD=Generic_1 is a valid device it is not a valid mixer. The mixer for plughw:CARD=Generic_1 is hw:CARD=Generic_1.

@ghost
Copy link
Copy Markdown

ghost commented Dec 20, 2021

Looking for testing from you @coderootme and a review and possibly merge from you @roderickvd.

Just compiling 042f5f9 on a Raspberry Pi 4. Will report back in a few minutes!

@ghost
Copy link
Copy Markdown

ghost commented Dec 20, 2021

OK, so here is my test.

I compiled with:

$ git clone -b fix-alsa-mixer-device https://github.com/JasonLG1979/librespot.git
$ cd librespot/
$ cargo build --no-default-features --features "alsa-backend" --release

My test on a DAC which has a hardware mixer – the one that previously failed to work without --alsa-mixer-device specified.

current librespot 0.3.1 7160dc1:

/usr/bin/librespot --mixer alsa --device "hw:1,0" --alsa-mixer-device "hw:1" -- works properly, as expected.

current librespot 0.3.1 7160dc1:

/usr/bin/librespot --mixer alsa --device "hw:1,0" -- panics, as we already know -- could not open alsa mixer.

JasonLG1979's fix-alsa-mixer-device, commit 042f5f9:

/usr/bin/librespot_new --mixer alsa --device "hw:1,0" --alsa-mixer-device "hw:1" -- still works properly, as we would hope.

JasonLG1979's fix-alsa-mixer-device, commit 042f5f9:

/usr/bin/librespot_new --mixer alsa --device "hw:1,0" -- now works properly, finding and opening the correct alsa mixer.
/usr/bin/librespot_new --mixer alsa --device "hw:CARD=Audio,DEV=0" -- also works.
/usr/bin/librespot_new --mixer alsa --device "plughw:CARD=Audio,DEV=0" -- also works.

Is this a good enough test for you guys, or could I test something else, or report some more information?

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

JasonLG1979's fix-alsa-mixer-device, commit 042f5f9:
/usr/bin/librespot_new --mixer alsa --device "hw:1,0" --alsa-mixer-device "hw:1" -- still works properly, as we would hope.

JasonLG1979's fix-alsa-mixer-device, commit 042f5f9:
/usr/bin/librespot_new --mixer alsa --device "hw:1,0" -- now works properly, finding and opening the correct alsa mixer.
/usr/bin/librespot_new --mixer alsa --device "hw:CARD=Audio,DEV=0" -- also works.
/usr/bin/librespot_new --mixer alsa --device "plughw:CARD=Audio,DEV=0" -- also works.

Awesome, I also fixed/handled some other situations.

  1. While --device values like pulse, default, and jack may be valid devices there is no way (that I can tell anyway?) to get the mixer for those "virtual" devices. So if a device is not the something:x,x or something:CARD=x,DEV=x type librespot will exit with an error if --mixer is alsa and --alsa-mixer-device is not specified.

  2. All something:x,x or something:CARD=x,DEV=x type devices are turned to hw:x or hw:CARD=x as a fallback for the mixer when --alsa-mixer-device is not specified, because generally as far as I can tell mixers have to be hw:. plughw:CARD=Audio will not work but hw:CARD=Audio will for example.

  3. dev Index fallback is also handled.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

Is this a good enough test for you guys, or could I test something else, or report some more information?

I think that about covers it, thank you.

@roderickvd balls in your court.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

@coderootme I guess I forgot:

--mixer alsa --device hw:1
--mixer alsa --device hw:CARD=Audio
--mixer alsa --device plughw:CARD=Audio

Should also all work in your case can you test those also?

Sorry.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

0 is the default dev index. It's implied if you omit it.

@roderickvd
Copy link
Copy Markdown
Member

roderickvd commented Dec 20, 2021

On related side note I looked into unpanicking the mixer and there are 2 things I see as temporary blockers for me:

  1. The mixer(s) touch network code so I'd rather wait for you to finish your work on the new API @roderickvd.
  2. Much like the backends open can not return an error. Either the mixers will have to be refactored to be able to return an error from open or the alsa mixer will need to be refactored to move all panicky code out of open.

I think it’s a good idea to have open and possibly others to return a Result if the sink can fail. But I suggest you wait for me to introduce the global Error type before you do it. I decided to work on that before moving to other things, and am glad that I did. Refactoring all the Error return types is quite a lot of work that I would like to spare you.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

Refactoring all the Error return types is quite a lot of work that I would like to spare you.

Have at it. It's all yours,lol!!!

I think I've done all I can here without changing the mixer(s).

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

Another question why open and not new, open seems weird to me?

@ghost
Copy link
Copy Markdown

ghost commented Dec 20, 2021

--mixer alsa --device hw:1
--mixer alsa --device hw:CARD=Audio
--mixer alsa --device plughw:CARD=Audio

Just tested these. All 3 work properly.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

Just tested these. All 3 work properly.

Great!!! Thank you very much. @roderickvd what say you?

@JasonLG1979 JasonLG1979 changed the title Fix auto fallback --alsa-mixer-device Fix auto fallback for --alsa-mixer-device and --alsa-mixer-index Dec 20, 2021
@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

A little force push to get past a code 101. I like the little green check mark 😃

Copy link
Copy Markdown
Member

@roderickvd roderickvd left a comment

Choose a reason for hiding this comment

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

Nice enhancement. As usual a few questions about idiom.

Comment thread src/main.rs Outdated
.chars()
.last()
.unwrap_or_default()
.to_digit(10)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Converting to base-10 this way is rather explicit 😆 can't we just do .parse() or any of the usual idioms?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This way anything but 0-9 is 0. It's shorter than parse u32 unwrap_or_default().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hold up. I'm still thinking about this one?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Will this work for card numbers >= 10? That might not be so common but while we're going for robustness here...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will this work for card numbers >= 10? That might not be so common but while we're going for robustness here...

That's part of what I'm ciphering.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's pretty smart which I mean both in a good and a cautious way. Meaning: this catches a lot of variants in a way that is not entirely obvious. I think it would be wise to document possible values with a little code documentation for future readers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's pretty smart which I mean both in a good and a cautious way. Meaning: this catches a lot of variants in a way that is not entirely obvious. I think it would be wise to document possible values with a little code documentation for future readers.

Done. I've put comments in the code that explain what it does.

So it now reads:

                .unwrap_or_else(|| match device {
                    // Look for the dev index portion of --device.
                    // Specifically <dev index> when --device is <something>:CARD=<card name>,DEV=<dev index>
                    // or <something>:<card index>,<dev index>.

                    // If --device does not contain a ',' it does not contain a dev index.
                    // In the case that the dev index is omitted it is assumed to be 0 (mixer_default_config.index).
                    // Malformed --device values will also fallback to mixer_default_config.index.
                    Some(ref device_name) if device_name.contains(',') => {
                        // Turn <something>:CARD=<card name>,DEV=<dev index> or <something>:<card index>,<dev index>
                        // into DEV=<dev index> or <dev index>.
                        let dev = &device_name[device_name.find(',').unwrap_or_default()..]
                            .trim_start_matches(',');

                        // Turn DEV=<dev index> into <dev index> (noop if it's already <dev index>)
                        // and then parse <dev index>.
                        // Malformed --device values will fail the parse and fallback to mixer_default_config.index.
                        dev[dev.find('=').unwrap_or_default()..]
                            .trim_start_matches('=')
                            .parse::<u32>()
                            .unwrap_or(mixer_default_config.index)
                    }
                    _ => mixer_default_config.index,
                })

And:

                    Some(ref device_name) => {
                        // Look for the card name or card index portion of --device.
                        // Specifically <card name> when --device is <something>:CARD=<card name>,DEV=<dev index>
                        // or card index when --device is <something>:<card index>,<dev index>.
                        // --device values like `pulse`, `default`, `jack` may be valid but there is no way to
                        // infer automatically what the mixer should be so they fail auto fallback
                        // so --alsa-mixer-device must be manually specified in those situations.
                        let start_index = device_name.find(':').unwrap_or_default();

                        let end_index = match device_name.find(',') {
                            Some(index) if index > start_index => index,
                            _ => device_name.len(),
                        };

                        let card = &device_name[start_index..end_index];

                        if card.starts_with(':') {
                            // mixers are assumed to be hw:CARD=<card name> or hw:<card index>.
                            "hw".to_owned() + card
                        } else {
                            error!(
                                "Could not find an alsa mixer for \"{}\", it must be specified with `--{}` / `-{}`",
                                device_name,
                                ALSA_MIXER_DEVICE,
                                ALSA_MIXER_DEVICE_SHORT
                            );

                            exit(1);
                        }
                    }

Copy link
Copy Markdown
Contributor Author

@JasonLG1979 JasonLG1979 Dec 20, 2021

Choose a reason for hiding this comment

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

The assumption during parsing either is that --device is not malformed. If --device is malformed it would fail when the alsa backend tried to open it anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@roderickvd is that more to your liking?

Copy link
Copy Markdown
Contributor Author

@JasonLG1979 JasonLG1979 Dec 21, 2021

Choose a reason for hiding this comment

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

A lot of it is just taking advantage of the fact that you can use find to walk though a string especially if you know what to expect and/or what you want to find for example:

random_string.find("random chars").unwrap_or_default()

The above will either return the left most index of "random chars" (the "r") or 0 if the string does not include "random chars".

Comment thread src/main.rs
@roderickvd
Copy link
Copy Markdown
Member

Another question why open and not new, open seems weird to me?

That's why I said "open and possibly others". I can imagine both occurring. Err on new meaning: this sink cannot be initialized (e.g. device does not exist or is not accessible). Err on open meaning: this sink or stream cannot be opened at this time (e.g. the sink exists but is currently taken or otherwise unavailable, but may be available at a later time).

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

Another question why open and not new, open seems weird to me?

That's why I said "open and possibly others". I can imagine both occurring. Err on new meaning: this sink cannot be initialized (e.g. device does not exist or is not accessible). Err on open meaning: this sink or stream cannot be opened at this time (e.g. the sink exists but is currently taken or otherwise unavailable, but may be available at a later time).

No what I mean is for new to replace open because open creates a new thing in those cases so it's strange to me that the functions are called open.

@roderickvd
Copy link
Copy Markdown
Member

Another question why open and not new, open seems weird to me?

That's why I said "open and possibly others". I can imagine both occurring. Err on new meaning: this sink cannot be initialized (e.g. device does not exist or is not accessible). Err on open meaning: this sink or stream cannot be opened at this time (e.g. the sink exists but is currently taken or otherwise unavailable, but may be available at a later time).

No what I mean is for new to replace open because open creates a new thing in those cases so it's strange to me that the functions are called open.

Ah I see. I agree that open sounds like a transition of state on something that already exists, and not instantiate something new. So things should quack like "new, start, finish` or "create, open, close" but not both.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

For the backends it should be new, start, stop. Open in that case is an implementation detail IMHO. A backend might open on new or start depending.

For the mixers it should just be new, as there's no close or even stop. Again whatever alsa calls messing with the mixer is an implementation detail.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

So things should quack like "new, start, finish` or "create, open, close" but not both.

I'm not even really a fan of create as really how is it different from new? Although create is better than open, new is a very basic, pretty universal programming idiom. A function named new very clearly I think would be for creating a new thing.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

JasonLG1979 commented Dec 20, 2021

If there are different or alternative ways to make a new thing I prefer new_from_foo if there are ways to make slightly different kinds or more complete versions of a thing I would prefer something like new_with_foo.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

So in my example:

Creds::new() = new empty creds.
Creds::new_from_file(file) = new creds from a file.
Creds::new_with_username(username) = new creds with a username.

@JasonLG1979
Copy link
Copy Markdown
Contributor Author

@roderickvd this is failing on nightly with:

thread 'rustc' panicked at 'failed to lookup `SourceFile` in new context', compiler/rustc_query_impl/src/on_disk_cache.rs:500:22

It appears to be caused by this (in code quotes so it doesn't add noise to that issue):

https://github.com/rust-lang/rust/issues/92163

Other than that I think this is ready to merge. I'm pretty sure I've addressed all of your comments?

@roderickvd roderickvd merged commit 4370aa1 into librespot-org:dev Dec 22, 2021
@JasonLG1979 JasonLG1979 deleted the fix-alsa-mixer-device branch December 22, 2021 07:59
paulfariello pushed a commit to paulfariello/librespot that referenced this pull request Sep 23, 2025
…device

Fix auto fallback for --alsa-mixer-device and --alsa-mixer-index
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants