音视频开发进阶指南(第四章)-OpenSL-ES播放PCM音频

使用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 则代表着输出的信息,即数据输出到哪儿、以什么样的参数来输出。
  1. 基本定义
    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 对象的具体类型和应用场景来配置。

  1. 示例说明
    不同的 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。

创建接口过程

创建接口有两种方法:

  1. CreateXXX,这种获取的都是通用类型的接口,需要实例化
  2. 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(下)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270