Merge pull request #109 from H-Chris233/main
[谨慎发布,需要测试]fix(recorder): 上报录音流运行期错误
This commit is contained in:
2 files changed
+100
-12
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user