☰
Quill 捕获自愈机制深剖:1 秒看门狗、路由监听与分段重启如何救回断掉的录音
2026/9/30 6:14:23 网站建设 项目流程

Quill 捕获自愈机制深剖:1 秒看门狗、路由监听与分段重启如何救回断掉的录音

【免费下载链接】quillUltra-minimalist macOS recording + transcription.项目地址: https://gitcode.com/gh_mirrors/quill26/quill

Quill 是一款极简的 macOS 本地会议录音转写工具:它把麦克风与系统声音分成两条音轨录制,并在本地转写成带说话人标记的纪要。但 macOS 的音频路由天生不稳定——一次蓝牙耳机切换就可能让录音悄悄断流。本文带你看懂 Quill 的捕获自愈机制:1 秒看门狗、750 毫秒路由监听防抖、三次分段重启,是如何在无人干预下救回一条断掉的录音的。

一、为什么 macOS 录音容易"悄悄断掉" 🔌

问题根源写在 macos/README.md 里:

连接或断开 AirPods、更改默认音频设备,都可能让捕获流静默停止——系统不会报错,音频只是不再流入。

对会议场景这是致命的:你戴着耳机开会,中途把 AirPods 放回盒里再戴上,录音文件照常存在,后半段却是一片空白。Quill 的对策不是"检测到出错再修",而是给每条音轨装上一台独立的健康状态机,让录音自己"复活"。

所有机制集中在 CaptureHealth.swift,核心参数一目了然:

参数默认值作用
staleThresholdMs3000 ms超过 3 秒没写入新音频 → 判定"停滞"
routeDebounceMs750 ms路由事件防抖窗口,过滤通知风暴
retryDelaysMs0 / 500 / 2000 ms最多 3 次重启,间隔递增
stabilityWindowMs5000 ms新片段须连续写入 5 秒才算"真正恢复"
startupGraceMs5000 ms启动宽限期,防止误判冷启动

这些阈值被刻意收拢在 CapturePolicy 一个结构体里——测试可以把它们换成毫秒级的短值,全程无需真实硬件和睡眠等待。

二、1 秒看门狗:如何判定一条音轨"死了" ⏱️

看门狗由 RecordingSession.startWatchdog 创建,每秒唤醒一次,逐轨调用 tick():

let timer = Timer(timeInterval: 1, repeats: true) { _ in MainActor.assumeIsolated { [weak self] in self?.tick() } } RunLoop.main.add(timer, forMode: .common)

这里有个容易忽略的细节:定时器被显式加入.common运行模式。默认模式下的 Timer 在用户按住菜单栏展开菜单时会暂停触发——那看门狗就"瞎"了。注释里原话是"which would blind the watchdog"。

看门狗本身不做 I/O,它只读取一个轻量遥测快照:TrackTelemetry 由实时音频回调写入(上次写入时间、缓冲末端、帧数、静音长度),看门狗加锁读取,锁只持有纳秒级。判"死"的逻辑极其简洁,见 isStale:

最后一次成功写入距今> 3 秒→ 停滞。音频回调正常节奏约为 85 毫秒一次,所以 3 秒既是"远大于噪声"、又是"远小于一场会议"的甜点位。

判定原因还会区分两种:遥测里有写错误 →write_failed;否则 →callback_stalled(tickLive)。

两条关键防误报规则:

  • 启动宽限期:片段刚启动的前 5 秒内,引擎构建、权限检查都要花真实时间,没有写入不算停滞;
  • 精确静音 ≠ 故障:连续 15 秒的"数字零"(silenceWarningMs)只会记录一条诊断警告——比如"对方没在说话、扬声器没在放音"是合法状态,看门狗绝不会因为安静就重启捕获。

三、路由监听:750 毫秒防抖,拒绝"狼来了" 🔔

光靠看门狗还要等 3 秒,太慢。所以每条音轨都挂了路由观察者,把"危险信号"提前送达:

  • 麦克风轨(MicRecorder.observeRoute):监听引擎配置变更通知 + 系统默认输入设备属性;
  • 系统音轨(SystemAudioRecorder.observeRoute):监听默认输出设备与设备列表变化。

但一次物理切换会触发一连串路由通知,而且——通知不代表失败。耳塞拔插的瞬间回调可能完好无损。因此路由事件只把音轨标记为suspect(可疑),并开启 750 ms 防抖窗口(routeEvent):

  1. 窗口内如果写入持续正常 → 自动回归healthy,全程无感;
  2. 事件成串到达时(通知风暴),每来一条就重置计时,等风暴平息再评估;
  3. 窗口结束仍停滞 → 以route_change为由立即进入恢复流程。

真正的"硬故障"(引擎停止、写盘失败)则走 fault():直接视为防抖已耗尽,下一拍就裁决——该快时绝不等。

四、分段重启:已写下的音频永远不动 ✂️

这是整个设计里最克制也最关键的一条铁律:重启从不修补旧文件,而是开新文件。

看 performRestart 的流程:

  1. 关闭当前片段——它已经录下的内容原样封存;
  2. 片段编号 +1,在当前音频路由上重建整条录制链路;
  3. 新片段写入mic-002.caf、system-002.caf这样编号的文件(命名规则见 segmentFile)。

重启不是一击即中,而是带递增延迟的三次预算(nextAttemptOrDegrade):

第 1 次:立即 → 第 2 次:+500 ms → 第 3 次:+2 s。三次全败 → 轨道标记degraded,只发一条通知,不再骚扰用户。

而且"恢复成功"有严格验收:新片段必须连续写入满 5 秒稳定窗口,才算真正恢复、重置重试预算(tickRecovering);恢复中途再次卡死,消耗的是同一个事件的下一份预算。这一切都被确定性单测逐条钉死,见 CaptureHealthTests.swift——测试用纯整数时间驱动状态机,无需音频设备、无需真实等待。

五、你在界面上会看到什么 🪶

自愈全程对用户近乎无感,只有状态在变(macOS 文档):

  • 🟥 红色羽毛● recording · 28:11—— 双轨健康;
  • 🟧 橙色羽毛◐ recovering microphone—— 某轨停滞,正在自动重启(至多三次尝试);
  • 🟧 橙色⚠ microphone capture lost—— 三次救不回来,发一条通知,该轨标记 incomplete;
  • △ system audio silent—— 次级诊断:轨道活着但在输出精确静音(可能只是没声音在放)。

六、事后验尸:meta.json 不会撒谎 📋

停止录音后,meta.json(schema v2)会原子写入,逐轨交代清楚:

  • segments[]:每个片段文件在统一单调会话时钟上的起止偏移(中途改系统时间也不会让片段错位);
  • interruptions[]:何时检测到中断、何时恢复、几次尝试、失败原因;
  • status:complete/recovered/incomplete。会话状态取最差的轨道。

设计哲学就一句话:recovered 的录音可用,但永远不会被伪装成"从未断过"。转写照常进行——各片段独立转写后按偏移拼回同一时钟,中断造成的空档在时间戳里清晰可见。

写在最后

Quill 的捕获自愈没有魔法,只有四层克制的设计:看门狗管"结果"(音频真的进来了吗)、路由监听管"征兆"(危险提前 3 秒上报)、防抖管"误报"(正常切换不惊动任何人)、分段重启管"善后"(旧数据不动、新文件续命)。配合 quill doctor 可做前置体检,更多架构契约见 docs/architecture.md。对任何做长时录音的工具,这套"监控 → 判定 → 重启 → 留痕"的链路都值得直接抄走。

【免费下载链接】quillUltra-minimalist macOS recording + transcription.项目地址: https://gitcode.com/gh_mirrors/quill26/quill

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询