如何合并多个音频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
相关产品推荐
相关产品推荐

