修复语音/通话音频断续:改为持续运行的队列处理器

原实现:取出所有块精确调度 → 等播完 → 退出处理器 → 新块到达无人处理
→ 下一批启动有间隙

改为:持续运行的处理器循环,新块到达时立即调度到上一块结束时间,
消除批次间的间隙。用 nextScheduleTime 追踪精确调度位置,
防止累积延迟。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
wsb1224 2026-06-10 15:29:54 +08:00
parent e328df95ae
commit d52073adb4

View File

@ -554,7 +554,7 @@ export const useChatStore = defineStore('chat', () => {
// 如果没有在播放,开始播放队列
if (!isPlayingQueue.value) {
playAudioQueue()
startAudioQueueProcessor()
}
}
break
@ -991,7 +991,7 @@ export const useChatStore = defineStore('chat', () => {
// 如果没有在播放,开始播放队列
if (!isPlayingQueue.value) {
playAudioQueue()
startAudioQueueProcessor()
}
}
break
@ -1433,57 +1433,65 @@ export const useChatStore = defineStore('chat', () => {
/**
* 播放音频队列 - 顺序播放所有缓冲的音频片段
*/
async function playAudioQueue() {
if (isPlayingQueue.value) return
if (audioQueue.value.length === 0) return
// 持续运行的音频队列处理器
let audioQueueRunning = false
let nextScheduleTime = 0
async function startAudioQueueProcessor() {
if (audioQueueRunning) return
audioQueueRunning = true
isPlayingQueue.value = true
isPlaying.value = true
// 初始化AudioContext
if (!currentAudioContext || currentAudioContext.state === 'closed') {
currentAudioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: 24000
})
}
// 确保 AudioContext 处于运行状态
await ensureAudioContext(currentAudioContext);
await ensureAudioContext(currentAudioContext)
nextScheduleTime = currentAudioContext.currentTime
try {
// 一次性取出队列中所有音频块
const chunks = []
while (audioQueue.value.length > 0) {
const item = audioQueue.value.shift()
if (item) chunks.push(item)
}
while (audioQueueRunning) {
// 取出当前队列中所有块
const chunks = []
while (audioQueue.value.length > 0) {
const item = audioQueue.value.shift()
if (item) chunks.push(item)
}
if (chunks.length === 0) return
if (chunks.length === 0) {
// 无新块,短暂等待后继续检查
await new Promise(r => setTimeout(r, 20))
continue
}
// 用精确时间调度所有块,消除间隙
let nextStartTime = currentAudioContext.currentTime
let lastSource = null
// 确保调度时间不早于当前时间(防止累积延迟)
if (nextScheduleTime < currentAudioContext.currentTime) {
nextScheduleTime = currentAudioContext.currentTime
}
for (const chunk of chunks) {
const result = schedulePCMChunk(chunk.data, chunk.sampleRate, nextStartTime)
nextStartTime += result.duration
lastSource = result.source
}
// 等待最后一块播放完成
if (lastSource) {
await new Promise((resolve) => {
lastSource.onended = () => resolve()
})
// 精确调度每个块
for (const chunk of chunks) {
const result = schedulePCMChunk(chunk.data, chunk.sampleRate, nextScheduleTime)
nextScheduleTime += result.duration
}
}
} catch (error) {
console.error('[ChatStore] Audio queue playback error:', error)
console.error('[ChatStore] Audio queue processor error:', error)
} finally {
isPlayingQueue.value = false
isPlaying.value = false
audioQueueRunning = false
}
}
function stopAudioQueueProcessor() {
audioQueueRunning = false
isPlayingQueue.value = false
isPlaying.value = false
}
/**
* PCM 数据解码为 AudioBuffer
*/
@ -1580,6 +1588,7 @@ export const useChatStore = defineStore('chat', () => {
* 重置音频状态
*/
function resetAudioState() {
stopAudioQueueProcessor()
audioQueue.value = []
isPlayingQueue.value = false
isPlaying.value = false
@ -1626,6 +1635,8 @@ export const useChatStore = defineStore('chat', () => {
* 停止音频播放
*/
function stopAudioPlayback() {
// 停止持续队列处理器
stopAudioQueueProcessor()
// 清空音频队列
audioQueue.value = []
isPlayingQueue.value = false
@ -1854,7 +1865,6 @@ export const useChatStore = defineStore('chat', () => {
// 音频播放
playAudio,
playPCMAudio,
playAudioQueue,
stopAudio,
stopAudioPlayback,
resetAudioState,