添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
阳光的甜瓜  ·  EntityState Enum ...·  6 月前    · 
安静的牛肉面  ·  响应 CheckBox 单击 - ...·  1 年前    · 
绅士的凉面  ·  Cocos ...·  1 年前    · 

Android音视频【十二】使用OpenSLES和AudioTrack进行播放PCM

介绍
播放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_8BIT 8位。 minBufferSize 是播放器缓冲的大小,也是根据采样率和采样位数,声道数 进行获取,只有满足最小的buffer才去操作底层进程播放。
最后一个参数mode。可以指定的值有 AudioTrack.MODE_STREAM AudioTrack.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。
【相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】

【纯干货免费分享】C++音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击 788280672 加群免费领取哦~


播放进度
因为是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 pcmPlayerPlay pcmPlayerPlay 之后就可以给播放器设置不同的状态比如 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);