计算机只能处理0和1,即离散值。音频这种模拟信号得转换成离散值才能被计算机处理。这个转化过程称为模拟信号数字化,分为三个步骤:
1. 采样
采样是对连续信号在时间上进行离散,即按照特定的时间间隔在原始的模拟信号上逐点采集瞬时值。采样的可视化效果如下图所示:
原本连续的曲线被一根根离散的竖直线条代替。这些线条越密集,将它们相连后形成的曲线就越接近原始模拟信号。
物理中用采样频率来表示采样的密集程度,即每秒采样次数(采样数/秒),它用赫兹(Hz)表示
2. 量化
虽然连续值已被采样成若干离散值,但每个离散值的取值可能有无限多个。为了给每一个离散值都对应一个数字码,必须将无限种取值转化为有限种取值(对于只能处理二进制的计算机来说,取值的可能数应该是2的倍数)。物理中把这种通过四舍五入分级取整的方法称为量化。量化后的数字信号如下图所示:
量化后的音频变得死板有棱角,就好像人类和机器人的差别。
3. 编码
模拟信号经过采样变成离散值,每一个离散值经过量化都对应一个二进制,将这些二进制按时间序列组合在一起就称为编码。
经过采样量化编码形成的是音频的原始数据,这种原始数据格式称为PCM(Pulse Code Modulation),即是采样量化编码的英文表示。
.pcm 后缀的文件是非常非常大的,这增加了存储和网络传输的成本。遂 PCM 这样原始的无损音频还得经过一次压缩编码。
音频存在冗余信息,才能被压缩。比如人耳能辨识的声音频率范围为20Hz~20KHz,该频率以外的声音都是冗余信息。再比如强弱信号同时出现,强弱差距过大,以至于弱信号完全被掩盖,弱信号就是冗余信息。
音频有很多压缩编码的格式,以下是 Android 官方支持的格式:
在移动端最为常用的格式是 AAC,即 Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。它采用了更加高效的编码方式,使得它拥有和 MP3 相当的音质及更小的体积。
压缩编码由两种执行方式,交由 GPU 或是 CPU 执行,前者称为硬编码后者称为软编码,硬编码速度快,但兼容差,会存在编码失败的情况。软编码速度慢,但兼容性好。
录制 PCM 音频
Android 提供了两种录制音频的方式:1. MediaRecorder
2. AudioRecord
如果没有优化音频的需求,完全可以使用 MediaRecorder 直接输出 AAC 格式的音频。
而音频优化,比如降噪,增益算法都是基于 PCM 格式的。这就不得不使用 AudioRecord 来录制音频。
构建 AudioRecord 对象
AudioRecord 的构造函数包含 6 个参数:
- 音频源:表示从哪里采集音频,通常是麦克风。
- 采样频率:即每秒钟采用次数,44100 Hz是目前所有安卓设备都支持的采样频率。
- 声道数:表示声音由几个声道组成,单声道是目前所有安卓设备都支持的声道数。
- 量化精度:表示采用多少位二进制来表达一次量化的离散值,通常用 16 位。
- 缓冲区大小:表示在内存开辟一块多大的缓冲区用于存放硬件采集的音频数据。
构建 AudioRecord 的模板代码如下:
const val SOURCE = MediaRecorder.AudioSource.MIC //通过麦克风采集音频
const val SAMPLE_RATE = 44100 // 采样频率为 44100 Hz
const val CHANNEL_IN_MONO = AudioFormat.CHANNEL_IN_MONO // 单声道
const val ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT //量化精度为 16 位
var bufferSize: Int = 0 // 音频缓冲区大小
val audioRecord by lazy {
// 计算缓冲区大小
bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
// 构建 AudioRecord 实例
AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize)
}
将构建 AudioRecord 的参数都常量化,以便在其他地方引用。其中缓冲区大小是通过AudioRecord.getMinBufferSize()
动态计算的,计算的依据是采样平率、声道数、量化精度。
读取音频数据写入文件
有了 AudioRecord 实例,就可以调用它的方法从硬件设备中读取音频数据了。它提供了 3 个方法来控制音频数据的读取,分别是开始录制startRecording()
、读一批音频数据read()
、停止录制stop()
,这 3 个方法通常用下面的模板来组合:
audioRecord.startRecording()
while(是否继续录制){ audioRecord.read() }
audioRecord.stop()
音频数据的大小以字节为单位,音频数据的读取是一批一批进行的,所以需要一个 while 循环持续不断地读取