音频之旅(二):音频属性

1,151 阅读5分钟

上次只是草草的完成了任务,但秉承着知其所以然的态度还是要继续深入下。

音频的几个重要参数: 采样频率,采样位数,采样声道,码率。逐一了解。在此之前我们要了解下电脑录音的本质:电脑中的声音文件是用数字0和1来表示的,所以在电脑上录音的本质就是把模拟声音信号转换成数字信号,在播放时则是把数字信号还原成模拟声音信号输出。

**对于一个没有什么计算机基础知识的我来说,了解这个计算机底层的编码简直是噩梦。

1. 采样频率

1s内设备采集音频数据的次数。每一次叫做一个采样点。不同的设备有固定的采样频率,在采集时不能修改采样频率。采样频率越高,声音的质量也就越好,声音的还原也就越真实,但同时它占的资源比较多。

// web录音 通过AudioContext查看采样频率
let audioCtx = new AudioContext()
audioCtx.sampleRate // 获取采样频率

在后期处理音频数据的过程中,可以修改采样频率获得我们 想要的采样频率的数据。比如当前设备的采样频率为48kHz,但是我们需要16kHz的数据,如何做?

先分析下: 这个问题的本质也就是采集的样本中每秒有48k个采样点,我们只需要其中16k个就够了。处理中只要删除一些采样点就ok了。再反向思考下,如果我们采集的是16kHZ频率的数据,要转成48kHZ的数据呢?就需要在每个采样点中间插入新的采样点,采样点的数据可以采样线性插值法计算。

先实现降低采样率的代码

// buffer为音频流数据 rate为采样率
export function downsampleBuffer(buffer, rate) {
  let audioContext = new AudioContext()
  let sampleRate = audioContext.sampleRate; // 当前设备采样频率
  if (rate == sampleRate) {
    return buffer;
  }
  if (rate > sampleRate) {
    throw "downsampling rate show be smaller than original sample rate";
  }
  // 获取降低采样的倍率
  var sampleRateRatio = sampleRate / rate;
  // 计算新的buffer长度 也就是我们删减采样节点后的长度
  var newLength = Math.round(buffer.length / sampleRateRatio);
  // 创建一个接收容器 (我们获取的pcm流为 Float32Array的数组)
  var result = new Float32Array(newLength);
  var offsetResult = 0; // 控制循环次数
  var offsetBuffer = 0; // 指针
  /*
  * 在具体如何删除节点的细节上要做个分析,还拿48k转为16k为例:
  * 比如这是一个频率为48k的采样数据,数组元素的本质是一个32位的浮点数这里为了方便采用整数代* 替 [1,2,3,4,5,6,7,8...], 
  * 我们要降低采样频率,就是减少数组元素,但怎么减少?减少那个元素合理?
  * 其实都不合理: 为了最大程度的跟原数组保持紧接,我们采用平均值
  * 48/16 = 3; 数组中每3个元素我们取平均值,生成一个元素
  * 1,2,3取平均值生成2; 4,5,6取平均值生成4; 新数组为[2,5,...]
  */
  while (offsetResult < result.length) {
    var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
    var accum = 0, count = 0;
    for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
      accum += buffer[i];
      count++;
    }
    result[offsetResult] = accum / count;
    offsetResult++;
    offsetBuffer = nextOffsetBuffer;
  }
  return result;
}

提高采用率的插值算法参考这里

2. 采样位数

也叫采样位深或采样深度或采样位宽。声卡的位是指声卡在采集和播放声音文件时所使用数字声音信号的二进制位数。反映了数字声音信号对输入声音信号描述的准确程度,位数越高声音信号的准确度越高。

在此引申一个计算机存储最小的计量单位 ‘字节’,byte。 这些表达式应该很熟悉:1k = 1024B; 1M = 1024k;

计算机最底层的存储都是二进制(0或1),那么1B的容量可以存储多少位二进制数字呢?又要引入一个概念bit,计算机中的最小数据单位:说白了就是0或者1。

1B = 1个英文字母 // 1B可以存储1个英文字母
2B = 1个汉字 // 2B可以存储1个汉字
1B = 8bit // 1B可以存储一个8位的二进制数据

那我们需要采样位数为16的数据本质就是: 每一个采样点采集2B大小的数据,也就是2个字节。再直白点就是每个采样点采集1个16位数的二进制数据。

到此就可以根据采集频率和位数计算每秒采集的数据大小了。(这里暂考虑单声道采样)

// 比如采样频率为 16KHZ 采样位数为16 
dataSize = 16000 * 2B / 1024 = 31.25k // 1s内采集的数据大小
dataSize = 31.25k * 60 / 1024 = 1.83M // 1分钟内采集的数据大小

人生不如意事十之八九,我们设备采集的音频采样是24bit 也就是 3Byte大小的二进制,而我们需要的是16bit 2Byte大小二进制。又该如何转呢?

分析出问题的本质,就容易找到解决问题的方法。

拿1个采样点分析: 当前采样点是一个3Byte大小的数据,需要一个2Byte大小的数据。很明显减少每个采样点数据的大小。再直白些:将24位的二进制数据缩减为16位的二进制数据。

如此可以得到如下:

  1. 高位位数向低位位数转换时:每个采样点中减少n个字节,并且为了保证原有的音频数据不失真的情况下,我们只需要把低位去除即可。
  2. 低位位数向高位位数转换时:要在每个采样点中增加n个字节,并且为了保证原有的音频数据不失真的情况下,我们只需要在高位补0即可。
// 以转成 采样位数为16 为例 samples为传入的buffer样本
export function floatTo16BitPCM(samples) {
  let offset = 0
  var buffer = new ArrayBuffer(samples.length * 2)
  var view = new DataView(buffer)

  for (var i = 0; i < samples.length; i++ , offset += 2) {
    var s = Math.max(-1, Math.min(1, samples[i]));
    view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
  }

  return view
}

...未完待续。