use anyhow::{anyhow, Context, Result};
use cpal::traits::{DeviceTrait, HostTrait};
use cpal::{
Device, Host, InputDevices, OutputDevices, SupportedStreamConfig, SupportedStreamConfigRange,
};
use dialoguer::{theme::ColorfulTheme, Select};
use std::collections::HashSet;
use std::sync::Arc;
use tracing::{debug, info, warn};
#[derive(Clone)]
pub struct AudioDevice {
pub name: String,
pub cpal_device: Device, pub is_input: bool, }
impl AudioDevice {
pub fn name(&self) -> String {
self.name.clone()
}
pub fn is_input(&self) -> bool {
self.is_input
}
fn from_cpal_device(device: &Device, is_input: bool) -> Result<Self> {
Ok(AudioDevice {
name: device.name().context("Failed to get device name")?,
cpal_device: device.clone(),
is_input,
})
}
pub fn default_input() -> Result<Arc<Self>> {
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or_else(|| anyhow!("No default input device available"))?;
Ok(Arc::new(Self::from_cpal_device(&device, true)?))
}
pub fn default_output() -> Result<Arc<Self>> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow!("No default output device available"))?;
Ok(Arc::new(Self::from_cpal_device(&device, false)?))
}
pub fn list_input_devices() -> Result<Vec<Arc<Self>>> {
let host = cpal::default_host();
Self::list_devices_internal(host.input_devices()?, true)
}
pub fn list_output_devices() -> Result<Vec<Arc<Self>>> {
let host = cpal::default_host();
Self::list_devices_internal(host.output_devices()?, false)
}
fn list_devices_internal<I>(devices: I, is_input: bool) -> Result<Vec<Arc<Self>>>
where
I: Iterator<Item = Device>,
{
let mut result = Vec::new();
for device in devices {
match Self::from_cpal_device(&device, is_input) {
Ok(audio_device) => result.push(Arc::new(audio_device)),
Err(e) => warn!("Failed to process device: {}", e),
}
}
Ok(result)
}
pub async fn get_cpal_device_and_config(&self) -> Result<(Device, SupportedStreamConfig)> {
let host = cpal::default_host();
let devices = if self.is_input {
host.input_devices()?
} else {
host.output_devices()?
};
for device in devices {
let device_name = device.name()?;
if device_name == self.name {
let config = device
.default_input_config()
.or_else(|_| device.default_output_config())
.context("Failed to get device config")?;
debug!("Found device '{}' with config: {:?}", self.name, config);
return Ok((device, config));
}
}
Err(anyhow!("Device '{}' not found", self.name))
}
}
pub fn select_audio_device(host: &Host) -> Result<Arc<AudioDevice>> {
let devices =
AudioDevice::list_input_devices().context("Failed to list input devices for selection")?;
if devices.is_empty() {
return Err(anyhow!("No input devices available to select"));
}
let device_names: Vec<String> = devices.iter().map(|d| d.name.clone()).collect();
info!("Please select an audio input device:");
let selection = Select::with_theme(&ColorfulTheme::default())
.items(&device_names)
.default(0)
.interact()
.context("Failed to get user selection")?;
info!("Selected device: {}", device_names[selection]);
Ok(devices[selection].clone())
}
pub fn find_device_by_name(host: &Host, name: &str) -> Result<Arc<AudioDevice>> {
debug!("Searching for device: '{}'", name);
let lower_name = name.to_lowercase();
if let Ok(devices) = AudioDevice::list_input_devices() {
if let Some(device) = devices
.into_iter()
.find(|d| d.name.to_lowercase() == lower_name)
{
info!("Found input device: {}", device.name);
return Ok(device);
}
}
warn!("Device '{}' not found.", name);
Err(anyhow!("Device '{}' not found", name))
}
pub fn get_device_capabilities(device: &Device) -> Result<String> {
let device_name = device.name().unwrap_or_else(|_| "Unknown".to_string());
let mut capabilities = format!("Capabilities for device: {}\n", device_name);
append_configs(&mut capabilities, device.supported_input_configs(), "Input")?;
append_configs(
&mut capabilities,
device.supported_output_configs(),
"Output",
)?;
Ok(capabilities)
}
fn append_configs(
caps: &mut String,
configs: Result<
impl Iterator<Item = SupportedStreamConfigRange>,
cpal::SupportedStreamConfigsError,
>,
config_type: &str,
) -> Result<()> {
match configs {
Ok(configs) => {
let mut unique_configs = HashSet::new();
for config in configs {
let config_str = format!(
" {} Channels: {}, Sample Rate: {}-{} Hz, Format: {:?}\n",
config_type,
config.channels(),
config.min_sample_rate().0,
config.max_sample_rate().0,
config.sample_format()
);
unique_configs.insert(config_str);
}
let mut sorted_configs: Vec<_> = unique_configs.into_iter().collect();
sorted_configs.sort();
for config in sorted_configs {
caps.push_str(&config);
}
Ok(())
}
Err(e) => {
caps.push_str(&format!(" Error getting {} configs: {}\n", config_type, e));
Ok(())
}
}
}
pub fn list_audio_devices() -> Result<Vec<Arc<AudioDevice>>> {
info!("Listing available audio devices...");
let mut devices = Vec::new();
match AudioDevice::list_input_devices() {
Ok(inputs) => devices.extend(inputs),
Err(e) => warn!("Failed to list input devices: {}", e),
}
match AudioDevice::list_output_devices() {
Ok(outputs) => devices.extend(outputs),
Err(e) => warn!("Failed to list output devices: {}", e),
}
if devices.is_empty() {
warn!("No audio devices found");
Err(anyhow!("No audio devices found"))
} else {
debug!("Found {} audio devices", devices.len());
Ok(devices)
}
}