Skip to content

Conversation

roderickvd
Copy link
Collaborator

On one of my systems, cpal v0.16 enumerates these and only these devices:

  1. "default"
  2. "Yggdrasil+"

As reported in #991 this is problematic because it:

  • misses a lot of virtual devices
  • is unclear whether it's hw, plughw, something else
  • only provides access to DEV=0 when there could be more
  • differs from ALSA idioms

New behavior

This PR changes this behavior to enumerate all devices like aplay -L puts out:

  1. "null"
  2. "default"
  3. "sysdefault"
  4. "_audioout"
  5. "_audioout__"
  6. "alsaequal"
  7. "plug_alsaequal"
  8. "btstream"
  9. "camilladsp"
  10. "crossfeed"
  11. "plug_bs2b"
  12. "eqfa12p"
  13. "plug_eqfa12p"
  14. "invpolarity"
  15. "trx_send"
  16. "hw:CARD=Yggdrasil,DEV=0"
  17. "plughw:CARD=Yggdrasil,DEV=0"
  18. "default:CARD=Yggdrasil"
  19. "sysdefault:CARD=Yggdrasil"
  20. "front:CARD=Yggdrasil,DEV=0"
  21. "surround21:CARD=Yggdrasil,DEV=0"
  22. "surround40:CARD=Yggdrasil,DEV=0"
  23. "surround41:CARD=Yggdrasil,DEV=0"
  24. "surround50:CARD=Yggdrasil,DEV=0"
  25. "surround51:CARD=Yggdrasil,DEV=0"
  26. "surround71:CARD=Yggdrasil,DEV=0"
  27. "iec958:CARD=Yggdrasil,DEV=0"
  28. "dmix:CARD=Yggdrasil,DEV=0"
  29. "hw:CARD=vc4hdmi0,DEV=0"
  30. "plughw:CARD=vc4hdmi0,DEV=0"
  31. "default:CARD=vc4hdmi0"
  32. "sysdefault:CARD=vc4hdmi0"
  33. "hdmi:CARD=vc4hdmi0,DEV=0"
  34. "dmix:CARD=vc4hdmi0,DEV=0"
  35. "hw:CARD=vc4hdmi1,DEV=0"
  36. "plughw:CARD=vc4hdmi1,DEV=0"
  37. "default:CARD=vc4hdmi1"
  38. "sysdefault:CARD=vc4hdmi1"
  39. "hdmi:CARD=vc4hdmi1,DEV=0"
  40. "dmix:CARD=vc4hdmi1,DEV=0"

This solves all problems mentioned.

System configuration

For reference the configuration as aplay reports:

$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: Yggdrasil [Yggdrasil+], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 2: vc4hdmi1 [vc4-hdmi-1], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

$ aplay -L
null
    Discard all samples (playback) or generate zero samples (capture)
default
    Default Audio Device
sysdefault
    Default Audio Device
_audioout
_audioout__
alsaequal
plug_alsaequal
btstream
camilladsp
crossfeed
plug_bs2b
eqfa12p
plug_eqfa12p
invpolarity
trx_send
hw:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    Direct hardware device without any conversions
plughw:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    Hardware device with all software conversions
default:CARD=Yggdrasil
    Yggdrasil+, USB Audio
    Default Audio Device
sysdefault:CARD=Yggdrasil
    Yggdrasil+, USB Audio
    Default Audio Device
front:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    Front output / input
surround21:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    2.1 Surround output to Front and Subwoofer speakers
surround40:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    4.0 Surround output to Front and Rear speakers
surround41:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
iec958:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    IEC958 (S/PDIF) Digital Audio Output
dmix:CARD=Yggdrasil,DEV=0
    Yggdrasil+, USB Audio
    Direct sample mixing device
hw:CARD=vc4hdmi0,DEV=0
    vc4-hdmi-0, MAI PCM i2s-hifi-0
    Direct hardware device without any conversions
plughw:CARD=vc4hdmi0,DEV=0
    vc4-hdmi-0, MAI PCM i2s-hifi-0
    Hardware device with all software conversions
default:CARD=vc4hdmi0
    vc4-hdmi-0, MAI PCM i2s-hifi-0
    Default Audio Device
sysdefault:CARD=vc4hdmi0
    vc4-hdmi-0, MAI PCM i2s-hifi-0
    Default Audio Device
hdmi:CARD=vc4hdmi0,DEV=0
    vc4-hdmi-0, MAI PCM i2s-hifi-0
    HDMI Audio Output
dmix:CARD=vc4hdmi0,DEV=0
    vc4-hdmi-0, MAI PCM i2s-hifi-0
    Direct sample mixing device
hw:CARD=vc4hdmi1,DEV=0
    vc4-hdmi-1, MAI PCM i2s-hifi-0
    Direct hardware device without any conversions
plughw:CARD=vc4hdmi1,DEV=0
    vc4-hdmi-1, MAI PCM i2s-hifi-0
    Hardware device with all software conversions
default:CARD=vc4hdmi1
    vc4-hdmi-1, MAI PCM i2s-hifi-0
    Default Audio Device
sysdefault:CARD=vc4hdmi1
    vc4-hdmi-1, MAI PCM i2s-hifi-0
    Default Audio Device
hdmi:CARD=vc4hdmi1,DEV=0
    vc4-hdmi-1, MAI PCM i2s-hifi-0
    HDMI Audio Output
dmix:CARD=vc4hdmi1,DEV=0
    vc4-hdmi-1, MAI PCM i2s-hifi-0
    Direct sample mixing device

References

Fixes #991

@roderickvd
Copy link
Collaborator Author

cc: @abique

@roderickvd
Copy link
Collaborator Author

For reference, this is how it enumerated on v0.15 on the same system:

  1. "default"
  2. "sysdefault"
  3. "_audioout"
  4. "camilladsp"

So my PR restores what v0.15 was able to do and adds access to:

I'd like to have a bit more community feedback on this than #916 did.
Barring any, I intend to merge this in a few weeks time.

cc'ing some users that put in ALSA contributions relatively recently. What do you think @attackgoat, @cdellacqua, @lautarodragan, @jwagner? (hope that's OK - trying to build the community and can use your help - but let me know if you don't want me to do that again)

@roderickvd roderickvd self-assigned this Jul 29, 2025
@roderickvd roderickvd requested a review from Copilot July 29, 2025 20:20
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR changes the ALSA device enumeration approach to match aplay -L behavior, providing access to all available ALSA devices including virtual devices and specific device variants.

  • Replaces card-based enumeration with ALSA hints enumeration using HintIter
  • Removes hardcoded builtin device list in favor of comprehensive device discovery
  • Adds deduplication logic to prevent duplicate devices in enumeration

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/host/alsa/enumerate.rs Replaces card iteration with ALSA hints enumeration to match aplay -L output
CHANGELOG.md Documents the enumeration behavior change

@attackgoat
Copy link
Contributor

LGTM and doesn't break my usage.

As a brand new user I was confused about the devices CPAL reported, and now that I run this branch I see recognizable devices that make sense.

@cdellacqua
Copy link
Contributor

Looks good. The only thing that I would change is the error handling, although it might be like that for compatibility reasons, so feel free to discard the following suggestion.

I would eagerly fail if ALSA can't provide hints. This should also simplify the Iterator impl.

Something like:

impl Devices {
    pub fn new() -> Result<Self, DevicesError> {
        // Enumerate ALL devices from ALSA hints (same as aplay -L)
        alsa::device_name::HintIter::new_str(None, "pcm")
            .map(|hint_iter| Devices {
                hint_iter,
                enumerated_pcm_ids: HashSet::new(),
            })
            .map_err(DevicesError::from)
    }
}

And in the Iterator the next function could then be simplified as:

fn next(&mut self) -> Option<Device> {
    loop {
        match self.hint_iter.next().map(|hint| hint.name) {
            // Skip if we've already enumerated this device by PCM ID
            Some(Some(name)) if !self.enumerated_pcm_ids.contains(&name) => {
                let device = open_device(&name, name.clone());
                self.enumerated_pcm_ids.insert(name.clone());
                return Some(device);
            }
            Some(_) => (),
            None => return None, // All devices enumerated
        }
    }
}

note: try_open_device -> open_device, I just removed the wrapper as the function is private and never returns None

@roderickvd
Copy link
Collaborator Author

Thank you for responding to the ping @attackgoat and @cdellacqua and taking the time to review.

Looks good. The only thing that I would change is the error handling, although it might be like that for compatibility reasons, so feel free to discard the following suggestion.

As this change will need to push the minor semver anyway to 0.17 or beyond, we can entertain improvements to the error handling. What are you thinking?

I would eagerly fail if ALSA can't provide hints. This should also simplify the Iterator impl.

Great idea. Coming weeks I don't have an ALSA device to test on, so I'll get on it later.

@cdellacqua
Copy link
Contributor

Thank you for responding to the ping

Thank you for maintaining this library! Glad to help

What are you thinking?

Oh, sorry for the confusion. The first and second paragraphs were about the same thing: "eagerly fail if ALSA can't provide hints"

@jwagner
Copy link
Contributor

jwagner commented Aug 22, 2025

Thanks a lot for picking this up Roderick!

I'm wondering how user friendly the device names are going to be. The DeviceTrait define name as

/// The human-readable name of the device.

A name like dmix:CARD=Pro70790250,DEV=0 will likely not mean much to your average user (arguable for the average linux use but ;). Also the flood of options could be overwhelming for many end users.

I think it would be best to expose to different methods in the DeviceTrait, one delivering a human readable label, another one delivering a (hopefully) stable platform specific 'id'. This could happen by adding an id() -> Vec<u8> and keeping the name as the human readable label, or by adding a label() -> Result<String> and returning the 'id' in the existing name method. In pulseaudio name seems to be the 'id' and description the human readable label. Pipewire looks similar. I don't have a strong opinion either way.

As a bonus this would then could also address the lack of persistable unique ids for cpal devices.

As for the sheer amount of (virtual) devices being overwhelming exposing a device type (Hardware, Virtual, Monitor, ..) might help.

Lastly, I'm not arguing that this has to be in scope for this PR but it might be good to get it sorted before the next release since in a way not having "human readable" (again arguable) labels is a bit of a regression.

@jduncanator
Copy link

jduncanator commented Aug 23, 2025

For my personal use-case, it doesn't matter to me whether enumeration returns verbose ALSA device names, or just friendlier names (as the original PR to cpal does), so long as cpal can open any ALSA device using a verbose ASLA device name. I don't see why there is a need to unnecessarily restrict which device cpal can open, so long as it exists. Device enumeration is then free to return whatever it likes (although that may still break some consumers of cpal if they're doing some kind of comparison or lookup in the enumerated device list).

@roderickvd roderickvd force-pushed the fix/enumerate-virtual-devices branch 2 times, most recently from 489f5e2 to 03a517c Compare August 30, 2025 18:03
@roderickvd
Copy link
Collaborator Author

roderickvd commented Aug 30, 2025

@cdellacqua I implemented your proposal. Much cleaner indeed.

@jwagner love the ideas. While it'd be a bigger change indeed, it wasn't difficult to provide a middle ground in 73f3dd6 already. For devices that have a description, it's appended in parentheses:

ALSA
  Default Input Device:
    Some("default (Default Audio Device)")
  Default Output Device:
    Some("default (Default Audio Device)")
  Devices: 
  1. "null (Discard all samples (playback) or generate zero samples (capture))"
  2. "default (Default Audio Device)"
  3. "sysdefault (Default Audio Device)"
  4. "_audioout"
  5. "_audioout__"
  6. "alsaequal"
  7. "plug_alsaequal"
  8. "btstream"
  9. "camilladsp"
  10. "crossfeed"
  11. "plug_bs2b"
  12. "eqfa12p"
  13. "plug_eqfa12p"
  14. "invpolarity"
  15. "trx_send"
  16. "hw:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, Direct hardware device without any conversions)"
  17. "plughw:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, Hardware device with all software conversions)"
  18. "default:CARD=Yggdrasil (Yggdrasil+, USB Audio, Default Audio Device)"
  19. "sysdefault:CARD=Yggdrasil (Yggdrasil+, USB Audio, Default Audio Device)"
  20. "front:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, Front output / input)"
  21. "surround21:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, 2.1 Surround output to Front and Subwoofer speakers)"
  22. "surround40:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, 4.0 Surround output to Front and Rear speakers)"
  23. "surround41:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, 4.1 Surround output to Front, Rear and Subwoofer speakers)"
  24. "surround50:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, 5.0 Surround output to Front, Center and Rear speakers)"
  25. "surround51:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, 5.1 Surround output to Front, Center, Rear and Subwoofer speakers)"
  26. "surround71:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, 7.1 Surround output to Front, Center, Side, Rear and Woofer speakers)"
  27. "iec958:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, IEC958 (S/PDIF) Digital Audio Output)"
  28. "dmix:CARD=Yggdrasil,DEV=0 (Yggdrasil+, USB Audio, Direct sample mixing device)"
  29. "hw:CARD=vc4hdmi0,DEV=0 (vc4-hdmi-0, MAI PCM i2s-hifi-0, Direct hardware device without any conversions)"
  30. "plughw:CARD=vc4hdmi0,DEV=0 (vc4-hdmi-0, MAI PCM i2s-hifi-0, Hardware device with all software conversions)"
  31. "default:CARD=vc4hdmi0 (vc4-hdmi-0, MAI PCM i2s-hifi-0, Default Audio Device)"
  32. "sysdefault:CARD=vc4hdmi0 (vc4-hdmi-0, MAI PCM i2s-hifi-0, Default Audio Device)"
  33. "hdmi:CARD=vc4hdmi0,DEV=0 (vc4-hdmi-0, MAI PCM i2s-hifi-0, HDMI Audio Output)"
  34. "dmix:CARD=vc4hdmi0,DEV=0 (vc4-hdmi-0, MAI PCM i2s-hifi-0, Direct sample mixing device)"
  35. "hw:CARD=vc4hdmi1,DEV=0 (vc4-hdmi-1, MAI PCM i2s-hifi-0, Direct hardware device without any conversions)"
  36. "plughw:CARD=vc4hdmi1,DEV=0 (vc4-hdmi-1, MAI PCM i2s-hifi-0, Hardware device with all software conversions)"
  37. "default:CARD=vc4hdmi1 (vc4-hdmi-1, MAI PCM i2s-hifi-0, Default Audio Device)"
  38. "sysdefault:CARD=vc4hdmi1 (vc4-hdmi-1, MAI PCM i2s-hifi-0, Default Audio Device)"
  39. "hdmi:CARD=vc4hdmi1,DEV=0 (vc4-hdmi-1, MAI PCM i2s-hifi-0, HDMI Audio Output)"
  40. "dmix:CARD=vc4hdmi1,DEV=0 (vc4-hdmi-1, MAI PCM i2s-hifi-0, Direct sample mixing device)"

I am with @jduncanator that I expect an average Linux user to be able to use aplay -L and take the device name, even if verbose. To you other point:

I don't see why there is a need to unnecessarily restrict which device cpal can open, so long as it exists.

That private open_device() function name may be misleading. Its behavior is that it would enumerate any existing device, as long as it's got a PCM ID, even when it cannot open it during enumeration. Refactored that in 401ff61 to use TryFrom.

Would appreciate a short thumbs up if this is working for y'all 👍

roderickvd and others added 5 commits August 30, 2025 20:23
- Return single config when all sample rate ranges have identical min/max values
- Improve iterator usage for better code clarity and consistency
- Follow RtAudio approach for handling device sample rate inconsistencies

Fixes issues with redundant sample rate configs that most CoreAudio devices report.
- Enumerate all ALSA devices directly from hints
- Store device description and display it in Device::name
- Simplify default device creation and handling
- Use Rust format string syntax for error messages
- Remove redundant reference in DeviceHandles::open call
- Simplify timespec_to_nanos calculation
@roderickvd roderickvd force-pushed the fix/enumerate-virtual-devices branch from 03a517c to 8dcee4a Compare August 30, 2025 18:24
Move device construction logic into a TryFrom implementation for
alsa::device_name::Hint. Simplifies iterator and improves error handling.
@jwagner
Copy link
Contributor

jwagner commented Aug 31, 2025

Hey @roderickvd,

Gave the branch a quick test in my application, seems to work. At least in my context the descriptions definitely help. I like it! 👍

Screenshot From 2025-08-31 11-22-55

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.

[ALSA] Custom virtual devices no longer enumerable since v0.16 (regression from PR #916)
7 participants