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时,把containerSizerepresentation搞混了。记住: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 核心知识体系 SL Engine(引擎) Audio Player 播放器对象 Audio Recorder 录音器对象 Output Mix 输出混音器 SLPlayItf 播放控制接口 SLBufferQueueItf 数据缓冲接口 SLVolumeItf 音量控制接口 SLRecordItf 录音控制接口 SLBufferQueueItf 数据缓冲接口 SLAndroidItf Android扩展接口 核心对象 接口(Interface)

这张图把OpenSL ES的核心结构说清楚了。引擎在最上层,下面挂着播放器、录音器和混音器。每个对象又通过接口暴露功能。你想想看,这种设计其实很灵活——你不需要的功能就不获取对应的接口,代码自然就精简了。

9.6 总结与避坑

好,咱们把这一章的核心点串一下:

  • OpenSL ES是Android上实现低延时音频的首选方案,延时能压到20-30ms
  • 引擎是万物之源,所有对象都得从引擎创建
  • 播放器和录音器的配置核心是数据源和数据接收器,格式必须匹配
  • Buffer queue是数据流动的通道,长度至少2,避免卡顿

最后提醒几个坑:

  • 别忘了调用Realize——我见过有人调试了一整天,结果就是忘了这步
  • 音频格式配置错了,出来的声音要么是噪音要么是静音
  • 录音权限必须在Java层申请,Native层拿不到权限
  • Buffer queue的回调是在工作线程里执行的,别在里面做耗时操作

嗯,这一章的内容就到这儿。OpenSL ES入门其实不难,关键是理解它的对象模型和接口机制。下一章咱们会深入Buffer queue的数据流转,到时候再聊。


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