Published on

pcm 数据编码

Authors
  • avatar
    Name
    Magikarp
    Twitter

pcm 编码

PCM(Pulse Code Modulation),脉冲编码调制,是一种模拟信号的数字化方法。

模拟信号数字化需要经过三个过程,即抽样、量化和编码,这儿就不详细介绍了。我们来看下 pcm 编码中几个比较重要的概念。

采样率

又可以称为采样频率,我们可以通过 sampleRate 得到输入的采样率:

var context = new (window.AudioContext || window.webkitAudioContext)()
console.log(context.sampleRate) // 输入音频采样率(HZ)  48000

即横向坐标(可以理解为 x 轴)在单位时间内采集了 48000(我的 chrome 输出这么多)次样本。从维基百科上偷张图来,加深下理解(先不管 y 轴)。因为采集的样本多,所以未处理的 pcm 编码占的空间都比较大,但是他未经过任何编码和压缩处理,是种无损压缩的格式,也能得到相当好的音质效果。

Untitled

采样频率一般共分为 22.05KHz、44.1KHz、48KHz 三个等级,采样频率越高,音质越精确。正常人听觉的频率范围大约在 20Hz~20kHz 之间,根据奈奎斯特采样理论(只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音),为了保证声音不失真,采样频率应该在 40kHz 左右。所以对于高于 48KHz 的采样频率人耳已无法辨别出来了,所以并没有什么实用价值。

采样位数

采样位数可以用来描述连续变化的幅度值。 依旧是前面盗来的图,我们来关注下 y 轴上的信息,y 轴共分为了 16 份,也就是 2 的 4 次方,即使用 4bit 就可以存储这些信息了,这儿的采样位数是 4。将这些点连起来,是不是并不能完全(较好)地恢复原有的曲线?如果 16 份恢复地不够完美,那么 32,64 更多份数是不是就能恢复这波形图了呢?

一般情况下,采样位数是 8 或 16,8 位的可以划分为 2^8=256 份,范围是 0-255。16 位的可以划分位 2^16=65536 份,范围是-32768 到 32767。 到这就是量化了,采样位数这个数值越大,解析度就越高,录制和回放的声音就越真实。

可以打印看下先前我们搜集到的 pcm 数据,这些数据都是在[-1, 1]之间的,要转成 8 位的形式,负数128,正数127,然后整体向上平移 128(+128),即可得到[0,255]范围的数据。16 位的数据,只需要对负数32768,对正数32767 即可。 注:实际存储都是二进制形式的。

声道

声道有单声道和双声道的区别,当然也有 8 位和 16 位之分,

每个采样占用8bit,也就是一个字节。

每个采样占用 8bit,也就是一个字节。

16位的就是double了。

16 位的就是 double 了。

双声道的话,在 audioprocess 事件种就要特殊处理下,可以用 e.inputBuffer.getChannelData(1) 取第二个声道的数据,当然,createScriptProcessor 方法中的声道数也要设置成 2。

注:双声道取得的数据不能直接使用,需要拼接成 LRLRLR 这种格式。

js 实现转化

前面讲的差不多了,紧接着前一篇获取到[-1, 1]的 pcm 的代码。首先,在 audioprocess 事件中,是用 inputData 数组收集的,即是二维数组,为了后边处理方便,现处理成一位数组。

收集数据简单处理

export const connectFloat32Array = (buffer: Float32Array[]): Float32Array => {
  // 获取所有长度,进行初始化
  const length = buffer.reduce<number>((length, item) => {
    return length + item.length
  }, 0)

  const connectArray = new Float32Array(length)

  // 进行 赋值操作,set 一次的偏移量都为上次的 length
  buffer.reduce<number>((offset, item) => {
    connectArray.set(item, offset)
    return offset + item.length
  }, 0)
  return connectArray
}

size 和 inputData 都是全局变量,当然这样写不好,这只是 demo。因为 audioprocess 返回的也是 Float32Array 类型的数据,故此处接收容器也是 Float32Array 类型,关于该类型的详细介绍可以查看这篇文章:前端二进制学习(三)

注:比如阿里云的语音识别,要求是 16000 采样率的 pcm 音频,所以,此处会有压缩的过程,由于 48k 与 16k 是三倍关系,即三个样本中要删除一个样本,利用循环过滤就可以了,不理解的可以查看recorder中的 compress 函数,我这没有做压缩采样样本。

编码

接下来就是编码了,我们这默认设置采样位数为 16 位。

/**
 * 输出采样数位
 */
const OUTPUT_SAMPLE_BITS = 16

由于 8 位刚好一字节,此处若是 16 位的采样位数,需要两倍的大小。所以,在取 arraybuffer 时,需要处理下,

let bytes = decompress(),
  sampleBits = oututSampleBits,
  dataLength = bytes.length * (sampleBits / 8),
  buffer = new ArrayBuffer(dataLength),
  data = new DataView(buffer)

依据前面提及的算法,当采样位数是 8 位的,只要将负数128正数127,然后整体向上平移 128(+128)就可以了。

export const OUTPUT_SAMPLE_BITS = 8

export const transformFloat32ArrayToPCM = (data: Float32Array): DataView => {
  const dataLength = data.length * (OUTPUT_SAMPLE_BITS / 8)
  const buffer = new ArrayBuffer(dataLength)
  const result = new DataView(buffer)

  // 当前只实现 8 位,对于其他暂时实现
  for (let i = 0; i < data.length; i++) {
    // 范围\[-1, 1\]
    const s = Math.max(-1, Math.min(1, data[i]))
    // 对于8位的话,负数\*128,正数\*127,然后整体向上平移128(+128),即可得到\[0,255\]范围的数据。
    const val = (s < 0 ? s * 128 : s * 127) + 128
    result.setInt8(i, val)
  }

  return result
}

当时 16 位的,只需要对负数32768,对正数32767 就行了,记得 offset 取 2,并使用 setInt16,第三个参数要置为 true,牵扯到大端和小端字节序。

for (var i = 0; i < bytes.length; i++, offset += 2) {
    var s = Math.max(-1, Math.min(1, bytes\[i\]));
    // 16位直接乘就行了
    data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}

此时,data 的数据就是处理后的 pcm 流数据了。

参考链接

https://github.com/2fps/recorder