diff --git a/hook/useAudioSpeak.js b/hook/useAudioSpeak.js index 0f43d67..07b0d84 100644 --- a/hook/useAudioSpeak.js +++ b/hook/useAudioSpeak.js @@ -37,6 +37,7 @@ export const useAudioSpeak = (config = {}) => { let firstSegmentHeader = null let lastPlayedIndex = -1 let currentPlayingIndex = -1 // 当前正在播放的片段索引 + let isInterrupted = false // 是否被中断 // 按标点分割文本 const splitTextByPunctuation = (text) => { @@ -167,7 +168,7 @@ export const useAudioSpeak = (config = {}) => { // 初始化音频上下文 const initAudioContext = () => { - if (!audioContext) { + if (!audioContext || audioContext.state === 'closed') { audioContext = new (window.AudioContext || window.webkitAudioContext)() console.log('音频上下文已初始化') } @@ -178,6 +179,7 @@ export const useAudioSpeak = (config = {}) => { const decodeAndPlayBlob = async (audioBlob, segmentIndex) => { return new Promise((resolve, reject) => { if (isCancelled || !audioContext) { + console.log('播放已取消或音频上下文不存在,跳过播放') resolve() return } @@ -193,6 +195,13 @@ export const useAudioSpeak = (config = {}) => { const arrayBuffer = e.target.result const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) + // 如果在此期间被取消,直接返回 + if (isCancelled) { + console.log('播放过程中被取消') + resolve() + return + } + audioSource = audioContext.createBufferSource() audioSource.buffer = audioBuffer audioSource.connect(audioContext.destination) @@ -212,6 +221,12 @@ export const useAudioSpeak = (config = {}) => { } console.log(`▶️开始播放第${segmentIndex + 1}个片段`) + + // 如果音频上下文被暂停,先恢复 + if (audioContext.state === 'suspended') { + await audioContext.resume() + } + audioSource.start(0) } catch (error) { @@ -295,7 +310,7 @@ export const useAudioSpeak = (config = {}) => { isPlayingQueue = true try { - while (audioQueue.length > 0 && !isCancelled) { + while (audioQueue.length > 0 && !isCancelled && !isInterrupted) { const audioItem = audioQueue[0] currentSegmentIndex.value = audioItem.segmentIndex @@ -303,6 +318,11 @@ export const useAudioSpeak = (config = {}) => { console.log(`准备播放第${audioItem.segmentIndex + 1}个片段: "${audioItem.text}"`) await decodeAndPlayBlob(audioItem.blob, audioItem.segmentIndex) + if (isCancelled || isInterrupted) { + console.log('播放被中断,退出播放队列') + break + } + // 播放完成后移除 audioQueue.shift() console.log(`片段${audioItem.segmentIndex + 1}播放完成,队列剩余: ${audioQueue.length}`) @@ -329,6 +349,12 @@ export const useAudioSpeak = (config = {}) => { // 向队列添加音频 const addToQueue = (audioItem) => { + // 如果被取消或中断,不添加到队列 + if (isCancelled || isInterrupted) { + console.log('播放已中断,不添加到队列') + return + } + // 按segmentIndex插入到正确位置 let insertIndex = 0 for (let i = audioQueue.length - 1; i >= 0; i--) { @@ -471,19 +497,23 @@ export const useAudioSpeak = (config = {}) => { // 清理资源 const cleanup = () => { + console.log('开始清理资源') isCancelled = true + isInterrupted = true if (audioSource) { try { audioSource.stop() + console.log('音频源已停止') } catch (e) { - // 忽略错误 + console.warn('停止音频源失败:', e) } audioSource = null } if (audioContext && audioContext.state !== 'closed') { audioContext.close() + console.log('音频上下文已关闭') } audioContext = null @@ -500,38 +530,57 @@ export const useAudioSpeak = (config = {}) => { isPaused.value = false isLoading.value = false progress.value = 0 + console.log('资源清理完成') } // 停止播放 const stopAudio = () => { + console.log('停止音频播放') isCancelled = true + isInterrupted = true + if (audioSource) { try { audioSource.stop() + console.log('音频源已停止') } catch (e) { - // 忽略错误 + console.warn('停止音频源失败:', e) } audioSource = null } + + audioQueue = [] isPlayingQueue = false currentPlayingIndex = -1 isSpeaking.value = false isPaused.value = false isLoading.value = false + + // 恢复中断标志,为下一次播放准备 + setTimeout(() => { + isCancelled = false + isInterrupted = false + }, 100) } // 主speak方法 const speak = async (text) => { + console.log('开始新的语音播报') + + // 先停止当前播放 + if (isSpeaking.value || audioQueue.length > 0) { + console.log('检测到正在播放,先停止') + stopAudio() + // 等待一小段时间确保资源清理完成 + await new Promise(resolve => setTimeout(resolve, 200)) + } + text = extractSpeechText(text) console.log('开始语音播报:', text) - // 如果正在播放,先停止 - if (isSpeaking.value) { - stopAudio() - } - // 重置状态 isCancelled = false + isInterrupted = false currentText.value = text isLoading.value = true isSpeaking.value = true @@ -562,7 +611,10 @@ export const useAudioSpeak = (config = {}) => { // 1. 串行请求所有音频片段 for (let i = 0; i < segments.length; i++) { - if (isCancelled) break + if (isCancelled || isInterrupted) { + console.log('播放被取消或中断,停止请求') + break + } console.log(`串行请求第${i + 1}/${segments.length}个片段`) @@ -572,6 +624,11 @@ export const useAudioSpeak = (config = {}) => { // 请求音频片段 const audioItem = await fetchAudioSegment(segments[i], i) + if (isCancelled || isInterrupted) { + console.log('播放被取消或中断,停止添加队列') + break + } + // 添加到播放队列 addToQueue({ blob: audioItem.blob, @@ -586,6 +643,11 @@ export const useAudioSpeak = (config = {}) => { } } + if (isCancelled || isInterrupted) { + console.log('播放被取消或中断,退出播放') + return + } + // 2. 所有请求完成 console.log('所有音频片段请求完成') allRequestsCompleted = true @@ -594,7 +656,7 @@ export const useAudioSpeak = (config = {}) => { tryMergeRemainingSegments() // 4. 等待所有音频播放完成 - while (audioQueue.length > 0 && !isCancelled) { + while (audioQueue.length > 0 && !isCancelled && !isInterrupted) { await new Promise(resolve => setTimeout(resolve, 100)) } @@ -604,9 +666,11 @@ export const useAudioSpeak = (config = {}) => { console.error('语音播报失败:', error) } finally { // 最终清理 - if (isCancelled) { + if (isCancelled || isInterrupted) { + console.log('播放被取消或中断,进行清理') cleanup() } else { + console.log('播放正常完成') isSpeaking.value = false isPaused.value = false isLoading.value = false @@ -642,8 +706,6 @@ export const useAudioSpeak = (config = {}) => { cleanup() } - // 组件卸载时清理 - return { // 状态 isSpeaking, diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index 097d7c1..79825c1 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -734,8 +734,8 @@ function colseFeeBack() { } function readMarkdown(value, index) { - speechIndex.value = index; if (speechIndex.value !== index) { + speechIndex.value = index; speak(value); return; }