使用OpenSL-ES播放PCM音频文件
今天学习了使用OpenSL播放PCM文件,简单记录一下。
感觉OpenSL入门的有些难度,搞得头晕,所以只介绍功能性代码,暂时不考虑健壮性,只抓学习重点。
学习OpenSL ES要先做好心理准备,拿出时间认真学习,下一番功夫。
一、讲在前面
在代码之前先讲一下原理,代码讲解和实例在第二节。懂了原理,那么在看代码的时候才可能更容易理解。
1.1 OpenSL ES是什么?
OpenSL ES 全称是:Open Sound Library for Embedded Systems,简单说来OpenSL ES 是一套针对嵌入式平台的音频标准。
1.2 Android与OpenSL ES的关系
Android 2.3 (API 9) 即开始支持 OpenSL ES 标准了,通过 NDK 提供相应的 API 开发接口,下图是 Android 官方给出的关系图:
image.png
由该图可以看出,Android 实现的 OpenSL ES 只是 OpenSL 1.0.1 的子集,并且进行了扩展,因此,对于 OpenSL ES API 的使用,我们还需要特别留意哪些是 Android 支持的,哪些是不支持的,具体相关文档的地址位于 NDK docs 目录下:
NDKroot/docs/Additional_library_docs/opensles/index.html
NDKroot/docs/Additional_library_docs/opensles/OpenSL_ES_Specification_1.0.1.pdf
1.3 OpenSL ES的功能特点
支持以下特点:
1)C 语言接口,兼容 C++,需要在 NDK 下开发,能更好地集成在 native 应用中
2)运行于 native 层,需要自己管理资源的申请与释放,没有 Dalvik 虚拟机的垃圾回收机制
3)支持 PCM 数据的采集,支持的配置:16bit 位宽,16000 Hz采样率,单通道。(其他的配置不能保证兼容所有平台)
4)支持 PCM 数据的播放,支持的配置:8bit/16bit 位宽,单通道/双通道,小端模式,采样率(8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 Hz)
5)支持播放的音频数据来源:res 文件夹下的音频、assets 文件夹下的音频、sdcard 目录下的音频、在线网络音频、代码中定义的音频二进制数据等
不支持的:
不支持:
1)不支持版本低于 Android 2.3 (API 9) 的设备
2)没有全部实现 OpenSL ES 定义的特性和功能
3)不支持 MIDI
4)不支持直接播放 DRM 或者 加密的内容
5)不支持音频数据的编解码,如需编解码,需要使用 MediaCodec API 或者第三方库
6)在音频延时方面,相比于上层 API,并没有特别明显地改进
优势:
1)避免音频数据频繁在 native 层和 Java 层拷贝,提高效率
2)相比于 Java API,可以更灵活地控制参数
3)由于是 C 代码,因此可以做深度优化,比如采用 NEON 优化
4)代码细节更难被反编译
1.4 OpenSL ES设计和概念
1.4.1 面向对象的 C 语言接口
OpenSL ES 虽然是 C 语言编写,但是它的接口采用的是面向对象的方式,并不是提供一系列的函数接口,而是以 Interface 的方式来提供 API。
例如:
// 下面代码是对 Audio Engine 对象进行 “初始化” SLEngineItf engineObject; SLresult result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
是不是很像C++的调用方式。
1.4.2 Objects 和 Interfaces
OpenSL ES 有两个必须理解的概念,就是 Object 和 Interface,Object 可以想象成 Java 的 Object 类,Interface 可以想象成 Java 的 Interface,但它们并不完全相同,下面进一步解释他们的关系:
1) 每个 Object 可能会存在一个或者多个 Interface,官方为每一种 Object 都定义了一系列的 Interface
2)每个 Object 对象都提供了一些最基础的操作,比如:Realize,Resume,GetState,Destroy 等等,如果希望使用该对象支持的功能函数,则必须通过其 GetInterface 函数拿到 Interface 接口,然后通过 Interface 来访问功能函数
3)并不是每个系统上都实现了 OpenSL ES 为 Object 定义的所有 Interface,所以在获取 Interface 的时候需要做一些选择和判断。
查看 OpenSLES.h
文件,我们可以看到 OpenSL ES 定义的所有 Object 对象的 ID,我们可以通过 Object ID 来创建对应的对象实例,下面是一部分对象ID
/* Objects ID's */ #define SL_OBJECTID_ENGINE ((SLuint32) 0x00001001) #define SL_OBJECTID_LEDDEVICE ((SLuint32) 0x00001002) #define SL_OBJECTID_VIBRADEVICE ((SLuint32) 0x00001003) #define SL_OBJECTID_AUDIOPLAYER ((SLuint32) 0x00001004) #define SL_OBJECTID_AUDIORECORDER ((SLuint32) 0x00001005) #define SL_OBJECTID_MIDIPLAYER ((SLuint32) 0x00001006) #define SL_OBJECTID_LISTENER ((SLuint32) 0x00001007) #define SL_OBJECTID_3DGROUP ((SLuint32) 0x00001008) #define SL_OBJECTID_OUTPUTMIX ((SLuint32) 0x00001009) #define SL_OBJECTID_METADATAEXTRACTOR ((SLuint32) 0x0000100A)
其中,我们比较常用的应该就是:ENGINE、AUDIOPLAYER 和 AUDIORECORDER 对象了。
同样,“OpenSLES.h” 文件中还定义了所有的 Interface ID,通过 Interface ID 我们可以从对象中获取到对应的功能接口。
例如:
extern SL_API const SLInterfaceID SL_IID_MIDITIME;
1.4.3 OpenSL ES的状态机制
OpenSL ES的另外一个重要概念就是它的状态机制:
image.png
任何一个 OpenSL ES 的对象,创建成功后,都进入 SL_OBJECT_STATE_UNREALIZED
状态,这种状态下,系统不会为它分配任何资源,直到调用 Realize 函数为止。
Realize 后的对象,就会进入 SL_OBJECT_STATE_REALIZED
状态,这是一种“可用”的状态,只有在这种状态下,对象的各个功能和资源才能正常地访问。
当一些系统事件发生后,比如出现错误或者 Audio 设备被其他应用抢占,OpenSL ES 对象会进入 SL_OBJECT_STATE_SUSPENDED
状态,如果希望恢复正常使用,需要调用 Resume 函数。
当调用对象的 Destroy 函数后,则会释放资源,并回到SL_OBJECT_STATE_UNREALIZED
状态。
简言之,一个 OpenSL ES 对象的生命周期,就是从 create 到 destroy 的过程,生命周期的控制,都是通过开发者显示调用来完成的。
1.4.4 常用的对象和结构体
在 OpenSL ES 中,一切 API 的访问和控制都是通过 Interface 来完成的,连 OpenSL ES 里面的 Object 也是通过 SLObjectItf Interface 来访问和使用的。
1) Engine 对象和SLEngineItf 接口
OpenSL ES 里面最核心的对象就是:Engine Object,音频引擎对象,它主要提供如下几个功能:
(1)管理 Audio Engine 的生命周期
(2)提供管理接口: SLEngineItf,该接口可以用来创建所有其他的 Object 对象
(3)提供设备属性查询接口:SLEngineCapabilitiesItf 和 SLAudioIODeviceCapabilitiesItf,这些接口可以查询设备的一些属性信息
Engine Object 对象的创建方法如下:
SLObjectItf engineObject; slCreateEngine( &engineObject, 0, nullptr, 0, nullptr, nullptr );
初始化/销毁:
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); (*engineObject)->Destroy(engineObject);
获取管理接口:
SLEngineItf engineEngine; (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(engineEngine));
2) Media Object
OpenSL ES 里面另一组比较重要的对象就是 Media Object ,代表着多媒体功能的抽象,比如:player、recorder 等等。
我们可以通过 SLEngineItf 提供的 CreateAudioPlayer
方法来创建一个 player 对象实例,可以通过 SLEngineItf 提供的 CreateAudioRecorder
方法来创建一个 recorder 实例。
3) Data Source 和 Data Sink
OpenSL ES 里面,这两个结构体均是作为创建 Media Object 对象时的参数而存在的。
- data source 代表着输入源的信息,即数据从哪儿来、输入的数据参数是怎样的;
- data sink 则代表着输出的信息,即数据输出到哪儿、以什么样的参数来输出。
- 基本定义
DataSource 和DataSink定义如下:
typedef struct SLDataSource_ { void *pLocator; void *pFormat; } SLDataSource; typedef struct SLDataSink_ { void *pLocator; void *pFormat; } SLDataSink;
可以看到这两者的结构体成员相同,都是一个locator和一个format,即资源定位器和资源格式。
Locator的格式定义了以下几种 :
/** Data locator macros */ #define SL_DATALOCATOR_URI ((SLuint32) 0x00000001) //URI类型 #define SL_DATALOCATOR_ADDRESS ((SLuint32) 0x00000002) // #define SL_DATALOCATOR_IODEVICE ((SLuint32) 0x00000003) //IO设备 #define SL_DATALOCATOR_OUTPUTMIX ((SLuint32) 0x00000004) #define SL_DATALOCATOR_RESERVED5 ((SLuint32) 0x00000005) #define SL_DATALOCATOR_BUFFERQUEUE ((SLuint32) 0x00000006)//缓冲区 #define SL_DATALOCATOR_MIDIBUFFERQUEUE ((SLuint32) 0x00000007) #define SL_DATALOCATOR_RESERVED8 ((SLuint32) 0x00000008)
也就是说,Media Object 对象的输入源/输出源,既可以是 URL,也可以 Device,或者来自于缓冲区队列等等,完全是由 Media Object 对象的具体类型和应用场景来配置。
- 示例说明
不同的 Media Object 对象实例,data source 和 data sink 的具体内容是不一样的。
对于Player而言:
image.png
而对于Recorder而言:
image.png
二、代码流程讲解
之前写的一篇音视频开发进阶指南(第四章)-AudioTrack播放PCM,相信大家都可以很容易看懂,因为Java的API非常清晰,方法命名和类型都很直观,这就是OpenSL与AudioTrack学习起来的不同。
一、初始化播放器
先介绍两个概念:创建接口,实例化。OpenSL里面的类型大体分成两种SLObjectItf
和其它类型,前者称为通用类型,其它的称为具体类型。
- 通用类型
SLObjectItf
,这样的需要创建接口并实例化,才能使用;因为你不知道它的具体类型。一般这种接口对象通过CreateXXX
函数来获得 - 具体类型,例如
SLEngineItf
只需要创建接口就能使用,一般具体类型的接口对象通过GetInterface
,该函数需要传入具体的类型ID。