Skip to main content

cpal/host/alsa/
enumerate.rs

1use std::collections::HashSet;
2
3use super::{alsa, Device, Host};
4use crate::{BackendSpecificError, DeviceDirection, DevicesError};
5
6const HW_PREFIX: &str = "hw";
7const PLUGHW_PREFIX: &str = "plughw";
8
9/// Information about a physical device
10struct PhysicalDevice {
11    card_index: u32,
12    card_name: Option<String>,
13    device_index: u32,
14    device_name: Option<String>,
15    direction: DeviceDirection,
16}
17
18/// Iterator over available ALSA PCM devices (physical hardware and virtual/plugin devices).
19pub type Devices = std::vec::IntoIter<Device>;
20
21impl Host {
22    /// Enumerates all available ALSA PCM devices (physical hardware and virtual/plugin devices).
23    ///
24    /// We enumerate both ALSA hints and physical devices because:
25    /// - Hints provide virtual devices, user configs, and card-specific devices with metadata
26    /// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility
27    pub(super) fn enumerate_devices(&self) -> Result<Devices, DevicesError> {
28        let mut devices = Vec::new();
29        let mut seen_pcm_ids = HashSet::new();
30
31        let physical_devices = physical_devices();
32
33        // Add all hint devices, including virtual devices
34        if let Ok(hints) = alsa::device_name::HintIter::new_str(None, "pcm") {
35            for hint in hints {
36                if let Some(pcm_id) = hint.name {
37                    // Per ALSA docs (https://alsa-project.org/alsa-doc/alsa-lib/group___hint.html),
38                    // NULL IOID means both Input/Output. Whether a stream can actually open in a
39                    // given direction can only be determined by attempting to open it.
40                    let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into);
41                    let device = Device {
42                        pcm_id,
43                        desc: hint.desc,
44                        direction,
45                        _context: self.inner.clone(),
46                    };
47
48                    seen_pcm_ids.insert(device.pcm_id.clone());
49                    devices.push(device);
50                }
51            }
52        }
53
54        // Add hw:/plughw: for all physical devices with numeric index (traditional naming)
55        for phys_dev in physical_devices {
56            for prefix in [HW_PREFIX, PLUGHW_PREFIX] {
57                let pcm_id = format!(
58                    "{}:CARD={},DEV={}",
59                    prefix, phys_dev.card_index, phys_dev.device_index
60                );
61
62                if seen_pcm_ids.insert(pcm_id.clone()) {
63                    devices.push(Device {
64                        pcm_id,
65                        desc: Some(format_device_description(&phys_dev, prefix)),
66                        direction: phys_dev.direction,
67                        _context: self.inner.clone(),
68                    });
69                }
70            }
71        }
72
73        Ok(devices.into_iter())
74    }
75}
76
77/// Formats device description in ALSA style: "Card Name, Device Name\nPurpose"
78fn format_device_description(phys_dev: &PhysicalDevice, prefix: &str) -> String {
79    // "Card Name, Device Name" or variations
80    let first_line = match (&phys_dev.card_name, &phys_dev.device_name) {
81        (Some(card), Some(device)) => format!("{}, {}", card, device),
82        (Some(card), None) => card.clone(),
83        (None, Some(device)) => device.clone(),
84        (None, None) => format!("Card {}", phys_dev.card_index),
85    };
86
87    // ALSA standard description
88    let second_line = match prefix {
89        HW_PREFIX => "Direct hardware device without any conversions",
90        PLUGHW_PREFIX => "Hardware device with all software conversions",
91        _ => "",
92    };
93
94    format!("{}\n{}", first_line, second_line)
95}
96
97fn physical_devices() -> Vec<PhysicalDevice> {
98    let mut devices = Vec::new();
99    for card in alsa::card::Iter::new().filter_map(Result::ok) {
100        let card_index = card.get_index() as u32;
101        let ctl = match alsa::Ctl::new(&format!("{}:{}", HW_PREFIX, card_index), false) {
102            Ok(ctl) => ctl,
103            Err(_) => continue,
104        };
105        let card_name = ctl
106            .card_info()
107            .ok()
108            .and_then(|info| info.get_name().ok().map(|s| s.to_string()));
109
110        for device_index in alsa::ctl::DeviceIter::new(&ctl) {
111            let device_index = device_index as u32;
112            let playback_info = ctl
113                .pcm_info(device_index, 0, alsa::Direction::Playback)
114                .ok();
115            let capture_info = ctl.pcm_info(device_index, 0, alsa::Direction::Capture).ok();
116
117            let (direction, device_name) = match (&playback_info, &capture_info) {
118                (Some(p_info), Some(_c_info)) => (
119                    DeviceDirection::Duplex,
120                    p_info.get_name().ok().map(|s| s.to_string()),
121                ),
122                (Some(p_info), None) => (
123                    DeviceDirection::Output,
124                    p_info.get_name().ok().map(|s| s.to_string()),
125                ),
126                (None, Some(c_info)) => (
127                    DeviceDirection::Input,
128                    c_info.get_name().ok().map(|s| s.to_string()),
129                ),
130                (None, None) => {
131                    // Device doesn't exist - skip
132                    continue;
133                }
134            };
135
136            let device_name = device_name.unwrap_or_else(|| format!("Device {}", device_index));
137            devices.push(PhysicalDevice {
138                card_index,
139                card_name: card_name.clone(),
140                device_index,
141                device_name: Some(device_name),
142                direction,
143            });
144        }
145    }
146
147    devices
148}
149
150impl From<alsa::Error> for DevicesError {
151    fn from(err: alsa::Error) -> Self {
152        let err: BackendSpecificError = err.into();
153        err.into()
154    }
155}
156
157impl From<alsa::Direction> for DeviceDirection {
158    fn from(direction: alsa::Direction) -> Self {
159        match direction {
160            alsa::Direction::Playback => DeviceDirection::Output,
161            alsa::Direction::Capture => DeviceDirection::Input,
162        }
163    }
164}