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
+12 -4
View File
@@ -141,8 +141,8 @@
};
const AC = window.AudioContext || window.webkitAudioContext;
const voiceNodes = new Set(); // 连接中的 SoundTouch ScriptProcessorNode(断开即移除)
if (AC) {
const voiceNodes = new WeakSet(); // SoundTouch 管线创建的 ScriptProcessorNode
const origCSP = AC.prototype.createScriptProcessor;
AC.prototype.createScriptProcessor = function (...a) {
const n = origCSP.apply(this, a);
@@ -153,12 +153,20 @@
AudioNode.prototype.disconnect = function (...a) {
// voice 节点断开 = 引擎 killVoice(自然结束/被顶掉/stopVoice 都走这里)。
// 玩家实际听到的尾音就到此刻(FIFO 里未播部分被丢弃),用 now() 而非 lastAct。
if (voiceNodes.has(this) && seg) closeSeg(now(), 'kill');
if (voiceNodes.has(this)) {
voiceNodes.delete(this);
if (seg) closeSeg(now(), 'kill');
}
return origDisc.apply(this, a);
};
}
// 兜底清扫:长静默后关闭未结段
setInterval(() => { if (seg && now() - seg.lastAct > VOICE_GAP_MS) closeSeg(seg.lastAct, 'end'); }, 200);
// 兜底清扫:长静默后关闭未结段。
// 守卫(2026-09 三修适配):引擎的尾部静音垫让 SoundTouch 读的是新建
// buffer(未注册 url),gcd 抽读信号消失、lastAct 停在句首——只要仍有
// 连接中的语音 SPN 就说明句在播,不许清扫;段关闭只信 disconnect。
setInterval(() => {
if (seg && now() - seg.lastAct > VOICE_GAP_MS && voiceNodes.size === 0) closeSeg(seg.lastAct, 'end');
}, 200);
} catch (e) { push({ ev: 'probe_error', where: 'voice-detect', msg: String(e) }); }
// ---------- AudioBufferSourceNode:bgm/sfx 播放事件 ----------