人間觀察
年齡到了,有些事就妥協(xié)了,這個世界上沒有人可以隨心所欲,生活會逼著你選擇答案……最困難的是你什么都改變不了……
介紹
播放pcm的兩種方式
本節(jié)我們學(xué)習(xí)下如何播放pcm數(shù)據(jù),在Android中有兩種方法:一種是使用java層的AudioTrack方法,一種是使用底層的OpenSLES直接在jni層調(diào)用系統(tǒng)的OpenSLES的c方法實現(xiàn)。
使用場景
兩種使用場景不一樣:
AudioTrack 一般用于 比如本地播放一個pcm文件/流,又或者播放解碼后的音頻的pcm流,API較簡單。
OpenSLES 一般用于一些播放器中開發(fā)中,比如音頻/視頻播放器,聲音/音頻的播放采用的OpenSLES,一是播放器一般是c/c++實現(xiàn),便于直接在c層調(diào)用OpenSLES的API,二也是如果用AudioTrack進行播放,務(wù)必會帶來java和jni層的反射調(diào)用的開銷,API較復(fù)雜。
可以根據(jù)業(yè)務(wù)自行決定來進行選擇。
一.AudioTrack方式
AudioTrack的方式使用較簡單,直接在java層。
初始化
指定采樣率,采樣位數(shù),聲道數(shù)進行創(chuàng)建。
需要注意的是比如數(shù)據(jù)是解碼后的pcm數(shù)據(jù),如果每次的采樣率或者采樣位數(shù)或者聲道數(shù)和上次的不一樣,你需要銷毀重建AudioTrack,因為AudioTrack并沒有提供動態(tài)修改采樣率,采樣位數(shù),聲道數(shù)的方法,它只能在構(gòu)造方法中指定。
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為采樣位數(shù)16位,還有ENCODING_PCM_8BIT8位。minBufferSize是播放器緩沖的大小,也是根據(jù)采樣率和采樣位數(shù),聲道數(shù) 進行獲取,只有滿足最小的buffer才去操作底層進程播放。
最后一個參數(shù)mode??梢灾付ǖ闹涤?code>AudioTrack.MODE_STREAM和AudioTrack.MODE_STATIC。
MODE_STREAM 適用于大多數(shù)的場景,比如動態(tài)的處理audio buffer,或者播放很長的音頻文件,它是將audio buffers從java層傳遞到native層。音頻播放時音頻數(shù)據(jù)從Java流式傳輸?shù)絥ative層的創(chuàng)建模式。
MODE_STATIC 適用場景,比如播放很短的音頻,它是一次性將全部的音頻資源從java傳遞到native層。音頻數(shù)據(jù)在音頻開始播放前僅從Java傳輸?shù)絥ative層的創(chuàng)建模式。
寫入數(shù)據(jù)進行播放
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {}
audioData 就是要播放的pcm數(shù)據(jù)
offsetInBytes audioData字節(jié)數(shù)組的的開始位置
sizeInBytes 要寫入audioData字節(jié)數(shù)組的大小
返回值 ,真實寫入的字節(jié)數(shù)
是的,就這么一個方法。注意此方法是同步方法,是個耗時方法,一般是開啟一個線程循環(huán)調(diào)用write方法進行寫入。
注意在調(diào)用write方法前需要調(diào)用 audioTrack.play()方法開始播放。
暫停銷毀等其他方法
mAudioTrack.pause(); // 暫停,注意下次恢復(fù)播放,需要重新調(diào)用play方法,然后循壞調(diào)用write寫入暫停后的數(shù)據(jù)即可
mAudioTrack.flush(); // 清空丟掉當(dāng)前排隊播放的音頻數(shù)據(jù)
mAudioTrack.stop(); // 停止播放音頻數(shù)據(jù)
mAudioTrack.release();// 銷毀播放器
mAudioTrack.setStereoVolume(volume, volume); 音量設(shè)置,范圍[0-1]
mAudioTrack.setVolume(float gain) 設(shè)置此軌道所有通道上的指定輸出增益值。
更多的API可以參考官網(wǎng)開發(fā)文檔。需要注意的是在有些手機上pause耗時,甚至耗時1s。
播放進度
因為是pcm裸數(shù)據(jù),無法像mediaplayer一樣提供了API。所以需要自己處理下??梢岳?code>getPlaybackHeadPosition方法。
getPlaybackHeadPosition()的意思是返回以幀為單位表示的播放頭位置
getPlaybackRate()的意思是返回以Hz為單位返回當(dāng)前播放采樣率。
所以當(dāng)前播放時間可以通過如下方式獲取
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是跨平臺是針對嵌入式系統(tǒng)精心優(yōu)化的硬件音頻加速API。使用OpenSLES進行音頻播放的好處是可以不依賴第三方。比如一些音頻或者視頻播放器中都是用OpenSLES進行播放解碼后的pcm的,這樣免去了和java層的交互。
使用OpenSLES
在Android中使用OpenSLES首先需要把Android 系統(tǒng)提供的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
// ...省略其它
)
然后導(dǎo)入頭文件即可使用了OpenSLES提供的底層方法了。
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
創(chuàng)建OpenSLES
創(chuàng)建&使用的步驟大致分為:
- 創(chuàng)建引擎 獲取SLEngineItf
- 創(chuàng)建并設(shè)置混音器
- 創(chuàng)建并設(shè)置播放器
- 注冊播放器回調(diào)并寫入播放緩沖區(qū)隊列
- 其它操作播放的方法,比如暫停,音量設(shè)置,聲道設(shè)置
創(chuàng)建引擎 獲取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");
}
- 創(chuàng)建引擎。使用
slCreateEngine第一個參數(shù)是要創(chuàng)建的引擎對象,是一個SLObjectItf類型。返回值是SLresult類型,如果成功則返回SL_RESULT_SUCCESS,其他參數(shù)都傳0即可。 - 創(chuàng)建引擎成功后必須先調(diào)用Realize方法做初始化
(*slObjectItf)->Realize(),實例化成功則返回SL_RESULT_SUCCESS。 - 引擎實例化之后從引擎對象獲取接口。
SLresult (*GetInterface) (
SLObjectItf self, //實例化后的引擎對象
const SLInterfaceID iid, //SL_IID_ENGINE
void * pInterface //輸出的接口對象指針
);
一個SLObjectItf里面可能包含了多個Interface,獲取Interface通過GetInterface方法,而GetInterface方法的地2個參數(shù)SLInterfaceID參數(shù)來指定到的需要獲取Object里面的那個Interface。比如通過指定SL_IID_ENGINE的類型來獲取SLEngineItf。我們可以通過SLEngineItf去創(chuàng)建各種Object,例如播放器、錄音器、混音器的Object,然后在用這些Object去獲取各種Interface去實現(xiàn)各種功能。
創(chuàng)建混音器
如上所說,SLEngineItf可以創(chuàng)建混音器的Object。
- 創(chuàng)建混音器。
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方法來調(diào)用接口等。
配置音頻信息
在創(chuàng)建播放器前需要創(chuàng)建音頻的配置信息(比如采樣率,聲道數(shù),每個采樣的位數(shù)等)
//音頻格式
SLDataFormat_PCM pcmFormat = {
SL_DATAFORMAT_PCM, //播放pcm格式的數(shù)據(jù)
2, //聲道數(shù)
static_cast<SLuint32>(getCurrentSampleRateForOpensles(sample_rate)),
SL_PCMSAMPLEFORMAT_FIXED_16, //位數(shù) 16位
SL_PCMSAMPLEFORMAT_FIXED_16, //和位數(shù)一致就行
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, //立體聲(前左前右)
//字節(jié)序,小端
SL_BYTEORDER_LITTLEENDIAN
};
創(chuàng)建播放器
- 通過 引擎(*engineEngine)->CreateAudioPlayer 方法來創(chuàng)建播放器。
result = (*engineEngine)->CreateAudioPlayer(
engineEngine, //引擎對象本身
&pcmPlayerObject, //輸出的播放器對象,同樣是SLObjectItf類型
&slDataSource, //數(shù)據(jù)的來源
&slDataSink, //數(shù)據(jù)的去處,和SLDataSource是相對的
sizeof(ids) / sizeof(SLInterfaceID), //與下面的SLInterfaceID和SLboolean配合使用,用于標記SLInterfaceID數(shù)組和SLboolean的大小
ids,//這里需要傳入一個數(shù)組,指定創(chuàng)建的播放器會包含哪些Interface
req//這里也是一個數(shù)組,用來標記每個需要包含的Interface);
- 獲取播放器接口
(*pcmPlayerObject)->GetInterface(slPlayerItf, SL_IID_PLAY, &pcmPlayerPlay);得到播放器接口SLPlayItf pcmPlayerPlay。pcmPlayerPlay之后就可以給播放器設(shè)置不同的狀態(tài)比如SL_PLAYSTATE_PAUSED進行播放暫停等操作,后文介紹。
SLresult (*GetInterface) (
SLObjectItf self, //實例化后的播放器對象
const SLInterfaceID iid, //SL_IID_PLAY
void * pInterface //輸出的接口對象指針
);
- 獲取播放隊列接口
result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
- 給播放隊列注冊回調(diào)函數(shù)。
開始播放后會不斷的回調(diào)這個pcmBufferCallBack函數(shù)將音頻數(shù)據(jù)壓入隊列
(*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, this);
// OpenSLES 會自動回調(diào)
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());
}
}
}
- 設(shè)置播放狀態(tài)為播放中
//設(shè)置播放狀態(tài)
(*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);
如果想要暫停播放參數(shù)直接設(shè)置為SL_PLAYSTATE_PAUSED,若暫停后繼續(xù)播放設(shè)置參數(shù)為SL_PLAYSTATE_PLAYING即可。若想要停止播放參數(shù)設(shè)置為SL_PLAYSTATE_STOPPED即可。
- 開始播放
需要手動調(diào)用一次 (*pcmBufferQueue)->Enqueue,也就是可以直接調(diào)用下 pcmBufferCallBack(pcmBufferQueue, this);
OpenSLES的音量控制
首先獲取播放器的用于控制音量的接口SLVolumeItf pcmVolumePlay
// 音量
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_VOLUME, &pcmVolumePlay);
然后動態(tài)設(shè)置
// 聲音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);
然后動態(tài)設(shè)置
// 立體聲
(*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,自己也是學(xué)習(xí)網(wǎng)上的一些文章和源碼,參考了下網(wǎng)上的代碼。僅供學(xué)習(xí)。