4、音频采集入门:AudioRecord使用、PCM数据格式、音频缓冲区管理、采样率与声道设置
音频采集,说白了就是让手机麦克风把声音变成数字信号。很多初学者一上来就怼着摄像头折腾视频,结果发现录出来的视频没声音,或者声音断断续续、有杂音。嗯,这多半是音频采集没处理好。
我个人习惯把音频采集比作「水龙头接水」——麦克风是水源,AudioRecord 是水管,缓冲区就是水桶。水桶太小会溢出来,太大又浪费。咱们今天就把这套流程彻底讲透。
核心知识点一览:
- AudioRecord 的初始化与生命周期
- PCM 原始音频数据长什么样
- 缓冲区大小怎么算、怎么调
- 采样率和声道到底怎么选
4.1 AudioRecord 的初始化与生命周期
AudioRecord 是 Android 官方提供的音频采集 API。它不负责编码,只负责拿原始 PCM 数据。我刚开始用的时候,总以为它跟 MediaRecorder 一样能直接出文件,结果折腾半天发现只有一堆 byte 数组——嗯,这就是 PCM。
初始化 AudioRecord 需要五个参数:
int sampleRate = 44100; // 采样率,单位 Hz
int channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位位深
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioRecord audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC, // 麦克风音源
sampleRate,
channelConfig,
audioFormat,
bufferSize * 2 // 缓冲区取最小值的2倍,更稳定
);
个人经验:bufferSize 我建议取 getMinBufferSize() 返回值的 2 倍。取最小值的话,系统压力大时容易丢数据,录出来的声音会「卡顿」。我在一个直播项目里吃过这个亏,后来改成 2 倍就稳了。
AudioRecord 的生命周期很简单:
- 初始化:new 出来,检查状态是否为 STATE_INITIALIZED
- 开始采集:调用 startRecording()
- 读取数据:循环调用 read() 方法,把数据塞进缓冲区
- 停止采集:调用 stop()
- 释放资源:调用 release()
注意:startRecording() 和 stop() 必须在子线程调用,不能在主线程里搞。我曾经在面试时问过候选人这个问题,好几个人答不上来——AudioRecord 的 read() 是阻塞的,放主线程会 ANR。
4.2 PCM 数据格式——最原始的音频
PCM 就是脉冲编码调制,说白了就是麦克风把声波的振幅每隔一小段时间「拍个照」,记录成数字。你想想看,一张照片是一帧画面,PCM 的一个采样点就是一帧声音。
咱们常用的配置是 16 位位深、单声道。16 位意味着每个采样点占 2 个字节,取值范围是 -32768 到 32767。0 代表静音,正数代表声波正半周,负数代表负半周。
举个例子:采样率 44100Hz、单声道、16 位,每秒产生的数据量是:
44100 采样点/秒 × 2 字节/采样点 × 1 声道 = 88200 字节/秒 ≈ 86 KB/s
这个数据量其实不小。录一分钟就是 5MB 左右,纯原始数据,没压缩。所以后面咱们要编码成 AAC 或者 MP3,不然存本地都费劲。
避坑指南:我曾经在解析 PCM 数据时,直接把 byte 数组当成 short 数组来用,忘了 Android 是小端序。结果波形全是反的,调试了半天才发现是字节序的问题。如果你要手动处理 PCM,记得用 ByteBuffer 并设置 ByteOrder.LITTLE_ENDIAN。
4.3 音频缓冲区管理——别让水桶溢出来
缓冲区管理是音频采集里最容易出问题的地方。缓冲区太小,read() 调用频繁,CPU 开销大;缓冲区太大,延迟高,直播场景下口型对不上。
我一般遵循这个原则:
- 非实时场景(比如本地录音):缓冲区取 getMinBufferSize() 的 4 倍,稳定优先
- 实时场景(比如直播推流):缓冲区取 getMinBufferSize() 的 2 倍,延迟和稳定性折中
- 低延迟场景(比如 K 歌):缓冲区取 getMinBufferSize() 的 1 倍,但要做好丢包处理
读取数据的典型循环:
byte[] buffer = new byte[bufferSize];
while (isRecording) {
int bytesRead = audioRecord.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
// 把 buffer 前 bytesRead 个字节送去编码或推流
// 注意:bytesRead 可能小于 buffer.length,别把脏数据送出去
} else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
// 操作错误,建议重启 AudioRecord
}
}
小技巧:read() 返回 -1 表示读取失败,返回 0 表示没有数据。我习惯在每次 read() 之后检查返回值,如果连续 3 次返回 0 或负数,就主动重启 AudioRecord。这个逻辑救过我一次线上事故——用户手机麦克风被占用时,程序不会直接崩溃,而是自动恢复。
4.4 采样率与声道设置——选对了事半功倍
采样率和声道是音频采集的两个核心参数。选错了,轻则音质差,重则设备不支持直接报错。
采样率:
- 44100Hz:CD 音质,兼容性最好,几乎所有 Android 设备都支持。我建议无脑选这个。
- 48000Hz:DVD 音质,部分设备支持,常用于专业录音。
- 8000Hz / 16000Hz:电话音质,语音通话场景用,省带宽。
声道:
- CHANNEL_IN_MONO(单声道):一个麦克风采集,数据量小,处理简单。直播推流首选。
- CHANNEL_IN_STEREO(双声道):两个麦克风,有立体感,但数据量翻倍。除非你做音乐录制,否则不推荐。
| 场景 | 推荐采样率 | 推荐声道 | 位深 |
|---|---|---|---|
| 直播推流 | 44100Hz | 单声道 | 16bit |
| 语音通话 | 16000Hz | 单声道 | 16bit |
| 音乐录制 | 48000Hz | 双声道 | 16bit 或 24bit |
| 本地录音笔记 | 44100Hz | 单声道 | 16bit |
重要提醒:不是所有设备都支持 48000Hz 或双声道。初始化之前,最好用 getMinBufferSize() 测试一下,如果返回负数或抛出异常,就降级到 44100Hz 单声道。我在适配某款低端平板时就遇到过这个问题,直接崩溃了,后来加了降级逻辑才搞定。
最后说一句,音频采集是整个音视频 pipeline 的起点。起点稳了,后面编码、推流才顺。我见过太多人一上来就搞编码推流,结果音频数据本身就有问题,折腾半天找不到原因。先把 AudioRecord 玩明白,后面的事就简单了。
公众号:蓝海资料掘金营,微信deep3321