|
@@ -312,13 +312,17 @@ const toggleSpeechInput = async () => {
|
|
|
|
|
|
|
|
// 停止音频处理
|
|
// 停止音频处理
|
|
|
if (scriptProcessor.value) {
|
|
if (scriptProcessor.value) {
|
|
|
- scriptProcessor.value.disconnect()
|
|
|
|
|
|
|
+ // 对于AudioWorkletNode,需要先关闭端口
|
|
|
|
|
+ if (scriptProcessor.value.port) {
|
|
|
|
|
+ scriptProcessor.value.port.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ scriptProcessor.value.disconnect();
|
|
|
}
|
|
}
|
|
|
if (mediaStreamSource.value) {
|
|
if (mediaStreamSource.value) {
|
|
|
- mediaStreamSource.value.disconnect()
|
|
|
|
|
|
|
+ mediaStreamSource.value.disconnect();
|
|
|
}
|
|
}
|
|
|
if (audioContext.value) {
|
|
if (audioContext.value) {
|
|
|
- audioContext.value.close()
|
|
|
|
|
|
|
+ audioContext.value.close();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 释放媒体流资源
|
|
// 释放媒体流资源
|
|
@@ -356,23 +360,72 @@ const toggleSpeechInput = async () => {
|
|
|
// 使用Web Audio API捕获音频并转换为PCM格式
|
|
// 使用Web Audio API捕获音频并转换为PCM格式
|
|
|
audioContext.value = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 })
|
|
audioContext.value = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 })
|
|
|
mediaStreamSource.value = audioContext.value.createMediaStreamSource(stream)
|
|
mediaStreamSource.value = audioContext.value.createMediaStreamSource(stream)
|
|
|
- scriptProcessor.value = audioContext.value.createScriptProcessor(4096, 1, 1)
|
|
|
|
|
-
|
|
|
|
|
- // 处理音频数据
|
|
|
|
|
- scriptProcessor.value.onaudioprocess = async (event) => {
|
|
|
|
|
- const inputData = event.inputBuffer.getChannelData(0)
|
|
|
|
|
- // 转换为16位PCM
|
|
|
|
|
- const pcmData = new Int16Array(inputData.length)
|
|
|
|
|
- for (let i = 0; i < inputData.length; i++) {
|
|
|
|
|
- pcmData[i] = inputData[i] * 32768
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试使用AudioWorkletNode(现代浏览器推荐),如果不支持则回退到ScriptProcessorNode
|
|
|
|
|
+ if (audioContext.value.audioWorklet && window.AudioWorkletNode) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 注册音频处理工作线程
|
|
|
|
|
+ await audioContext.value.audioWorklet.addModule('data:text/javascript,' + encodeURIComponent(`
|
|
|
|
|
+ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
|
|
+ process(inputs, outputs, params) {
|
|
|
|
|
+ const input = inputs[0];
|
|
|
|
|
+ if (input.length > 0) {
|
|
|
|
|
+ const channelData = input[0];
|
|
|
|
|
+ const pcmData = new Int16Array(channelData.length);
|
|
|
|
|
+ for (let i = 0; i < channelData.length; i++) {
|
|
|
|
|
+ pcmData[i] = channelData[i] * 32768;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 通过消息传递数据到主线程
|
|
|
|
|
+ this.port.postMessage({ pcmData: pcmData.buffer }, [pcmData.buffer]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ registerProcessor('audio-processor', AudioProcessor);
|
|
|
|
|
+ `));
|
|
|
|
|
+
|
|
|
|
|
+ // 创建AudioWorkletNode
|
|
|
|
|
+ scriptProcessor.value = new AudioWorkletNode(audioContext.value, 'audio-processor');
|
|
|
|
|
+
|
|
|
|
|
+ // 处理来自工作线程的消息
|
|
|
|
|
+ scriptProcessor.value.port.onmessage = async (event) => {
|
|
|
|
|
+ if (event.data.pcmData) {
|
|
|
|
|
+ await sendAudioData(event.data.pcmData);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('AudioWorkletNode不支持,回退到ScriptProcessorNode:', error);
|
|
|
|
|
+ // 回退到ScriptProcessorNode
|
|
|
|
|
+ scriptProcessor.value = audioContext.value.createScriptProcessor(4096, 1, 1);
|
|
|
|
|
+ scriptProcessor.value.onaudioprocess = async (event) => {
|
|
|
|
|
+ const inputData = event.inputBuffer.getChannelData(0);
|
|
|
|
|
+ // 转换为16位PCM
|
|
|
|
|
+ const pcmData = new Int16Array(inputData.length);
|
|
|
|
|
+ for (let i = 0; i < inputData.length; i++) {
|
|
|
|
|
+ pcmData[i] = inputData[i] * 32768;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 发送到后端
|
|
|
|
|
+ await sendAudioData(pcmData.buffer);
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
- // 发送到后端
|
|
|
|
|
- await sendAudioData(pcmData.buffer)
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 使用传统的ScriptProcessorNode
|
|
|
|
|
+ scriptProcessor.value = audioContext.value.createScriptProcessor(4096, 1, 1);
|
|
|
|
|
+ scriptProcessor.value.onaudioprocess = async (event) => {
|
|
|
|
|
+ const inputData = event.inputBuffer.getChannelData(0);
|
|
|
|
|
+ // 转换为16位PCM
|
|
|
|
|
+ const pcmData = new Int16Array(inputData.length);
|
|
|
|
|
+ for (let i = 0; i < inputData.length; i++) {
|
|
|
|
|
+ pcmData[i] = inputData[i] * 32768;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 发送到后端
|
|
|
|
|
+ await sendAudioData(pcmData.buffer);
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 连接音频节点
|
|
// 连接音频节点
|
|
|
- mediaStreamSource.value.connect(scriptProcessor.value)
|
|
|
|
|
- scriptProcessor.value.connect(audioContext.value.destination)
|
|
|
|
|
|
|
+ mediaStreamSource.value.connect(scriptProcessor.value);
|
|
|
|
|
+ scriptProcessor.value.connect(audioContext.value.destination);
|
|
|
|
|
|
|
|
// 先设置UI状态,让用户知道系统正在准备
|
|
// 先设置UI状态,让用户知道系统正在准备
|
|
|
isRecording.value = true
|
|
isRecording.value = true
|
|
@@ -393,13 +446,17 @@ const toggleSpeechInput = async () => {
|
|
|
|
|
|
|
|
// 停止音频处理
|
|
// 停止音频处理
|
|
|
if (scriptProcessor.value) {
|
|
if (scriptProcessor.value) {
|
|
|
- scriptProcessor.value.disconnect()
|
|
|
|
|
|
|
+ // 对于AudioWorkletNode,需要先关闭端口
|
|
|
|
|
+ if (scriptProcessor.value.port) {
|
|
|
|
|
+ scriptProcessor.value.port.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ scriptProcessor.value.disconnect();
|
|
|
}
|
|
}
|
|
|
if (mediaStreamSource.value) {
|
|
if (mediaStreamSource.value) {
|
|
|
- mediaStreamSource.value.disconnect()
|
|
|
|
|
|
|
+ mediaStreamSource.value.disconnect();
|
|
|
}
|
|
}
|
|
|
if (audioContext.value) {
|
|
if (audioContext.value) {
|
|
|
- audioContext.value.close()
|
|
|
|
|
|
|
+ audioContext.value.close();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 释放媒体流资源
|
|
// 释放媒体流资源
|