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

如何通过Node.js将音频直播分片生成多个音频文件

解决WAV音频分片后后续文件无法播放的问题

你遇到的问题核心是WAV文件的结构特性:WAV文件必须包含文件头(记录音频格式、声道数、采样率等关键解析信息)+ 音频数据块。你的代码里第一个文件从原文件开头写入,自带完整的WAV头,所以能正常播放;但后续分片只写入了原始音频数据,没有对应的WAV文件头,播放器无法识别数据格式,自然无法播放。

解决方案思路

  1. 解析原始WAV头:先读取原文件的WAV头,提取格式参数(声道数、采样率等)和头的长度
  2. 生成分片专属WAV头:每个新分片文件,先写入修改后的WAV头(根据当前分片的数据大小更新头里的文件总大小、数据块大小字段)
  3. 按阈值分片写入:累计音频数据量,达到设定的大小/时长阈值后,关闭当前文件,创建新文件并重复上述步骤

修改后的代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 12:24:54