fix(voice): TTS 只播前几字就断——tts_end 别打断,等排队音频放完
火山合成远快于真实语速:一句话的 PCM 秒级全推到客户端,被 nextStart 预约到未来时刻顺序播。此前 tts_end 直接 resetPlayback() stop 掉所有还 没播的 source,于是只听到前三四个字就断。 改:tts_end 走 drainThenReady()——不打断,按 nextStart 算剩余时长等排队 音频自然放完,末段结束再回 ready;speaking 态维持到真播完(HUD 频谱靠它)。 resetPlayback 只留给 barge-in/close,并清 drainTimer 防迟到定时器改状态。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ export class VoiceClient {
|
||||
private sources: AudioBufferSourceNode[] = [];
|
||||
private playAnalyser: AnalyserNode | null = null;
|
||||
private lvlBuf = new Uint8Array(512); // 复用的时域采样缓冲(level() 每帧读,别每帧新建)
|
||||
private drainTimer: number | null = null; // tts_end 后等排队音频放完的定时器
|
||||
|
||||
constructor(cb: VoiceCallbacks) {
|
||||
this.cb = cb;
|
||||
@@ -126,8 +127,10 @@ export class VoiceClient {
|
||||
this.setState("speaking");
|
||||
break;
|
||||
case "tts_end":
|
||||
this.resetPlayback();
|
||||
this.setState("ready");
|
||||
// 别 resetPlayback!服务端"音频发完" ≠ 客户端"播完":音频按 nextStart 预约到未来时刻播,
|
||||
// 而火山合成远快于真实语速,收到 tts_end 时大半音频还排在队列里没播。stop 掉就只剩前几个字。
|
||||
// 让排队音频自然放完,最后一段结束再回 ready。
|
||||
this.drainThenReady();
|
||||
break;
|
||||
case "error":
|
||||
this.cb.onError?.(m.msg ?? "语音出错");
|
||||
@@ -295,6 +298,10 @@ export class VoiceClient {
|
||||
}
|
||||
|
||||
private resetPlayback(): void {
|
||||
if (this.drainTimer !== null) {
|
||||
window.clearTimeout(this.drainTimer);
|
||||
this.drainTimer = null;
|
||||
}
|
||||
this.sources.forEach((s) => {
|
||||
try {
|
||||
s.stop();
|
||||
@@ -306,6 +313,20 @@ export class VoiceClient {
|
||||
this.nextStart = 0;
|
||||
}
|
||||
|
||||
// drainThenReady 处理 tts_end:不打断,等排队音频按调度自然放完,最后回 ready。
|
||||
// speaking 态要维持到真正播完(HUD 频谱靠它读 playAnalyser);播完把队列清干净。
|
||||
private drainThenReady(): void {
|
||||
if (this.drainTimer !== null) window.clearTimeout(this.drainTimer);
|
||||
const ctx = this.playCtx;
|
||||
const remainMs = ctx ? Math.max(0, (this.nextStart - ctx.currentTime) * 1000) : 0;
|
||||
this.drainTimer = window.setTimeout(() => {
|
||||
this.drainTimer = null;
|
||||
this.sources = [];
|
||||
this.nextStart = 0;
|
||||
if (this.state === "speaking") this.setState("ready"); // 期间被打断/新一轮改了态就不覆盖
|
||||
}, remainMs + 120); // +120ms 余量,等末尾 source 真正 onended
|
||||
}
|
||||
|
||||
private send(m: { type: string; graph?: string }): void {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(m));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user