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

如何合并多个音频Blob、Buffer或URL?Deno合并后仅播首段排查

音频合并失败原因及解决方案

你这问题是因为直接拼接了完整音频文件的二进制数据。像WAV这类格式的音频,每个文件都带有独立的文件头(记录采样率、声道数、数据长度等关键信息),你把多个完整WAV文件的ArrayBuffer直接连在一起,播放器只会识别第一个文件的头,播放完第一段音频后,后面的内容会被判定为无效数据,自然不会继续播放。

要正确合并音频,得先剥离每个音频的文件头,提取出原始的PCM音频数据,合并PCM后再生成新的统一文件头,最后组合成完整的WAV Blob。注意:所有要合并的音频必须采样率、声道数、位深度完全一致,否则合并后的音频会出现杂音或无法播放。

具体实现代码

1. WAV文件解析函数

用来提取音频的关键参数和原始PCM数据:

interface WavInfo {
  sampleRate: number;
  channels: number;
  bitDepth: number;
  pcmData: Uint8Array;
}

function parseWav(buffer: ArrayBuffer): WavInfo {
  const view = new DataView(buffer);
  
  // 验证是否为WAV文件
  if (view.getUint32(0, true) !== 0x46464952) { // RIFF标识
    throw new Error("不是有效的WAV文件");
  }
  if (view.getUint32(8, true) !== 0x45564157) { // WAVE标识
    throw new Error("不是有效的WAV文件");
  }
  
  // 定位fmt chunk
  let offset = 12;
  while (offset < buffer.byteLength) {
    const chunkId = view.getUint32(offset, true);
    const chunkSize = view.getUint32(offset + 4, true);
    if (chunkId === 0x20746d66) { // fmt chunk标识
      const audioFormat = view.getUint16(offset + 8, true);
      if (audioFormat !== 1) { // 仅支持PCM编码的WAV
        throw new Error("仅支持PCM编码的WAV文件");
      }
      const channels = view.getUint16(offset + 10, true);
      const sampleRate = view.getUint32(offset + 12, true);
      const bitDepth = view.getUint16(offset + 22, true);
      
      // 定位data chunk
      offset += 8 + chunkSize;
      while (offset < buffer.byteLength) {
        const dataChunkId = view.getUint32(offset, true);
        const dataChunkSize = view.getUint32(offset + 4, true);
        if (dataChunkId === 0x61746164) { // data chunk标识
          const pcmData = new Uint8Array(buffer, offset + 8, dataChunkSize);
          return { sampleRate, channels, bitDepth, pcmData };
        }
        offset += 8 + dataChunkSize;
      }
      throw new Error("未找到WAV文件的data chunk");
    }
    offset += 8 + chunkSize;
  }
  throw new Error("未找到WAV文件的fmt chunk");
}

2. 音频合并主函数

替换你原来的合并逻辑:

async function combineAudioUrls(audioUrls: string[]): Promise<Blob> {
  // 拉取所有音频并解析
  const wavInfos = await Promise.all(
    audioUrls.map(url => fetch(url).then(res => res.arrayBuffer()).then(parseWav))
  );
  
  // 验证所有音频参数一致
  const firstInfo = wavInfos[0];
  for (const info of wavInfos.slice(1)) {
    if (info.sampleRate !== firstInfo.sampleRate || 
        info.channels !== firstInfo.channels || 
        info.bitDepth !== firstInfo.bitDepth) {
      throw new Error("所有音频的采样率、声道数、位深度必须一致");
    }
  }
  
  // 合并PCM数据
  const totalPcmLength = wavInfos.reduce((sum, info) => sum + info.pcmData.length, 0);
  const combinedPcm = new Uint8Array(totalPcmLength);
  let offset = 0;
  for (const info of wavInfos) {
    combinedPcm.set(info.pcmData, offset);
    offset += info.pcmData.length;
  }
  
  // 生成新的WAV文件头
  const { sampleRate, channels, bitDepth } = firstInfo;
  const byteRate = sampleRate * channels * (bitDepth / 8);
  const blockAlign = channels * (bitDepth / 8);
  const dataSize = combinedPcm.length;
  const headerSize = 44; // 标准PCM WAV头长度
  const totalSize = headerSize + dataSize;
  
  const header = new ArrayBuffer(headerSize);
  const headerView = new DataView(header);
  
  // 写入RIFF chunk
  headerView.setUint32(0, 0x46464952, true); // RIFF
  headerView.setUint32(4, totalSize - 8, true); // 总长度减去RIFF标识的8字节
  headerView.setUint32(8, 0x45564157, true); // WAVE
  
  // 写入fmt chunk
  headerView.setUint32(12, 0x20746d66, true); // fmt
  headerView.setUint32(16, 16, true); // fmt chunk大小(PCM固定为16)
  headerView.setUint16(20, 1, true); // PCM编码格式
  headerView.setUint16(22, channels, true); // 声道数
  headerView.setUint32(24, sampleRate, true); // 采样率
  headerView.setUint32(28, byteRate, true); // 字节率
  headerView.setUint16(32, blockAlign, true); // 块对齐
  headerView.setUint16(34, bitDepth, true); // 位深度
  
  // 写入data chunk
  headerView.setUint32(36, 0x61746164, true); // data
  headerView.setUint32(40, dataSize, true); // PCM数据长度
  
  // 组合文件头和PCM数据
  const combinedBuffer = new Uint8Array(headerSize + dataSize);
  combinedBuffer.set(new Uint8Array(header), 0);
  combinedBuffer.set(combinedPcm, headerSize);
  
  return new Blob([combinedBuffer], { type: "audio/wav" });
}

使用方式

直接调用combineAudioUrls(audioUrls)即可得到正确合并的音频Blob,替换你原来的合并逻辑就行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 07:47:35