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::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
|
||||||
use crate::recorder::Recorder;
|
use crate::recorder::{Recorder, RecorderError};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
CapsulePayload, CapsuleState, DictationSession, HotkeyCapability, HotkeyMode, HotkeyStatus,
|
CapsulePayload, CapsuleState, DictationSession, HotkeyCapability, HotkeyMode, HotkeyStatus,
|
||||||
HotkeyStatusState, InsertStatus, PolishMode,
|
HotkeyStatusState, InsertStatus, PolishMode,
|
||||||
@@ -586,8 +586,9 @@ fn start_recorder_for_starting(
|
|||||||
});
|
});
|
||||||
|
|
||||||
match Recorder::start(consumer, level_handler) {
|
match Recorder::start(consumer, level_handler) {
|
||||||
Ok(rec) => {
|
Ok((rec, runtime_errors)) => {
|
||||||
*inner.recorder.lock() = Some(rec);
|
*inner.recorder.lock() = Some(rec);
|
||||||
|
spawn_recorder_error_monitor(inner, runtime_errors);
|
||||||
stop_recorder_if_pending_start_stop(inner);
|
stop_recorder_if_pending_start_stop(inner);
|
||||||
log::info!("[coord] recorder started (asr={active_asr}, phase=Starting)");
|
log::info!("[coord] recorder started (asr={active_asr}, phase=Starting)");
|
||||||
}
|
}
|
||||||
@@ -616,6 +617,56 @@ fn start_recorder_for_starting(
|
|||||||
Ok(())
|
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(
|
async fn start_recorder_and_enter_listening(
|
||||||
inner: &Arc<Inner>,
|
inner: &Arc<Inner>,
|
||||||
active_asr: &str,
|
active_asr: &str,
|
||||||
@@ -1169,6 +1220,24 @@ mod tests {
|
|||||||
assert!(state.pending_stop);
|
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]
|
#[tokio::test]
|
||||||
async fn repeated_pressed_edge_during_hold_session_does_not_restart() {
|
async fn repeated_pressed_edge_during_hold_session_does_not_restart() {
|
||||||
let coordinator = Coordinator::new();
|
let coordinator = Coordinator::new();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
//! - 主线程通过 `AtomicBool` 通知"该停了",并 `join` 线程;线程内 `drop` Stream。
|
//! - 主线程通过 `AtomicBool` 通知"该停了",并 `join` 线程;线程内 `drop` Stream。
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
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::sync::Arc;
|
||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
|
|
||||||
@@ -57,16 +57,24 @@ impl Recorder {
|
|||||||
pub fn start(
|
pub fn start(
|
||||||
consumer: Arc<dyn AudioConsumer>,
|
consumer: Arc<dyn AudioConsumer>,
|
||||||
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
|
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
|
||||||
) -> Result<Self, RecorderError> {
|
) -> Result<(Self, Receiver<RecorderError>), RecorderError> {
|
||||||
// 启动信号:子线程构造 Stream 完成后通过 startup_tx 报告结果。
|
// 启动信号:子线程构造 Stream 完成后通过 startup_tx 报告结果。
|
||||||
let (startup_tx, startup_rx) = channel::<Result<(), RecorderError>>();
|
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_flag = Arc::new(AtomicBool::new(false));
|
||||||
let stop_for_thread = Arc::clone(&stop_flag);
|
let stop_for_thread = Arc::clone(&stop_flag);
|
||||||
|
|
||||||
let join_handle = thread::Builder::new()
|
let join_handle = thread::Builder::new()
|
||||||
.name("openless-recorder".into())
|
.name("openless-recorder".into())
|
||||||
.spawn(move || {
|
.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}")))?;
|
.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}")))?;
|
.map_err(|e| RecorderError::EngineFailed(format!("audio thread vanished: {e}")))?;
|
||||||
startup_result?;
|
startup_result?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok((
|
||||||
stop_flag,
|
Self {
|
||||||
join_handle: Mutex::new(Some(join_handle)),
|
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>,
|
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
|
||||||
stop_flag: Arc<AtomicBool>,
|
stop_flag: Arc<AtomicBool>,
|
||||||
startup_tx: Sender<Result<(), RecorderError>>,
|
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,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// 启动失败:通知主线程后即退出。
|
// 启动失败:通知主线程后即退出。
|
||||||
@@ -133,6 +145,7 @@ fn run_audio_thread(
|
|||||||
fn build_input_stream(
|
fn build_input_stream(
|
||||||
consumer: Arc<dyn AudioConsumer>,
|
consumer: Arc<dyn AudioConsumer>,
|
||||||
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
|
level_handler: Arc<dyn Fn(f32) + Send + Sync>,
|
||||||
|
runtime_error_tx: Sender<RecorderError>,
|
||||||
) -> Result<cpal::Stream, RecorderError> {
|
) -> Result<cpal::Stream, RecorderError> {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let device = host
|
let device = host
|
||||||
@@ -165,6 +178,7 @@ fn build_input_stream(
|
|||||||
state,
|
state,
|
||||||
input_sr,
|
input_sr,
|
||||||
channels,
|
channels,
|
||||||
|
runtime_error_tx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,14 +216,19 @@ fn build_stream_for_format(
|
|||||||
state: Arc<StreamState>,
|
state: Arc<StreamState>,
|
||||||
input_sr: u32,
|
input_sr: u32,
|
||||||
channels: usize,
|
channels: usize,
|
||||||
|
runtime_error_tx: Sender<RecorderError>,
|
||||||
) -> Result<cpal::Stream, RecorderError> {
|
) -> Result<cpal::Stream, RecorderError> {
|
||||||
let err_cb = |err| log::error!("[recorder] stream error: {err}");
|
|
||||||
|
|
||||||
macro_rules! make_stream {
|
macro_rules! make_stream {
|
||||||
($t:ty, $to_f32:expr) => {{
|
($t:ty, $to_f32:expr) => {{
|
||||||
let consumer = Arc::clone(&consumer);
|
let consumer = Arc::clone(&consumer);
|
||||||
let level_handler = Arc::clone(&level_handler);
|
let level_handler = Arc::clone(&level_handler);
|
||||||
let state = Arc::clone(&state);
|
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
|
device
|
||||||
.build_input_stream::<$t, _, _>(
|
.build_input_stream::<$t, _, _>(
|
||||||
config,
|
config,
|
||||||
|
|||||||
Reference in new issue
Block a user