- Published on
pcm 数据编码
- Authors
- Name
- Magikarp
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 编码占的空间都比较大,但是他未经过任何编码和压缩处理,是种无损压缩的格式,也能得到相当好的音质效果。
采样频率一般共分为 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,也就是一个字节。
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 流数据了。