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

合并音频Blob数组时仅保留第一个Blob内容的问题求助

问题原因

你遇到的核心问题是:每个ondataavailable回调返回的Blob都是完整的WAV文件(包含独立的文件头),直接用new Blob()合并时,多个WAV头会堆叠在一起。音频播放器只会解析第一个文件头,读取对应长度的音频数据,后面的Blob内容会被忽略,所以只能听到第一个块的声音。

解决方案

方案1:改用原生MediaRecorder API(推荐)

原生MediaRecorder会自动处理分片媒体数据的拼接,生成单个完整的媒体文件,无需手动合并时处理文件头问题。替换你的代码如下:

// 初始化原生MediaRecorder,指定WAV格式
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/wav' });
const recordedBlobs = [];

mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    recordedBlobs.push(event.data);
    // 原生MediaRecorder返回的是媒体片段,临时转为Blob用于预览
    const tempBlob = new Blob([event.data], { type: 'audio/wav' });
    wavesurfer.loadBlob(tempBlob);
  }
};

// 启动录制
mediaRecorder.start();

// 停止录制时合并生成完整Blob
mediaRecorder.onstop = () => {
  const recordedBlob = new Blob(recordedBlobs, { type: 'audio/wav' });
  // 此时recordedBlob是完整可播放的WAV文件
};

方案2:手动处理WAV文件头(仅当必须使用MediaStreamRecorder时)

如果一定要用MediaStreamRecorder,需要剥离每个WAV块的文件头,只保留原始PCM音频数据,合并后再重新生成统一的WAV文件头。

代码实现

// 合并多个WAV Blob的函数
async function mergeWavBlobs(wavBlobs) {
  const pcmChunks = [];
  let totalPcmSize = 0;

  // 遍历所有Blob,剥离每个WAV的文件头(前44字节)
  for (const blob of wavBlobs) {
    const arrayBuffer = await blob.arrayBuffer();
    // WAV文件头通常为44字节,取后面的PCM数据
    const pcmData = arrayBuffer.slice(44);
    pcmChunks.push(pcmData);
    totalPcmSize += pcmData.byteLength;
  }

  // 生成新的WAV文件头(参数需和录制时一致)
  const wavHeader = createWavHeader(
    totalPcmSize,
    1,          // 声道数,和你的audioChannels一致
    44100,      // 采样率,根据MediaStreamRecorder的实际设置调整
    16          // 位深度,通常为16位
  );

  // 合并文件头和所有PCM数据
  const mergedBuffer = new ArrayBuffer(wavHeader.byteLength + totalPcmSize);
  const view = new Uint8Array(mergedBuffer);
  
  // 写入文件头
  view.set(wavHeader, 0);
  // 写入PCM数据
  let offset = wavHeader.byteLength;
  for (const pcm of pcmChunks) {
    view.set(new Uint8Array(pcm), offset);
    offset += pcm.byteLength;
  }

  return new Blob([mergedBuffer], { type: 'audio/wav' });
}

// 生成标准WAV文件头的工具函数
function createWavHeader(pcmLength, channels, sampleRate, bitDepth) {
  const buffer = new ArrayBuffer(44);
  const dataView = new DataView(buffer);

  // RIFF chunk
  dataView.setUint8(0, 0x52); // R
  dataView.setUint8(1, 0x49); // I
  dataView.setUint8(2, 0x46); // F
  dataView.setUint8(3, 0x46); // F
  dataView.setUint32(4, 36 + pcmLength, true); // 文件总长度
  // WAVE chunk
  dataView.setUint8(8, 0x57); // W
  dataView.setUint8(9, 0x41); // A
  dataView.setUint8(10, 0x56); // V
  dataView.setUint8(11, 0x45); // E
  // fmt subchunk
  dataView.setUint8(12, 0x66); // f
  dataView.setUint8(13, 0x6D); // m
  dataView.setUint8(14, 0x74); // t
  dataView.setUint8(15, 0x20); // 空格
  dataView.setUint32(16, 16, true); // subchunk长度(PCM固定为16)
  dataView.setUint16(20, 1, true); // 音频格式(PCM为1)
  dataView.setUint16(22, channels, true); // 声道数
  dataView.setUint32(24, sampleRate, true); // 采样率
  dataView.setUint32(28, sampleRate * channels * (bitDepth / 8), true); // 字节率
  dataView.setUint16(32, channels * (bitDepth / 8), true); // 块对齐
  dataView.setUint16(34, bitDepth, true); // 位深度
  // data subchunk
  dataView.setUint8(36, 0x64); // d
  dataView.setUint8(37, 0x61); // a
  dataView.setUint8(38, 0x74); // t
  dataView.setUint8(39, 0x61); // a
  dataView.setUint32(40, pcmLength, true); // PCM数据长度

  return new Uint8Array(buffer);
}

// 停止录制后调用合并函数
mediaRecorder.onstop = async () => {
  const recordedBlob = await mergeWavBlobs(recordedBlobs);
  // 此时recordedBlob是完整可播放的WAV文件
};

注意事项

  • 确保createWavHeader中的参数(声道数、采样率、位深度)和你MediaStreamRecorder的录制设置完全一致,否则合并后的音频会出现播放速度异常、无声音等问题。
  • MediaStreamRecorder的默认采样率可能因浏览器或设备不同而变化,建议手动指定:mediaRecorder.sampleRate = 44100;

内容的提问来源于stack exchange,提问作者INTODAN

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 14:05:31