音频处理之文件编码与解码(一)

1,230 阅读4分钟

前言

本文讲述的编码指的是PCM编码,PCM 即脉冲编码调制 (Pulse Code Modulation)。在PCM 过程中,将输入的模拟信号进行采样、量化和编码,用二进制进行编码的数来代表模拟信号的幅度 ;接收端再将这些编码还原为原来的模拟信号。即数字音频的 A/D 转换包括三个过程 :采样,量化,编码。

翻译成人话就是从一段录音里按一定的采样频率取到一组二进制数据(模拟信号的幅度[-1,1]),按采样位数(8位或者16位)编码成对应位数的另一组二进制数据,如果想生成文件,可以在数据内容前加文件头标识(比如WAV),或者是将信号数据优化压缩过的文件格式(比如MP3、OGG等)。

而解码就是编码的逆操作,将这些编码的数据还原回原来的模拟信号的数据,让扬声器能够识别播放。

编码PCM

在我们得到录音数据之后,编码成可以播放的文件,PCM格式是最直接最简单的。

录音采集

录音的方式我大概说一下就好了,主要目的是拿到录音后的所有采样数据,下面是我录音的主要代码:

// 开始录音 =========================================
recordStart = function (isUnClaer) {
  var _this = this;
  navigator.mediaDevices.getUserMedia({
    audio: true
  }).then(function (stream) {
    // audioInput表示音频源节点
    // stream是通过navigator.getUserMedia获取的外部(如麦克风)stream音频输出,对于这就是输入
    _this.audioInput = _this.AudioContext.createMediaStreamSource(stream);
  }, function (error) {
    // 抛出异常
    Recorder.throwError(error.name + " : " + error.message);
  }).then(function () {
    // audioInput 为声音源,连接到处理节点 recorder
    _this.audioInput.connect(_this.recorder);
    // 处理节点 recorder 连接到扬声器
    _this.recorder.connect(_this.context.destination);
  });
};




// this.recorder 处理节点=========================================
var createScript = this.AudioContext.createScriptProcessor || this.context.createJavaScriptNode;
// 创建 scriptProcessor 音频节点
this.recorder = createScript.apply(this.context, [4096, 1, 1]);
// 临时存储录音数据
_this.buffer = []
_this.size = 0
// 每4096次采样接收一次数据
this.recorder.onaudioprocess = function (e) {
  var inputData = e.inputBuffer.getChannelData(0);
  var outputData = e.outputBuffer.getChannelData(0);

  // 如果inputData赋值给outputData,就会边录边播,因为上面有句代码是连接了扬声器
  // outputData = inputData;

  // 把接收的数据存起来
  _this.buffer.push(new Float32Array(inputData));
  _this.size += inputData.length;
}



// 停止录音 =========================================
recordStop = function() {
  // 断开连接
  this.audioInput && this.audioInput.disconnect();
  this.recorder.disconnect();
  // 将this.buffer扁平为一维数组
  var data = new Float32Array(this.size), offset = 0; // 偏移量计算
  // 将二维数据,转成一维数据
  for (var i = 0; i < this.buffer.length; i++) {
    data.set(this.buffer[i], offset);
    offset += this.buffer[i].length;
  }
  // 得到所有录音采样数据的一维数组
  return data
}

编码

得到所有采样数据后就可以进行PCM编码了:

/**
* PCM编码
*
* @static
* @param {float32array} bytes      采样数据
* @param {number} sampleBits       采样位数
* @returns {dataview}              pcm二进制数据
*/
encodePCM = function (bytes, sampleBits) {
		var offset = 0, dataLength = bytes.length * (sampleBits / 8), buffer = new ArrayBuffer(dataLength), data = new DataView(buffer);
  // 写入采样数据
  if (sampleBits === 8) {
    for (var i = 0; i < bytes.length; i++ , offset++) {
      // 范围[-1, 1]
      var s = Math.max(-1, Math.min(1, bytes[i]));
      // 8位采样位划分成2^8=256份,它的范围是0-255; 
      // 对于8位的话,负数*128,正数*127,然后整体向上平移128(+128),即可得到[0,255]范围的数据。
      var val = s < 0 ? s * 128 : s * 127;
      val = +val + 128;
      data.setInt8(offset, val);
    }
  }
  else {
    for (var i = 0; i < bytes.length; i++ , offset += 2) {
      var s = Math.max(-1, Math.min(1, bytes[i]));
      // 16位的划分的是2^16=65536份,范围是-32768到32767
      // 因为我们收集的数据范围在[-1,1],那么你想转换成16位的话,只需要对负数*32768,对正数*32767,即可得到范围在[-32768,32767]的数据。
      data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
    }
  }
  return data;
};

生成文件

编码完成后是DataView对象,你可以直接调用new Blob(data)生成可直接在浏览器执行的文件格式或者触发下载:

/**
* 触发下载
*
* @param {string} [name='recorder']    重命名的名字
*/
download(data, filename='recorder', type='pcm') {
  const dataBlob = new Blob([data]);
  try {
    var oA = document.createElement('a');
    oA.href = window.URL.createObjectURL(blob);
    oA.download = filename + '.' + type;
    oA.click();
  }
  catch (e) {
    Recorder.throwError(e);
  }
}

这样就完成了从录音->采集->编码->生成文件的完整流程,接下来要讲解码了

解码PCM

解码过程就像开头说的就是编码的逆操作,我们按获取文件二进制数据->解码的流程走。

假设我们有一个已经生成好的PCM文件,我们可以通过axios直接获取到文件的arraybuffer二进制数据

获取文件二进制数据

// axios请求获取文件二进制
axios.get('pcm文件地址', {
  responseType: 'arraybuffer',
}).then((res) => {
  decodePCM(res.data)
}

解码

/**
   * 解码PCM
   *
   * @static
   * @param {ArrayBuffer}}              文件二进制数据
   * @param {Number} sampleBits         输出采样位数
   * @returns {Float32Array}            解码后的音频数据
   */
decodePCM = function(arraybuffer, sampleBits=16) {
  var dataview = new DataView(arraybuffer)
  var offset = 0;
  var data;
  if (sampleBits === 8) {
    data = new Float32Array(dataview.byteLength)
    for (var i = 0; i < data.length; i++ , offset++) {
      // 8位采样位划分成2^8=256份,它的范围是0-255; 
      // 将得到的[0,255]整数整体向下平移128,即可得到[-128, 127]的整数
      var s = Math.max(0, Math.min(255, bytes.getInt8(offset, true))) - 128;
      // 再解析成[-1, 1]的数据
      data[i] =  s < 0 ? s / 128 : s / 127;
    }
  }
  else {
    data = new Float32Array(dataview.byteLength / 2)
    for (var i = 0; i < data.length; i++, offset += 2) {
      // 在encodePCM的时候是setInt16,所以用getInt16的时候取到的是整数没有小数点,这里会有些误差
      var s = Math.max(-32768, Math.min(32767, bytes.getInt16(offset, true)));
      // 16位的划分的是2^16=65536份,范围是-32768到32767
      // 转换为[-1, 1]的数据
      data[i] = s < 0 ? s / 0x8000 : s / 0x7FFF
    }
  }
  return data
}

解码之后

解码之后能干什么呢? 解码之后就可以做更底层的音频处理(剪辑、变音变调、合成、混音、压缩),可以参考这两篇博客《音频处理之音频文件拼接录音及裁剪》、《音频处理之变音变调》,除了音频处理,还可以做格式转换,再编码,比如PCM是最原始的数据生成的文件格式,我们可以解码再编码成MP3格式,文件大小比PCM可以小十倍,音质还不会损失很多。

由于篇幅有点长,还有WAV编码解码和MP3编码就分到下一篇讲,第二篇传送门:《音频处理之文件编码与解码(二)