Skip to content
Merged
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
10 changes: 5 additions & 5 deletions openless-all/app/src-tauri/src/coordinator/dictation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
current_session_id,
ActiveAsr::Bailian(Arc::clone(&asr)),
);
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer)?;
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer).await?;

if let Err(e) = asr.open_session().await {
log::error!("[coord] open Bailian ASR session failed: {e}");
Expand Down Expand Up @@ -324,7 +324,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
current_session_id,
ActiveAsr::Volcengine(Arc::clone(&asr)),
);
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer)?;
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer).await?;

if let Err(e) = asr.open_session().await {
log::error!("[coord] open ASR session failed: {e}");
Expand Down Expand Up @@ -393,7 +393,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
Ok(())
}

pub(super) fn start_recorder_for_starting(
pub(super) async fn start_recorder_for_starting(
inner: &Arc<Inner>,
session_id: SessionId,
active_asr: &str,
Expand Down Expand Up @@ -438,7 +438,7 @@ pub(super) fn start_recorder_for_starting(

let microphone_device_name = selected_microphone_device_name(inner);
stop_microphone_preview_monitor(inner, "dictation recorder");
acquire_recording_mute(inner, "dictation");
acquire_recording_mute(inner, "dictation").await;
match Recorder::start(microphone_device_name, consumer, level_handler) {
Ok((rec, runtime_errors)) => {
store_recorder_for_session(inner, session_id, rec);
Expand Down Expand Up @@ -545,7 +545,7 @@ pub(super) async fn start_recorder_and_enter_listening(
active_asr: &str,
consumer: Arc<dyn crate::recorder::AudioConsumer>,
) -> Result<(), String> {
start_recorder_for_starting(inner, session_id, active_asr, consumer)?;
start_recorder_for_starting(inner, session_id, active_asr, consumer).await?;
finish_starting_session(inner, session_id).await;
Ok(())
}
Expand Down
96 changes: 70 additions & 26 deletions openless-all/app/src-tauri/src/coordinator/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,37 +110,81 @@ pub(super) fn stop_microphone_preview_monitor(inner: &Arc<Inner>, owner: &str) {
}
}

pub(super) fn acquire_recording_mute(inner: &Arc<Inner>, owner: &str) {
/// Acquire system-output mute for the duration of a recording session.
///
/// `AudioMuteGuard::activate()` on macOS shells out to `osascript` (~100–300 ms)
/// and on Linux to `wpctl`/`pactl` (similar). When called from the async
/// `begin_session` path that blocks the tokio worker thread for the entire
/// duration, delaying the recorder start by exactly that much. Wrap the
/// activate + bookkeeping in `spawn_blocking` so the tokio worker is freed
/// while the shell-out runs. Parking-lot `Mutex` guards never cross an await
/// (they live entirely inside the blocking task). Audit 3.2.4.
pub(super) async fn acquire_recording_mute(inner: &Arc<Inner>, owner: &'static str) {
if !inner.prefs.get().mute_during_recording {
return;
}
let mut mute = inner.recording_mute.lock();
if mute.holders == 0 {
match crate::audio_mute::AudioMuteGuard::activate() {
Ok(guard) => {
mute.guard = Some(guard);
log::info!("[audio-mute] system output muted for recording");
}
Err(err) => {
log::warn!("[audio-mute] failed to mute output for {owner}: {err}");
return;
let inner = Arc::clone(inner);
let join_result = tokio::task::spawn_blocking(move || {
let mut mute = inner.recording_mute.lock();
if mute.holders == 0 {
match crate::audio_mute::AudioMuteGuard::activate() {
Ok(guard) => {
mute.guard = Some(guard);
log::info!("[audio-mute] system output muted for recording");
}
Err(err) => {
log::warn!("[audio-mute] failed to mute output for {owner}: {err}");
return;
}
}
}
}
mute.holders = mute.holders.saturating_add(1);
log::info!("[audio-mute] acquired by {owner}; holders={}", mute.holders);
}

pub(super) fn release_recording_mute(inner: &Arc<Inner>, owner: &str) {
let mut mute = inner.recording_mute.lock();
if mute.holders == 0 {
return;
}
mute.holders -= 1;
log::info!("[audio-mute] released by {owner}; holders={}", mute.holders);
if mute.holders == 0 {
mute.guard.take();
log::info!("[audio-mute] system output mute restored after recording");
mute.holders = mute.holders.saturating_add(1);
log::info!("[audio-mute] acquired by {owner}; holders={}", mute.holders);
})
.await;
// 显式记录 spawn_blocking 任务的 panic(之前是 `let _ = .await` 静默吞掉)。
// holders/guard 状态本身在 panic 路径下仍然一致 —— 因为 panic 只能发生在
// activate() 抛 / lock 抛,前者会让 holders 不增 + guard 仍 None,后者根本
// 进不到 mutate 阶段;但用户碰到 system audio 在录音时漏出系统声却找不到
// 任何 [audio-mute] 日志,没法 debug。pr_agent feedback on PR #391。
if let Err(join_err) = join_result {
log::error!(
"[audio-mute] acquire task panicked for {owner}: {join_err}; mute did not activate"
);
}
}

/// Release the recording-mute guard. The Drop impl on `AudioMuteGuard` shells
/// out to `osascript` / `wpctl` again, so when holders reaches 0 we hand the
/// drop off to a blocking task to keep the tokio worker free. Audit 3.2.4.
///
/// Fire-and-forget (no await): callers — `cancel_session`, `end_session`,
/// recorder error monitor — don't need the mute restoration to complete
/// before they continue. The user has already stopped recording; system audio
/// recovery happening 100 ms later is fine.
///
/// `release_recording_mute` is also called from non-tokio threads (the recorder
/// error monitor uses `std::thread::spawn`), so fall back to a synchronous
/// run when there's no current tokio handle — running synchronously on a std
/// thread blocks nothing.
pub(super) fn release_recording_mute(inner: &Arc<Inner>, owner: &'static str) {
let inner = Arc::clone(inner);
let work = move || {
let mut mute = inner.recording_mute.lock();
if mute.holders == 0 {
return;
}
mute.holders -= 1;
log::info!("[audio-mute] released by {owner}; holders={}", mute.holders);
if mute.holders == 0 {
mute.guard.take();
log::info!("[audio-mute] system output mute restored after recording");
}
};
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn_blocking(work);
} else {
work();
}
}

Expand Down
Loading