人间观察
年龄到了,有些事就妥协了,这个世界上没有人可以随心所欲,生活会逼着你选择答案……最困难的是你什么都改变不了……
介绍
播放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。
播放进度
因为是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);
// 左声道
(*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,自己也是学习网上的一些文章和源码,参考了下网上的代码。仅供学习。