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

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

作者: DD_Dog | 来源:发表于2019-11-22 17:12 被阅读0次

    使用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(下)

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/xjyfwctx.html