美文网首页Android 音视频android技术收藏Android音视频系列
Android音视频【十二】使用OpenSLES和AudioTr

Android音视频【十二】使用OpenSLES和AudioTr

作者: 后厂村追寻 | 来源:发表于2021-04-24 14:20 被阅读0次

    人间观察
    年龄到了,有些事就妥协了,这个世界上没有人可以随心所欲,生活会逼着你选择答案……最困难的是你什么都改变不了……

    介绍

    播放pcm的两种方式

    本节我们学习下如何播放pcm数据,在Android中有两种方法:一种是使用java层的AudioTrack方法,一种是使用底层的OpenSLES直接在jni层调用系统的OpenSLES的c方法实现。

    使用场景

    两种使用场景不一样:
    AudioTrack 一般用于 比如本地播放一个pcm文件/流,又或者播放解码后的音频的pcm流,API较简单。
    OpenSLES 一般用于一些播放器中开发中,比如音频/视频播放器,声音/音频的播放采用的OpenSLES,一是播放器一般是c/c++实现,便于直接在c层调用OpenSLES的API,二也是如果用AudioTrack进行播放,务必会带来java和jni层的反射调用的开销,API较复杂。

    可以根据业务自行决定来进行选择。

    一.AudioTrack方式

    AudioTrack的方式使用较简单,直接在java层。

    初始化

    指定采样率,采样位数,声道数进行创建。

    需要注意的是比如数据是解码后的pcm数据,如果每次的采样率或者采样位数或者声道数和上次的不一样,你需要销毁重建AudioTrack,因为AudioTrack并没有提供动态修改采样率,采样位数,声道数的方法,它只能在构造方法中指定。

    public void initAudioTrack() {
        int minBufferSize = AudioTrack.getMinBufferSize(44100,
                AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
        audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                44100,
                AudioFormat.CHANNEL_OUT_STEREO,
                AudioFormat.ENCODING_PCM_16BIT,
                minBufferSize,
                AudioTrack.MODE_STREAM);
        audioTrack.play();
    }
    

    其中44100是采样率,AudioFormat.CHANNEL_OUT_STEREO为双声道,还有CHANNEL_OUT_MONO单声道。AudioFormat.ENCODING_PCM_16BIT为采样位数16位,还有ENCODING_PCM_8BIT8位。minBufferSize是播放器缓冲的大小,也是根据采样率和采样位数,声道数 进行获取,只有满足最小的buffer才去操作底层进程播放。

    最后一个参数mode。可以指定的值有AudioTrack.MODE_STREAMAudioTrack.MODE_STATIC

    MODE_STREAM 适用于大多数的场景,比如动态的处理audio buffer,或者播放很长的音频文件,它是将audio buffers从java层传递到native层。音频播放时音频数据从Java流式传输到native层的创建模式。

    MODE_STATIC 适用场景,比如播放很短的音频,它是一次性将全部的音频资源从java传递到native层。音频数据在音频开始播放前仅从Java传输到native层的创建模式。

    写入数据进行播放

    public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {}
    

    audioData 就是要播放的pcm数据
    offsetInBytes audioData字节数组的的开始位置
    sizeInBytes 要写入audioData字节数组的大小
    返回值 ,真实写入的字节数

    是的,就这么一个方法。注意此方法是同步方法,是个耗时方法,一般是开启一个线程循环调用write方法进行写入。
    注意在调用write方法前需要调用 audioTrack.play()方法开始播放。

    暂停销毁等其他方法

    mAudioTrack.pause(); // 暂停,注意下次恢复播放,需要重新调用play方法,然后循坏调用write写入暂停后的数据即可
    mAudioTrack.flush(); //  清空丢掉当前排队播放的音频数据
    mAudioTrack.stop(); // 停止播放音频数据
    mAudioTrack.release();// 销毁播放器
    mAudioTrack.setStereoVolume(volume, volume); 音量设置,范围[0-1]
    mAudioTrack.setVolume(float gain) 设置此轨道所有通道上的指定输出增益值。
    

    更多的API可以参考官网开发文档。需要注意的是在有些手机上pause耗时,甚至耗时1s。

    播放进度

    因为是pcm裸数据,无法像mediaplayer一样提供了API。所以需要自己处理下。可以利用getPlaybackHeadPosition方法。

    getPlaybackHeadPosition()的意思是返回以帧为单位表示的播放头位置
    getPlaybackRate()的意思是返回以Hz为单位返回当前播放采样率。

    所以当前播放时间可以通过如下方式获取

    int currentFrame = mAudioTrack.getPlaybackHeadPosition();
    LogUtil.dc(TAG, "currentFrame=" + currentFrame);
    int rate = mAudioTrack.getPlaybackRate();
    if (rate > 0) {
        float playTime = currentFrame * 1.0f / rate;
        currentPlayTimeMs = (long) (1000 * playTime);
        LogUtil.dc(TAG, "currentPlayTimeMs=" + currentPlayTimeMs);
    }
    

    二.OpenSLES方式

    OpenSLES:(Open Sound Library for Embedded Systems).
    OpenSLES是跨平台是针对嵌入式系统精心优化的硬件音频加速API。使用OpenSLES进行音频播放的好处是可以不依赖第三方。比如一些音频或者视频播放器中都是用OpenSLES进行播放解码后的pcm的,这样免去了和java层的交互。

    使用OpenSLES

    在Android中使用OpenSLES首先需要把Android 系统提供的so链接到外面自己的so。在CMakeLists.txt脚本中添加链接库OpenSLES。库的名字可以在 类似如下目录中

    /Users/guixiuzhong/Library/Android/sdk/ndk/21.1.6352462/platforms/android-19/arch-x86/usr/lib/libOpenSLES.so
    

    需要去掉lib

    target_link_libraries(
                    OpenSLES
       // ...省略其它
            )
    

    然后导入头文件即可使用了OpenSLES提供的底层方法了。

    #include <SLES/OpenSLES.h>
    #include <SLES/OpenSLES_Android.h>
    

    创建OpenSLES

    创建&使用的步骤大致分为:

    • 创建引擎 获取SLEngineItf
    • 创建并设置混音器
    • 创建并设置播放器
    • 注册播放器回调并写入播放缓冲区队列
    • 其它操作播放的方法,比如暂停,音量设置,声道设置

    创建引擎 获取SLEngineItf

        SLresult result;
        result = slCreateEngine(&engineObject, 0, 0, 0, 0, 0);
        if (result != SL_RESULT_SUCCESS)
            return;
        result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
        if (result != SL_RESULT_SUCCESS)
            return;
        result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
        if (result != SL_RESULT_SUCCESS)
            return;
        if (engineEngine) {
            LOGD("get SLEngineItf success");
        } else {
            LOGE("get SLEngineItf failed");
        }
    
    • 创建引擎。使用slCreateEngine 第一个参数是要创建的引擎对象,是一个SLObjectItf类型。返回值是SLresult类型,如果成功则返回SL_RESULT_SUCCESS,其他参数都传0即可。
    • 创建引擎成功后必须先调用Realize方法做初始化(*slObjectItf)->Realize(),实例化成功则返回SL_RESULT_SUCCESS
    • 引擎实例化之后从引擎对象获取接口。
        SLresult (*GetInterface) (
            SLObjectItf self,  //实例化后的引擎对象
            const SLInterfaceID iid, //SL_IID_ENGINE
            void * pInterface  //输出的接口对象指针
        );
    

    一个SLObjectItf里面可能包含了多个Interface,获取Interface通过GetInterface方法,而GetInterface方法的地2个参数SLInterfaceID参数来指定到的需要获取Object里面的那个Interface。比如通过指定SL_IID_ENGINE的类型来获取SLEngineItf。我们可以通过SLEngineItf去创建各种Object,例如播放器、录音器、混音器的Object,然后在用这些Object去获取各种Interface去实现各种功能。

    创建混音器

    如上所说,SLEngineItf可以创建混音器的Object。

    • 创建混音器。
    const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
    result = (*engineEngine)->CreateOutputMix(
    engineEngine, //引擎接口
     &outputMixObject,  //输出的混音器
     1, mids, mreq);
    if (result != SL_RESULT_SUCCESS) {
        LOGE("CreateOutputMix failed");
        return;
    } else {
        LOGD("CreateOutputMix success");
    }
    
    • 实例化混音器。拿到SLObjectItf 类型的实例化的混音器。
    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if (result != SL_RESULT_SUCCESS) {
        LOGE("mixer init failed");
    } else {
        LOGD("mixer init success");
    }
    
    • 实例化混音器后也可以通过混音器的GetInterface方法来调用接口等。

    配置音频信息

    在创建播放器前需要创建音频的配置信息(比如采样率,声道数,每个采样的位数等)

     //音频格式
        SLDataFormat_PCM pcmFormat = {
                SL_DATAFORMAT_PCM, //播放pcm格式的数据
                2,   //声道数
                static_cast<SLuint32>(getCurrentSampleRateForOpensles(sample_rate)),
                SL_PCMSAMPLEFORMAT_FIXED_16, //位数 16位
                SL_PCMSAMPLEFORMAT_FIXED_16, //和位数一致就行
                SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, //立体声(前左前右)
                //字节序,小端
                SL_BYTEORDER_LITTLEENDIAN
        };
    

    创建播放器

    • 通过 引擎(*engineEngine)->CreateAudioPlayer 方法来创建播放器。
    result = (*engineEngine)->CreateAudioPlayer(
    engineEngine,  //引擎对象本身
    &pcmPlayerObject, //输出的播放器对象,同样是SLObjectItf类型
    &slDataSource, //数据的来源
    &slDataSink,  //数据的去处,和SLDataSource是相对的
    sizeof(ids) / sizeof(SLInterfaceID), //与下面的SLInterfaceID和SLboolean配合使用,用于标记SLInterfaceID数组和SLboolean的大小
    ids,//这里需要传入一个数组,指定创建的播放器会包含哪些Interface
    req//这里也是一个数组,用来标记每个需要包含的Interface);
    
    • 获取播放器接口
      (*pcmPlayerObject)->GetInterface(slPlayerItf, SL_IID_PLAY, &pcmPlayerPlay);得到播放器接口SLPlayItf pcmPlayerPlaypcmPlayerPlay 之后就可以给播放器设置不同的状态比如SL_PLAYSTATE_PAUSED进行播放暂停等操作,后文介绍。
        SLresult (*GetInterface) (
            SLObjectItf self, //实例化后的播放器对象
            const SLInterfaceID iid,  //SL_IID_PLAY
            void * pInterface //输出的接口对象指针
        );
    
    • 获取播放队列接口
         result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
    
    • 给播放队列注册回调函数。

    开始播放后会不断的回调这个pcmBufferCallBack函数将音频数据压入队列
    (*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, this);

        // OpenSLES 会自动回调
    void pcmBufferCallBack(SLAndroidSimpleBufferQueueItf bf, void *context) {
    //    LOGD("pcmBufferCallBack ok");
    
        Audio *audio = (Audio *) context;
        if (audio != NULL) {
            PcmData *data = audio->dataQueue->getPcmData();
            if (NULL != data) {
                LOGD("Enqueue ok");
                (*audio->pcmBufferQueue)->Enqueue(audio->pcmBufferQueue,
                                                  data->getData(),
                                                  data->getSize());
            }
        }
    }
    
    • 设置播放状态为播放中
        //设置播放状态
        (*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);
    

    如果想要暂停播放参数直接设置为SL_PLAYSTATE_PAUSED,若暂停后继续播放设置参数为SL_PLAYSTATE_PLAYING即可。若想要停止播放参数设置为SL_PLAYSTATE_STOPPED即可。

    • 开始播放
      需要手动调用一次 (*pcmBufferQueue)->Enqueue,也就是可以直接调用下 pcmBufferCallBack(pcmBufferQueue, this);

    OpenSLES的音量控制

    首先获取播放器的用于控制音量的接口SLVolumeItf pcmVolumePlay

    // 音量
    (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_VOLUME, &pcmVolumePlay);
    

    然后动态设置

    // 声音0是最大声音,-5000就听不见了
    // 音量 0 是最大,负值是越来越小。
    float v = (1.0f - volume * 1.0f / 100.0f) * -5000;
    LOGD("volume %f", v);
    (*pcmVolumePlay)->SetVolumeLevel(pcmVolumePlay, (SLmillibel) v);
    

    OpenSLES的声道控制

    首先也是获取播放器的用于控制音量的接口SLMuteSoloItf pcmMutePlay

     // 获取声道操作接口
    (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_MUTESOLO, &pcmMutePlay);
    

    然后动态设置

    // 立体声
    (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 1, false);
    (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 0, false);
    // 左声道
     (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 1, true);
    (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 0, false);
    // 右声道
    (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 1, false);
    (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 0, true);
    

    看起来控制还是蛮简单的哈。先熟悉这么多,OpenSLES还是蛮强大的。

    完整的源码

    https://github.com/ta893115871/PCMPlay

    备注, OpenSLES的方式进行播放pcm,自己也是学习网上的一些文章和源码,参考了下网上的代码。仅供学习。

    相关文章

      网友评论

        本文标题:Android音视频【十二】使用OpenSLES和AudioTr

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