如何通过Node.js将音频直播分片生成多个音频文件
解决WAV音频分片后后续文件无法播放的问题
你遇到的问题核心是WAV文件的结构特性:WAV文件必须包含文件头(记录音频格式、声道数、采样率等关键解析信息)+ 音频数据块。你的代码里第一个文件从原文件开头写入,自带完整的WAV头,所以能正常播放;但后续分片只写入了原始音频数据,没有对应的WAV文件头,播放器无法识别数据格式,自然无法播放。
解决方案思路
- 解析原始WAV头:先读取原文件的WAV头,提取格式参数(声道数、采样率等)和头的长度
- 生成分片专属WAV头:每个新分片文件,先写入修改后的WAV头(根据当前分片的数据大小更新头里的文件总大小、数据块大小字段)
- 按阈值分片写入:累计音频数据量,达到设定的大小/时长阈值后,关闭当前文件,创建新文件并重复上述步骤
修改后的代码
const fs = require("fs"); // 解析WAV文件头,返回头信息和头的长度 function parseWavHeader(buffer) { if (buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WAVE') { throw new Error('不是有效的WAV文件'); } const fmtChunkOffset = 12; const fmtChunkSize = buffer.readUInt32LE(fmtChunkOffset + 4); const audioFormat = buffer.readUInt16LE(fmtChunkOffset + 8); const numChannels = buffer.readUInt16LE(fmtChunkOffset + 10); const sampleRate = buffer.readUInt32LE(fmtChunkOffset + 12); const byteRate = buffer.readUInt32LE(fmtChunkOffset + 16); const blockAlign = buffer.readUInt16LE(fmtChunkOffset + 20); const bitsPerSample = buffer.readUInt16LE(fmtChunkOffset + 22); // 定位data块起始位置(兼容含额外块的WAV文件) let dataOffset = fmtChunkOffset + 8 + fmtChunkSize; while (dataOffset < buffer.length) { const chunkId = buffer.toString('ascii', dataOffset, dataOffset + 4); if (chunkId === 'data') break; const chunkSize = buffer.readUInt32LE(dataOffset + 4); dataOffset += 8 + chunkSize; } return { header: buffer.slice(0, dataOffset), // 完整文件头(含所有非data块) headerLength: dataOffset, audioFormat, numChannels, sampleRate, byteRate, blockAlign, bitsPerSample }; } // 根据原始头和分片数据大小,生成新的WAV文件头 function generateNewWavHeader(originalHeader, dataSize) { const newHeader = Buffer.from(originalHeader.header); // 更新RIFF块总大小:总文件大小 - 8(RIFF块大小字段规则) const totalFileSize = originalHeader.headerLength + dataSize; newHeader.writeUInt32LE(totalFileSize - 8, 4); // 更新data块的大小 newHeader.writeUInt32LE(dataSize, originalHeader.headerLength + 4); return newHeader; } // 配置参数 const inputFile = "bigFile.wav"; const chunkDataThreshold = 500000; // 每个分片的音频数据最大字节数 const baseFileName = `${Date.now()}_audio_`; // 全局状态变量 let wavHeaderInfo = null; let currentFileIndex = 0; let currentWriteStream = null; let currentDataSize = 0; const readStream = fs.createReadStream(inputFile, { highWaterMark: 64 * 1024 }); readStream.on('data', (chunk) => { if (!wavHeaderInfo) { // 第一次读取:解析WAV头并创建第一个分片文件 try { wavHeaderInfo = parseWavHeader(chunk); } catch (err) { console.error(err); readStream.destroy(); return; } const firstFileName = `${baseFileName}${currentFileIndex}.wav`; currentWriteStream = fs.createWriteStream(firstFileName); currentWriteStream.write(wavHeaderInfo.header); currentWriteStream.on('error', console.error); // 写入头之后的音频数据 const audioData = chunk.slice(wavHeaderInfo.headerLength); currentDataSize += audioData.length; currentWriteStream.write(audioData); // 检查是否达到分片阈值 if (currentDataSize >= chunkDataThreshold) finishCurrentFile(); } else { // 处理后续音频数据块 const remainingSpace = chunkDataThreshold - currentDataSize; if (chunk.length <= remainingSpace) { // 当前分片还能容纳,直接写入 currentDataSize += chunk.length; currentWriteStream.write(chunk); } else { // 先写满当前分片 const part1 = chunk.slice(0, remainingSpace); currentWriteStream.write(part1); finishCurrentFile(); // 处理剩余数据 let remainingData = chunk.slice(remainingSpace); while (remainingData.length > chunkDataThreshold) { const part = remainingData.slice(0, chunkDataThreshold); createNewFileAndWrite(part); remainingData = remainingData.slice(chunkDataThreshold); } if (remainingData.length > 0) createNewFileAndWrite(remainingData); } } }); readStream.on('end', () => { if (currentWriteStream) { currentWriteStream.end(() => console.log('所有分片处理完成')); } }); readStream.on('error', console.error); // 完成当前分片的写入并修正文件头 function finishCurrentFile() { const currentFileName = `${baseFileName}${currentFileIndex}.wav`; currentWriteStream.end(() => { // 读取文件并更新头中的大小字段 fs.readFile(currentFileName, (err, fileBuffer) => { if (err) return console.error(err); const newHeader = generateNewWavHeader(wavHeaderInfo, currentDataSize); const newFileBuffer = Buffer.concat([newHeader, fileBuffer.slice(wavHeaderInfo.headerLength)]); fs.writeFile(currentFileName, newFileBuffer, err => err && console.error(err)); }); }); currentFileIndex++; currentDataSize = 0; currentWriteStream = null; } // 创建新分片文件并写入数据 function createNewFileAndWrite(data) { const newFileName = `${baseFileName}${currentFileIndex}.wav`; currentWriteStream = fs.createWriteStream(newFileName); currentWriteStream.on('error', console.error); // 写入新的WAV头和音频数据 const newHeader = generateNewWavHeader(wavHeaderInfo, data.length); currentWriteStream.write(newHeader); currentWriteStream.write(data); currentDataSize = data.length; currentFileIndex++; }
关键说明
- 若要按时长分片,可根据音频参数计算字节阈值:比如16位双声道44.1kHz的音频,每秒字节数为
44100 * 2 * 2 = 176400,10分钟的阈值就是176400 * 600 = 105840000 - 处理直播流时,逻辑类似:先接收并解析初始的WAV头,后续按阈值分片写入头+数据即可
内容的提问来源于stack exchange,提问作者Imran Rafiq
相关产品推荐
相关产品推荐

