Files
Practical_Training_Assignment/web/src/stores/asr_store.ts
2025-07-01 01:27:29 +08:00

164 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string[]>([]);
// 音频相关对象
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<void>((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<string, any> = { 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
};
});