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 <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,自己也是学习网上的一些文章和源码,参考了下网上的代码。仅供学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容