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_COMMUNICATION或SL_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 录音的完整流程串起来:
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