You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将麦克风音频流转为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 21:25:14