Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src-tauri/src/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct RecordingState {
pub system_sample_tx: Option<mpsc::SyncSender<Vec<f32>>>,
pub transcription_tx: Option<mpsc::SyncSender<Vec<f32>>>,
pub transcription_rx: Option<mpsc::Receiver<Vec<f32>>>,
pub transcription_sample_rate: u32,
pub encoder_tx: Option<mpsc::SyncSender<Vec<i16>>>,
pub mixer_handle: Option<JoinHandle<()>>,
pub encoder_handle: Option<JoinHandle<()>>,
Expand All @@ -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,
Expand All @@ -53,6 +55,15 @@ fn state_handle_from_app(app: &AppHandle) -> Result<RecordingStateHandle, String
.ok_or_else(|| "recording state is not registered".to_string())
}

// Mic feeds transcription in Mic/Both modes; System mode feeds loopback.
fn transcription_source_rate(mic_rate: u32, loopback_rate: Option<u32>, 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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
5 changes: 3 additions & 2 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;
Expand All @@ -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
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;
Expand All @@ -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();
Expand All @@ -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()),
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/transcription/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct TranscriptionState {
pub struct StartWorkerArgs {
pub audio_tx: mpsc::SyncSender<Vec<f32>>,
pub audio_rx: mpsc::Receiver<Vec<f32>>,
pub sample_rate: u32,
pub on_segment: Channel<TranscriptEvent>,
pub data_dir: PathBuf,
pub db_pool: Option<SqlitePool>,
Expand Down Expand Up @@ -86,6 +87,7 @@ pub fn start_transcription_worker(
let StartWorkerArgs {
audio_tx,
audio_rx,
sample_rate,
on_segment,
data_dir,
db_pool,
Expand All @@ -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,
};

Expand Down
20 changes: 20 additions & 0 deletions src-tauri/src/transcription/resampler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
12 changes: 9 additions & 3 deletions src-tauri/src/transcription/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SegmentResult>,
}

Expand Down Expand Up @@ -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}");
Expand Down Expand Up @@ -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}");
Expand Down
Loading