Merge pull request #109 from H-Chris233/main

[谨慎发布,需要测试]fix(recorder): 上报录音流运行期错误
This commit is contained in:
TRIP authored and GitHub committed 2026-05-01 08:14:14 +08:00
commit 4f45dee2d0
2 files changed
+100 -12

No files matched your search

+71 -2
View File
@@ -26,7 +26,7 @@ use crate::persistence::{
};
use crate::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
use crate::recorder::Recorder;
use crate::recorder::{Recorder, RecorderError};
use crate::types::{
CapsulePayload, CapsuleState, DictationSession, HotkeyCapability, HotkeyMode, HotkeyStatus,
HotkeyStatusState, InsertStatus, PolishMode,
@@ -586,8 +586,9 @@ fn start_recorder_for_starting(
});
match Recorder::start(consumer, level_handler) {
Ok(rec) => {
Ok((rec, runtime_errors)) => {
*inner.recorder.lock() = Some(rec);
spawn_recorder_error_monitor(inner, runtime_errors);
stop_recorder_if_pending_start_stop(inner);
log::info!("[coord] recorder started (asr={active_asr}, phase=Starting)");
}
@@ -616,6 +617,56 @@ fn start_recorder_for_starting(
Ok(())
}
fn spawn_recorder_error_monitor(inner: &Arc<Inner>, rx: mpsc::Receiver<RecorderError>) {
let inner = Arc::clone(inner);
std::thread::Builder::new()
.name("openless-recorder-error-monitor".into())
.spawn(move || {
if let Ok(err) = rx.recv() {
log::error!("[coord] recorder runtime error: {err}");
abort_recording_with_error(&inner, format!("录音中断: {err}"));
}
})
.ok();
}
fn abort_recording_with_error(inner: &Arc<Inner>, message: String) {
let elapsed = {
let mut state = inner.state.lock();
if state.cancelled
|| !matches!(
state.phase,
SessionPhase::Starting | SessionPhase::Listening
)
{
return;
}
state.cancelled = true;
state.phase = SessionPhase::Idle;
state.started_at.elapsed().as_millis() as u64
};
if let Some(rec) = inner.recorder.lock().take() {
rec.stop();
}
if let Some(asr) = inner.asr.lock().take() {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
emit_capsule(
inner,
CapsuleState::Error,
0.0,
elapsed,
Some(message),
None,
);
schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS);
}
async fn start_recorder_and_enter_listening(
inner: &Arc<Inner>,
active_asr: &str,
@@ -1169,6 +1220,24 @@ mod tests {
assert!(state.pending_stop);
}
#[test]
fn recorder_runtime_error_aborts_active_session() {
let coordinator = Coordinator::new();
{
let mut state = coordinator.inner.state.lock();
state.phase = SessionPhase::Listening;
state.cancelled = false;
}
abort_recording_with_error(&coordinator.inner, "录音中断: stream failed".to_string());
let state = coordinator.inner.state.lock();
assert_eq!(state.phase, SessionPhase::Idle);
assert!(state.cancelled);
assert!(coordinator.inner.recorder.lock().is_none());
assert!(coordinator.inner.asr.lock().is_none());
}
#[tokio::test]
async fn repeated_pressed_edge_during_hold_session_does_not_restart() {
let coordinator = Coordinator::new();
+29 -10
View File
@@ -11,7 +11,7 @@
//! - 主线程通过 `AtomicBool` 通知"该停了",并 `join` 线程;线程内 `drop` Stream。
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
@@ -57,16 +57,24 @@ impl Recorder {
pub fn start(
consumer: Arc<dyn AudioConsumer>,
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
) -> Result<Self, RecorderError> {
) -> Result<(Self, Receiver<RecorderError>), RecorderError> {
// 启动信号:子线程构造 Stream 完成后通过 startup_tx 报告结果。
let (startup_tx, startup_rx) = channel::<Result<(), RecorderError>>();
// 运行期错误:Stream 已成功启动后,cpal 通过 err_cb 异步上报。
let (runtime_error_tx, runtime_error_rx) = channel::<RecorderError>();
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_for_thread = Arc::clone(&stop_flag);
let join_handle = thread::Builder::new()
.name("openless-recorder".into())
.spawn(move || {
run_audio_thread(consumer, level_handler, stop_for_thread, startup_tx);
run_audio_thread(
consumer,
level_handler,
stop_for_thread,
startup_tx,
runtime_error_tx,
);
})
.map_err(|e| RecorderError::EngineFailed(format!("spawn audio thread: {e}")))?;
@@ -77,10 +85,13 @@ impl Recorder {
.map_err(|e| RecorderError::EngineFailed(format!("audio thread vanished: {e}")))?;
startup_result?;
Ok(Self {
stop_flag,
join_handle: Mutex::new(Some(join_handle)),
})
Ok((
Self {
stop_flag,
join_handle: Mutex::new(Some(join_handle)),
},
runtime_error_rx,
))
}
/// 停止采集并等待音频线程退出。
@@ -102,8 +113,9 @@ fn run_audio_thread(
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
stop_flag: Arc<AtomicBool>,
startup_tx: Sender<Result<(), RecorderError>>,
runtime_error_tx: Sender<RecorderError>,
) {
let stream = match build_input_stream(consumer, level_handler) {
let stream = match build_input_stream(consumer, level_handler, runtime_error_tx) {
Ok(s) => s,
Err(err) => {
// 启动失败:通知主线程后即退出。
@@ -133,6 +145,7 @@ fn run_audio_thread(
fn build_input_stream(
consumer: Arc<dyn AudioConsumer>,
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
runtime_error_tx: Sender<RecorderError>,
) -> Result<cpal::Stream, RecorderError> {
let host = cpal::default_host();
let device = host
@@ -165,6 +178,7 @@ fn build_input_stream(
state,
input_sr,
channels,
runtime_error_tx,
)
}
@@ -202,14 +216,19 @@ fn build_stream_for_format(
state: Arc<StreamState>,
input_sr: u32,
channels: usize,
runtime_error_tx: Sender<RecorderError>,
) -> Result<cpal::Stream, RecorderError> {
let err_cb = |err| log::error!("[recorder] stream error: {err}");
macro_rules! make_stream {
($t:ty, $to_f32:expr) => {{
let consumer = Arc::clone(&consumer);
let level_handler = Arc::clone(&level_handler);
let state = Arc::clone(&state);
let runtime_error_tx = runtime_error_tx.clone();
let err_cb = move |err| {
log::error!("[recorder] stream error: {err}");
let _ =
runtime_error_tx.send(RecorderError::EngineFailed(format!("stream: {err}")));
};
device
.build_input_stream::<$t, _, _>(
config,