Handy/src-tauri/src/managers/audio.rs
2025-07-10 11:59:05 -07:00

233 lines
7.8 KiB
Rust

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<AudioRecorder, anyhow::Error> {
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<Mutex<RecordingState>>,
mode: Arc<Mutex<MicrophoneMode>>,
app_handle: tauri::AppHandle,
recorder: Arc<Mutex<Option<AudioRecorder>>>,
is_open: Arc<Mutex<bool>>,
is_recording: Arc<Mutex<bool>>,
}
impl AudioRecordingManager {
/* ---------- construction ------------------------------------------------ */
pub fn new(app: &App) -> Result<Self, anyhow::Error> {
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<Vec<f32>> {
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,
}
}
}