use cpal::{DeviceDescription, DeviceId, InterfaceType};
use std::collections::HashMap;
pub(crate) fn audio_device_labels(
devices: &[(DeviceId, DeviceDescription)],
) -> Vec<(DeviceId, String)> {
let mut duplicate_names = HashMap::<&str, usize>::new();
for (_, description) in devices {
*duplicate_names.entry(description.name()).or_default() += 1;
}
let preliminary_labels = devices
.iter()
.map(|(device_id, description)| {
(
device_id.clone(),
format_audio_device_label(
description.name(),
description.manufacturer(),
description.address(),
description.interface_type(),
description.driver(),
&description.extended().collect::<Vec<_>>(),
duplicate_names[description.name()] > 1,
),
)
})
.collect::<Vec<_>>();
let mut duplicate_labels = HashMap::<String, usize>::new();
for (_, label) in &preliminary_labels {
*duplicate_labels.entry(label.clone()).or_default() += 1;
}
let mut duplicate_index = HashMap::<String, usize>::new();
preliminary_labels
.into_iter()
.map(|(device_id, label)| {
if duplicate_labels[&label] <= 1 {
return (device_id, label);
}
let index = duplicate_index
.entry(label.clone())
.and_modify(|count| *count += 1)
.or_insert(1);
(device_id, format!("{} ({})", label, index))
})
.collect()
}
pub(crate) fn audio_device_label(_device_id: &DeviceId, description: &DeviceDescription) -> String {
format_audio_device_label(
description.name(),
description.manufacturer(),
description.address(),
description.interface_type(),
description.driver(),
&description.extended().collect::<Vec<_>>(),
false,
)
}
fn format_audio_device_label(
name: &str,
manufacturer: Option<&str>,
address: Option<&str>,
interface_type: InterfaceType,
driver: Option<&str>,
extended: &[&str],
duplicate_name: bool,
) -> String {
let primary = preferred_primary_label(name, manufacturer, extended);
let detail = label_detail(
&primary,
manufacturer,
address,
interface_type,
driver,
extended,
duplicate_name,
);
if let Some(detail) = detail {
format!("{} ({})", primary, detail)
} else {
primary
}
}
fn preferred_primary_label(name: &str, manufacturer: Option<&str>, extended: &[&str]) -> String {
let normalized_name = normalize_label(name);
let best_extended = preferred_extended_line(extended, &normalized_name);
if is_generic_device_name(&normalized_name) {
if let Some(best_extended) = best_extended {
return best_extended;
}
if let Some(manufacturer) = manufacturer.and_then(sanitize_metadata_line) {
if manufacturer.to_lowercase().contains("audio") {
return manufacturer;
}
return format!("{} Audio", manufacturer);
}
}
normalized_name
}
fn label_detail(
primary_label: &str,
manufacturer: Option<&str>,
address: Option<&str>,
interface_type: InterfaceType,
driver: Option<&str>,
extended: &[&str],
duplicate_name: bool,
) -> Option<String> {
if duplicate_name {
if let Some(manufacturer) =
manufacturer
.and_then(sanitize_metadata_line)
.filter(|manufacturer| {
!primary_label
.to_lowercase()
.contains(&manufacturer.to_lowercase())
})
{
return Some(manufacturer);
}
if let Some(detail) = secondary_extended_line(extended, primary_label) {
return Some(detail);
}
if let Some(address) = address.and_then(pretty_address) {
return Some(address);
}
if interface_type != InterfaceType::Unknown {
return Some(interface_type.to_string());
}
if let Some(driver) = driver.and_then(sanitize_metadata_line) {
return Some(driver);
}
}
None
}
fn sanitize_metadata_line(text: &str) -> Option<String> {
let normalized = normalize_label(text);
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
fn preferred_extended_line(extended: &[&str], name: &str) -> Option<String> {
extended
.iter()
.filter_map(|line| sanitize_metadata_line(line))
.find(|line| is_useful_extended_line(line, name))
}
fn secondary_extended_line(extended: &[&str], primary_label: &str) -> Option<String> {
extended
.iter()
.filter_map(|line| sanitize_metadata_line(line))
.find(|line| {
!line.eq_ignore_ascii_case(primary_label)
&& !line.eq_ignore_ascii_case("Direct hardware device without any conversions")
&& !line.eq_ignore_ascii_case("Default Audio Device")
})
}
fn is_useful_extended_line(line: &str, name: &str) -> bool {
!line.eq_ignore_ascii_case(name)
&& !line.eq_ignore_ascii_case("Direct hardware device without any conversions")
&& !line.eq_ignore_ascii_case("Default Audio Device")
}
fn is_generic_device_name(name: &str) -> bool {
let lower = name.to_lowercase();
lower == "default"
|| lower == "default audio device"
|| lower == "audio"
|| lower == "usb audio"
|| lower.starts_with("monitor of")
}
fn normalize_label(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn pretty_address(address: &str) -> Option<String> {
let normalized = normalize_label(address);
if normalized.is_empty() {
return None;
}
if let Some(value) = normalized.strip_prefix("hw:") {
let parts = value.split(',').collect::<Vec<_>>();
if parts.len() == 2 {
return Some(format!("Card {}, Device {}", parts[0], parts[1]));
}
}
(normalized.len() <= 24).then_some(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
use cpal::DeviceDescriptionBuilder;
use std::str::FromStr;
#[test]
fn format_audio_device_label_prefers_manufacturer_and_address() {
let label = format_audio_device_label(
"USB Audio",
Some("Focusrite"),
Some("hw:2,0"),
InterfaceType::Usb,
Some("ALSA"),
&[],
true,
);
assert_eq!(label, "Focusrite Audio (Card 2, Device 0)");
}
#[test]
fn format_audio_device_label_uses_pretty_address_for_duplicate_names() {
let label = format_audio_device_label(
"USB Audio",
None,
Some("hw:2,0"),
InterfaceType::Unknown,
None,
&[],
true,
);
assert_eq!(label, "USB Audio (Card 2, Device 0)");
}
#[test]
fn format_audio_device_label_uses_driver_when_other_metadata_is_missing() {
let label = format_audio_device_label(
"USB Audio",
None,
None,
InterfaceType::Unknown,
Some("ALSA"),
&[],
true,
);
assert_eq!(label, "USB Audio (ALSA)");
}
#[test]
fn format_audio_device_label_prefers_extended_human_description() {
let description = DeviceDescriptionBuilder::new("USB Audio")
.manufacturer("Focusrite Audio")
.address("hw:2,0")
.extended(vec![
"USB Audio".to_string(),
"Scarlett 2i2 USB Audio Interface".to_string(),
])
.build();
let label = format_audio_device_label(
description.name(),
description.manufacturer(),
description.address(),
description.interface_type(),
description.driver(),
&description.extended().collect::<Vec<_>>(),
true,
);
assert_eq!(label, "Scarlett 2i2 USB Audio Interface (Focusrite Audio)");
}
#[test]
fn audio_device_labels_add_number_suffix_for_identical_final_labels() {
let devices = vec![
(
DeviceId::from_str("ALSA://device/1").expect("valid device id"),
DeviceDescriptionBuilder::new("USB Audio").build(),
),
(
DeviceId::from_str("ALSA://device/2").expect("valid device id"),
DeviceDescriptionBuilder::new("USB Audio").build(),
),
];
let labels = audio_device_labels(&devices)
.into_iter()
.map(|(_, label)| label)
.collect::<Vec<_>>();
assert_eq!(labels, vec!["USB Audio (1)", "USB Audio (2)"]);
}
}