7、AudioRecord与AudioTrack:低延迟音频采集与播放、回调模式与阻塞模式的选择

说到 Android 上的音频采集和播放,AudioRecordAudioTrack 这对兄弟绝对是绕不开的。很多做音频处理的朋友,一开始都会纠结:到底用哪个 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
);
我的经验: 采样率选 16kHz 就够了。我在项目中试过 44.1kHz,延迟没降多少,CPU 开销反而上去了。做回声消除,16kHz 是业界标配。

7.1.2 低延迟的关键:缓冲区大小

缓冲区大小直接影响延迟。你想想看,缓冲区越大,一次读到的数据越多,但等待的时间也越长。反之,缓冲区太小,又容易丢数据。

getMinBufferSize() 返回的是系统建议的最小值。但我建议你在此基础上适当放大一点,比如 2 倍或 4 倍。为什么?

  • 太小的缓冲区会导致频繁的读写操作,CPU 开销大
  • 系统调度有不确定性,缓冲区太小容易欠载(underrun)
  • 回声消除算法本身也需要一定的数据块大小才能工作
注意: 不要盲目追求最小缓冲区。我曾经在一个项目里把缓冲区设成最小值,结果在低端手机上频繁出现音频卡顿。后来改成 2 倍大小,问题就解决了。

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_STREAMMODE_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.OnRecordPositionUpdateListenerAudioTrack.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+
避坑指南: 我曾经在一个项目里用了阻塞模式做回声消除,结果在华为某款手机上延迟忽高忽低。换成回调模式后,延迟稳定在 20ms 以内。所以,如果你的目标 API 是 21 以上,果断用回调模式。

7.4 核心逻辑:采集-处理-播放的流水线

回声消除的本质是一条流水线:采集麦克风数据 → 回声消除算法处理 → 播放处理后的数据。下面这张图展示了整个流程:

回声消除核心流水线 AudioRecord 麦克风采集 回调模式 / 阻塞模式 PCM 数据 回声消除算法 AEC / NLP / 自适应滤波 参考信号 + 麦克风信号 处理后数据 AudioTrack 扬声器播放 流模式 / 回调模式 参考信号反馈(用于自适应滤波) 关键要点: • 采集和播放使用独立的线程,避免互相阻塞 • 缓冲区大小一致,保证数据对齐 • 回调模式下,系统自动调度,延迟更稳定

7.5 实战建议:搭建低延迟音频管道

说了这么多理论,来点实际的。我个人搭建音频管道时,会遵循这几个原则:

  1. 采集和播放用不同的线程。千万别在 UI 线程里做音频读写,会卡死。
  2. 缓冲区大小保持一致。采集的缓冲区大小和播放的缓冲区大小最好一样,这样数据对齐方便。
  3. 优先使用回调模式。如果你的 minSdk 是 21 以上,回调模式是更好的选择。
  4. 预留缓冲区余量。在 getMinBufferSize() 的基础上,放大 2-4 倍,应对系统抖动。
核心原则: 低延迟不是一味地减小缓冲区,而是在稳定性和延迟之间找到平衡点。我见过太多人为了追求极致延迟,把缓冲区设得特别小,结果音频断断续续,得不偿失。

嗯,关于 AudioRecordAudioTrack 的基础就讲到这里。下一节我们会深入回调模式的具体实现,以及如何用它们搭建完整的回声消除管道。


公众号:蓝海资料掘金营,微信deep3321