use crate::audio::buffer::{BUFFER_SIZE, SAMPLE_RATE};
use anyhow::{Context, Result};
use cpal::{
traits::{DeviceTrait, HostTrait},
Device, Host,
};
use std::collections::HashSet;
use tracing::{debug, error, info, instrument, trace, warn};
pub struct AudioDeviceManager {
current_device_id: String,
known_devices: HashSet<String>,
host: Host,
}
impl AudioDeviceManager {
pub fn new(_data_dir: String) -> Result<Self> {
let host = cpal::default_host();
std::env::remove_var("ALSA_DEVICE");
std::env::remove_var("ALSA_CONFIG_PATH");
std::env::set_var("JACK_NO_START_SERVER", "1");
let default_device = Self::get_working_input_device(&host).or_else(|_| {
warn!("Primary device selection failed, trying system default");
host.default_input_device()
.context("No working audio input device found")
})?;
let current_device_id = match default_device.name() {
Ok(name) => name,
Err(_) => {
warn!("Device name unavailable, using fallback identifier");
"default_input_device".to_string()
}
};
info!("Using audio device: {}", current_device_id);
let mut known_devices = HashSet::new();
known_devices.insert(current_device_id.clone());
Ok(Self {
current_device_id,
known_devices,
host,
})
}
fn get_working_input_device(host: &Host) -> Result<Device> {
if let Some(device) = host.default_input_device() {
match Self::test_device(&device) {
Ok(_) => return Ok(device),
Err(e) => warn!("Default device failed: {}. Trying alternatives...", e),
}
}
for device in host.input_devices()? {
if Self::test_device(&device).is_ok() {
info!(
"Found working device: {}",
device.name().unwrap_or_default()
);
return Ok(device);
}
}
anyhow::bail!("No working audio input devices found")
}
fn test_device(device: &Device) -> Result<()> {
let name = device
.name()
.unwrap_or_else(|_| "unnamed_device".to_string());
debug!("Testing device: {}", name);
let mut supported_configs = device.supported_input_configs()?.collect::<Vec<_>>();
supported_configs.sort_by(|a, b| {
let a_diff = (a.min_sample_rate().0 as i32 - SAMPLE_RATE as i32).abs();
let b_diff = (b.min_sample_rate().0 as i32 - SAMPLE_RATE as i32).abs();
a_diff.cmp(&b_diff)
});
let config_range = supported_configs
.first()
.context("No supported input configurations found")?;
let target_rate = SAMPLE_RATE;
let supported_rate = config_range
.min_sample_rate()
.0
.max(target_rate)
.min(config_range.max_sample_rate().0);
let buffer_size = match config_range.buffer_size() {
cpal::SupportedBufferSize::Range { min, max } => {
if BUFFER_SIZE >= *min && BUFFER_SIZE <= *max {
BUFFER_SIZE
} else {
*min }
}
cpal::SupportedBufferSize::Unknown => BUFFER_SIZE,
};
let config = cpal::StreamConfig {
channels: config_range.channels(),
sample_rate: cpal::SampleRate(supported_rate),
buffer_size: cpal::BufferSize::Fixed(buffer_size),
};
if config_range.sample_format() != cpal::SampleFormat::F32 {
return Err(anyhow::anyhow!(
"Unsupported sample format: {:?} (required F32)",
config_range.sample_format()
));
}
if supported_rate != SAMPLE_RATE {
warn!(
"Using sample rate {}Hz instead of 16000Hz (device supports {}-{}Hz)",
supported_rate,
config_range.min_sample_rate().0,
config_range.max_sample_rate().0
);
}
debug!("Device {} passed validation", name);
Ok(())
}
#[instrument(skip(self))]
pub async fn start_recording(&self) -> Result<()> {
info!("Starting recording on device: {}", self.current_device_id);
Ok(())
}
#[instrument(skip(self))]
pub async fn stop_recording(&self) {
info!("Stopping recording");
}
#[instrument(skip(self))]
pub async fn toggle_recording(&self) {
info!("Toggling recording state");
}
#[instrument(skip(self))]
pub fn scan_for_new_devices(&mut self) -> Result<Vec<String>> {
trace!("Scanning for new audio devices...");
let devices = self.host.input_devices()?;
let mut new_devices = Vec::new();
let mut current_known = HashSet::new();
for device in devices {
match device.name() {
Ok(name) => {
current_known.insert(name.clone());
if !self.known_devices.contains(&name) {
debug!("Found new audio device: {}", name);
self.known_devices.insert(name.clone());
new_devices.push(name);
}
}
Err(e) => warn!("Failed to get device name: {}", e),
}
}
let removed: Vec<_> = self
.known_devices
.difference(¤t_known)
.cloned()
.collect();
if !removed.is_empty() {
debug!("Removed devices: {:?}", removed);
for r in removed {
self.known_devices.remove(&r);
if self.current_device_id == r {
warn!("Currently active device '{}' was removed!", r);
}
}
}
if !new_devices.is_empty() {
info!("Discovered {} new audio devices", new_devices.len());
}
Ok(new_devices)
}
#[instrument(skip(self))]
pub async fn switch_device(&mut self, device_name: &str) -> Result<()> {
if self.current_device_id == device_name {
info!("Already using device: {}", device_name);
return Ok(());
}
if !self.known_devices.contains(device_name) {
warn!("Device '{}' is not in the known devices list", device_name);
}
info!(
"Switching audio device: {} -> {}",
self.current_device_id, device_name
);
self.current_device_id = device_name.to_string();
Ok(())
}
pub fn current_device(&self) -> &str {
&self.current_device_id
}
pub fn known_devices(&self) -> Vec<String> {
self.known_devices.iter().cloned().collect()
}
}