use crate::settings::get_settings; use audio_toolkit::{vad::SmoothedVad, AudioRecorder, SileroVad}; use log::{debug, info}; use std::sync::{Arc, Mutex}; use std::time::Instant; use tauri::{App, Manager}; 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) -> Result { let silero = SileroVad::new(vad_path, 0.5) .map_err(|e| anyhow::anyhow!("Failed to create SileroVad: {}", e))?; let smoothed_vad = SmoothedVad::new(Box::new(silero), 15, 15); let recorder = AudioRecorder::new() .map_err(|e| anyhow::anyhow!("Failed to create AudioRecorder: {}", e))? .with_vad(Box::new(smoothed_vad)); Ok(recorder) } /* ──────────────────────────────────────────────────────────────── */ #[derive(Clone)] pub struct AudioRecordingManager { state: Arc>, mode: Arc>, app_handle: tauri::AppHandle, recorder: Arc>>, is_open: Arc>, is_recording: Arc>, } impl AudioRecordingManager { /* ---------- construction ------------------------------------------------ */ pub fn new(app: &App) -> Result { let settings = get_settings(&app.handle()); 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.handle().clone(), recorder: Arc::new(Mutex::new(None)), is_open: Arc::new(Mutex::new(false)), is_recording: Arc::new(Mutex::new(false)), }; // Always-on? Open immediately. if matches!(mode, MicrophoneMode::AlwaysOn) { manager.start_microphone_stream()?; } Ok(manager) } /* ---------- microphone life-cycle -------------------------------------- */ 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(); 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())?); } if let Some(rec) = recorder_opt.as_mut() { rec.open(None) .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; } 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() { eprintln!("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; } } eprintln!("Recorder not available"); false } else { false } } 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) => { eprintln!("stop() failed: {e}"); Vec::new() } } } else { eprintln!("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(); if s_len < WHISPER_SAMPLE_RATE && s_len > 1000 { let mut padded = samples; padded.resize(WHISPER_SAMPLE_RATE * 5 / 4, 0.0); Some(padded) } else { Some(samples) } } _ => None, } } }