stt-cli 0.2.1

Speech to text Cli using Groq API and OpenAI API
// src/audio/device_manager.rs
//
// This module handles audio device management, including:
// - Device discovery and selection
// - Device switching
// - Device capability reporting

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};

/// Manages audio devices and their selection
pub struct AudioDeviceManager {
    /// Name of the currently active device
    current_device_id: String,
    /// Set of known device names
    known_devices: HashSet<String>,
    /// CPAL host for device operations
    host: Host,
}

impl AudioDeviceManager {
    /// Create a new AudioDeviceManager
    ///
    /// Initializes with the default audio device and sets up communication channels.
    pub fn new(_data_dir: String) -> Result<Self> {
        let host = cpal::default_host();

        // Clean up any existing ALSA environment variables that might interfere
        std::env::remove_var("ALSA_DEVICE");
        std::env::remove_var("ALSA_CONFIG_PATH");
        std::env::set_var("JACK_NO_START_SERVER", "1"); // Prevent JACK server auto-start

        // Try to get the default input device, with fallback logic
        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);

        // Initialize known devices with the default device
        let mut known_devices = HashSet::new();
        known_devices.insert(current_device_id.clone());

        Ok(Self {
            current_device_id,
            known_devices,
            host,
        })
    }

    /// Try to find a working input device with fallbacks
    fn get_working_input_device(host: &Host) -> Result<Device> {
        // First try the default 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),
            }
        }

        // If default fails, try other input devices
        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")
    }

    /// Test if a device is working
    fn test_device(device: &Device) -> Result<()> {
        // Try to get device name
        let name = device
            .name()
            .unwrap_or_else(|_| "unnamed_device".to_string());
        debug!("Testing device: {}", name);

        // Get supported config ranges and find the best match
        let mut supported_configs = device.supported_input_configs()?.collect::<Vec<_>>();

        // Sort by closest sample rate to target first, then preferred buffer size
        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")?;

        // Find closest supported sample rate within device's range
        let target_rate = SAMPLE_RATE;
        let supported_rate = config_range
            .min_sample_rate()
            .0
            .max(target_rate)
            .min(config_range.max_sample_rate().0);

        // Get supported buffer size (use default if unknown)
        let buffer_size = match config_range.buffer_size() {
            cpal::SupportedBufferSize::Range { min, max } => {
                if BUFFER_SIZE >= *min && BUFFER_SIZE <= *max {
                    BUFFER_SIZE
                } else {
                    *min // Use minimum buffer size if target is out of range
                }
            }
            cpal::SupportedBufferSize::Unknown => BUFFER_SIZE,
        };

        // Build config with selected parameters
        let config = cpal::StreamConfig {
            channels: config_range.channels(),
            sample_rate: cpal::SampleRate(supported_rate),
            buffer_size: cpal::BufferSize::Fixed(buffer_size),
        };

        // Validate sample format
        if config_range.sample_format() != cpal::SampleFormat::F32 {
            return Err(anyhow::anyhow!(
                "Unsupported sample format: {:?} (required F32)",
                config_range.sample_format()
            ));
        }

        // Warn if using non-ideal sample rate but continue
        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(())
    }

    /// Start recording from the current device
    #[instrument(skip(self))]
    pub async fn start_recording(&self) -> Result<()> {
        info!("Starting recording on device: {}", self.current_device_id);

        // Removed command sending logic
        Ok(())
    }

    /// Stop recording on the current device
    #[instrument(skip(self))]
    pub async fn stop_recording(&self) {
        info!("Stopping recording");

        // Removed command sending logic
    }

    /// Toggle recording state (pause/resume)
    #[instrument(skip(self))]
    pub async fn toggle_recording(&self) {
        info!("Toggling recording state");

        // Removed command sending logic
    }

    /// Scan for new audio devices
    ///
    /// Returns a list of newly discovered device names.
    #[instrument(skip(self))]
    pub fn scan_for_new_devices(&mut self) -> Result<Vec<String>> {
        trace!("Scanning for new audio devices...");

        // Get all input devices
        let devices = self.host.input_devices()?;
        let mut new_devices = Vec::new();
        let mut current_known = HashSet::new();

        // Process each device
        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),
            }
        }

        // Check for removed devices
        let removed: Vec<_> = self
            .known_devices
            .difference(&current_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)
    }

    /// Switch to a different audio device
    #[instrument(skip(self))]
    pub async fn switch_device(&mut self, device_name: &str) -> Result<()> {
        // Check if already using this device
        if self.current_device_id == device_name {
            info!("Already using device: {}", device_name);
            return Ok(());
        }

        // Warn if device is not in known list
        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
        );

        // Update current device
        self.current_device_id = device_name.to_string();

        Ok(())
    }

    /// Get the current device ID
    pub fn current_device(&self) -> &str {
        &self.current_device_id
    }

    /// Get a list of all known devices
    pub fn known_devices(&self) -> Vec<String> {
        self.known_devices.iter().cloned().collect()
    }
}