import { useAudioWebSocket } from "@/services"; import { createAudioUrl, mergeAudioChunks } from "@/utils"; interface AudioState { isPlaying: boolean; isLoading: boolean; audioElement: HTMLAudioElement | null; audioUrl: string | null; audioChunks: ArrayBuffer[]; hasError: boolean; errorMessage: string; } export const useTtsStore = defineStore("tts", () => { // 多音频状态管理 - 以消息ID为key const audioStates = ref>(new Map()); // 当前活跃的转换请求(保留用于兼容性) const activeConversion = ref(null); // 会话状态 const hasActiveSession = ref(false); // WebSocket连接 const { sendMessage, ensureConnection } = useAudioWebSocket(); /** * 获取或创建音频状态 */ const getAudioState = (messageId: string): AudioState => { if (!audioStates.value.has(messageId)) { audioStates.value.set(messageId, { isPlaying: false, isLoading: false, audioElement: null, audioUrl: null, audioChunks: [], hasError: false, errorMessage: "" }); } return audioStates.value.get(messageId)!; }; /** * 发送文本进行TTS转换 */ const convertText = async (text: string, messageId: string) => { try { await ensureConnection(); // 暂停其他正在播放的音频 pauseAll(); // 获取当前消息的状态 const state = getAudioState(messageId); // 清理之前的音频和错误状态 clearAudioState(state); state.isLoading = true; state.audioChunks = []; // 设置当前活跃转换 activeConversion.value = messageId; hasActiveSession.value = true; // 发送文本到TTS服务 sendMessage(JSON.stringify({ type: "tts_text", text, messageId })); } catch (error) { handleError(`连接失败: ${error}`, messageId); } }; /** * 处理接收到的音频数据 - 修改为支持messageId参数 */ const handleAudioData = (data: ArrayBuffer, messageId?: string) => { // 如果传递了messageId就使用它,否则使用activeConversion const targetMessageId = messageId || activeConversion.value; if (!targetMessageId) { console.warn("handleAudioData: 没有有效的messageId"); return; } console.log(`接收音频数据 [${targetMessageId}],大小:`, data.byteLength); const state = getAudioState(targetMessageId); state.audioChunks.push(data); }; /** * 完成TTS转换,创建播放器并自动播放 - 修改为支持messageId参数 */ const finishConversion = async (messageId?: string) => { // 如果传递了messageId就使用它,否则使用activeConversion const targetMessageId = messageId || activeConversion.value; if (!targetMessageId) { console.warn("finishConversion: 没有有效的messageId"); return; } const state = getAudioState(targetMessageId); console.log( `完成TTS转换 [${targetMessageId}],音频片段数量:`, state.audioChunks.length ); if (state.audioChunks.length === 0) { handleError("没有接收到音频数据", targetMessageId); return; } try { // 合并音频片段 const mergedAudio = mergeAudioChunks(state.audioChunks); console.log( `合并后音频大小 [${targetMessageId}]:`, mergedAudio.byteLength ); // 创建音频URL和元素 state.audioUrl = createAudioUrl(mergedAudio); state.audioElement = new Audio(state.audioUrl); // 设置音频事件 setupAudioEvents(state, targetMessageId); state.isLoading = false; // 清除activeConversion(如果是当前活跃的) if (activeConversion.value === targetMessageId) { activeConversion.value = null; } console.log(`TTS音频准备完成 [${targetMessageId}],开始自动播放`); // 自动播放 await play(targetMessageId); } catch (error) { handleError(`音频处理失败: ${error}`, targetMessageId); } }; /** * 设置音频事件监听 */ const setupAudioEvents = (state: AudioState, messageId: string) => { if (!state.audioElement) return; const audio = state.audioElement; audio.addEventListener("ended", () => { state.isPlaying = false; console.log(`音频播放结束 [${messageId}]`); }); audio.addEventListener("error", (e) => { console.error(`音频播放错误 [${messageId}]:`, e); handleError("音频播放失败", messageId); }); audio.addEventListener("canplaythrough", () => { console.log(`音频可以播放 [${messageId}]`); }); }; /** * 播放指定消息的音频 */ const play = async (messageId: string) => { const state = getAudioState(messageId); if (!state.audioElement) { handleError("音频未准备好", messageId); return; } try { // 暂停其他正在播放的音频 pauseAll(messageId); await state.audioElement.play(); state.isPlaying = true; state.hasError = false; state.errorMessage = ""; console.log(`开始播放音频 [${messageId}]`); } catch (error) { handleError(`播放失败: ${error}`, messageId); } }; /** * 暂停指定消息的音频 */ const pause = (messageId: string) => { const state = getAudioState(messageId); if (!state.audioElement) return; state.audioElement.pause(); state.isPlaying = false; console.log(`暂停音频 [${messageId}]`); }; /** * 暂停所有音频 */ const pauseAll = (excludeMessageId?: string) => { audioStates.value.forEach((state, messageId) => { if (excludeMessageId && messageId === excludeMessageId) return; if (state.isPlaying && state.audioElement) { state.audioElement.pause(); state.isPlaying = false; } }); }; /** * 处理TTS错误 - 修改为支持messageId参数 */ const handleError = (errorMsg: string, messageId?: string) => { // 如果传递了messageId就使用它,否则使用activeConversion const targetMessageId = messageId || activeConversion.value; if (!targetMessageId) { console.error(`TTS错误 (无messageId): ${errorMsg}`); return; } console.error(`TTS错误 [${targetMessageId}]: ${errorMsg}`); const state = getAudioState(targetMessageId); state.hasError = true; state.errorMessage = errorMsg; state.isLoading = false; if (activeConversion.value === targetMessageId) { activeConversion.value = null; hasActiveSession.value = false; } }; /** * 清理指定消息的音频资源 */ const clearAudio = (messageId: string) => { const state = getAudioState(messageId); clearAudioState(state); audioStates.value.delete(messageId); }; /** * 清理音频状态 */ const clearAudioState = (state: AudioState) => { if (state.audioElement) { state.audioElement.pause(); state.audioElement = null; } if (state.audioUrl) { URL.revokeObjectURL(state.audioUrl); state.audioUrl = null; } state.isPlaying = false; state.audioChunks = []; state.hasError = false; state.errorMessage = ""; }; // 状态查询方法 const isPlaying = (messageId: string) => getAudioState(messageId).isPlaying; const isLoading = (messageId: string) => getAudioState(messageId).isLoading; const hasAudio = (messageId: string) => !!getAudioState(messageId).audioElement; const hasError = (messageId: string) => getAudioState(messageId).hasError; const getErrorMessage = (messageId: string) => getAudioState(messageId).errorMessage; // 组件卸载时清理所有资源 onUnmounted(() => { audioStates.value.forEach((state) => clearAudioState(state)); audioStates.value.clear(); }); return { // 状态查询方法 isPlaying, isLoading, hasAudio, hasError, getErrorMessage, // 核心方法 convertText, handleAudioData, finishConversion, play, pause, pauseAll, clearAudio, handleError }; });