use crate::audio_toolkit::{list_input_devices, vad::SmoothedVad, AudioRecorder, SileroVad}; use crate::helpers::clamshell; use crate::settings::{get_settings, AppSettings}; use crate::utils; use log::{debug, error, info}; use std::sync::{Arc, Mutex}; use std::time::Instant; use tauri::Manager; fn set_mute(mute: bool) { // Expected behavior: // - Windows: works on most systems using standard audio drivers. // - Linux: works on many systems (PipeWire, PulseAudio, ALSA), // but some distros may lack the tools used. // - macOS: works on most standard setups via AppleScript. // If unsupported, fails silently. #[cfg(target_os = "windows")] { unsafe { use windows::Win32::{ Media::Audio::{ eMultimedia, eRender, Endpoints::IAudioEndpointVolume, IMMDeviceEnumerator, MMDeviceEnumerator, }, System::Com::{CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED}, }; macro_rules! unwrap_or_return { ($expr:expr) => { match $expr { Ok(val) => val, Err(_) => return, } }; } // Initialize the COM library for this thread. // If already initialized (e.g., by another library like Tauri), this does nothing. let _ = CoInitializeEx(None, COINIT_MULTITHREADED); let all_devices: IMMDeviceEnumerator = unwrap_or_return!(CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)); let default_device = unwrap_or_return!(all_devices.GetDefaultAudioEndpoint(eRender, eMultimedia)); let volume_interface = unwrap_or_return!( default_device.Activate::(CLSCTX_ALL, None) ); let _ = volume_interface.SetMute(mute, std::ptr::null()); } } #[cfg(target_os = "linux")] { use std::process::Command; let mute_val = if mute { "1" } else { "0" }; let amixer_state = if mute { "mute" } else { "unmute" }; // Try multiple backends to increase compatibility // 1. PipeWire (wpctl) if Command::new("wpctl") .args(["set-mute", "@DEFAULT_AUDIO_SINK@", mute_val]) .output() .map(|o| o.status.success()) .unwrap_or(false) { return; } // 2. PulseAudio (pactl) if Command::new("pactl") .args(["set-sink-mute", "@DEFAULT_SINK@", mute_val]) .output() .map(|o| o.status.success()) .unwrap_or(false) { return; } // 3. ALSA (amixer) let _ = Command::new("amixer") .args(["set", "Master", amixer_state]) .output(); } #[cfg(target_os = "macos")] { use std::process::Command; let script = format!( "set volume output muted {}", if mute { "true" } else { "false" } ); let _ = Command::new("osascript").args(["-e", &script]).output(); } } const WHISPER_SAMPLE_RATE: usize = 16000; /* ──────────────────────────────────────────────────────────────── */ #[derive(Clone, Debug)] pub enum RecordingState { Idle, Recording { binding_id: String }, } #[derive(Clone, Debug)] pub enum MicrophoneMode { AlwaysOn, OnDemand, } /* ──────────────────────────────────────────────────────────────── */ fn create_audio_recorder( vad_path: &str, app_handle: &tauri::AppHandle, ) -> Result { let silero = SileroVad::new(vad_path, 0.3) .map_err(|e| anyhow::anyhow!("Failed to create SileroVad: {}", e))?; let smoothed_vad = SmoothedVad::new(Box::new(silero), 15, 15, 2); // Recorder with VAD plus a spectrum-level callback that forwards updates to // the frontend. let recorder = AudioRecorder::new() .map_err(|e| anyhow::anyhow!("Failed to create AudioRecorder: {}", e))? .with_vad(Box::new(smoothed_vad)) .with_level_callback({ let app_handle = app_handle.clone(); move |levels| { utils::emit_levels(&app_handle, &levels); } }); Ok(recorder) } /* ──────────────────────────────────────────────────────────────── */ #[derive(Clone)] pub struct AudioRecordingManager { state: Arc>, mode: Arc>, app_handle: tauri::AppHandle, recorder: Arc>>, is_open: Arc>, is_recording: Arc>, did_mute: Arc>, } impl AudioRecordingManager { /* ---------- construction ------------------------------------------------ */ pub fn new(app: &tauri::AppHandle) -> Result { let settings = get_settings(app); let mode = if settings.always_on_microphone { MicrophoneMode::AlwaysOn } else { MicrophoneMode::OnDemand }; let manager = Self { state: Arc::new(Mutex::new(RecordingState::Idle)), mode: Arc::new(Mutex::new(mode.clone())), app_handle: app.clone(), recorder: Arc::new(Mutex::new(None)), is_open: Arc::new(Mutex::new(false)), is_recording: Arc::new(Mutex::new(false)), did_mute: Arc::new(Mutex::new(false)), }; // Always-on? Open immediately. if matches!(mode, MicrophoneMode::AlwaysOn) { manager.start_microphone_stream()?; } Ok(manager) } /* ---------- helper methods --------------------------------------------- */ fn get_effective_microphone_device(&self, settings: &AppSettings) -> Option { // Check if we're in clamshell mode and have a clamshell microphone configured let use_clamshell_mic = if let Ok(is_clamshell) = clamshell::is_clamshell() { is_clamshell && settings.clamshell_microphone.is_some() } else { false }; let device_name = if use_clamshell_mic { settings.clamshell_microphone.as_ref().unwrap() } else { settings.selected_microphone.as_ref()? }; // Find the device by name match list_input_devices() { Ok(devices) => devices .into_iter() .find(|d| d.name == *device_name) .map(|d| d.device), Err(e) => { debug!("Failed to list devices, using default: {}", e); None } } } /* ---------- microphone life-cycle -------------------------------------- */ /// Applies mute if mute_while_recording is enabled and stream is open pub fn apply_mute(&self) { let settings = get_settings(&self.app_handle); let mut did_mute_guard = self.did_mute.lock().unwrap(); if settings.mute_while_recording && *self.is_open.lock().unwrap() { set_mute(true); *did_mute_guard = true; debug!("Mute applied"); } } /// Removes mute if it was applied pub fn remove_mute(&self) { let mut did_mute_guard = self.did_mute.lock().unwrap(); if *did_mute_guard { set_mute(false); *did_mute_guard = false; debug!("Mute removed"); } } pub fn start_microphone_stream(&self) -> Result<(), anyhow::Error> { let mut open_flag = self.is_open.lock().unwrap(); if *open_flag { debug!("Microphone stream already active"); return Ok(()); } let start_time = Instant::now(); // Don't mute immediately - caller will handle muting after audio feedback let mut did_mute_guard = self.did_mute.lock().unwrap(); *did_mute_guard = false; let vad_path = self .app_handle .path() .resolve( "resources/models/silero_vad_v4.onnx", tauri::path::BaseDirectory::Resource, ) .map_err(|e| anyhow::anyhow!("Failed to resolve VAD path: {}", e))?; let mut recorder_opt = self.recorder.lock().unwrap(); if recorder_opt.is_none() { *recorder_opt = Some(create_audio_recorder( vad_path.to_str().unwrap(), &self.app_handle, )?); } // Get the selected device from settings, considering clamshell mode let settings = get_settings(&self.app_handle); let selected_device = self.get_effective_microphone_device(&settings); if let Some(rec) = recorder_opt.as_mut() { rec.open(selected_device) .map_err(|e| anyhow::anyhow!("Failed to open recorder: {}", e))?; } *open_flag = true; info!( "Microphone stream initialized in {:?}", start_time.elapsed() ); Ok(()) } pub fn stop_microphone_stream(&self) { let mut open_flag = self.is_open.lock().unwrap(); if !*open_flag { return; } let mut did_mute_guard = self.did_mute.lock().unwrap(); if *did_mute_guard { set_mute(false); } *did_mute_guard = false; if let Some(rec) = self.recorder.lock().unwrap().as_mut() { // If still recording, stop first. if *self.is_recording.lock().unwrap() { let _ = rec.stop(); *self.is_recording.lock().unwrap() = false; } let _ = rec.close(); } *open_flag = false; debug!("Microphone stream stopped"); } /* ---------- mode switching --------------------------------------------- */ pub fn update_mode(&self, new_mode: MicrophoneMode) -> Result<(), anyhow::Error> { let mode_guard = self.mode.lock().unwrap(); let cur_mode = mode_guard.clone(); match (cur_mode, &new_mode) { (MicrophoneMode::AlwaysOn, MicrophoneMode::OnDemand) => { if matches!(*self.state.lock().unwrap(), RecordingState::Idle) { drop(mode_guard); self.stop_microphone_stream(); } } (MicrophoneMode::OnDemand, MicrophoneMode::AlwaysOn) => { drop(mode_guard); self.start_microphone_stream()?; } _ => {} } *self.mode.lock().unwrap() = new_mode; Ok(()) } /* ---------- recording --------------------------------------------------- */ pub fn try_start_recording(&self, binding_id: &str) -> bool { let mut state = self.state.lock().unwrap(); if let RecordingState::Idle = *state { // Ensure microphone is open in on-demand mode if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) { if let Err(e) = self.start_microphone_stream() { error!("Failed to open microphone stream: {e}"); return false; } } if let Some(rec) = self.recorder.lock().unwrap().as_ref() { if rec.start().is_ok() { *self.is_recording.lock().unwrap() = true; *state = RecordingState::Recording { binding_id: binding_id.to_string(), }; debug!("Recording started for binding {binding_id}"); return true; } } error!("Recorder not available"); false } else { false } } pub fn update_selected_device(&self) -> Result<(), anyhow::Error> { // If currently open, restart the microphone stream to use the new device if *self.is_open.lock().unwrap() { self.stop_microphone_stream(); self.start_microphone_stream()?; } Ok(()) } pub fn stop_recording(&self, binding_id: &str) -> Option> { let mut state = self.state.lock().unwrap(); match *state { RecordingState::Recording { binding_id: ref active, } if active == binding_id => { *state = RecordingState::Idle; drop(state); let samples = if let Some(rec) = self.recorder.lock().unwrap().as_ref() { match rec.stop() { Ok(buf) => buf, Err(e) => { error!("stop() failed: {e}"); Vec::new() } } } else { error!("Recorder not available"); Vec::new() }; *self.is_recording.lock().unwrap() = false; // In on-demand mode turn the mic off again if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) { self.stop_microphone_stream(); } // Pad if very short let s_len = samples.len(); // debug!("Got {} samples", s_len); if s_len < WHISPER_SAMPLE_RATE && s_len > 0 { let mut padded = samples; padded.resize(WHISPER_SAMPLE_RATE * 5 / 4, 0.0); Some(padded) } else { Some(samples) } } _ => None, } } /// Cancel any ongoing recording without returning audio samples pub fn cancel_recording(&self) { let mut state = self.state.lock().unwrap(); if let RecordingState::Recording { .. } = *state { *state = RecordingState::Idle; drop(state); if let Some(rec) = self.recorder.lock().unwrap().as_ref() { let _ = rec.stop(); // Discard the result } *self.is_recording.lock().unwrap() = false; // In on-demand mode turn the mic off again if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) { self.stop_microphone_stream(); } } } }