使用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 官方给出的关系图:
由该图可以看出,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的另外一个重要概念就是它的状态机制:
任何一个 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而言:
而对于Recorder而言:
二、代码流程讲解
之前写的一篇音视频开发进阶指南(第四章)-AudioTrack播放PCM,相信大家都可以很容易看懂,因为Java的API非常清晰,方法命名和类型都很直观,这就是OpenSL与AudioTrack学习起来的不同。
一、初始化播放器
先介绍两个概念:创建接口,实例化。OpenSL里面的类型大体分成两种SLObjectItf
和其它类型,前者称为通用类型,其它的称为具体类型。
- 通用类型
SLObjectItf
,这样的需要创建接口并实例化,才能使用;因为你不知道它的具体类型。一般这种接口对象通过CreateXXX
函数来获得 - 具体类型,例如
SLEngineItf
只需要创建接口就能使用,一般具体类型的接口对象通过GetInterface
,该函数需要传入具体的类型ID。
创建接口过程
创建接口有两种方法:
-
CreateXXX
,这种获取的都是通用类型的接口,需要实例化 -
GetInterface
,获取的是具体类型的接口,因为它需要传入接口类型ID,不需要实例化
实例化过程
实例化就是自己给自己实例化,所有类型的实例化是固定的方法:
//obj是通用类型
//第二个参数表示是否异步执行 一般为false
(*obj)->Realize(obj, SL_BOOLEAN_FALSE);
播放的初始化工作是比较麻烦的,参数非常多,关键参数一定要弄清楚,否则不知其所以然。
1.1 引擎对象
想要调用OpenSL的API,它有一个唯一的门口slCreateEngine
,很多文章里叫它引擎,我就叫引擎门口,直观一点,门口里面还有其它的小门口。
SLObjectItf engineObj; //API门口
//1.1获取引擎对象接口
SLresult result = slCreateEngine(&engineObj, 0, 0, 0, 0, 0);
//1.2 SLObjectItf 类型,需要实例化门口引擎对象接口
result = (*engineObj)->Realize(engineObj, SL_BOOLEAN_FALSE);
1.2 获取引擎管理接口
有了引擎对象,接下来就要获取需要的引擎管理接口了,OpenSL有多种引擎管理接口,通过ID区分,例如下面的SL_IID_ENGINE
SLEngineItf engineEngine;
//2.1获取SLEngineItf类型引擎接口,后续操作将会使用这个接口
result = (*engineObj)->GetInterface(engineObj, SL_IID_ENGINE, &engineEngine);
//SLEngineItf 是具体类型不需要实例化
1.3 音频混音
混音器用于将多个音频混合并且输出到喇叭
SLObjectItf outputMixObj;
const SLInterfaceID ids[] = {SL_IID_VOLUME};
const SLboolean req[] = {SL_BOOLEAN_FALSE};
//3.1创建音频输出混音对象接口
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObj, 0, ids, req);
//3.2 SLObjectItf 类型,实例化音频输出混音对象接口
result = (*outputMixObj)->Realize(outputMixObj, SL_BOOLEAN_FALSE);
//3.3 配置输出管道
SLDataLocator_OutputMix outputMixLocator = {SL_DATALOCATOR_OUTPUTMIX, outputMixObj};
SLDataSink outputSink = {&outputMixLocator, NULL};
// 配置输出源
//4.1配置缓冲区Buffer Queue参数
outputLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
//4.2设置音频源的音频格式
SLDataFormat_PCM outputFormat = {
SL_DATAFORMAT_PCM, //指定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 //字节序:小端
};
//4.3输出源
SLDataSource outputSource = {&outputLocator, &outputFormat};
1.4 获取播放器对象门口
播放器门口不是具体执行播放的工具,而是管理播放相关的缓冲,音频格式,混音,输出等
//5.1获取播放器对象接口
SLObjectItf audioPlayerObj;
const SLInterfaceID outputInterfaces[1] = {SL_IID_BUFFERQUEUE};
const SLboolean requireds[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine,
&audioPlayerObj,
&outputSource,//输出源
&outputSink,//输出管道
1,//接口个数
outputInterfaces,//输出接口
requireds); //接口配置
//看到了没,又是SLObjectItf 类型,还得实例化
//5.2实例化播放器对象接口
result = (*audioPlayerObj)->Realize(audioPlayerObj, SL_BOOLEAN_FALSE);
1.5 音频输出对象
音频输出对象就是音频数据本身,具体一点就是存放即将被播放的数据所在的缓冲区
//6.1获取具体音频输出对象接口
SLAndroidSimpleBufferQueueItf outputBufferQueueInterface;
result = (*audioPlayerObj)->GetInterface(audioPlayerObj, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&outputBufferQueueInterface);
//SLAndroidSimpleBufferQueueItf 是具体类型,不用实例化
1.6 具体的播放器对象
它是用来执行播放功能的,其它的条件都给它准备好了
SLPlayItf audioPlayerPlay;
//7.1获取播放器播放对象接口
result = (*audioPlayerObj)->GetInterface(audioPlayerObj, SL_IID_PLAY, &audioPlayerPlay);
if (result != SL_RESULT_SUCCESS) {
LOGD("audioPlayerObj SL_IID_PLAY GetInterface failed,result=%d", result);
return result;
}
//具体类型,不用实例化
1.7 设置回调
回调函数的作用是:通知。
通知什么?在播放的时候,OpenSL不会一次性把所有数据都读到缓冲区,需要用一点,拷贝一点,这个函数就是播放器告诉你,缓存用光了,需要新的数据。
所以在回调函数中需要把新的数据拷贝到缓冲区。
//8.1设置回调
result = (*outputBufferQueueInterface)->RegisterCallback(outputBufferQueueInterface,
PlayCallback,
this);
二、开始播放
//9设置为播放状态
(*audioPlayerPlay)->SetPlayState(audioPlayerPlay, SL_PLAYSTATE_PLAYING);
LOGI("setPlayerState:SL_PLAYSTATE_PLAYING");
//10启动回调机制,开始播放
PlayCallback(outputBufferQueueInterface, this);
三、写数据
前面说了,回调函数中需要填充新的数据:
SLuint32 getPcmData(void **pcm, FILE *pcmFile, uint8_t *out_buffer) {
while (!feof(pcmFile)) {
//因为PCM采样率为44100,采样精度为16BIT,所以一次读取2秒钟的采样
size_t size = fread(out_buffer, 1, 44100 * 2 * 2, pcmFile);
*pcm = out_buffer;
return size;
}
return 0;
}
//当outputBufferQueueInterface中的数据消耗完就会触发回调
void PlayCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {
LOGI("PlayCallback");
//获取数据
SLuint32 size = getPcmData(&readPCMBuffer, pcmFile, tempBuffer);
LOGI("PlayCallback, size=%d", size);
if (NULL != readPCMBuffer && size > 0) {
SLresult result = (*outputBufferQueueInterface)->Enqueue(outputBufferQueueInterface,
readPCMBuffer, size);
}
}
停止播放
//11.停止播放
(*audioPlayerPlay)->SetPlayState(audioPlayerPlay, SL_PLAYSTATE_STOPPED);
释放OpenSL ES资源
只需要销毁OpenSL ES对象,接口不需要做Destroy处理
(*engineObj)->Destroy(engineObj);
(*outputMixObj)->Destroy(outputMixObj);
(*audioPlayerObj)->Destroy(audioPlayerObj);
源码Demo地址github
参考
Android音频开发(6):使用 OpenSL ES API(上)
Android音频开发(7):使用 OpenSL ES API(下)