diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index 6db6d6e..871d3e0 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -20,6 +20,7 @@ pub struct RecordingState { pub system_sample_tx: Option>>, pub transcription_tx: Option>>, pub transcription_rx: Option>>, + pub transcription_sample_rate: u32, pub encoder_tx: Option>>, pub mixer_handle: Option>, pub encoder_handle: Option>, @@ -37,6 +38,7 @@ impl Default for RecordingState { system_sample_tx: None, transcription_tx: None, transcription_rx: None, + transcription_sample_rate: 48_000, encoder_tx: None, mixer_handle: None, encoder_handle: None, @@ -53,6 +55,15 @@ fn state_handle_from_app(app: &AppHandle) -> Result, capture_mic: bool) -> u32 { + if capture_mic { + mic_rate + } else { + loopback_rate.unwrap_or(mic_rate) + } +} + pub fn start_recording( app: &AppHandle, output_path: PathBuf, @@ -111,6 +122,8 @@ pub fn start_recording( } let encoder_sample_rate = loopback_sample_rate.unwrap_or(mic_sample_rate); + let transcription_sample_rate = + transcription_source_rate(mic_sample_rate, loopback_sample_rate, capture_mic); let output_path_for_encoder = output_path.clone(); let encoder_handle = thread::spawn(move || { encoder::run_encoder( @@ -196,6 +209,7 @@ pub fn start_recording( state.system_sample_tx = if capture_system { Some(system_tx) } else { None }; state.transcription_tx = Some(transcription_tx); state.transcription_rx = Some(transcription_rx); + state.transcription_sample_rate = transcription_sample_rate; state.encoder_tx = Some(encoder_tx); state.mixer_handle = Some(mixer_handle); state.encoder_handle = Some(encoder_handle); @@ -303,3 +317,25 @@ pub fn resume_recording(app: &AppHandle) -> Result<(), String> { let _ = app.emit("recording-resumed", ()); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn transcription_source_rate_uses_mic_when_capturing_mic() { + // Mic/Both modes: transcription follows the mic rate (e.g. 16k Bluetooth HFP). + assert_eq!(transcription_source_rate(16_000, Some(48_000), true), 16_000); + } + + #[test] + fn transcription_source_rate_uses_loopback_when_mic_not_captured() { + // System mode: transcription follows the loopback rate. + assert_eq!(transcription_source_rate(48_000, Some(44_100), false), 44_100); + } + + #[test] + fn transcription_source_rate_falls_back_to_mic_when_no_loopback() { + assert_eq!(transcription_source_rate(48_000, None, false), 48_000); + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9e111c0..e13c6c6 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -491,7 +491,7 @@ pub async fn start_transcription( } } - let (audio_tx, audio_rx) = { + let (audio_tx, audio_rx, transcription_sample_rate) = { let mut state = recording_state .lock() .map_err(|_| "recording state lock poisoned".to_string())?; @@ -509,7 +509,7 @@ pub async fn start_transcription( .take() .ok_or_else(|| "transcription receiver unavailable; restart recording".to_string())?; - (audio_tx, audio_rx) + (audio_tx, audio_rx, state.transcription_sample_rate) }; let mut state = transcription_state @@ -518,6 +518,7 @@ pub async fn start_transcription( transcription::start_transcription_worker(&mut state, transcription::StartWorkerArgs { audio_tx, audio_rx, + sample_rate: transcription_sample_rate, on_segment, data_dir: data_dir.inner().0.clone(), db_pool: None, diff --git a/src-tauri/src/session.rs b/src-tauri/src/session.rs index 4c4724b..b6aff62 100644 --- a/src-tauri/src/session.rs +++ b/src-tauri/src/session.rs @@ -137,7 +137,7 @@ impl SessionCoordinator { } let _ = update_audio_sources(pool, meeting_id, &selected_audio_source); - let (audio_tx, audio_rx) = { + let (audio_tx, audio_rx, transcription_sample_rate) = { let mut recording_state = recording_state_handle .lock() .map_err(|_| "recording state lock poisoned".to_string())?; @@ -150,7 +150,7 @@ impl SessionCoordinator { .transcription_rx .take() .ok_or_else(|| "transcription receiver unavailable; restart recording".to_string())?; - (tx, rx) + (tx, rx, recording_state.transcription_sample_rate) }; let app_for_degraded = app.clone(); @@ -169,6 +169,7 @@ impl SessionCoordinator { transcription::start_transcription_worker(&mut transcription_state, transcription::StartWorkerArgs { audio_tx, audio_rx, + sample_rate: transcription_sample_rate, on_segment, data_dir: data_dir.to_path_buf(), db_pool: Some(pool.clone()), diff --git a/src-tauri/src/transcription/mod.rs b/src-tauri/src/transcription/mod.rs index 7eae782..9032d19 100644 --- a/src-tauri/src/transcription/mod.rs +++ b/src-tauri/src/transcription/mod.rs @@ -23,6 +23,7 @@ pub struct TranscriptionState { pub struct StartWorkerArgs { pub audio_tx: mpsc::SyncSender>, pub audio_rx: mpsc::Receiver>, + pub sample_rate: u32, pub on_segment: Channel, pub data_dir: PathBuf, pub db_pool: Option, @@ -86,6 +87,7 @@ pub fn start_transcription_worker( let StartWorkerArgs { audio_tx, audio_rx, + sample_rate, on_segment, data_dir, db_pool, @@ -109,6 +111,7 @@ pub fn start_transcription_worker( model_dir: model::whisper_turbo_model_dir(data_dir.as_path()), vad_model: vad_model.to_string_lossy().to_string(), recording_start_ms: 0, + sample_rate, result_tx, }; diff --git a/src-tauri/src/transcription/resampler.rs b/src-tauri/src/transcription/resampler.rs index 694871b..45fb9fa 100644 --- a/src-tauri/src/transcription/resampler.rs +++ b/src-tauri/src/transcription/resampler.rs @@ -75,3 +75,23 @@ impl RingAccumulator { chunks.into_iter() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resampler_handles_16k_input_rate() { + // Bluetooth HFP mics deliver 16 kHz; resampling must work from the real rate. + let mut resampler = AudioResampler::new(16_000, 16_000, 1_536).expect("builds"); + let out = resampler.process(&vec![0.0f32; 1_536]).expect("processes"); + assert!(!out.is_empty()); + } + + #[test] + fn resampler_downsamples_44100_input_rate() { + let mut resampler = AudioResampler::new(44_100, 16_000, 1_536).expect("builds"); + let out = resampler.process(&vec![0.0f32; 1_536]).expect("processes"); + assert!(!out.is_empty() && out.len() < 1_536); + } +} diff --git a/src-tauri/src/transcription/worker.rs b/src-tauri/src/transcription/worker.rs index f29885a..255cc72 100644 --- a/src-tauri/src/transcription/worker.rs +++ b/src-tauri/src/transcription/worker.rs @@ -17,6 +17,7 @@ pub struct WorkerConfig { pub model_dir: PathBuf, pub vad_model: String, pub recording_start_ms: u64, + pub sample_rate: u32, pub result_tx: mpsc::Sender, } @@ -77,7 +78,12 @@ pub fn run_worker( ); eprintln!("[transcription] creating audio resampler..."); - let mut resampler = match AudioResampler::new(48_000, 16_000, 1_536) { + let input_rate = if config.sample_rate == 0 { + 48_000 + } else { + config.sample_rate as usize + }; + let mut resampler = match AudioResampler::new(input_rate, ASR_SAMPLE_RATE as usize, 1_536) { Ok(resampler) => resampler, Err(err) => { eprintln!("failed to create audio resampler: {err}"); @@ -162,8 +168,8 @@ pub fn run_worker( match audio_rx.recv_timeout(Duration::from_millis(100)) { Ok(chunk) => { ring.push(&chunk); - for chunk_48k in ring.drain_chunks() { - let chunk_16k = match resampler.process(&chunk_48k) { + for input_chunk in ring.drain_chunks() { + let chunk_16k = match resampler.process(&input_chunk) { Ok(chunk_16k) => chunk_16k, Err(err) => { eprintln!("resampling failed: {err}");