3、Android AudioRecord与音频采集:AudioRecord基础用法、音频数据格式与缓冲区、音频采集实战
音频采集,是音视频传输的起点。你想想看,如果源头的声音数据都拿不好,后面编码、推流全是白搭。这一章,我们就来啃下 Android 上最核心的音频采集 API——AudioRecord。
说实话,我早年刚接触音视频开发时,也踩过不少 AudioRecord 的坑。比如缓冲区设太小导致丢帧,或者采样率不匹配导致声音变调。嗯,今天咱们就把这些经验一次性说透。
3.1 AudioRecord 是什么?
AudioRecord 是 Android 提供的原生音频采集类。它直接从硬件麦克风读取 PCM 裸数据,不经过任何编码。说白了,它给你的就是最原始的声音样本。
为什么不用 MediaRecorder?因为 MediaRecorder 直接输出编码后的文件(如 MP3、AAC),你拿不到原始数据。而做 RTMP 推流,我们需要自己控制编码和封装,所以必须用 AudioRecord。
3.2 AudioRecord 基础用法
使用 AudioRecord 其实就三步:初始化、启动采集、读取数据。但每一步都有讲究。
3.2.1 初始化参数
构造函数长这样:
AudioRecord(
int audioSource, // 音频源,一般用 MediaRecorder.AudioSource.MIC
int sampleRateInHz, // 采样率,如 44100、48000
int channelConfig, // 声道配置,如 CHANNEL_IN_MONO
int audioFormat, // 音频格式,如 ENCODING_PCM_16BIT
int bufferSizeInBytes // 缓冲区大小
)
这里我重点说一下几个参数的选择:
- audioSource:大部分场景用
MIC就行。如果需要消除回声,可以用VOICE_COMMUNICATION,它会启用内置的 AEC(声学回声消除)。 - sampleRateInHz:44100Hz 是 CD 音质,48000Hz 是专业级。我个人习惯用 44100,兼容性最好。但如果你要做高清语音,48000 更合适。
- channelConfig:手机麦克风通常是单声道,所以用
CHANNEL_IN_MONO。双声道采集需要两个麦克风,大部分手机不支持。 - audioFormat:
ENCODING_PCM_16BIT是主流,每个采样点占 2 字节。16 位精度足够,再高意义不大。
3.2.2 缓冲区大小
缓冲区大小不能随便设。Android 提供了一个工具方法:
int minBufferSize = AudioRecord.getMinBufferSize(
44100,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
);
这个 getMinBufferSize 返回的是系统建议的最小缓冲区。你实际使用时,可以取这个值的 2 倍或 4 倍。为什么?
我曾经在一个低端设备上遇到过问题:缓冲区设得太小,导致读取线程频繁阻塞,最终丢帧。后来我把缓冲区放大到 4 倍,问题就解决了。说白了,缓冲区大一点,抗抖动能力更强,但延迟也会增加。你需要根据场景权衡。
getMinBufferSize,AudioRecord 初始化会直接抛异常。所以千万别自作聪明设一个很小的值。
3.3 音频数据格式:PCM 详解
AudioRecord 输出的 PCM 数据,本质上就是一堆字节。但你要理解它的结构,才能正确处理。
以 16 位单声道为例:
- 每个采样点用 2 个字节表示(小端序)
- 每秒有 44100 个采样点
- 所以每秒数据量 = 44100 × 2 = 88200 字节
如果是双声道 16 位:
- 每个采样点用 4 个字节(左声道 2 字节 + 右声道 2 字节)
- 每秒数据量 = 44100 × 4 = 176400 字节
你想想看,如果缓冲区大小是 4096 字节,那么一次 read() 最多能读到 4096 字节,也就是约 46 毫秒的音频数据(单声道 44100Hz)。这个时间窗口很重要,后面做实时推流时会用到。
3.4 音频采集实战:完整代码
下面是一个完整的 AudioRecord 采集示例。我把它封装成了一个工具类,方便复用。
public class AudioCapturer {
private AudioRecord audioRecord;
private boolean isRecording = false;
private int bufferSize;
private int sampleRate = 44100;
public void startCapture() {
bufferSize = AudioRecord.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
) * 2; // 我习惯用 2 倍缓冲区
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
);
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
throw new RuntimeException("AudioRecord 初始化失败");
}
audioRecord.startRecording();
isRecording = true;
new Thread(new Runnable() {
@Override
public void run() {
byte[] audioData = new byte[bufferSize];
while (isRecording) {
int bytesRead = audioRecord.read(audioData, 0, bufferSize);
if (bytesRead > 0) {
// 这里拿到 PCM 数据,可以送去编码或推流
onAudioData(audioData, bytesRead);
}
}
}
}).start();
}
public void stopCapture() {
isRecording = false;
if (audioRecord != null) {
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
}
private void onAudioData(byte[] data, int size) {
// 回调给上层处理
}
}
这段代码有几个关键点:
- 读取操作放在子线程,不能阻塞主线程
read()是阻塞的,直到有数据或超时- 每次读取后要判断
bytesRead,可能小于缓冲区大小
read() 返回 -1 的情况,原因是麦克风权限没给。所以记得在 AndroidManifest 中添加 RECORD_AUDIO 权限,并且在运行时动态申请。
3.5 知识体系图
下面这张图展示了 AudioRecord 音频采集的完整流程和关键参数:
3.6 常见问题与避坑
最后,我总结几个实际开发中容易踩的坑:
- 权限问题:Android 6.0 以上需要动态申请
RECORD_AUDIO权限。我见过不少新手在真机上跑不起来,就是因为忘了这个。 - 设备兼容性:有些低端设备不支持 48000Hz 采样率。建议先用
getMinBufferSize测试一下,如果返回负数,说明不支持。 - 内存泄漏:AudioRecord 使用完后一定要
release(),否则会一直占用麦克风资源。最好在onPause()或onDestroy()中释放。 - 数据对齐:PCM 数据是连续的字节流,没有帧边界。如果你要分片处理,必须自己维护缓冲区,确保数据完整。
read() 返回后直接修改 audioData 数组的内容,除非你已经拷贝了一份。因为下一次 read() 会覆盖原数据。
好了,这一章的内容就到这里。AudioRecord 虽然基础,但它是整个音视频传输的基石。把这一章吃透,后面编码和推流才能走得稳。
公众号:蓝海资料掘金营,微信deep3321