将麦克风音频流转为WAV通过WebSocket传输时解码失败求助
问题:麦克风音频流转WAV通过WebSocket传输时解码失败
我需要将麦克风采集的音频流以WAV格式通过WebSocket传输,但在将音频Blob转为ArrayBuffer并解码时出现错误:DOMException: Failed to execute 'decodeAudioData' on 'BaseAudioContext': Unable to decode audio data
我曾尝试使用已废弃的JavaScriptProcessorNode,认为AudioWorklet对于这类简单转换任务来说过于复杂,相关代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body id="body"> <button id="button">test</button> <script defer> const btn = document.getElementById("button"); btn.addEventListener("click", () => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { const recorder = new MediaRecorder(stream); recorder.start(10); recorder.addEventListener("dataavailable", (event) => { const streamBlob = new Blob([event.data]); streamBlob .arrayBuffer() .then((arrayBuffer) => { const audioCtx = new AudioContext({ sampleRate: 16000 }); audioCtx ?.decodeAudioData(arrayBuffer) ?.then((buffer) => { console.log("🚀 ?.then wavBlob", buffer); }) .catch((error) => { console.log("🚀 . error1", error); }) .finally(() => {}); }) .catch((error) => { console.log("🚀 . error2", error); }); }); }), function (e) { alert("Error capturing audio."); }; }); </script> </body> </html>
解决方案
核心问题分析
- MediaRecorder输出非WAV格式:默认情况下,MediaRecorder会输出浏览器支持的编码格式(如Chrome的WebM/Opus),而非标准WAV,且
dataavailable返回的是片段数据,缺少WAV文件头,无法被decodeAudioData识别。 - 频繁创建AudioContext:每次事件触发都新建
AudioContext,既浪费资源,也可能引发解码异常。 - 片段化数据无法解码:
decodeAudioData需要完整的音频文件数据,片段化的Blob不满足解码要求。
解决步骤
1. 直接采集PCM并封装为WAV(推荐用于实时流)
跳过MediaRecorder,直接通过AudioContext获取原始PCM样本,手动添加WAV文件头,生成标准WAV数据用于WebSocket传输。
修改后的代码示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>WAV WebSocket Stream</title> </head> <body> <button id="startBtn">开始采集</button> <button id="stopBtn" disabled>停止采集</button> <script> const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); let audioCtx; let scriptProcessor; let mediaStream; let ws; // 将PCM数据封装为标准WAV格式 function pcmToWav(pcmData, sampleRate, channels) { const buffer = new ArrayBuffer(44 + pcmData.length * 2); const view = new DataView(buffer); // 写入WAV文件头 view.setUint8(0, 0x52); // R view.setUint8(1, 0x49); // I view.setUint8(2, 0x46); // F view.setUint8(3, 0x46); // F view.setUint32(4, 44 + pcmData.length * 2, true); // 文件总大小 view.setUint8(8, 0x57); // W view.setUint8(9, 0x41); // A view.setUint8(10, 0x56); // V view.setUint8(11, 0x45); // E view.setUint8(12, 0x66); // f view.setUint8(13, 0x6D); // m view.setUint8(14, 0x74); // t view.setUint8(15, 0x20); // 空格 view.setUint32(16, 16, true); // 子块大小 view.setUint16(20, 1, true); // 音频格式(PCM) view.setUint16(22, channels, true); // 声道数 view.setUint32(24, sampleRate, true); // 采样率 view.setUint32(28, sampleRate * channels * 2, true); // 字节率 view.setUint16(32, channels * 2, true); // 块对齐 view.setUint16(34, 16, true); // 位深度 view.setUint8(36, 0x64); // d view.setUint8(37, 0x61); // a view.setUint8(38, 0x74); // t view.setUint8(39, 0x61); // a view.setUint32(40, pcmData.length * 2, true); // 数据大小 // 将PCM浮点数据转为16位有符号整数写入 for (let i = 0; i < pcmData.length; i++) { const sample = Math.max(-1, Math.min(1, pcmData[i])); const intSample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; view.setInt16(44 + i * 2, intSample, true); } return buffer; } startBtn.addEventListener('click', async () => { // 初始化WebSocket(替换为你的服务器地址) ws = new WebSocket('ws://your-server-url'); ws.binaryType = 'arraybuffer'; // 获取麦克风流,指定采样率和声道数 mediaStream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1 } }); audioCtx = new AudioContext({ sampleRate: 16000 }); const source = audioCtx.createMediaStreamSource(mediaStream); // 使用ScriptProcessorNode处理音频数据 scriptProcessor = audioCtx.createScriptProcessor(4096, 1, 1); scriptProcessor.onaudioprocess = (event) => { const channelData = event.inputBuffer.getChannelData(0); const wavBuffer = pcmToWav(channelData, audioCtx.sampleRate, 1); // WebSocket连接正常时发送数据 if (ws.readyState === WebSocket.OPEN) { ws.send(wavBuffer); } }; source.connect(scriptProcessor); scriptProcessor.connect(audioCtx.destination); startBtn.disabled = true; stopBtn.disabled = false; }); stopBtn.addEventListener('click', () => { // 停止采集并释放资源 mediaStream.getTracks().forEach(track => track.stop()); scriptProcessor.disconnect(); audioCtx.close(); ws.close(); startBtn.disabled = false; stopBtn.disabled = true; }); </script> </body> </html>
2. 关键说明
- 原始PCM采集:通过
AudioContext直接获取未编码的音频样本,避免MediaRecorder的格式限制。 - WAV头封装:标准WAV文件必须包含文件头信息,
pcmToWav函数完成这一步骤,确保数据可被解码。 - 资源管理:采集结束后及时关闭媒体流、音频上下文和WebSocket,避免资源泄漏。
3. 若坚持使用MediaRecorder(仅适用于完整文件录制)
如果不需要实时流,仅录制完整WAV文件,需收集所有片段后再解码:
// 修改MediaRecorder逻辑,仅在停止时处理完整数据 const chunks = []; recorder.addEventListener("dataavailable", (event) => { chunks.push(event.data); }); recorder.addEventListener('stop', () => { const wavBlob = new Blob(chunks, { type: 'audio/wav' }); wavBlob.arrayBuffer().then(arrayBuffer => { const audioCtx = new AudioContext(); audioCtx.decodeAudioData(arrayBuffer) .then(buffer => console.log('解码成功:', buffer)) .catch(err => console.error('解码失败:', err)); }); }); // 调用recorder.stop()后才会触发stop事件处理完整数据
内容的提问来源于stack exchange,提问作者Mohamad Ahmadi
相关产品推荐
相关产品推荐

