js 转换wav音频 支持多声道

102 阅读3分钟

网上看的都是双声道转换,改造了一下,支持更多声道

改造自 juejin.cn/post/712797…

const audioCtx = new window.AudioContext()
      const res = await fetch(audioRef.current?.src)
      const arrayBuffer = await res.arrayBuffer()
      audioCtx.decodeAudioData(
        arrayBuffer,
        function (buffer) {
          const audioBuffer = buffer
          const wav = audioBufferToWav(audioBuffer, {
            outputRate: audioBuffer.sampleRate,
            bitDepth: 16,
            numChannels: audioBuffer.numberOfChannels,
          })
          const blob = new Blob([wav], { type: 'audio/wav' })
          genFile(blob, '.wav')
        },
        function (err) {
          console.error(err)
        },
      )
// toWav.js
/** audioBuffer 转wav
 * @param {audioBuffer} samples 音频样本
 * @param {Object} opt={} 配置项 outputRate采样率 bitDepth位深 numChannels声道数
 */
export function audioBufferToWav(samples: any, opt: any = {}) {
  const outputRate = opt.outputRate || 16000 // 指定输出采样率
  const bitDepth = opt.bitDepth || 16 // 指定位数
  const numChannels = opt.numChannels || 2 // 指定声道数
  const times = (samples.sampleRate / outputRate) >> 0 // 计算压缩率
  // 采样率压缩核心实现在这一步
  const interleaved = interleave(times, samples, opt.numChannels)
  return encodeWAV(interleaved, outputRate, bitDepth, numChannels)
}

/** 压缩采样率
 * @params {Number} times 压缩率
 * @params {Float32Array} inputL 左声道
 * @params {Float32Array} inputR 右声道
 */
function interleave(times: any, samples: any, channelsNum: any) {
  let length = 0
  for (let i = 0; i < channelsNum; i++) {
    length = length + samples.getChannelData(i).length
  }
  length = (length / times) >> 0

  const result = new Float32Array(length)
  let index = 0,
    inputIndex = 0
  while (index < length) {
    for (let i = 0; i < channelsNum; i++) {
      result[index++] = samples.getChannelData(i)[inputIndex]
    }

    inputIndex += times // 每次跳过压缩率的倍数
  }
  return result
}

/** buffer转wav
 * @params {audioBuffer} samples 样本
 * @params {Number} outputRate 采样率
 * @params {Number} bitDepth 位深 8 16 32
 * @params {Number} numChannels 声道数
 */
function encodeWAV(
  samples: any,
  outputRate: any,
  bitDepth: any,
  numChannels: any,
) {
  const bytesPerSample = bitDepth / 8 // 字节
  const blockAlign = numChannels * bytesPerSample // 采样一次占用字节数
  const len = samples.length * bytesPerSample // 样本长度
  const buffer = new ArrayBuffer(44 + len) // 有44字节是头部文件
  const view = new DataView(buffer)
  /* 资源交换文件标识符 */
  writeString(view, 0, 'RIFF')
  /* 下个地址开始到文件尾总字节数,即文件大小-8 */
  view.setUint32(4, /*32*/ 36 + len, true)
  /* WAV文件标志 */
  writeString(view, 8, 'WAVE')
  /* 波形格式标志 */
  writeString(view, 12, 'fmt ')
  /* 过滤字节,一般为 0x10 = 16 */
  view.setUint32(16, 16, true)
  /* 格式类别 (PCM形式采样数据) */
  view.setUint16(20, 1, true)
  /* 通道数 */
  view.setUint16(22, numChannels, true)
  /* 采样率,每秒样本数,表示每个通道的播放速度 */
  view.setUint32(24, outputRate, true)
  /* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
  view.setUint32(28, outputRate * blockAlign * 8, true)
  /* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
  view.setUint16(32, blockAlign, true)
  /* 每样本数据位数 */
  view.setUint16(34, bitDepth, true)
  /* 数据标识符 */
  writeString(view, 36, 'data')
  /* 采样数据总数,即数据总大小-44 */
  view.setUint32(40, len, true)
  /* 采样数据 */
  if (bitDepth === 8) floatTo8BitPCM(view, 44, samples)
  // 8位
  else if (bitDepth === 16) floatTo16BitPCM(view, 44, samples)
  // 16位
  else writeFloat32(view, 44, samples) // 32位

  return buffer
}
function writeString(view: any, offset: any, string: any) {
  for (let i = 0; i < string.length; i++) {
    view.setUint8(offset + i, string.charCodeAt(i))
  }
}
function floatTo8BitPCM(output: any, offset: any, input: any) {
  for (let i = 0; i < input.length; i++, offset++) {
    //这里只能加1了
    const s = Math.max(-1, Math.min(1, input[i]))
    let val = s < 0 ? s * 0x8000 : s * 0x7fff
    val = parseInt(255 / (65535 / (val + 32768)) + '') // 有人声的时候会有杂音
    // let val = ((s < 0 ? s * 0x8000 : s * 0x7fff) >> 8) + 128; // 有人声无人声都会有杂音
    output.setInt8(offset, val, true)
  }
}
function floatTo16BitPCM(output: any, offset: any, input: any) {
  for (let i = 0; i < input.length; i++, offset += 2) {
    //因为是int16所以占2个字节,所以偏移量是+2
    const s = Math.max(-1, Math.min(1, input[i]))
    output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
  }
}
function writeFloat32(output: any, offset: any, input: any) {
  for (let i = 0; i < input.length; i++, offset += 4) {
    output.setFloat32(offset, input[i], true)
  }
}