音视频开发进阶指南(第四章)-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 官方给出的关系图:

40.png

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的另外一个重要概念就是它的状态机制:


41.png

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而言:
    42.png



image.png

而对于Recorder而言:


43.png

image.png

二、代码流程讲解


之前写的一篇音视频开发进阶指南(第四章)-AudioTrack播放PCM,相信大家都可以很容易看懂,因为Java的API非常清晰,方法命名和类型都很直观,这就是OpenSL与AudioTrack学习起来的不同。


一、初始化播放器

先介绍两个概念:创建接口,实例化。OpenSL里面的类型大体分成两种SLObjectItf和其它类型,前者称为通用类型,其它的称为具体类型。

  • 通用类型SLObjectItf,这样的需要创建接口并实例化,才能使用;因为你不知道它的具体类型。一般这种接口对象通过CreateXXX函数来获得
  • 具体类型,例如SLEngineItf只需要创建接口就能使用,一般具体类型的接口对象通过GetInterface,该函数需要传入具体的类型ID。
目录
相关文章
|
机器学习/深度学习 编解码 语音技术
音频基础知识 2
音频基础知识
313 0
|
编解码 API 语音技术
Opus从入门到精通(七)Opus编码基础之认识声音
前面我们分析完Opus的编解码api使用,封装原理等,接下来我们准备分析Opus编码原理.Opus编码是一个复杂的工作,我们需要做一些基本铺垫,包括认识声音,压缩编码基础.认识音频有助于我们了解音频特征,不仅对语音有助于我们理解编码技术,同时在语音识别,TTS等场景提供帮助
569 0
Opus从入门到精通(七)Opus编码基础之认识声音
|
存储 编解码 算法
音视频之音频知识入门
信息论的观点来看,描述信源的数据是信息和数据冗余之和,即:数据=信息+数据冗余。音频信号在时域和频域上具有相关性,也即存在数据冗余。将音频作为一个信源,音频编码的实质是减少音频中的冗余。自然界中的声音非常复杂,波形极其复杂,通常我们采用的是脉冲代码调制编码,即PCM编码。PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。
723 0
|
1月前
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
56 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
6月前
|
安全 数据处理 数据格式
深入浅出:FFmpeg 音频解码与处理AVFrame全解析(三)
深入浅出:FFmpeg 音频解码与处理AVFrame全解析
297 0
|
6月前
|
缓存 编解码
FFmpeg开发笔记(十四)FFmpeg音频重采样的缓存
FFmpeg在视频流重编码和音频重采样中使用缓存机制。在音频文件格式转换时,特别是对于帧长度不固定的格式如ogg、amr、wma,需处理重采样缓存。通过调用`swr_convert`,传入空输入和0大小来清空缓存。在`swrmp3.c`中,修改帧样本数处理,并在循环结束后添加代码以冲刷缓存。编译并运行程序,将ogg文件重采样为MP3,日志显示操作成功,播放转换后的文件确认功能正常。
129 7
FFmpeg开发笔记(十四)FFmpeg音频重采样的缓存
|
6月前
|
存储 缓存 算法
ffmpeg 音视频同步进阶 剖析:ffmpeg音视频同步中特殊情况处理策略
ffmpeg 音视频同步进阶 剖析:ffmpeg音视频同步中特殊情况处理策略
163 0
|
6月前
|
存储 编解码 数据处理
深入浅出:FFmpeg 音频解码与处理AVFrame全解析(二)
深入浅出:FFmpeg 音频解码与处理AVFrame全解析
496 0
|
6月前
|
存储 编解码 算法
深入浅出:FFmpeg 音频解码与处理AVFrame全解析(一)
深入浅出:FFmpeg 音频解码与处理AVFrame全解析
1317 0
|
6月前
|
存储 编解码 算法
声音的变奏:深入理解ffmpeg音频格式转换的奥秘与应用(一)
声音的变奏:深入理解ffmpeg音频格式转换的奥秘与应用
290 0