From 2784e9764bfe96cc393cf7fb57995ff4fb9eedcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=9F=8F=E9=9D=92?= Date: Sun, 31 May 2026 09:44:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(audioCue):=20resume=20=E6=9C=9F=E9=97=B4?= =?UTF-8?q?=E8=A2=AB=E5=8F=AB=E5=81=9C=E5=88=99=E4=BD=9C=E5=BA=9F=E6=9C=AC?= =?UTF-8?q?=E6=AC=A1=E6=92=AD=E6=94=BE=EF=BC=88pr=5Fagent=20=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit suspended 的 AudioContext 先 resume 再排期,若等待 resume 期间录音已结束 (stopAudioCue)或有新一轮播放,旧的 resume 回调不再排期,避免「录音已停、 提示音晚到」。引入 playGeneration 代号:stopAudioCue 自增并停节点; scheduleCueVoices 只 stopVoices 不动代号;deferred 播放回调比对代号决定是否排期。 --- openless-all/app/src/lib/audioCue.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/openless-all/app/src/lib/audioCue.ts b/openless-all/app/src/lib/audioCue.ts index 05ff0deb..ef7a79f8 100644 --- a/openless-all/app/src/lib/audioCue.ts +++ b/openless-all/app/src/lib/audioCue.ts @@ -47,6 +47,10 @@ interface ActiveVoice { gain: GainNode; } let activeVoices: ActiveVoice[] = []; +// 每次「关闭」或「新一轮播放」自增。suspended 时 play 会等 resume() 再排期, +// 这个代号让挂起的 resume 回调能判断「等待期间是否已被叫停/被新一轮取代」, +// 避免录音已经结束、提示音却姗姗来迟地响起来(冷启动 WebView 上快按热键可复现)。 +let playGeneration = 0; function resolveAudioContextCtor(): AudioContextCtor | null { if (typeof window === 'undefined') return null; @@ -69,8 +73,8 @@ function getContext(): AudioContext | null { return sharedCtx; } -/** 关闭/停止当前正在播放的提示音;无声音在播时为 no-op。 */ -export function stopAudioCue(): void { +// 停掉当前正在发声的节点(不影响 playGeneration —— 仅做去叠音 / 收尾)。 +function stopVoices(): void { const ctx = sharedCtx; const now = ctx?.currentTime ?? 0; for (const { osc, gain } of activeVoices) { @@ -86,11 +90,18 @@ export function stopAudioCue(): void { activeVoices = []; } +/** 关闭/停止提示音:停掉在播节点,并作废任何还挂在 resume() 上、尚未排期的播放。 */ +export function stopAudioCue(): void { + playGeneration++; + stopVoices(); +} + // 实际排期合成节点。必须在 AudioContext 处于 running(非 suspended)时调用: // suspended 时 currentTime 冻结在暂停时刻,节点会排到过期时间点 → 不发声还堆积。 function scheduleCueVoices(ctx: AudioContext): void { - // 连按热键时先停掉上一轮,避免叠音越来越响。 - stopAudioCue(); + // 连按热键时先停掉上一轮,避免叠音越来越响。用 stopVoices 而非 stopAudioCue: + // 这里不该作废自己这一轮的 generation。 + stopVoices(); const base = ctx.currentTime + 0.01; for (const tone of recordStartCueTones()) { @@ -134,9 +145,15 @@ export function playRecordStartCue(): void { // WKWebView / WebView2 的 AudioContext 常处于 suspended:必须先 resume 再排期, // 不能在 resume 未完成时就用冻结的 currentTime 排节点。resume() 失败也不抛(无声降级)。 if (ctx.state === 'suspended') { + const gen = ++playGeneration; ctx .resume() - .then(() => scheduleCueVoices(ctx)) + .then(() => { + // 等待 resume 期间若已 stopAudioCue(录音结束)或有新一轮播放,本次作废, + // 否则会出现「录音已停,提示音却晚到」。 + if (gen !== playGeneration) return; + scheduleCueVoices(ctx); + }) .catch(() => undefined); return; }