fix(audio-cue): 重建/唤醒退化的 AudioContext,修「用久了提示音消失」#737
Conversation
长时间使用后「开始录音」提示音与设置页「试听」双双静音,且重启才恢复。 根因在 audioCue.ts 的 AudioContext 单例 sharedCtx 从不检查/恢复运行期状态: - WKWebView/WebView2 在频繁录音抢占音频会话后,ctx 可能转入 WebKit 非标准的 `interrupted` 态。旧代码只判断 `suspended`,interrupted 被当作可直接排期 → 排到非 running 的 ctx 上,无声且不尝试 resume → 永久静默(macOS 最可能根因)。 - ctx 也可能被系统 `closed`。closed 无法复活(resume 必拒、createOscillator 必抛、 被 catch 静默吞掉),而 getContext 从不重建 → 永久静默。 - 胶囊窗口与设置窗口各持一份 ctx,都会这样退化 → 两处提示音同时失效。 修复: - 新增纯函数 audioContextActionForState(state):closed→recreate / running→ready / 其余(suspended/interrupted/未知非运行态)→resume,单一真相、可单测。 - getContext 在 ctx 为 closed 时丢弃重建(并清空挂在旧 ctx 上的 activeVoices)。 - playRecordStartCue / primeAudioCue 改用该分类器,把 interrupted 一并按 resume 唤醒。 公共 API 签名不变,仅 AudioCue.tsx / RecordingInputSection.tsx 复用,行为向后兼容。 tsc --noEmit 通过;audioCue.test.ts 新增 5 条状态分类断言,全部通过。
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
@claude 请审查这条已合并目标为 beta 的修复 PR:重建/唤醒退化的 AudioContext,修「用久了提示音消失」。重点看 |
|
Claude finished @appergb's task in 2m 10s —— View job PR #737 审查:重建/唤醒退化的 AudioContext
总体评价逻辑正确,实现精准,可以合并。 改动做到了 PR 描述中承诺的"surgical":仅改动必须改动的部分,不引入新的异步路径,不破坏现有 API。以下是详细分析。
|
| 旧代码 | 新代码 | |
|---|---|---|
| 条件 | ctx.state === 'suspended' |
audioContextActionForState(ctx.state) === 'resume' |
| 覆盖 | suspended | suspended + interrupted + 未知非运行态 |
功能向后兼容(suspended 路径行为不变),新增对 interrupted 的处理是修复 macOS 抢占根因的关键。
resume 异步路径中捕获的 ctx 引用不会过期:此时 ctx 不处于 closed,getContext() 不会在并发调用中将其替换(并发第二次调用会得到同一个 suspended ctx,由 playSeq 机制去重)。✓
向后兼容性验证
AudioCue.tsx(胶囊监听):
- 导入
playRecordStartCue、primeAudioCue、stopAudioCue— 签名与语义不变 ✓
RecordingInputSection.tsx(试听按钮):
- 仅导入
playRecordStartCue— 签名与语义不变 ✓
两个消费方无需任何改动。
单测质量
audioCue.test.ts 新增的 5 条断言覆盖了所有关键路径:
| 断言 | 意义 |
|---|---|
closed → 'recreate' |
bug 回归钉子,闭合了「用久了没声音」的核心路径 |
running → 'ready' |
正常路径不退化 |
suspended → 'resume' |
原有唤醒路径保持 |
interrupted → 'resume' |
新修复的 macOS 路径 |
unknown → 'resume' |
保守兜底不静默 |
测试风格与仓库现有约定(轻量自执行断言,无外部 runner)一致。已有的 shouldPlayDeferredCue 边界测试(含"恰好等于阈值"的 elapsedMs === 400 用例)也保留完好。✓
无回归风险项
以下场景均经过分析,无回归:
- 连按热键(快速录音):
playSeq/stopSeq机制不受本次改动影响。 stopVoices()操作失效节点:activeVoices在重建时已清空,try/catch静默保底。primeAudioCue遇到 closed ctx:getContext()会先重建,再由primeAudioCue唤醒新 ctx,路径完整。- 无 Web Audio 环境:
getContext()返回 null 的静默降级路径不变。
结论
修复思路清晰、代码精准、测试到位,向后兼容。建议合入 beta。
User description
问题
用户反馈:OpenLess 使用时间长了之后会没有音频——按下录音热键时的「开始提示音」消失,去 设置 → 录音与输入 点「试听」也没声音了。需要重启才恢复。
根因
提示音不打包音频文件,而是用 Web Audio API 即时合成(
src/lib/audioCue.ts)。模块持有一个 AudioContext 单例sharedCtx,创建后从不检查、也从不恢复它的运行期状态:interrupted(最可能的 macOS 根因):本 app 高频录音,每次录音会抢占系统音频会话,WKWebView 的 AudioContext 因此可能转入 WebKit 非标准的interrupted态。旧代码playRecordStartCue只判断ctx.state === 'suspended',interrupted被当作可直接排期 → 节点排到一个非running的 ctx 上,无声、且根本不尝试 resume → 永久静默。closed:ctx 也可能被系统关闭。closed的 ctx 无法复活(resume()必拒、createOscillator()必抛、异常被catch静默吞掉),而getContext()从不重建它 → 永久静默。sharedCtx,都会这样退化 → 「开始提示音」和「试听」同时失效,与现象吻合。修复(surgical,公共 API 签名不变)
audioContextActionForState(state):closed → recreate/running → ready/ 其余(suspended、interrupted、任何未知非运行态)→resume。单一真相。getContext():当持有的 ctx 为closed时丢弃并重建(并清空挂在旧 ctx 上的activeVoices,避免去叠音时操作失效节点)。playRecordStartCue()/primeAudioCue()改用该分类器,把interrupted一并按resume唤醒。任何环境(无 Web Audio / 被自动播放策略挂起 / 创建失败)仍保持原有的静默降级,绝不抛错影响录音主流程。
影响面
仅
src/components/AudioCue.tsx(胶囊监听)与src/pages/settings/RecordingInputSection.tsx(试听按钮)复用这几个公共函数,签名与行为向后兼容。测试
npx tsc --noEmit通过(exit 0)。src/lib/audioCue.test.ts新增 5 条状态分类断言(closed/running/suspended/interrupted/未知态),npx tsx跑全部通过。PR Type
Bug fix, Tests
Description
Add
audioContextActionForStateto handle closed, running, and interrupted states.Recreate closed AudioContext in
getContext; resume otherwise.Update
primeAudioCueandplayRecordStartCueto use new classification.Add unit tests for all state transitions.
Diagram Walkthrough
flowchart LR A["AudioContext state"] --> B{"audioContextActionForState"} B -- "closed" --> C["recreate"] B -- "running" --> D["ready"] B -- "suspended/interrupted/other" --> E["resume"] C --> F["Discard old ctx, create new"] E --> G["ctx.resume()"] D --> H["Schedule audio"]File Walkthrough
audioCue.test.ts
Add unit tests for AudioContext state classificationopenless-all/app/src/lib/audioCue.test.ts
audioContextActionForStatecovering'closed', 'running', 'suspended', 'interrupted', and unknown states.
audioCue.ts
Implement AudioContext state recovery logicopenless-all/app/src/lib/audioCue.ts
AudioContextActiontype andaudioContextActionForStatefunction.getContextto recreate closed AudioContext.primeAudioCueandplayRecordStartCueto use the new function,including handling of 'interrupted' state.