#[cfg(any(feature = "capture", feature = "output"))]
use cpal::traits::{DeviceTrait, HostTrait};
use crate::error::DecibriError;
#[derive(Debug, Clone)]
pub struct DeviceInfo {
pub index: usize,
pub name: String,
pub max_input_channels: u16,
pub default_sample_rate: u32,
pub is_default: bool,
}
#[derive(Debug, Clone)]
pub struct OutputDeviceInfo {
pub index: usize,
pub name: String,
pub max_output_channels: u16,
pub default_sample_rate: u32,
pub is_default: bool,
}
#[derive(Debug, Clone)]
pub enum DeviceSelector {
Default,
Index(usize),
Name(String),
}
#[cfg(any(feature = "capture", feature = "output"))]
struct ComputedRow {
index: usize,
name: String,
channels: u16,
sample_rate: u32,
is_default: bool,
}
#[cfg(any(feature = "capture", feature = "output"))]
fn compute_is_default<Id: PartialEq>(
rows: Vec<(Option<Id>, String, u16, u32)>,
default_id: Option<Id>,
) -> Vec<ComputedRow> {
rows.into_iter()
.enumerate()
.map(|(index, (id, name, channels, sample_rate))| {
let is_default = match (id.as_ref(), default_id.as_ref()) {
(Some(row_id), Some(default)) => row_id == default,
_ => false,
};
ComputedRow {
index,
name,
channels,
sample_rate,
is_default,
}
})
.collect()
}
#[cfg(any(feature = "capture", feature = "output"))]
pub fn enumerate_input_devices() -> Result<Vec<DeviceInfo>, DecibriError> {
let host = cpal::default_host();
let default_id = host.default_input_device().and_then(|d| d.id().ok());
let devices = host
.input_devices()
.map_err(|e| DecibriError::Other(format!("Failed to enumerate devices: {e}")))?;
let rows: Vec<(Option<cpal::DeviceId>, String, u16, u32)> = devices
.enumerate()
.map(|(index, device)| {
let id = device.id().ok();
let name = device
.description()
.map(|d| d.name().to_string())
.unwrap_or_else(|_| format!("Unknown Device {index}"));
let (channels, sample_rate) = match device.default_input_config() {
Ok(config) => (config.channels(), config.sample_rate()),
Err(_) => (0, 0),
};
(id, name, channels, sample_rate)
})
.collect();
Ok(compute_is_default(rows, default_id)
.into_iter()
.map(|r| DeviceInfo {
index: r.index,
name: r.name,
max_input_channels: r.channels,
default_sample_rate: r.sample_rate,
is_default: r.is_default,
})
.collect())
}
#[cfg(any(feature = "capture", feature = "output"))]
pub fn resolve_device(selector: &DeviceSelector) -> Result<cpal::Device, DecibriError> {
let host = cpal::default_host();
match selector {
DeviceSelector::Default => host
.default_input_device()
.ok_or(DecibriError::NoMicrophoneFound),
DeviceSelector::Index(idx) => {
let devices: Vec<_> = host
.input_devices()
.map_err(|e| DecibriError::Other(format!("Failed to enumerate devices: {e}")))?
.collect();
devices
.into_iter()
.nth(*idx)
.ok_or(DecibriError::DeviceIndexOutOfRange)
}
DeviceSelector::Name(query) => {
let query_lower = query.to_lowercase();
let devices = host
.input_devices()
.map_err(|e| DecibriError::Other(format!("Failed to enumerate devices: {e}")))?;
let mut matches: Vec<(usize, String, cpal::Device)> = Vec::new();
for (index, device) in devices.enumerate() {
let name = device
.description()
.map(|d| d.name().to_string())
.unwrap_or_default();
if name.to_lowercase().contains(&query_lower) {
matches.push((index, name, device));
}
}
match matches.len() {
0 => Err(DecibriError::DeviceNotFound(query.clone())),
1 => Ok(matches.into_iter().next().unwrap().2),
_ => {
let match_list = matches
.iter()
.map(|(idx, name, _)| format!(" [{idx}] {name}"))
.collect::<Vec<_>>()
.join("\n");
Err(DecibriError::MultipleDevicesMatch {
name: query.clone(),
matches: format!(
"{match_list}\nUse a more specific name or pass the device index directly."
),
})
}
}
}
}
}
#[cfg(any(feature = "capture", feature = "output"))]
pub fn enumerate_output_devices() -> Result<Vec<OutputDeviceInfo>, DecibriError> {
let host = cpal::default_host();
let default_id = host.default_output_device().and_then(|d| d.id().ok());
let devices = host
.output_devices()
.map_err(|e| DecibriError::Other(format!("Failed to enumerate devices: {e}")))?;
let rows: Vec<(Option<cpal::DeviceId>, String, u16, u32)> = devices
.enumerate()
.map(|(index, device)| {
let id = device.id().ok();
let name = device
.description()
.map(|d| d.name().to_string())
.unwrap_or_else(|_| format!("Unknown Device {index}"));
let (channels, sample_rate) = match device.default_output_config() {
Ok(config) => (config.channels(), config.sample_rate()),
Err(_) => (0, 0),
};
(id, name, channels, sample_rate)
})
.collect();
Ok(compute_is_default(rows, default_id)
.into_iter()
.map(|r| OutputDeviceInfo {
index: r.index,
name: r.name,
max_output_channels: r.channels,
default_sample_rate: r.sample_rate,
is_default: r.is_default,
})
.collect())
}
#[cfg(any(feature = "capture", feature = "output"))]
pub fn resolve_output_device(selector: &DeviceSelector) -> Result<cpal::Device, DecibriError> {
let host = cpal::default_host();
match selector {
DeviceSelector::Default => host
.default_output_device()
.ok_or(DecibriError::NoOutputDeviceFound),
DeviceSelector::Index(idx) => {
let devices: Vec<_> = host
.output_devices()
.map_err(|e| DecibriError::Other(format!("Failed to enumerate devices: {e}")))?
.collect();
devices
.into_iter()
.nth(*idx)
.ok_or(DecibriError::DeviceIndexOutOfRange)
}
DeviceSelector::Name(query) => {
let query_lower = query.to_lowercase();
let devices = host
.output_devices()
.map_err(|e| DecibriError::Other(format!("Failed to enumerate devices: {e}")))?;
let mut matches: Vec<(usize, String, cpal::Device)> = Vec::new();
for (index, device) in devices.enumerate() {
let name = device
.description()
.map(|d| d.name().to_string())
.unwrap_or_default();
if name.to_lowercase().contains(&query_lower) {
matches.push((index, name, device));
}
}
match matches.len() {
0 => Err(DecibriError::DeviceNotFound(query.clone())),
1 => Ok(matches.into_iter().next().unwrap().2),
_ => {
let match_list = matches
.iter()
.map(|(idx, name, _)| format!(" [{idx}] {name}"))
.collect::<Vec<_>>()
.join("\n");
Err(DecibriError::MultipleDevicesMatch {
name: query.clone(),
matches: format!(
"{match_list}\nUse a more specific name or pass the device index directly."
),
})
}
}
}
}
}
#[cfg(all(test, any(feature = "capture", feature = "output")))]
mod tests {
use super::*;
fn row(id: Option<&str>, name: &str) -> (Option<String>, String, u16, u32) {
(id.map(String::from), name.to_string(), 2, 48_000)
}
#[test]
fn test_is_default_two_devices_same_name_different_ids() {
let rows = vec![
row(Some("usb-mic-A"), "Microphone"),
row(Some("usb-mic-B"), "Microphone"),
];
let result = compute_is_default(rows, Some("usb-mic-B".to_string()));
assert_eq!(result.len(), 2);
assert!(
!result[0].is_default,
"first duplicate-named mic must NOT be flagged when default is the second"
);
assert!(
result[1].is_default,
"second duplicate-named mic (matching default id) must be flagged"
);
assert_eq!(result[0].name, "Microphone");
assert_eq!(result[1].name, "Microphone");
}
#[test]
fn test_is_default_no_default_reported() {
let rows = vec![row(Some("mic-A"), "Mic A"), row(Some("mic-B"), "Mic B")];
let result = compute_is_default::<String>(rows, None);
assert_eq!(result.len(), 2);
assert!(
result.iter().all(|r| !r.is_default),
"no row may be flagged when host reports no default"
);
}
#[test]
fn test_is_default_row_with_failed_id() {
let rows = vec![
row(None, "Mystery device with unavailable id"),
row(Some("mic-B"), "Mic B"),
];
let result = compute_is_default(rows, Some("mic-B".to_string()));
assert_eq!(result.len(), 2);
assert!(
!result[0].is_default,
"row with id() == None must never be flagged default"
);
assert!(
result[1].is_default,
"row whose id matches the default must be flagged"
);
}
#[test]
fn test_is_default_empty_device_list() {
let rows: Vec<(Option<String>, String, u16, u32)> = vec![];
let result_no_default = compute_is_default::<String>(rows.clone(), None);
let result_with_default = compute_is_default(rows, Some("phantom-mic".to_string()));
assert!(result_no_default.is_empty());
assert!(result_with_default.is_empty());
}
}