9、OpenSL ES入门:OpenSL ES在Android上的地位、Engine与Object的创建、播放器与录音器的配置
好,咱们今天聊聊OpenSL ES。说实话,在Android音频开发这个圈子里,OpenSL ES是个绕不开的话题。它不像AudioTrack那样简单易用,也不像AAudio那样新潮,但它就是那个“老大哥”——稳定、底层、跨平台。
我个人习惯把OpenSL ES比作音频开发的“C语言”。你想想看,Java层的MediaPlayer虽然方便,但延时动不动就上百毫秒。而OpenSL ES呢?它直接跟底层音频驱动打交道,延时能压到20-30ms。嗯,这就是它的价值所在。
9.1 OpenSL ES在Android上的地位
先说说它的位置。OpenSL ES是Khronos组织(就是搞OpenGL那个)制定的嵌入式音频加速标准。Android从2.3开始就支持了,一直到现在。
它的定位很明确:高性能、低延时的原生音频API。说白了,就是给那些对实时性要求高的应用准备的——比如专业音乐播放器、游戏音效引擎、VoIP通话、K歌App等等。
我在项目中遇到过不少开发者,一上来就用MediaPlayer做实时音频处理,结果发现延时根本压不下去。后来换成OpenSL ES,问题就解决了。所以我的建议是:如果你需要低于50ms的音频延时,别犹豫,直接上OpenSL ES。
核心要点:
- OpenSL ES是C语言API,运行在Native层
- 延时比Java层API低50%-70%
- 支持音频播放、录音、音效处理
- Android 4.2+支持低延时路径(需要设备支持)
不过要注意,OpenSL ES在Android上有个“小脾气”——它不支持所有OpenSL ES标准功能。比如音频混合器、MIDI这些,Android就没实现。所以开发前最好查一下Android NDK文档里的“Supported Features”列表。
9.2 Engine与Object的创建
好,咱们开始动手。OpenSL ES的编程模型跟其他SL API很像,都是基于对象的。但这里有个坑——它用的是面向对象的C语言,不是C++。什么意思呢?就是所有对象都是通过一个全局引擎创建的,然后通过接口来操作。
先看引擎创建。这是所有操作的起点:
// 1. 创建引擎对象
SLObjectItf engineObject = NULL;
SLEngineItf engineEngine = NULL;
SLresult result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
if (result != SL_RESULT_SUCCESS) {
// 处理错误
}
// 2. 实例化引擎
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
if (result != SL_RESULT_SUCCESS) {
// 处理错误
}
// 3. 获取引擎接口
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
if (result != SL_RESULT_SUCCESS) {
// 处理错误
}
这段代码我闭着眼睛都能写出来。为什么?因为每个OpenSL ES应用都得这么来一遍。你想想看,引擎就是整个音频系统的“工厂”,所有播放器、录音器、混音器都得从它这里创建。
个人经验: 我建议把引擎创建封装成一个函数,比如createOpenSLEngine()。这样每个模块调用起来都方便,而且错误处理可以统一做。我曾经在一个项目里,因为引擎创建失败没处理好,导致整个App无声运行了三天才被发现……
创建完引擎,接下来就是各种Object了。OpenSL ES里的Object分两种:可实例化的和不可实例化的。引擎本身是不可实例化的,而播放器、录音器这些是可实例化的。
每个Object创建后,都需要调用Realize来实例化。这个步骤很容易忘,我刚开始学的时候就经常漏掉,结果接口死活拿不到。
9.3 播放器的配置
播放器配置,说白了就是告诉引擎:我要播放什么格式的音频,从哪里播放,输出到哪里。
先看一个最简单的PCM播放器配置:
// 配置音频源(这里用buffer queue,即从内存播放)
SLDataLocator_AndroidSimpleBufferQueue locBufQueue = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
2 // buffer队列长度
};
SLDataFormat_PCM formatPcm = {
SL_DATAFORMAT_PCM,
2, // 声道数
SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN
};
SLDataSource audioSrc = {&locBufQueue, &formatPcm};
// 配置音频输出(通常输出到扬声器/耳机)
SLDataLocator_OutputMix locOutMix = {
SL_DATALOCATOR_OUTPUTMIX,
outputMixObject
};
SLDataSink audioSnk = {&locOutMix, NULL};
// 创建播放器
SLObjectItf playerObject = NULL;
SLPlayItf playerPlay = NULL;
SLAndroidSimpleBufferQueueItf playerBufferQueue = NULL;
const SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE};
const SLboolean req[] = {SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(
engineEngine,
&playerObject,
&audioSrc,
&audioSnk,
1, // 接口数量
ids,
req
);
// 实例化播放器
result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
// 获取播放接口
result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
// 获取buffer queue接口
result = (*playerObject)->GetInterface(
playerObject,
SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&playerBufferQueue
);
这里有个关键点:音频格式必须跟实际数据一致。比如你的PCM数据是16位、44100Hz、立体声,那配置里就必须这么写。我曾经帮一个同事调试,他播放出来的声音全是噪音,最后发现是采样率写成了48000,但实际数据是44100的。
避坑指南: 我曾经在配置SLDataFormat_PCM时,把containerSize和representation搞混了。记住:containerSize是存储位宽,representation是编码格式。对于16位PCM,containerSize填16,representation填SL_PCMSAMPLEFORMAT_FIXED_16。
9.4 录音器的配置
录音器的配置跟播放器很像,但方向是反的——数据从麦克风流到你的buffer里。
看代码:
// 配置音频接收器(数据写到哪里)
SLDataLocator_IODevice locIoDevice = {
SL_DATALOCATOR_IODEVICE,
SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT,
NULL
};
SLDataSource audioSrc = {&locIoDevice, NULL};
// 配置音频格式(录音数据格式)
SLDataLocator_AndroidSimpleBufferQueue locBufQueue = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
2
};
SLDataFormat_PCM formatPcm = {
SL_DATAFORMAT_PCM,
1, // 单声道录音
SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_CENTER,
SL_BYTEORDER_LITTLEENDIAN
};
SLDataSink audioSnk = {&locBufQueue, &formatPcm};
// 创建录音器
SLObjectItf recorderObject = NULL;
SLRecordItf recorderRecord = NULL;
SLAndroidSimpleBufferQueueItf recorderBufferQueue = NULL;
const SLInterfaceID ids[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[] = {SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioRecorder(
engineEngine,
&recorderObject,
&audioSrc,
&audioSnk,
1,
ids,
req
);
// 实例化
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
// 获取接口
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
result = (*recorderObject)->GetInterface(
recorderObject,
SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&recorderBufferQueue
);
录音器有个容易忽略的地方:权限。Android 6.0以上需要动态申请录音权限,而且必须在Native层调用之前就申请好。我见过有人把权限申请放在Native代码里,结果死活拿不到麦克风数据。
录音配置要点:
- 录音源用
SL_IODEVICE_AUDIOINPUT,不要用其他值 - 单声道录音通常就够用了,立体声会增加数据量
- 采样率建议用44100或48000,兼容性最好
- Buffer queue长度至少2,避免数据丢失
9.5 核心知识体系
说了这么多,咱们用一张图来梳理一下OpenSL ES的核心结构:
这张图把OpenSL ES的核心结构说清楚了。引擎在最上层,下面挂着播放器、录音器和混音器。每个对象又通过接口暴露功能。你想想看,这种设计其实很灵活——你不需要的功能就不获取对应的接口,代码自然就精简了。
9.6 总结与避坑
好,咱们把这一章的核心点串一下:
- OpenSL ES是Android上实现低延时音频的首选方案,延时能压到20-30ms
- 引擎是万物之源,所有对象都得从引擎创建
- 播放器和录音器的配置核心是数据源和数据接收器,格式必须匹配
- Buffer queue是数据流动的通道,长度至少2,避免卡顿
最后提醒几个坑:
- 别忘了调用
Realize——我见过有人调试了一整天,结果就是忘了这步 - 音频格式配置错了,出来的声音要么是噪音要么是静音
- 录音权限必须在Java层申请,Native层拿不到权限
- Buffer queue的回调是在工作线程里执行的,别在里面做耗时操作
嗯,这一章的内容就到这儿。OpenSL ES入门其实不难,关键是理解它的对象模型和接口机制。下一章咱们会深入Buffer queue的数据流转,到时候再聊。
公众号:蓝海资料掘金营,微信deep3321