Skip to content

Commit ea33da2

Browse files
committed
refactor(pause): 暂停来源单状态机 pause_state 换装 SessionPause 并实测暂停区间记账
1 parent 2548a27 commit ea33da2

4 files changed

Lines changed: 607 additions & 23 deletions

File tree

‎app/src-tauri/src/capture/audio_loopback.rs‎

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,17 @@ pub struct AudioLoopbackCapture {
5454
handle: Option<JoinHandle<()>>,
5555
}
5656

57-
/// 会话暂停共享状态(2026-08 A1 硬暂停:完全停采)。
57+
/// 会话暂停共享状态(2026-08 A1 硬暂停:完全停采)——批 2a 起实现在
58+
/// pause_state.rs(单状态机 + 来源层 + seq/edge 槽),本路径保留别名
59+
/// re-export:调用点(manager/loop/frame/lifecycle)零爆炸。
5860
///
59-
/// @ai-context: paused 由命令层置位(pause_live_session/resume_live_session);
60-
/// 捕获线程是 total_paused_ms 的**唯一维护者**(暂停开始/结束时
61-
/// 更新),屏幕 worker/其他消费方只读作时间戳补偿——多写者会
62-
/// 重复累计,单一写者是防错约束。
61+
/// @ai-context: paused 由 request API 置位(手动经 manager;媒体/前台经屏幕
62+
/// worker);捕获线程是 total_paused_ms/seq/edge 槽的**唯一维护者**
63+
/// (暂停开始/结束时更新)——多写者会重复累计,单一写者是防错约束。
6364
/// @ai-context: 暂停 = WASAPI 端点 Stop(对象不释放,恢复 Start 即可,无重连
6465
/// 风险)——暂停期系统声音照常播放但不采集,恢复后时间轴补偿
6566
/// 暂停时长,无跳跃、无内容混入。
66-
#[derive(Debug, Clone, Default)]
67-
pub struct SessionPause {
68-
pub paused: Arc<AtomicBool>,
69-
/// 累计暂停毫秒(原子 u64;捕获线程维护,消费方读作时间戳补偿)
70-
pub total_paused_ms: Arc<std::sync::atomic::AtomicU64>,
71-
}
72-
73-
impl SessionPause {
74-
/// 按会话复位(P2 补漏:标志/补偿时长不得跨会话残留——上次会话若在
75-
/// 暂停中停止,paused 残留会让新会话起始即暂停;补偿时长残留会让
76-
/// 新会话时间戳整体偏移)。
77-
pub fn reset(&self) {
78-
self.paused.store(false, Ordering::SeqCst);
79-
self.total_paused_ms.store(0, Ordering::SeqCst);
80-
}
81-
}
67+
pub use crate::pause_state::SessionPause;
8268

8369
impl AudioLoopbackCapture {
8470
/// 启动捕获。on_chunk 在捕获线程内被调用(消费者需自行做轻量处理或转发)。
@@ -201,6 +187,15 @@ fn reconnect_delay(attempt: u32) -> Duration {
201187
Duration::from_secs_f64((0.5 * 2f64.powi(attempt.min(5) as i32)).min(10.0))
202188
}
203189

190+
/// 会话时刻(ms,暂停补偿后)——捕获线程在物理边沿实测的冻结基准。
191+
///
192+
/// @ai-context: 与主循环 live_session_loop.rs:137-138 公式同口径(epoch.elapsed
193+
/// - 累计暂停时长);暂停区间在会话轴上为"冻结点",pause_ms 与
194+
/// resume_ms 相等(补偿后时刻回到同一冻结点附近)。
195+
fn session_moment_ms(epoch: &Instant, p: &SessionPause) -> u64 {
196+
epoch.elapsed().as_millis() as u64 - p.total_paused_ms.load(Ordering::SeqCst)
197+
}
198+
204199
/// COM 初始化 guard:drop 时自动 CoUninitialize(TD-028 修复——CoInitializeEx 无配对调用会泄漏线程 COM 状态)。
205200
struct ComInitGuard;
206201

@@ -303,12 +298,22 @@ where
303298
// 重复调用已启动的流:进入路径上 Start 恰好一次,别无分号。
304299
let mut capture_paused = matches!(pause.as_ref(), Some(p) if p.paused.load(Ordering::SeqCst));
305300
let mut paused_at: Option<std::time::Instant> = None;
301+
// 批 2a:当前物理暂停区间的实测会话时刻/来源(Stop 成功或预检命中
302+
// 暂停时记录起点;Start 成功时完成登记——经 pause_state::record_interval
303+
// 写 edge 槽并推进区间计数 seq,供主循环漏边沿代数补偿)
304+
let mut edge_pause_ms: Option<u64> = None;
305+
let mut edge_source: Option<crate::pause_state::PauseSource> = None;
306306
if capture_paused {
307307
eprintln!("[AudioLoopback] 端点保持停机(会话已处于暂停——免 Start/Stop 重连循环)");
308308
// 预检命中暂停时记下空转起点:恢复边沿靠 paused_at 触发 Start + 累计
309309
// 暂停时长(2026-09 修复:此前置 None——恢复边沿取 None 走空分支,
310-
// 端点永不启动、会话静默且暂停时长不补偿)
310+
// 端点永不启动、会话静默且暂停时长不补偿);区间起点同样记录——
311+
// 补偿与事件时刻的冻结基准都是"捕获实际停采时刻"(=重连成功时刻)
311312
paused_at = Some(std::time::Instant::now());
313+
if let Some(p) = pause {
314+
edge_pause_ms = Some(session_moment_ms(epoch, p));
315+
edge_source = p.snapshot_reason();
316+
}
312317
} else {
313318
audio_client
314319
.Start()
@@ -335,7 +340,7 @@ where
335340
// 暂停边沿状态(本线程内维护;paused 由命令层置位;已在启动预检处完成初始化)
336341

337342
while !stop_flag.load(Ordering::SeqCst) {
338-
// ── 暂停边沿处理(2026-08 A1)──
343+
// ── 暂停边沿处理(2026-08 A1;批 2a:区间实测记账)──
339344
if let Some(p) = pause {
340345
let paused_now = p.paused.load(Ordering::SeqCst);
341346
if paused_now != capture_paused {
@@ -344,6 +349,10 @@ where
344349
match audio_client.Stop() {
345350
Ok(()) => {
346351
paused_at = Some(std::time::Instant::now());
352+
// 实测暂停起点(会话时刻)+ 持因快照——Start
353+
// 成功时经 record_interval 登记完整区间
354+
edge_pause_ms = Some(session_moment_ms(epoch, p));
355+
edge_source = p.snapshot_reason();
347356
eprintln!("[AudioLoopback] 会话暂停(端点已停止)");
348357
}
349358
Err(e) => {
@@ -363,6 +372,20 @@ where
363372
t.elapsed().as_millis() as u64,
364373
Ordering::SeqCst,
365374
);
375+
// 批 2a:登记完成的物理暂停区间(edge 槽先写、
376+
// seq 后推进——主循环见新 seq 必见新槽)。
377+
// 起点缺失(理论不可达:Start 前必经 Stop/预检
378+
// 记录)→ 不登记(快速往返静默语义一致)
379+
if let (Some(ps), Some(src)) =
380+
(edge_pause_ms.take(), edge_source.take())
381+
{
382+
let resume_ms = session_moment_ms(epoch, p);
383+
p.record_interval(crate::pause_state::PauseInterval {
384+
pause_ms: ps,
385+
resume_ms,
386+
source: src,
387+
});
388+
}
366389
// 清空残留缓冲(2026-08 审查修复):Stop 前缓冲内
367390
// 的数据在恢复后已无意义——不清空会以补偿后时间戳
368391
// 混入暂停前的音频(内容错位);循环读到空为止
@@ -396,6 +419,9 @@ where
396419
}
397420
capture_paused = paused_now;
398421
}
422+
// 快速往返(置位/清位间隔 < 捕获轮询粒度,未执行 Stop):
423+
// 物理无暂停发生——不记区间、不推进 seq、不写事件(主循环
424+
// 同样看不到标志边沿,两侧一致静默),注释即语义文档
399425
if capture_paused {
400426
// 暂停期空转(10ms 粒度检查停止/恢复)
401427
std::thread::sleep(Duration::from_millis(10));

‎app/src-tauri/src/lib.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,12 @@ mod live_session_prepare;
365365
mod live_session_manager;
366366
#[cfg(target_os = "windows")]
367367
mod live_session_lifecycle;
368+
// 批 2a(暂停来源单状态机):pause_state=机器层+request API+seq/edge 槽
369+
// (audio_loopback 别名换装 SessionPause);live_session_pause=主循环暂停
370+
// 边沿收敛域(flush_no_rescore/漏边沿代数补偿/边界切断);foreground_pause=
371+
// 前台自动暂停门控(ForegroundGate 滞回状态机)
372+
#[cfg(target_os = "windows")]
373+
mod pause_state;
368374
#[cfg(target_os = "windows")]
369375
mod live_keyframes;
370376
mod load_monitor;

0 commit comments

Comments
 (0)