18、音频采集进阶:OpenSL ES介绍、OpenSL ES录音实现、低延迟音频采集

好,咱们继续往下走。上一章我们用 AudioRecord 搞定了基础录音,但说实话,那个方案在延迟和性能上,天花板很明显。尤其是做直播、K 歌、或者实时语音互动,AudioRecord 的延迟会让你抓狂。

这一章,我们来点硬核的——OpenSL ES。这是 Android 上真正能压榨出低延迟音频性能的 Native 层 API。我当年第一次接触它,是在做一个手机端低延迟耳返的项目,AudioRecord 怎么调都有 200ms+ 的延迟,换成 OpenSL ES 直接砍到 50ms 以内。嗯,那感觉,就像换了台机器。

18.1 OpenSL ES 是什么?为什么我们需要它?

OpenSL ES,全称 Open Sound Library for Embedded Systems。说白了,它是 Khronos 组织(就是搞 OpenGL 那个)为嵌入式设备定制的音频 API。

在 Android 上,它属于 NDK 的一部分。你可以在 C/C++ 层直接调用它,绕过 Java 层的 JNI 开销和系统服务层的调度延迟。

为什么它比 AudioRecord 快?

  • 更短的路径:AudioRecord 的数据从驱动 -> HAL -> AudioFlinger -> Java 层 -> 你的 App。OpenSL ES 直接从驱动 -> HAL -> 你的 Native 代码。
  • 零拷贝:OpenSL ES 支持直接操作音频缓冲区,不需要在 Java 和 Native 之间来回拷贝数据。
  • 低延迟模式:Android 从 4.2 开始引入了低延迟音频路径,OpenSL ES 是唯一能稳定触发这条路径的 API。

核心结论:如果你对延迟有硬性要求(比如 < 100ms),别犹豫,直接上 OpenSL ES。AudioRecord 更适合非实时场景,比如录音保存到文件。

18.2 OpenSL ES 的核心概念

OpenSL ES 的设计思路跟 OpenGL 很像,都是面向对象的 C 语言接口。它有几个核心对象,你得先搞明白:

对象 作用 类比
SLObjectItf 所有对象的基类,通过它创建和销毁其他对象 有点像 Java 的 Object
SLEngineItf 引擎接口,用来创建各种音频对象 工厂类
SLRecordItf 录音接口,控制录音的启停 AudioRecord 的 start/stop
SLBufferQueueItf 缓冲区队列接口,录音数据通过它回调给你 生产者-消费者模式
SLAndroidSimpleBufferQueueItf Android 扩展的简化版缓冲区队列,更易用 推荐使用这个

你想想看,整个流程其实就是:创建引擎 -> 配置录音器 -> 设置缓冲区队列 -> 启动录音 -> 在回调里拿数据。是不是很清晰?

18.3 OpenSL ES 录音实现:手把手代码

好,理论说完了,咱们直接上代码。我会把关键步骤拆开讲,你跟着走一遍就能跑起来。

18.3.1 初始化引擎

第一步,创建 OpenSL ES 引擎对象。这是所有操作的起点。

// 1. 创建引擎对象
SLObjectItf engineObject = nullptr;
slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);

// 2. 实例化(Realize)
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);

// 3. 获取引擎接口
SLEngineItf engineEngine = nullptr;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);

这里有个坑,我当年踩过:Realize 的第二个参数,传 SL_BOOLEAN_FALSE 表示同步初始化,传 SL_BOOLEAN_TRUE 是异步。我建议你直接用同步,简单可靠。异步的话,你得等回调,代码复杂度翻倍。

18.3.2 配置录音器

引擎有了,接下来配置录音器的输入源和输出接收器。

// 配置输入源:麦克风
SLDataLocator_IODevice locDev = {
    SL_DATALOCATOR_IODEVICE,
    SL_IODEVICE_AUDIOINPUT,
    SL_DEFAULTDEVICEID_AUDIOINPUT,
    nullptr
};
SLDataSource audioSource = {&locDev, nullptr};

// 配置输出接收器:缓冲区队列
SLDataLocator_AndroidSimpleBufferQueue locBufQueue = {
    SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
    2  // 缓冲区数量,建议 2 或 3
};

// 配置音频格式
SLDataFormat_PCM formatPcm = {
    SL_DATAFORMAT_PCM,
    2,                          // 声道数:立体声
    SL_SAMPLINGRATE_44_1,       // 采样率:44.1kHz
    SL_PCMSAMPLEFORMAT_FIXED_16,// 位深:16bit
    SL_PCMSAMPLEFORMAT_FIXED_16,
    SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
    SL_BYTEORDER_LITTLEENDIAN
};
SLDataSink audioSink = {&locBufQueue, &formatPcm};

嗯,这里要注意采样率的选择。虽然 44.1kHz 是 CD 音质,但很多 Android 设备对 48kHz 支持更好,延迟也更低。我个人习惯是:直播场景用 48kHz,录音保存用 44.1kHz

18.3.3 创建录音器并设置回调

// 创建录音器对象
SLObjectItf recorderObject = nullptr;
(*engineEngine)->CreateAudioRecorder(
    engineEngine,
    &recorderObject,
    &audioSource,
    &audioSink,
    0, nullptr, nullptr
);

// 实例化
(*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);

// 获取录音接口
SLRecordItf recordItf = nullptr;
(*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recordItf);

// 获取缓冲区队列接口
SLAndroidSimpleBufferQueueItf bufferQueueItf = nullptr;
(*recorderObject)->GetInterface(
    recorderObject,
    SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
    &bufferQueueItf
);

// 注册回调函数
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, recorderCallback, this);

回调函数长这样:

void recorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
    // context 可以传你的业务对象,比如一个 Java 回调的 JNI 封装
    MyRecorder* recorder = (MyRecorder*)context;
    
    // 这里 buffer 里已经有数据了,你可以处理了
    // 比如:编码、发送、或者写文件
    
    // 处理完后,必须重新 Enqueue 缓冲区,否则不会再收到回调
    (*bq)->Enqueue(bq, recorder->buffer, recorder->bufferSize);
}

重要提醒:回调是在 OpenSL ES 的内部线程中执行的,不要在这里做耗时操作!比如网络发送、文件写入,最好扔到工作线程去做。否则你会把音频管道堵死,导致丢帧或卡顿。

18.3.4 启动和停止录音

// 启动录音
(*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_RECORDING);

// 停止录音
(*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED);

// 销毁资源
(*recorderObject)->Destroy(recorderObject);
(*engineObject)->Destroy(engineObject);

是不是比 AudioRecord 啰嗦?但换来的是性能和延迟的巨大提升。值不值?我觉得值。

18.4 低延迟音频采集的实战技巧

代码能跑起来只是第一步。真正要做出低延迟的采集,有几个关键点你必须注意。

18.4.1 缓冲区大小:不是越小越好

缓冲区越小,延迟越低,但 CPU 开销越大,也越容易丢帧。我见过有人为了追求极致延迟,把缓冲区设成 128 帧,结果在低端手机上疯狂爆音。

我的建议

  • 高端机(骁龙 8 系、麒麟 9 系):256 帧,延迟约 5-10ms
  • 中端机:512 帧,延迟约 10-20ms
  • 低端机:1024 帧,延迟约 20-40ms

你可以做一个自适应策略:启动时先尝试 256 帧,如果连续丢帧就自动升到 512 帧。我曾经在一个项目中就是这么干的,效果很好。

18.4.2 使用 Android 的低延迟路径

Android 从 4.2 开始支持低延迟音频路径,但需要满足几个条件:

  • 采样率必须是设备原生支持的(通常是 48kHz)
  • 缓冲区大小必须是设备原生帧大小的整数倍
  • 必须使用 SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATIONSL_ANDROID_RECORDING_PRESET_CAMCORDER

怎么判断?你可以用 AudioManager.getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) 拿到设备的原生帧大小。嗯,这个值一般在 192 到 480 之间。

18.4.3 避免 JNI 开销

OpenSL ES 的回调是在 Native 层,如果你每次回调都通过 JNI 把数据传到 Java 层,那延迟优势就全没了。

我的做法

  • 音频处理逻辑全部用 C/C++ 实现,比如混音、降噪、编码
  • 只在需要 UI 更新时(比如音量指示器)才通过 JNI 通知 Java
  • 使用 NewDirectByteBuffer 共享内存,避免数据拷贝

小技巧:如果你必须把数据传到 Java 层,可以考虑用 ByteBuffer.allocateDirect() 分配 Native 内存,然后在 C 层直接操作这块内存。这样 JNI 调用时只需要传递指针,不需要拷贝数据。

18.5 本章知识体系

下面这张图,帮你把 OpenSL ES 录音的完整流程串起来:

OpenSL ES 录音完整流程 1. 创建引擎 (slCreateEngine) 2. 配置音频源 (麦克风) 和接收器 (缓冲区) 3. 创建录音器 (CreateAudioRecorder) 4. 注册回调 (RegisterCallback) 5. 启动录音 (SetRecordState) 关键要点 • 缓冲区数量建议 2-3 个 • 采样率优先用 48kHz • 回调中不要做耗时操作 • 使用低延迟路径需满足条件 • 销毁时先停止再释放资源 • 缓冲区大小根据设备自适应 • 避免频繁 JNI 调用 • 使用 DirectByteBuffer 共享内存 • 回调后必须重新 Enqueue • 引擎和录音器要分别销毁 延迟目标:< 50ms

18.6 避坑指南

最后,分享几个我当年踩过的坑,你遇到了能少走弯路。

  • 回调不触发:检查一下你有没有在启动前先 Enqueue 一次缓冲区。OpenSL ES 需要你先放一个空缓冲区进去,它才会开始往里面写数据。
  • 声音有杂音:大概率是缓冲区大小和采样率不匹配。试试把采样率改成 48kHz,或者增大缓冲区。
  • 在某些手机上崩溃:有些国产 ROM 对 OpenSL ES 的支持有 bug。我建议你在初始化时加一个 try-catch 的降级逻辑,如果 OpenSL ES 初始化失败,就回退到 AudioRecord。
  • 延迟反而比 AudioRecord 高:检查一下你是不是用了 SL_ANDROID_RECORDING_PRESET_GENERIC。这个预设不会触发低延迟路径,换成 SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION 试试。

好了,这一章的内容就到这。OpenSL ES 虽然上手门槛比 AudioRecord 高,但一旦你掌握了它,音频采集这块基本就没什么能难住你了。


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