7、AudioRecord与AudioTrack:低延迟音频采集与播放、回调模式与阻塞模式的选择
说到 Android 上的音频采集和播放,AudioRecord 和 AudioTrack 这对兄弟绝对是绕不开的。很多做音频处理的朋友,一开始都会纠结:到底用哪个 API?回调模式和阻塞模式又该怎么选?
我个人习惯把这两个 API 看作是 Android 音频世界的「底层砖块」。它们不像 MediaRecorder 那样封装得那么高,但正因为底层,我们才能拿到更低的延迟和更高的控制权。说白了,做回声消除这种实时性要求高的活儿,你绕不开它们。
7.1 AudioRecord:低延迟音频采集
AudioRecord 负责从麦克风采集原始 PCM 数据。它的核心优势在于:直接读取音频缓冲区,没有中间商赚差价。
7.1.1 基本配置
创建一个 AudioRecord 实例,你需要指定几个关键参数:
int sampleRate = 16000; // 采样率,16kHz 是语音处理的黄金标准
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.VOICE_COMMUNICATION, // 通信场景专用
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
7.1.2 低延迟的关键:缓冲区大小
缓冲区大小直接影响延迟。你想想看,缓冲区越大,一次读到的数据越多,但等待的时间也越长。反之,缓冲区太小,又容易丢数据。
getMinBufferSize() 返回的是系统建议的最小值。但我建议你在此基础上适当放大一点,比如 2 倍或 4 倍。为什么?
- 太小的缓冲区会导致频繁的读写操作,CPU 开销大
- 系统调度有不确定性,缓冲区太小容易欠载(underrun)
- 回声消除算法本身也需要一定的数据块大小才能工作
7.2 AudioTrack:低延迟音频播放
AudioTrack 负责把处理后的音频数据送出去。它的配置和 AudioRecord 很像,但方向相反。
int sampleRate = 16000;
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioTrack audioTrack = new AudioTrack(
AudioManager.STREAM_VOICE_CALL, // 语音通话流
sampleRate,
channelConfig,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM // 流模式,实时写入
);
这里有个关键点:AudioTrack.MODE_STREAM 和 MODE_STATIC 的区别。做回声消除,我们几乎只用 MODE_STREAM。因为数据是源源不断产生的,不是一次性加载完。
7.3 回调模式 vs 阻塞模式
这是很多开发者容易搞混的地方。两种模式各有适用场景,选错了,延迟和稳定性都会出问题。
7.3.1 阻塞模式(Blocking Mode)
阻塞模式是最传统的方式。你调用 read() 或 write(),线程就卡在那里,直到数据读写完成。
// 采集线程
byte[] buffer = new byte[bufferSize];
while (isRecording) {
int bytesRead = audioRecord.read(buffer, 0, buffer.length);
// 处理数据...
}
// 播放线程
while (isPlaying) {
audioTrack.write(buffer, 0, buffer.length);
}
这种模式的好处是简单、可控。坏处是:你需要自己管理线程。如果 read() 阻塞时间过长,整个采集流程就卡住了。
7.3.2 回调模式(Callback Mode)
Android 5.0 之后引入了 AudioRecord.OnRecordPositionUpdateListener 和 AudioTrack.OnPlaybackPositionUpdateListener。系统会在缓冲区准备好时主动通知你,而不是让你傻等。
audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
@Override
public void onMarkerReached(AudioRecord recorder) {
// 标记点到达,不常用
}
@Override
public void onPeriodicNotification(AudioRecord recorder) {
// 周期性通知,每读满一个缓冲区就触发一次
int bytesRead = recorder.read(buffer, 0, buffer.length);
// 处理数据...
}
});
audioRecord.setPositionNotificationPeriod(bufferSize / 2); // 每半缓冲区通知一次
回调模式的好处是:你不用自己轮询,系统帮你调度。延迟更稳定,CPU 占用也更低。
7.3.3 怎么选?一张表说清楚
| 对比维度 | 阻塞模式 | 回调模式 |
|---|---|---|
| 实现复杂度 | 低,自己管理线程 | 中,需要处理回调 |
| 延迟稳定性 | 一般,受线程调度影响 | 好,系统主动调度 |
| CPU 占用 | 较高,轮询消耗 | 较低,事件驱动 |
| 适用场景 | 简单录音/播放 | 实时通信、回声消除 |
| 兼容性 | 所有 Android 版本 | API 21+ |
7.4 核心逻辑:采集-处理-播放的流水线
回声消除的本质是一条流水线:采集麦克风数据 → 回声消除算法处理 → 播放处理后的数据。下面这张图展示了整个流程:
7.5 实战建议:搭建低延迟音频管道
说了这么多理论,来点实际的。我个人搭建音频管道时,会遵循这几个原则:
- 采集和播放用不同的线程。千万别在 UI 线程里做音频读写,会卡死。
- 缓冲区大小保持一致。采集的缓冲区大小和播放的缓冲区大小最好一样,这样数据对齐方便。
- 优先使用回调模式。如果你的 minSdk 是 21 以上,回调模式是更好的选择。
- 预留缓冲区余量。在
getMinBufferSize()的基础上,放大 2-4 倍,应对系统抖动。
嗯,关于 AudioRecord 和 AudioTrack 的基础就讲到这里。下一节我们会深入回调模式的具体实现,以及如何用它们搭建完整的回声消除管道。