electron 使用node js 将多个wav文件拼接成一个wav文件
1、各个wav文件格式需一致。包括:格式format,通道数:numChannels,采样率:sampleRate,位深度:bitDepth
2、本代码,拼接的时候正确解析音频data块,确保每个音频数据块都按正确的字节对齐,避免拼接后各段音频后有一个杂音。
3、代码实现
const fs = require('fs');
/**
* 合并多个相同格式的 WAV 文件(修复杂音问题)
* @param {string[]} inputFiles - 输入 WAV 文件路径数组
* @param {string} outputFile - 输出文件路径
* @returns {Promise<string>} 合并后的文件路径
*/
async function mergeWavFiles(inputFiles, outputFile) {
return new Promise((resolve, reject) => {
try {
if (!inputFiles || inputFiles.length === 0) {
console.log("输入文件列表不能为空")
return reject(false);
}
// 读取第一个文件获取基准格式
const firstFileBuffer = fs.readFileSync(inputFiles[0]);
const firstFileInfo = parseWavHeader(firstFileBuffer);
if (!firstFileInfo.valid) {
console.log("第一个文件不是有效的 WAV 文件")
return reject(false);
}
let totalDataSize = 0;
const dataChunks = [];
const fileInfoList = [firstFileInfo];
// 处理所有文件
for (let i = 0; i < inputFiles.length; i++) {
const filePath = inputFiles[i];
if (!fs.existsSync(filePath)) {
console.log(`文件不存在: ${filePath}`)
return reject(false);
}
const fileBuffer = fs.readFileSync(filePath);
const fileInfo = parseWavHeader(fileBuffer);
if (!fileInfo.valid) {
console.log(`无效的 WAV 文件: ${filePath}`)
return reject(false);
}
// 验证格式一致性(更严格的检查)
if (i > 0 && !areFormatsCompatible(firstFileInfo, fileInfo)) {
console.log(`文件格式不兼容: ${filePath}`)
return reject(false);
}
fileInfoList.push(fileInfo);
// 提取音频数据 - 确保只提取数据部分
const audioData = fileBuffer.slice(fileInfo.dataOffset, fileInfo.dataOffset + fileInfo.dataSize);
// 确保数据字节对齐
const alignedAudioData = ensureByteAlignment(audioData, fileInfo.blockAlign);
dataChunks.push(alignedAudioData);
totalDataSize += alignedAudioData.length;
}
// 创建新的文件头
const newHeader = createWavHeader(firstFileInfo, totalDataSize);
// 写入文件
const outputStream = fs.createWriteStream(outputFile);
outputStream.write(newHeader);
for (const chunk of dataChunks) {
outputStream.write(chunk);
}
outputStream.end();
outputStream.on('finish', () => {
// console.log(`成功合并 ${inputFiles.length} 个文件,输出: ${outputFile}`);
resolve(true);
});
outputStream.on('error', (error) => {
console.log(error)
reject(false);
});
} catch (error) {
console.log(error)
reject(false);
}
});
}
/**
* 解析 WAV 文件头
*/
function parseWavHeader(buffer) {
if (buffer.length < 44) {
return { valid: false };
}
const chunkID = buffer.toString('ascii', 0, 4);
const format = buffer.toString('ascii', 8, 12);
if (chunkID !== 'RIFF' || format !== 'WAVE') {
return { valid: false };
}
// 查找 'data' 块的位置
let dataOffset = 36; // 标准 WAV 文件中 data 块通常从 36 开始
let dataSize = 0;
// 更安全地查找 data 块
for (let i = 12; i < buffer.length - 8; i++) {
if (buffer.toString('ascii', i, i + 4) === 'data') {
dataOffset = i + 8; // 跳过 'data' 标识和大小字段
dataSize = buffer.readUInt32LE(i + 4);
break;
}
}
// 如果没找到明确的 data 块,使用标准位置
if (dataSize === 0) {
dataOffset = 44;
dataSize = buffer.readUInt32LE(40);
}
const audioFormat = buffer.readUInt16LE(20);
const numChannels = buffer.readUInt16LE(22);
const sampleRate = buffer.readUInt32LE(24);
const byteRate = buffer.readUInt32LE(28);
const blockAlign = buffer.readUInt16LE(32);
const bitsPerSample = buffer.readUInt16LE(34);
return {
valid: true,
audioFormat,
numChannels,
sampleRate,
byteRate,
blockAlign,
bitsPerSample,
dataOffset,
dataSize,
fileSize: buffer.readUInt32LE(4) + 8
};
}
/**
* 检查两个 WAV 文件格式是否兼容
*/
function areFormatsCompatible(info1, info2) {
return (
info1.audioFormat === info2.audioFormat &&
info1.numChannels === info2.numChannels &&
info1.sampleRate === info2.sampleRate &&
info1.bitsPerSample === info2.bitsPerSample &&
info1.blockAlign === info2.blockAlign
);
}
/**
* 确保音频数据字节对齐
*/
function ensureByteAlignment(audioData, blockAlign) {
const remainder = audioData.length % blockAlign;
if (remainder === 0) {
return audioData;
}
// 如果不对齐,填充零字节
const alignedData = Buffer.alloc(audioData.length + (blockAlign - remainder));
audioData.copy(alignedData);
return alignedData;
}
/**
* 创建新的 WAV 文件头
*/
function createWavHeader(fileInfo, dataSize) {
const header = Buffer.alloc(44);
// RIFF header
header.write('RIFF', 0);
header.writeUInt32LE(36 + dataSize, 4); // 文件总大小 - 8
header.write('WAVE', 8);
// fmt chunk
header.write('fmt ', 12);
header.writeUInt32LE(16, 16); // fmt chunk size
header.writeUInt16LE(fileInfo.audioFormat, 20);
header.writeUInt16LE(fileInfo.numChannels, 22);
header.writeUInt32LE(fileInfo.sampleRate, 24);
header.writeUInt32LE(fileInfo.byteRate, 28);
header.writeUInt16LE(fileInfo.blockAlign, 32);
header.writeUInt16LE(fileInfo.bitsPerSample, 34);
// data chunk
header.write('data', 36);
header.writeUInt32LE(dataSize, 40);
return header;
}
/**
* 验证 WAV 文件格式的辅助函数
*/
async function validateWavFiles(filePaths) {
const results = [];
for (const filePath of filePaths) {
try {
const buffer = fs.readFileSync(filePath);
const info = parseWavHeader(buffer);
results.push({
file: filePath,
valid: info.valid,
info: info.valid ? info : null
});
} catch (error) {
results.push({
file: filePath,
valid: false,
error: error.message
});
}
}
return results;
}
// 使用示例
async function example() {
try {
const inputFiles = [
'sound1.wav',
'sound2.wav',
'sound3.wav'
];
// 首先验证所有文件
const validation = await validateWavFiles(inputFiles);
console.log('文件验证结果:', validation);
// 合并文件
const outputFile = 'merged_sounds.wav';
const result = await mergeWavFiles(inputFiles, outputFile);
console.log(`文件合并成功: ${result}`);
return result;
} catch (error) {
console.error('合并失败:', error.message);
}
}
module.exports = {
mergeWavFiles,
validateWavFiles,
parseWavHeader
};

浙公网安备 33010602011771号