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_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 
#include 

创建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(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和AudioTrack进行播放PCM)