fix(engine): voice 句尾剪字真根因根治(三修:源尾垫静音冲刷 SoundTouch 未冲刷小节)+ onEnd 完成信号 + live 探针适配

- 真根因(live span 8.57s < 文件 8.80s 铁证):SoundTouch Stretch 源结束时不冲刷
  内部最后一个小节(~1 sequence),句尾 0.1-0.25s 从未交付音频图——任何平台皆然,
  ASR 因容错转写仍 sim=1(转写评审盲区的又一实证)。一修固定 300ms 排空窗只是
  把这段不存在的交付空等成静音;二修 onEnd 保证「已交付必播完」但交付不了这段
- 三修:源尾垫 VOICE_TAIL_PAD_SEC(0.25s) 静音把真实句尾推出管线;完成 = onEnd
  (平台无关,disconnect 已交付帧仍播);1.5s 兜底;顶掉复核移至新句数据访问前
  (垫长拷贝的 getChannelData 先于杀旧句会打乱探针时序)
- live 探针适配:尾部垫使 SoundTouch 读未注册 buffer、gcd 抽读信号消失——
  清扫器加「仍有连接中语音 SPN 则不关段」守卫,断开即移出追踪集合
- engine-voice.test:PAD+I1/I2/I2b/I3 五组 19 断言(含垫长存在性、onEnd 结算、
  兜底不早于 1.5s);10 套件全绿
- live 复验(男声 p1):span 8.57→9.04s(≥文件 8.80s),ASR sim=1 verdict=OK
This commit is contained in:
xsl
2026-09-07 23:18:11 +08:00
parent ec248e18da
commit 6f4f167efd
5 changed files with 182 additions and 100 deletions
+9 -7
View File
@@ -164,13 +164,15 @@ new WebAudioEngine(basePath = 'audio/', opts?)
`vendor/soundtouch.js`(SoundTouch 0.3.0,LGPL v2.1,ScriptProcessorNode
实现):`WebAudioBufferSource → SimpleFilter(SoundTouch tempo) →
ScriptProcessorNode → gain → voiceBus`。`settings.speed` 只改 tempo 不改
pitch,1.0/1.35/1.7 均不变调;自然结束以「源真实消耗位置」(SoundTouch
source.position)为准——源未耗尽时定时器只做短重排等待,耗尽后再留
300ms 排空窗让管线缓冲的尾音播出,然后才 disconnect(2026-09 真机人耳
反馈修复:旧实现纯墙钟定时器 +120ms 一到即断,管线固有延迟 ~190ms 意味着
每句尾字被剪)。onComplete/打断/排队语义不变。注意 voice 有
~100ms 处理延迟(对白场景无感),且 ScriptProcessorNode 已废弃但全平台
(含 iOS Safari)可用。
pitch,1.0/1.35/1.7 均不变调。**自然结束(2026-09 三修,真机人耳反馈)**:
① 播放源尾部垫 0.25s 静音——SoundTouch Stretch 源结束时不冲刷内部最后
一个小节,句尾 0.1-0.25s 若不垫将永远停留在管线里不播出(任何平台);
② 完成信号 = 管线 onEnd(extract 返回 0 = 已无未交付帧),平台无关,
此时 disconnect 已交付帧仍会播完;③ onEnd 异常缺失时 1.5s 定时器兜底
(宁迟勿剪);④ 源未耗尽时定时器只做 80ms 短重排等待。onComplete/打断/
排队语义不变。回归:web/harness/engine-voice.test.mjs(PAD+I1-I3 四不变式)。
注意 voice 有 ~100ms 处理延迟(对白场景无感),且 ScriptProcessorNode
已废弃但全平台(含 iOS Safari)可用。
**长按快进(游戏零改动)**:voice 播放中长按屏幕 → 旁白临时 1.7 倍速
(不变调),松开恢复 `settings.speed`。由 gesture.js 派发的 window 事件