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 通道 低延时路径 + 回声消除 AudioRecord 配置层 采样率:44100 | 单声道 | PCM_16BIT | getMinBufferSize() 同步模式(阻塞读取) read() 循环 + 工作线程 回调模式(非阻塞) OnRecordPositionUpdateListener 推荐:API 26+ 使用回调模式,老设备使用同步模式

从图中可以看到,数据从麦克风硬件出发,经过系统低延时通道,再到 AudioRecord 配置层,最后通过两种模式之一到达你的应用层。每一步的配置都会影响最终延时。

总结一下我的经验:

  • 配置用 VOICE_COMMUNICATION + 44100Hz + 单声道 + PCM_16BIT
  • 缓冲区用 getMinBufferSize() * 2,既安全又低延时
  • 新设备用回调模式,老设备用同步模式+高优先级线程
  • 回调里别做耗时操作,用队列+工作线程处理数据

低延时录制其实不难,关键是把这几个配置项吃透。你想想看,只要缓冲区大小、音频源、读取模式都选对了,延时就能降下来。我在多个项目里验证过这套方案,效果都很稳定。

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