Skip to content
Open
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
33 changes: 28 additions & 5 deletions src-tauri/src/audio_toolkit/audio/recorder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,30 @@ impl AudioRecorder {
if let Some(tx) = &self.cmd_tx {
tx.send(Cmd::Stop(resp_tx))?;
}
Ok(resp_rx.recv()?) // wait for the samples
// Bounded wait: if the audio stack is wedged (e.g. a WASAPI hang took
// the capture callback down), the worker may never reply. Losing one
// utterance is recoverable; a forever-busy pipeline is not.
Ok(resp_rx.recv_timeout(Duration::from_secs(5))?)
}

pub fn close(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(tx) = self.cmd_tx.take() {
let _ = tx.send(Cmd::Shutdown);
}
if let Some(h) = self.worker_handle.take() {
let _ = h.join();
// Bounded join via a reaper thread: dropping the cpal stream on a
// wedged audio engine can block indefinitely, and that must not
// freeze the caller. A detached parked thread is the lesser evil.
let (done_tx, done_rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = h.join();
let _ = done_tx.send(());
});
if done_rx.recv_timeout(Duration::from_secs(3)).is_err() {
log::warn!(
"Mic worker did not shut down within 3s; detaching it (audio stack wedged?)"
);
}
}
self.device = None;
Ok(())
Expand Down Expand Up @@ -597,13 +612,21 @@ fn run_consumer(
}
}

// Runs until the stream closes and `recv` returns `Err`.
while let Ok(chunk) = sample_rx.recv() {
// Runs until the stream closes and `recv` returns `Err(Disconnected)`.
// The timeout matters: commands must stay serviceable even when the
// capture callback stops delivering chunks (wedged audio engine, dead
// device) — otherwise a Stop/Shutdown sits unread and the caller hangs.
loop {
let chunk = match sample_rx.recv_timeout(Duration::from_millis(250)) {
Ok(chunk) => Some(chunk),
Err(mpsc::RecvTimeoutError::Timeout) => None,
Err(mpsc::RecvTimeoutError::Disconnected) => return,
};
// Handle pending commands BEFORE the in-flight chunk so a Start
// captures it. Commands used to be polled after processing, which
// silently dropped one buffer period of audio (~10ms built-in, up to
// ~100ms on Bluetooth) at every recording start.
let mut pending = Some(chunk);
let mut pending = chunk;
while let Ok(cmd) = cmd_rx.try_recv() {
match cmd {
Cmd::Start(policy, sent_at) => {
Expand Down
Loading