303 lines
8.0 KiB
TypeScript
303 lines
8.0 KiB
TypeScript
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<Map<string, AudioState>>(new Map());
|
||
|
||
// 当前活跃的转换请求(保留用于兼容性)
|
||
const activeConversion = ref<string | null>(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
|
||
};
|
||
});
|