OpenSL ES播放PCM文件(二)

作者: 仙人掌__ | 来源:发表于2018-12-03 18:24 被阅读0次

    前言

    前面学习了如何通过AudioTrack渲染PCM音频数据,本文学习如何通过通过OpenSL ES播放PCM文件中的裸音频;OpenSL ES是垮平台的音频处理库,它处于native层,灵活性比较大。

    目标

    实现OpenSL ES渲染PCM数据,这里播放的PCM文件是通过ffmpeg解码到文件后的裸数据进行模拟

    准备

    1、笔者这里开发环境为Mac 10.14.4,android studio为3.3.2;
    2、配置android studio中的c++环境,具体参考官网,在这里贴一下我这边的Cmakelists.txt配置文件

    cmake_minimum_required(VERSION 3.4.1)
    
    # ding yi cpp yuan wen jian mulu bian liang
    set(SRC_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp)
    set(COMMON_DIR ${SRC_DIR}/common)
    set(OPENSLES_DIR ${SRC_DIR}/opensles)
    
    # she zhi cpp yuan ma mu lu
    aux_source_directory(${SRC_DIR} src_cpp)
    aux_source_directory(${COMMON_DIR} com_cpp)
    aux_source_directory(${OPENSLES_DIR} opensles_cpp)
    
    # she zhi .h tou wen jian mu lu
    include_directories(${COMMON_DIR})
    include_directories(${OPENSLES_DIR})
    
    # yin ru android log ku
    find_library( # Sets the name of the path variable.
                  log-lib
    
                  # Specifies the name of the NDK library that
                  # you want CMake to locate.
                  log )
    
    # ding yi jiang gai ku da bao jin app de lei xing
    add_library(adMedia SHARED
            ${src_cpp}
            ${com_cpp}
            ${opensles_cpp}
            )
    target_link_libraries(adMedia android log
            OpenSLES
            mediandk
            ${log-lib}
        )
    

    app下的build.gradle的c++部分配置代码

    android {
        compileSdkVersion 28
        defaultConfig {
            applicationId "com.media"
            minSdkVersion 23
            targetSdkVersion 28
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
            // 对于abi的这个配置,一定要放在defaultConfig{}里面,否则会报错
            ndk {
                abiFilters 'armeabi-v7a','arm64-v8a'
            }
        }
        externalNativeBuild {
            cmake {
                path file('CMakeLists.txt')
            }
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    }
    

    对于abi的配置,一定要放在defaultConfig{}里面,否则会报错,如上注释。

    使用OpenSL ES

    必须要引用头文件

    // 这是标准的OpenSL ES库
    #include <SLES/OpenSLES.h>
    // 这里是针对安卓的扩展,如果要垮平台则需要注意
    #include <SLES/OpenSLES_Android.h>
    

    1、初始化OpenSL ES引擎对象

        // 用于OpenSL ES的各种操作结果;SL_RESULT_SUCCESS表示成功
        SLresult  result;
    
        // 1、创建OpenSL ES对象
        // OpenSL ES for Android is designed to be thread-safe,
        // so this option request will be ignored, but it will
        // make the source code portable to other platforms.
        SLEngineOption options[] = {{SL_ENGINEOPTION_THREADSAFE,SL_BOOLEAN_TRUE}};
        // 创建引擎对象
        result = slCreateEngine(&slObject,
                       ARRAY_LEN(options),
                       options,
                       0, // no interfaces
                       NULL,// no interfaces
                       NULL // no required
        );
        if (result != SL_RESULT_SUCCESS) {
            LOGD("slCreateEngine fail %lu",result);
        }
    

    对于slObject对象,安卓默认创建时就是线程安全的,不需要options[]这个选项。
    2、实例化对象

    // 2、实例化SL对象。就像声明了类变量,还需要初始化一个实例;阻塞方式初始化实例(第二个参数表示阻塞还是非阻塞方式)
        result = (*slObject)->Realize(slObject, SL_BOOLEAN_FALSE);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("Realize fail %lu",result);
        }
    

    3、获取引擎对象接口

    // 3、获取引擎接口对象,必须在SLObjectItf对象实例化以后获取
        result = (*slObject)->GetInterface(slObject, SL_IID_ENGINE, &engineObject);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("GetInterface fail %lu",result);
        }
    

    可以看到,OpenSL ES类,实例,接口三者的使用规律。首先创建类,然后初始化一个改类的实例,然后再获取该类的接口,接口对应着一个具体的功能。后面要使用的音频播放组件也是这个流程
    要想正常播放音频,还需要两个相关组件,分别是混音器,音频组件。这里介绍一下,混音器组件才是正在实现播放音频功能的,音频组件负责通过回调从app获取音频数据。下面分别介绍如何初始化混音器和将混音器与音频组件串联起来
    4、混音器

     /** 第二步 创建混音器;她可能不仅仅代表的是声音效果器,它默认是进行音频的播放输出,音频播放必不可少的组件;
         * CreateOutputMix()参数说明如下:
         * 参数1:引擎接口对象
         * 参数2:混音器对象地址
         * 参数3:组件的可配置属性ID个数;如果为0后面两个参数忽略;不同的组件,所拥有的属性种类和个数也不一样,如果某个组件不支持某个属性,GetInterface将
         * 返回SL_RESULT_FEATURE_UNSUPPORTED
         * 参数4:需要配置的属性ID,数组
         * 参数5:配置的这些属性ID对于组件来说是否必须,数组,与参数4一一对应
         * */
        // 这里只是进行简单的音频播放输出,所以不创建任何混音效果的属性ID,第三个参数传0即可
        result = (*engineItf)->CreateOutputMix(engineItf,&outputMixObject,0,NULL,NULL);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("CreateOutputMix fail %d",result);
        }
        // 实例化混响器,创建功能组件类型后还需要实例化;OpenSL ES
        result = (*outputMixObject)->Realize(outputMixObject,SL_BOOLEAN_FALSE);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("Realize outputMixObject fail %d",result);
        }
    

    5、创建音频组件并和混音器串联起来

    /** SLDataLocator_AndroidSimpleBufferQueue 表示数据缓冲区的结构,用来表示一块缓冲区
         * 1、第一个参数必须是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE
         * 2、第二个参数表示队列中缓冲区的个数,这里测试1 2 3 4都可以正常。
         * */
        SLDataLocator_AndroidSimpleBufferQueue android_queue={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
        SLuint32 chs = getChannel_layout_Channels(fChannel_layout);
        SLuint32 ch = getChannel_layout_Type(fChannel_layout);
        SLuint32 sr = getSampleRate(fSample_rate);
        SLuint32 fo = getPCMSample_format(fSample_format);
        LOGD("声道数 %d 声道类型 %d 采样率 %d 采样格式 %d",chs,ch,sr,fo);
        SLDataFormat_PCM pcm={
                SL_DATAFORMAT_PCM,//播放pcm格式的数据
                chs,//声道个数
                sr,//采样率
                fo,//位数
                fo,//和位数一致就行
                ch,//立体声(前左前右)
                SL_BYTEORDER_LITTLEENDIAN//数据存储是小端序
        };
        /** SLDataSource 表示输入缓冲区,和输出缓冲区一样,它由数据类型和数据格式组成
         * 1、参数1;指向指定的数据缓冲区,SLDataLocator_xxx结构体定义,可取值如下:
         *      SLDataLocator_Address
         *      SLDataLocator_BufferQueue   (一块数据缓冲区,对于安卓来说是SLDataLocator_AndroidSimpleBufferQueue)
         *      SLDataLocator_IODevice
         *      SLDataLocator_MIDIBufferQueue
         *      SLDataLocator_URI
         * 2、参数2;表示缓冲区中数据的格式,可取值如下:
         *      SLDataFormat_PCM
         *      SLDataFormat_MIME
         *   其中如果第一个参数是SLDataLocator_IODevice,此参数忽略,传NULL即可
         * */
        SLDataSource slDataSource = {&android_queue, &pcm};
    
        SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX,outputMixObject};
        /** SLDataSink 表示了输出缓冲区,包括数据类型和数据格式
         * 1、参数1;指向指定的数据缓冲区类型,一般由SLDataLocator_xxx结构体定义,可取值如下:
         *      SLDataLocator_Address
         *      SLDataLocator_IODevice
         *      SLDataLocator_OutputMix         (混音器,代表着音频输出)
         *      SLDataLocator_URI
         *      SLDataLocator_BufferQueue       (一块数据缓冲区,对于安卓来说是SLDataLocator_AndroidSimpleBufferQueue)
         *      SLDataLocator_MIDIBufferQueue
         * 2、参数2;表示缓冲区中数据的格式,可取值如下:
         *      SLDataFormat_PCM
         *      SLDataFormat_MIME
         *    其中如果第一个参数是SLDataLocator_IODevice或SLDataLocator_OutputMix,此参数忽略,传NULL即可
         * 住:我们要做的只是给定缓冲区类型和缓冲区中数据格式即可,系统自动为我们分配内存
         * */
        SLDataSink audioSink = {&outputMix, NULL};
    
    
        /** 第三步,创建AudioPlayer组件;该组件必须要有输入数据缓冲区(提供要播放的音频数据),输出数据缓冲区(硬件最终从这里获取数据)、
         * 一个音频数据中间缓冲区(由于应用无法直接向输入数据缓冲区写入数,是通过这个缓冲区间接写入的,所以必须要有这样一个缓冲区)和可选属性(比如控制音量等等)
         *  参数1:openSL es引擎接口
         *  参数2:AudioPlayer组件对象
         *  参数3:输入缓冲区地址
         *  参数4:输出缓冲区地址
         *  参数5:属性ID个数
         *  参数6:属性ID数组
         *  参数7:属性ID是否必须数组
         *  备注:可以看到 输入和输出缓冲区是通过参数3和4直接配置,而音频数据中间缓冲区是通过属性ID方式配置
         *
         *  audio player的数据驱动流程为:首先播放系统从audioSnk要数据进行播放,而audioSnk又从audioSrc要音频数据,audioSrc中数据是通过ids1中配置的回调函数不停的往
         *  其中写入数据,这样整个播放流程就理顺了。
         *  1、CreateAudioPlayer()第五个参数不能为0,否则audioSrc将没有数据送给audioSnk
         *  2、要写入数据的回调函数在单独的线程中,大概每12ms-20ms定期调用。
         * */
        const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
        const SLboolean req[1] = {SL_BOOLEAN_TRUE};
        result = (*engineItf)->CreateAudioPlayer(engineItf, &playerObject, &slDataSource, &audioSink, 1, ids, req);
    result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
    //    注册回调缓冲区 获取缓冲队列接口
        result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueueItf);
        //注册缓冲接口回调,开始播放后,此函数将
        result = (*pcmBufferQueueItf)->RegisterCallback(pcmBufferQueueItf, SLAudioPlayer::pcmBufferCallBack, this);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("RegisterCallback fail %d",result);
        }
    
        // 获取音量接口 用于设置音量
    //    (*playerObject)->GetInterface(playerObject, SL_IID_VOLUME, &volInf);
    //    (*volInf)->SetVolumeLevel(volInf,100*50);
    
        //    得到接口后调用  获取Player接口
        result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerInf);
    //    开始播放
        result = (*playerInf)->SetPlayState(playerInf, SL_PLAYSTATE_PLAYING);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("SetPlayState fail %d",result);
        }
    

    整个串联的过程为:
    首先播放系统从audioSnk要数据进行播放,而audioSnk又从audioSrc要音频数据,audioSrc中数据是通过ids1中配置的回调函数pcmBufferCallBack不停的往其中写入数据,这样整个播放流程就理顺了。
    6、app向OpenSL ES输送数据
    和AudioTrack一样,必须开启一个单独的线程作为app向OpenSL ES输送数据的工作线程,这里要注意的就是OpenSL ES只支持16位和32位的整数型音频数据,小端序。回调函数pcmBufferCallBack是一个不同于这里工作线程的渲染线程,所以这里要有线程同步的机制,这里用pthread_mutex_t和pthread_con_t实现。

    /**  outBufSamples:是一个固定的数值;表示每次发往音频数据缓冲区的采样个数
      *  outputBuffer:是一个包含两个缓冲区的内存块,这是一个巧妙的设计。即保证了不频繁创建内存,又保证了内存中数据的安全。(具体过程为:当一个缓冲区数据通过Enqueue
      *  函数输送给OpenSL ES时,那么应用端再没有收到OpenSL ES缓冲区回调接口需要数据的回调时,此内存块就不能再使用,所以就用另外一块缓冲区,如果另外一块缓冲区也被
      *  应用填满了数据还没有收到OpenSL ES缓冲区回调接口需要数据的回调怎么办?说明OpenSL ES比较忙,暂时不需要数据,应用端一直阻塞知直到收到通知)
      *  currentOutputIndex:表示目前应用端填充数据的缓冲区下个填充数据位置
      *  currentOutputBuffer:表示目前应用端使用的是哪一个缓冲区
     **/
    void SLAudioPlayer::putAudioData(char * buff,int size)
    {
        // 从上一次位置开始填充数据
        int indexOfOutput = currentOutputIndex;
    
        if (fSample_format == Sample_format_SignedInteger_8) {
            char *newBuffer = (char *)buff;
            char *useBuffer = outputBuffer[currentOutputBuffer];
    
            for (int i = 0; i < size; ++i) {
                useBuffer[indexOfOutput++] = newBuffer[i];
                if (indexOfOutput >= outBufSamples) {   // 说明填满了一个缓冲区,则发送给OpenSL ES
    
                    // 发送之前需要得到可以发送的条件
                    waitThreadLock();
    
                    // 发送数据
                    (*pcmBufferQueueItf)->Enqueue(pcmBufferQueueItf,useBuffer,outBufSamples* sizeof(char));
    
                    // 换成另外一个缓冲区
                    currentOutputBuffer = currentOutputBuffer?0:1;
                    indexOfOutput = 0;
                    useBuffer = outputBuffer[currentOutputBuffer];
                }
            }
    
            // 更新上一次位置
            currentOutputIndex = indexOfOutput;
    
        } else if (fSample_format == Sample_format_SignedInteger_16) {
            short *newBuffer = (short *)buff;
            short *useBuffer = (short *)outputBuffer[currentOutputBuffer];
    
            for (int i = 0; i < size; ++i) {
                useBuffer[indexOfOutput++] = newBuffer[i];
                if (indexOfOutput >= outBufSamples) {   // 说明填满了一个缓冲区,则发送给OpenSL ES
                    LOGD("被阻塞");
                    // 发送之前需要得到可以发送的条件
                    waitThreadLock();
                    LOGD("阻塞完成");
                    // 发送数据
                    (*pcmBufferQueueItf)->Enqueue(pcmBufferQueueItf,useBuffer,outBufSamples* sizeof(short));
    
                    // 换成另外一个缓冲区
                    currentOutputBuffer = currentOutputBuffer?0:1;
                    indexOfOutput = 0;
                    useBuffer = (short *)outputBuffer[currentOutputBuffer];
                }
            }
    
            // 更新上一次位置
            currentOutputIndex = indexOfOutput;
    
        } else if (fSample_format == Sample_format_SignedInteger_32) {
            int *newBuffer = (int *)buff;
            int *useBuffer = (int *)outputBuffer[currentOutputBuffer];
    
            for (int i = 0; i < size; ++i) {
                useBuffer[indexOfOutput++] = newBuffer[i];
                if (indexOfOutput >= outBufSamples) {   // 说明填满了一个缓冲区,则发送给OpenSL ES
    
                    // 发送之前需要得到可以发送的条件
                    waitThreadLock();
    
                    // 发送数据
                    (*pcmBufferQueueItf)->Enqueue(pcmBufferQueueItf,useBuffer,outBufSamples* sizeof(int));
    
                    // 换成另外一个缓冲区
                    currentOutputBuffer = currentOutputBuffer?0:1;
                    indexOfOutput = 0;
                    useBuffer = (int*)outputBuffer[currentOutputBuffer];
                }
            }
    
            // 更新上一次位置
            currentOutputIndex = indexOfOutput;
        }
    }
    

    7、释放资源

    ** 释放 OpenSL ES资源,
     *  由于OpenSL ES的内存是由系统自动分配的,所以释放内存时只需要调用Destroy释放对应的SLObjectItf对象即可,然后它的属性ID接口对象直接置为NULL
     * */
    void SLAudioPlayer::closeAudioPlayer() {
        LOGD("closeAudioPlayer()");
        // 释放音频播放器组件对象
        if (playerObject != NULL) {
            (*playerObject)->Destroy(playerObject);
            playerObject = NULL;
            playerInf = NULL;
            volInf = NULL;
            pcmBufferQueueItf = NULL;
        }
    
        // 释放混音器组件对象
        if (outputMixObject != NULL) {
            (*outputMixObject)->Destroy(outputMixObject);
            outputMixObject = NULL;
        }
    
        // 释放OpenSL ES引擎对象
        if (slContext != NULL) {
            slContext->releaseResources();
            slContext = NULL;
        }
    
        destroyBuffers();
        destroyThreadLock();
    }
    

    总结:

    audio player的数据驱动流程为:首先播放系统从audioSnk要数据进行播放,而audioSnk又从audioSrc要音频数据,audioSrc中数据是通过ids1中配置的回调函数不停的往其中写入数据,这样整个播放流程就理顺了。

    项目地址

    Demo
    )

    遇到问题

    1、在项目中引入c/c++文件时,需要添加CMakelists.txt文件,并且在build.gradle进行相关配置。定义api时,遇到了上面问题
    解决方案:
    通过查阅官方文档,发现要将ndk写在defaultConfig里面解决此问题,如下:


    image.png

    相关文档

    安卓官网
    官网示例代码

    OpenSL ES官网

    相关文章

      网友评论

        本文标题:OpenSL ES播放PCM文件(二)

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