6、低延时AudioRecord:低延时录制配置、使用AudioRecord.getMinBufferSize()、回调模式 vs 同步模式
低延时录制,说白了就是让麦克风的声音以最快的速度到达你的应用层。我刚开始做音视频通话优化时,发现录进去的声音总是比画面慢半拍,用户口型对不上,体验极差。后来才明白,问题出在AudioRecord的配置上。
这一节,我们专门聊聊低延时录制的三个核心点:怎么配置、缓冲区设多大、以及两种读取模式怎么选。
6.1 低延时录制的基本配置
AudioRecord的构造参数里,有几个关键项直接影响延时。我个人习惯这样配置:
int sampleRate = 44100; // 或 48000
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioRecord recorder = new AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
注意这里用了 VOICE_COMMUNICATION 作为音频源。为什么不用 MIC?因为 VOICE_COMMUNICATION 会启用系统级的低延时通道,并且会做回声消除和降噪处理。我在项目中遇到过,用 MIC 录制时延时比 VOICE_COMMUNICATION 高了将近 30ms。
核心配置要点:
- 采样率:44100Hz 或 48000Hz,不要用 8000Hz,虽然省带宽但延时更大
- 声道:单声道,双声道数据量翻倍,处理延时也会增加
- 编码格式:PCM_16BIT,这是最通用的格式,兼容性最好
- 音频源:优先使用 VOICE_COMMUNICATION
6.2 AudioRecord.getMinBufferSize() 的正确用法
这个方法返回的是系统建议的最小缓冲区大小。你想想看,缓冲区越小,数据流转越快,延时就越低。但太小了也不行,会导致数据溢出或者丢帧。
我曾经踩过一个坑:直接拿 getMinBufferSize() 的返回值当缓冲区,结果在某些低端设备上频繁出现 ERROR_DEAD_OBJECT 错误。后来我加了一个保护逻辑:
int minBufferSize = AudioRecord.getMinBufferSize(
sampleRate, channelConfig, audioFormat);
// 有些设备返回的值偏小,建议乘以2作为安全缓冲区
int safeBufferSize = minBufferSize * 2;
// 但不要超过某个上限,否则延时太大
int maxBufferSize = minBufferSize * 4;
int finalBufferSize = Math.min(safeBufferSize, maxBufferSize);
我的经验:
对于低延时场景,建议用 minBufferSize * 2 作为实际缓冲区大小。这样既不会频繁丢数据,也不会引入过多延时。如果你做的是专业音频应用,可以尝试 minBufferSize * 1.5,但要做好异常处理。
6.3 回调模式 vs 同步模式
AudioRecord 提供了两种读取数据的方式:同步模式(阻塞读取)和回调模式(非阻塞监听)。
嗯,这里要注意,两种模式各有适用场景,选错了会直接影响你的延时表现。
同步模式(阻塞读取)
就是调用 read() 方法,线程会一直阻塞直到数据到来。代码长这样:
byte[] buffer = new byte[finalBufferSize];
while (isRecording) {
int bytesRead = recorder.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
// 处理音频数据
processAudioData(buffer, bytesRead);
}
}
同步模式的优点是简单直接,缺点是你必须自己维护一个线程。如果 read() 阻塞时间过长,你的处理线程就会被卡住。
回调模式(非阻塞监听)
从 Android 8.0(API 26)开始,AudioRecord 支持了 setRecordPositionUpdateListener 回调。你可以注册一个监听器,当缓冲区填满时系统会通知你:
recorder.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
@Override
public void onMarkerReached(AudioRecord recorder) {
// 标记点到达,一般用不到
}
@Override
public void onPeriodicNotification(AudioRecord recorder) {
// 周期性通知,缓冲区数据已就绪
int bytesRead = recorder.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
processAudioData(buffer, bytesRead);
}
}
});
// 设置通知周期,比如每读取一次缓冲区就通知一次
recorder.setPositionNotificationPeriod(buffer.length / 2);
recorder.startRecording();
避坑指南:
我曾经在回调模式里直接做音频编码,结果导致回调线程被阻塞,后续数据全部丢失。记住:回调里只做数据拷贝,不要做耗时操作。如果需要编码或网络发送,请用队列+工作线程的方式处理。
两种模式对比
| 对比项 | 同步模式 | 回调模式 |
|---|---|---|
| 实现复杂度 | 低,只需一个循环 | 中,需要注册监听器 |
| 线程管理 | 自己维护线程 | 系统回调线程 |
| 延时表现 | 可控,取决于 read() 频率 | 较低,系统主动通知 |
| 适用场景 | 简单录制、非实时处理 | 低延时、实时音频处理 |
| 最低 API | 全部 | API 26+ |
我个人建议:如果你的目标设备都是 Android 8.0 以上,优先使用回调模式。延时更低,而且不用自己操心线程调度。如果还要兼容老设备,那就用同步模式,但记得把线程优先级调高:
Thread audioThread = new Thread(() -> {
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
// 你的录制循环
});
audioThread.start();
6.4 低延时录制的完整流程图
下面这张图展示了低延时录制的完整数据流,从麦克风到应用层:
从图中可以看到,数据从麦克风硬件出发,经过系统低延时通道,再到 AudioRecord 配置层,最后通过两种模式之一到达你的应用层。每一步的配置都会影响最终延时。
总结一下我的经验:
- 配置用 VOICE_COMMUNICATION + 44100Hz + 单声道 + PCM_16BIT
- 缓冲区用 getMinBufferSize() * 2,既安全又低延时
- 新设备用回调模式,老设备用同步模式+高优先级线程
- 回调里别做耗时操作,用队列+工作线程处理数据
低延时录制其实不难,关键是把这几个配置项吃透。你想想看,只要缓冲区大小、音频源、读取模式都选对了,延时就能降下来。我在多个项目里验证过这套方案,效果都很稳定。