import { useWebSocketStore } from "@/services"; import { convertToPCM16 } from "@/utils"; import { useChatStore } from "./chat_store"; export const useAsrStore = defineStore("asr", () => { // 是否正在录音 const isRecording = ref(false); // 识别结果消息列表 const messages = ref([]); // 音频相关对象 let audioContext: AudioContext | null = null; let mediaStreamSource: MediaStreamAudioSourceNode | null = null; let workletNode: AudioWorkletNode | null = null; const webSocketStore = useWebSocketStore(); const router = useRouter(); /** * 发送消息到 WebSocket * @param data 字符串或二进制数据 */ const sendMessage = (data: string | Uint8Array) => { // 仅在连接已建立时发送 if (webSocketStore.connected) { if (typeof data === "string") { webSocketStore.send(data); } else { webSocketStore.websocket?.send(data); } } }; // AudioWorklet 处理器代码,作为字符串 const audioProcessorCode = ` class AudioProcessor extends AudioWorkletProcessor { process(inputs, outputs, parameters) { const input = inputs[0] if (input.length > 0) { const inputChannel = input[0] // 发送音频数据到主线程 this.port.postMessage({ type: 'audiodata', data: inputChannel }) } return true } } registerProcessor('audio-processor', AudioProcessor) `; /** * 开始录音 */ const startRecording = async () => { if (isRecording.value) return; messages.value = []; // 确保 WebSocket 已连接 if (!webSocketStore.connected) { webSocketStore.connect(); // 等待连接建立 await new Promise((resolve) => { const check = () => { if (webSocketStore.connected) resolve(); else setTimeout(check, 100); }; check(); }); } try { // 获取麦克风音频流 const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // 创建音频上下文,采样率16kHz audioContext = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 16000 }); // 用Blob方式创建AudioWorklet模块的URL const blob = new Blob([audioProcessorCode], { type: "application/javascript" }); const processorUrl = URL.createObjectURL(blob); // 加载AudioWorklet模块 await audioContext.audioWorklet.addModule(processorUrl); // 释放URL对象防止内存泄漏 URL.revokeObjectURL(processorUrl); // 创建音频源节点 mediaStreamSource = audioContext.createMediaStreamSource(stream); // 创建AudioWorkletNode workletNode = new AudioWorkletNode(audioContext, "audio-processor", { numberOfInputs: 1, numberOfOutputs: 1, channelCount: 1 }); // 监听来自AudioWorklet的音频数据 workletNode.port.onmessage = (event) => { if (event.data.type === "audiodata") { // 转换为16位PCM格式 const pcmData = convertToPCM16(event.data.data); // 发送PCM数据到WebSocket sendMessage(pcmData); } }; // 连接音频节点 mediaStreamSource.connect(workletNode); workletNode.connect(audioContext.destination); isRecording.value = true; } catch (err) { // 麦克风权限失败或AudioWorklet加载失败 console.error("需要麦克风权限才能录音", err); } }; /** * 停止录音 */ const stopRecording = () => { if (!isRecording.value) return; const messageId = `voice_${Date.now()}`; // 通知后端录音结束 const msg: Record = { type: "asr_end" }; if (router.currentRoute.value.path === "/voice") { msg.messageId = messageId; msg.voiceConversation = true; msg.speaker = useChatStore().speakerInfo?.speaker_id; } sendMessage(JSON.stringify(msg)); // 停止所有音轨 if (mediaStreamSource?.mediaStream) { const tracks = mediaStreamSource.mediaStream.getTracks(); tracks.forEach((track) => track.stop()); } // 断开音频节点 workletNode?.disconnect(); mediaStreamSource?.disconnect(); setTimeout(() => { // TODO: 临时写法,这里的更新状态需要调整 // 确保在停止录音后延迟更新状态,因为要等待LLM请求 isRecording.value = false; }, 300); // 释放音频资源 audioContext?.close().then(() => { audioContext = null; mediaStreamSource = null; workletNode = null; }); }; return { isRecording, messages, startRecording, stopRecording, sendMessage }; });