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 
};
View Code

 

posted @ 2025-11-26 10:05  ziff123  阅读(2)  评论(0)    收藏  举报