| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | use std::collections::HashSet; |
| 2 | |
| 3 | use super::{alsa, Device, Host}; |
| 4 | use crate::{BackendSpecificError, DeviceDirection, DevicesError}; |
| 5 | |
| 6 | const HW_PREFIX: &str = "hw"; |
| 7 | const PLUGHW_PREFIX: &str = "plughw"; |
| 8 | |
| 9 | /// Information about a physical device |
| 10 | struct 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). |
| 19 | pub type Devices = std::vec::IntoIter<Device>; |
| 20 | |
| 21 | impl 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" |
| 78 | fn 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 | |
| 97 | fn 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 | |
| 150 | impl From<alsa::Error> for DevicesError { |
| 151 | fn from(err: alsa::Error) -> Self { |
| 152 | let err: BackendSpecificError = err.into(); |
| 153 | err.into() |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | impl 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 | } |