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
44 changes: 32 additions & 12 deletions src-tauri/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,6 @@ impl ShortcutAction for TranscribeAction {
tm.initiate_model_load();

let binding_id = binding_id.to_string();
change_tray_icon(app, TrayIconState::Recording);
show_recording_overlay(app);

let rm = app.state::<Arc<AudioRecordingManager>>();

// Get the microphone mode to determine audio feedback timing
Expand All @@ -237,17 +234,19 @@ impl ShortcutAction for TranscribeAction {
if is_always_on {
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
debug!("Always-on mode: Playing audio feedback immediately");
let rm_clone = Arc::clone(&rm);
let app_clone = app.clone();
// The blocking helper exits immediately if audio feedback is disabled,
// so we can always reuse this thread to ensure mute happens right after playback.
std::thread::spawn(move || {
play_feedback_sound_blocking(&app_clone, SoundType::Start);
rm_clone.apply_mute();
});

recording_started = rm.try_start_recording(&binding_id);
debug!("Recording started: {}", recording_started);

if recording_started {
let rm_clone = Arc::clone(&rm);
let app_clone = app.clone();
// The blocking helper exits immediately if audio feedback is disabled,
// so we can always reuse this thread to ensure mute happens right after playback.
std::thread::spawn(move || {
play_feedback_sound_blocking(&app_clone, SoundType::Start);
rm_clone.apply_mute();
});
}
} else {
// On-demand mode: Start recording first, then play audio feedback, then apply mute
// This allows the microphone to be activated before playing the sound
Expand All @@ -273,8 +272,17 @@ impl ShortcutAction for TranscribeAction {
}

if recording_started {
change_tray_icon(app, TrayIconState::Recording);
show_recording_overlay(app);
// Dynamically register the cancel shortcut in a separate task to avoid deadlock
shortcut::register_cancel_shortcut(app);
} else {
utils::hide_recording_overlay(app);
change_tray_icon(app, TrayIconState::Idle);

if let Ok(mut states) = app.state::<ManagedToggleState>().lock() {
states.active_toggles.insert(binding_id.clone(), false);
}
}

debug!(
Expand All @@ -292,6 +300,18 @@ impl ShortcutAction for TranscribeAction {

let ah = app.clone();
let rm = Arc::clone(&app.state::<Arc<AudioRecordingManager>>());

if !rm.is_recording() {
utils::hide_recording_overlay(app);
change_tray_icon(app, TrayIconState::Idle);

if let Ok(mut states) = app.state::<ManagedToggleState>().lock() {
states
.active_toggles
.insert(binding_id.to_string(), false);
}
return;
}
let tm = Arc::clone(&app.state::<Arc<TranscriptionManager>>());
let hm = Arc::clone(&app.state::<Arc<HistoryManager>>());

Expand Down
94 changes: 66 additions & 28 deletions src-tauri/src/audio_toolkit/audio/recorder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
io::Error,
io::{Error, ErrorKind},
sync::{mpsc, Arc, Mutex},
time::Duration,
};
Expand Down Expand Up @@ -129,18 +129,28 @@ impl AudioRecorder {
}

pub fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(tx) = &self.cmd_tx {
tx.send(Cmd::Start)?;
}
let tx = self
.cmd_tx
.as_ref()
.ok_or_else(|| Error::new(std::io::ErrorKind::NotConnected, "AudioRecorder not open"))?;
tx.send(Cmd::Start)?;
Ok(())
}

pub fn stop(&self) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
let (resp_tx, resp_rx) = mpsc::channel();
if let Some(tx) = &self.cmd_tx {
tx.send(Cmd::Stop(resp_tx))?;
}
Ok(resp_rx.recv()?) // wait for the samples
self.cmd_tx
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::NotConnected, "AudioRecorder not open"))?
.send(Cmd::Stop(resp_tx))?;

resp_rx.recv_timeout(Duration::from_secs(5)).map_err(|e| {
let (kind, msg) = match e {
mpsc::RecvTimeoutError::Timeout => (ErrorKind::TimedOut, "Stop timed out"),
mpsc::RecvTimeoutError::Disconnected => (ErrorKind::BrokenPipe, "Worker disconnected"),
};
Box::new(Error::new(kind, msg)) as _
})
}

pub fn close(&mut self) -> Result<(), Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -287,31 +297,15 @@ fn run_consumer(
}
}

loop {
let raw = match sample_rx.recv() {
Ok(s) => s,
Err(_) => break, // stream closed
};
let mut samples_disconnected = false;

// ---------- spectrum processing ---------------------------------- //
if let Some(buckets) = visualizer.feed(&raw) {
if let Some(cb) = &level_cb {
cb(buckets);
}
}

// ---------- existing pipeline ------------------------------------ //
frame_resampler.push(&raw, &mut |frame: &[f32]| {
handle_frame(frame, recording, &vad, &mut processed_samples)
});

// non-blocking check for a command
loop {
while let Ok(cmd) = cmd_rx.try_recv() {
match cmd {
Cmd::Start => {
processed_samples.clear();
recording = true;
visualizer.reset(); // Reset visualization buffer
visualizer.reset();
if let Some(v) = &vad {
v.lock().unwrap().reset();
}
Expand All @@ -320,7 +314,6 @@ fn run_consumer(
recording = false;

frame_resampler.finish(&mut |frame: &[f32]| {
// we still want to process the last few frames
handle_frame(frame, true, &vad, &mut processed_samples)
});

Expand All @@ -329,5 +322,50 @@ fn run_consumer(
Cmd::Shutdown => return,
}
}

if samples_disconnected {
match cmd_rx.recv() {
Ok(cmd) => {
match cmd {
Cmd::Start => {
processed_samples.clear();
recording = true;
visualizer.reset();
if let Some(v) = &vad {
v.lock().unwrap().reset();
}
}
Cmd::Stop(reply_tx) => {
recording = false;
frame_resampler.finish(&mut |frame: &[f32]| {
handle_frame(frame, true, &vad, &mut processed_samples)
});
let _ = reply_tx.send(std::mem::take(&mut processed_samples));
}
Cmd::Shutdown => return,
}
}
Err(_) => return,
}
continue;
}

match sample_rx.recv_timeout(Duration::from_millis(50)) {
Ok(raw) => {
if let Some(buckets) = visualizer.feed(&raw) {
if let Some(cb) = &level_cb {
cb(buckets);
}
}

frame_resampler.push(&raw, &mut |frame: &[f32]| {
handle_frame(frame, recording, &vad, &mut processed_samples)
});
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => {
samples_disconnected = true;
}
}
}
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ fn build_console_filter() -> env_filter::Filter {
struct ShortcutToggleStates {
// Map: shortcut_binding_id -> is_active
active_toggles: HashMap<String, bool>,
last_trigger_ms: HashMap<String, u64>,
}

type ManagedToggleState = Mutex<ShortcutToggleStates>;
Expand Down
100 changes: 74 additions & 26 deletions src-tauri/src/managers/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,11 @@ impl AudioRecordingManager {
/// 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();
let is_open = *self.is_open.lock().unwrap();
let is_recording = *self.is_recording.lock().unwrap();

if settings.mute_while_recording && *self.is_open.lock().unwrap() {
if settings.mute_while_recording && is_open && is_recording {
let mut did_mute_guard = self.did_mute.lock().unwrap();
set_mute(true);
*did_mute_guard = true;
debug!("Mute applied");
Expand Down Expand Up @@ -287,20 +289,24 @@ impl AudioRecordingManager {
return;
}

let mut did_mute_guard = self.did_mute.lock().unwrap();
if *did_mute_guard {
set_mute(false);
}
*did_mute_guard = false;
let mut recorder_guard = self.recorder.lock().unwrap();
let mut is_recording_guard = self.is_recording.lock().unwrap();

if let Some(rec) = self.recorder.lock().unwrap().as_mut() {
// If still recording, stop first.
if *self.is_recording.lock().unwrap() {
if *is_recording_guard {
if let Some(rec) = recorder_guard.as_mut() {
let _ = rec.stop();
*self.is_recording.lock().unwrap() = false;
}
*is_recording_guard = false;
}

if let Some(rec) = recorder_guard.as_mut() {
let _ = rec.close();
}
let mut did_mute_guard = self.did_mute.lock().unwrap();
if *did_mute_guard {
set_mute(false);
}
*did_mute_guard = false;

*open_flag = false;
debug!("Microphone stream stopped");
Expand Down Expand Up @@ -344,18 +350,35 @@ impl AudioRecordingManager {
}
}

if let Some(rec) = self.recorder.lock().unwrap().as_ref() {
if rec.start().is_ok() {
let start_result = {
let recorder_guard = self.recorder.lock().unwrap();
match recorder_guard.as_ref() {
Some(rec) => rec.start().map_err(|e| anyhow::anyhow!("{e}")),
None => Err(anyhow::anyhow!("Recorder not available")),
}
};

match start_result {
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;
}
Err(e) => {
error!("Failed to start recorder: {e}");
drop(state);
self.stop_microphone_stream();
if matches!(*self.mode.lock().unwrap(), MicrophoneMode::AlwaysOn) {
if let Err(e) = self.start_microphone_stream() {
error!("Failed to restart microphone stream after start failure: {e}");
}
}
return false;
}
}
error!("Recorder not available");
false
} else {
false
}
Expand All @@ -380,17 +403,28 @@ impl AudioRecordingManager {
*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()
let stop_result = {
let recorder_guard = self.recorder.lock().unwrap();
match recorder_guard.as_ref() {
Some(rec) => rec.stop().map_err(|e| anyhow::anyhow!("{e}")),
None => Err(anyhow::anyhow!("Recorder not available")),
}
};

let samples = match stop_result {
Ok(buf) => buf,
Err(e) => {
error!("stop() failed: {e}");
self.stop_microphone_stream();
if matches!(*self.mode.lock().unwrap(), MicrophoneMode::AlwaysOn) {
if let Err(e) = self.start_microphone_stream() {
error!(
"Failed to restart microphone stream after stop failure: {e}"
);
}
}
Vec::new()
}
} else {
error!("Recorder not available");
Vec::new()
};

*self.is_recording.lock().unwrap() = false;
Expand Down Expand Up @@ -429,8 +463,22 @@ impl AudioRecordingManager {
*state = RecordingState::Idle;
drop(state);

if let Some(rec) = self.recorder.lock().unwrap().as_ref() {
let _ = rec.stop(); // Discard the result
let stop_result = {
let recorder_guard = self.recorder.lock().unwrap();
match recorder_guard.as_ref() {
Some(rec) => rec.stop().map_err(|e| anyhow::anyhow!("{e}")),
None => Err(anyhow::anyhow!("Recorder not available")),
}
};

if let Err(e) = stop_result {
error!("cancel_recording stop() failed: {e}");
self.stop_microphone_stream();
if matches!(*self.mode.lock().unwrap(), MicrophoneMode::AlwaysOn) {
if let Err(e) = self.start_microphone_stream() {
error!("Failed to restart microphone stream after cancel failure: {e}");
}
}
}

*self.is_recording.lock().unwrap() = false;
Expand Down
Loading