本文主要介紹Android上可以進行音頻(PCM)播放的兩個組件–AudioTrack/OpenSL ES的簡單使用方法。
對一個音頻文件(如MP3文件),如何使用FFmpeg進行解碼獲取到PCM,之前的文章已經有相應的說明:
https://blog.csdn.net/myvest/article/details/89254452。
那麼解碼後或者mic採集的PCM數據,是如何播放的呢,首先一般會對PCM數據進行重採樣,也即是轉換爲指定的格式。重採樣可以參考:https://blog.csdn.net/myvest/article/details/89442000
最後,進入本文主題,介紹AudioTrack/OpenSL ES的簡單使用方法。
1 AudioTrack簡介
AudioTrack是Android系統中管理和播放單一音頻資源的類,Android提供了java層及native層的api,使用也比較簡單,一般我推薦使用這個組件播放。
但需要注意的是,它僅能播放已經解碼出來的PCM數據。
1.1 使用方法及API簡介
我們以java端的api爲例(native層基本一致),AudioTrack使用方法如下:
1、創建:
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes, int mode);
參數說明:
1)int streamType:指定即將播放的聲音類型,對於不同類型,Android的audio系統會有不同處理(如音量等級不同,音量控制不同等),一些常見類型如下,對於音樂文件,我們使用STREAM_MUSIC
- STREAM_ALARM:警告聲
- STREAM_MUSIC:音樂聲,例如music等
- STREAM_RING:鈴聲
- STREAM_SYSTEM:系統聲音,例如低電提示音,鎖屏音等
- STREAM_VOCIE_CALL:通話聲
AudioTrack有兩種數據加載模式(MODE_STREAM和MODE_STATIC),對應的是數據加載模式和音頻流類型, 對應着兩種完全不同的使用場景。
2)int sampleRateInHz:採樣率
3)int channelConfig:音頻聲道對應的layout,如立體聲是AudioFormat.CHANNEL_OUT_STEREO
4)int audioFormat:音頻格式
5)int bufferSizeInBytes:緩衝區大小
緩衝區大小可以通過函數getMinBufferSize獲取,傳入採樣率、聲道layout、音頻格式即可,如下:
public AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
6)int mode:數據加載模式(MODE_STREAM和MODE_STATIC),兩種模式對應着兩種不同的使用場景
- MODE_STREAM:在這種模式下,通過write一次次把音頻數據寫到AudioTrack中。這和平時通過write系統調用往文件中寫數據類似,但這種工作方式每次都需要把數據從用戶提供的Buffer中拷貝到AudioTrack內部的Buffer中,這在一定程度上會使引入延時。爲解決這一問題,AudioTrack就引入了第二種模式。
- MODE_STATIC:這種模式下,在play之前只需要把所有數據通過一次write調用傳遞到AudioTrack中的內部緩衝區,後續就不必再傳遞數據了。這種模式適用於像鈴聲這種內存佔用量較小,延時要求較高的文件。但它也有一個缺點,就是一次write的數據不能太多,否則系統無法分配足夠的內存來存儲全部數據。
2、啓動:
public AudioTrack.play();
3、數據注入:
public int write(byte[] audioData, int offsetInBytes, int sizeInBytes);
參數比較簡單,數據、偏移、size
4、停止:
public AudioTrack.stop();
5、釋放:
public AudioTrack.release();
6、獲取狀態:
STATE_INITIALIZED和STATE_UNINITIALIZED就不用說明了。STATE_NO_STATIC_DATA是個中間狀態,當使用MODE_STATIC模式時,創建AudioTrack ,首先會進入改狀態,需要write數據後,纔會變成STATE_INITIALIZED狀態。非STATE_INITIALIZED狀態下去進行play的話,會拋出異常,所以MODE_STATIC模式如果沒有write就去play是不行的。
/**
* Returns the state of the AudioTrack instance. This is useful after the
* AudioTrack instance has been created to check if it was initialized
* properly. This ensures that the appropriate resources have been acquired.
* @see #STATE_INITIALIZED
* @see #STATE_NO_STATIC_DATA
* @see #STATE_UNINITIALIZED
*/
public int getState() {
return mState;
}
1.2 AudioTrack使用示例
java端示例
private AudioTrack audioTrack = null;
private static int sampleRateInHz = 44100;
private static int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
class processThread implements Runnable {
......省略
public void run() {
byte[] outBuf = new byte[DECODE_BUFFER_SIZE];
if(audioTrack == null){
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bufferSize,
AudioTrack.MODE_STREAM);
if(audioTrack == null){
mFFdec.decodeDeInit();
return ;
}
}
try {
if ( audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.play();
}
while(true){
int size = mFFdec.decodeFrame(outBuf);
if(size > 0){
if(mFFdec.getMediaType() == mFFdec.MEDIA_TYPE_AUDIO){//audio
audioTrack.write(outBuf, 0, size);
}
}else{
break;
}
}
if (audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.stop();
audioTrack.release();
}
}catch (Exception ex) {
ex.printStackTrace();
} catch (Throwable t) {
t.printStackTrace();
}
audioTrack = null;
mFFdec.decodeDeInit();
}
}
2 OpenSL ES簡介
OpenSL ES(Open Sound Library for Embedded Systems,開源的嵌入式聲音庫)是一個免授權費、跨平臺、C語言編寫的適用於嵌入式系統的硬件加速音頻庫。簡單來說,它提供了一些標準化的api,讓開發者可以在不同硬件平臺上,操作音頻設備。包括播放,錄製等等。
雖然是C語音,但其採用了面向對象的方式,在開發中,我們使用對象(object)和接口(interface)來開發。
2.1 對象(object)和接口(interface)
- 對象:提供一組資源極其狀態的抽象,例如錄音器對象,播放器對象。
- 接口:對象提供一特定功能方法的抽象,例如播放器對象的播放接口。
也就是說每種對象都提供了一些最基礎的操作:Realize,Resume,GetState,Destroy 等等,但是對象不能直接使用,必須通過其 GetInterface 函數用ID號拿到指定接口(如播放器的播放接口),然後通過該接口來訪問功能函數。
所有對象在創建後都要調用Realize 進行初始化,釋放時需要調用Destroy 進行銷燬。
2.2 OpenSL ES 音頻播放方法
音頻播放場景:
從該場景圖來講解播放方法:
1、創建OpenSL engine,
2、通過engine創建 AudioPlayer和outputMix
3、指定AudioPlayer的輸入爲DataSource,輸出爲outputMix,outputMix是會關聯到設備的默認輸出。
4、啓動播放。
下面詳細講解各個部分
2.2.1 Engine
OpenSL ES 裏面最核心的對象,它主要提供如下兩個功能:
(1) 管理 Audio Engine 的生命週期。
(2) 提供管理接口: SLEngineItf,該接口可以用來創建所有其他的對象。
使用步驟:
1、創建Engine
SLresult SLAPIENTRY slCreateEngine(
SLObjectItf *pEngine,
SLuint32 numOptions
constSLEngineOption *pEngineOptions,
SLuint32 numInterfaces,
constSLInterfaceID *pInterfaceIds,
constSLboolean *pInterfaceRequired
)
參數說明:
- pEngine:指向輸出的engine對象的指針。 numOptions:可選配置數組的大小。
- pEngineOptions:可選配置數組。 numInterfaces:對象要求支持的接口數目,不包含隱含的接口。
- pInterfaceId:對象需要支持的接口id的數組。
- pInterfaceRequired:指定每個要求接口的接口是可選或者必須的標誌位數組。如果要求的接口沒有實現,創建對象會失敗並返回錯誤碼SL_RESULT_FEATURE_UNSUPPORTED。
2、創建完engine後就可以通過SL_IID_ENGINE
獲取管理接口:
//創建對象
SLObjectItf engineObject;
slCreateEngine( &engineObject, 0, nullptr, 0, nullptr, nullptr );
//初始化
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
//獲取管理接口
static SLEngineItf iengine = NULL;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(iengine));
3、然後通過管理接口iengine,就可以繼續創建其他需要的對象。
2.2.2 AudioPlayer
音頻播放對象,它需要指定輸入(DataSource)和輸出(DataSink)
Datasource 代表着輸入源的信息,即數據從哪兒來、輸入的數據參數是怎樣的;
DataSink 代表着輸出的信息,即數據輸出到哪兒、以什麼樣的參數來輸出。
- DataSource 的定義如下:
typedef struct SLDataSource_ {
void *pLocator;
void *pFormat;
} SLDataSource;
- DataSink 的定義如下:
typedef struct SLDataSink_ {
void *pLocator;
void *pFormat;
} SLDataSink;
其中,pLocator 主要有如下幾種:
- SLDataLocator_Address
- SLDataLocator_BufferQueue
- SLDataLocator_IODevice
- SLDataLocator_MIDIBufferQueue
- SLDataLocator_URI
也就是說,輸入源/輸出源,既可以是 URL,也可以 Device,或者來自於緩衝區隊列等等。
那麼我們在創建AudioPlayer對象前,還需要先創建輸入和輸出,輸入我們使用一個緩衝隊列,輸出則使用outputMix輸出到默認聲音設備。
使用步驟:
1、輸入源
//輸入源爲緩衝隊列
SLDataLocator_AndroidSimpleBufferQueue dsLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };
// 設置音頻格式
SLDataFormat_PCM outputFormat = { SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN
};
// 輸入源
SLDataSource audioSource = { &dsLocator , &outputFormat };
2、輸出源
outputMix對象需要通過CreateOutputMix
接口創建,並調用Realize
初始化
static SLObjectItf mix = NULL;
//創建mix
(*iengine)->CreateOutputMix(iengine, &mix, 0, NULL, NULL);
//初始化
re = (*mix)->Realize(mix, SL_BOOLEAN_FALSE);
if(re != SL_RESULT_SUCCESS )
{
ALOGE("mix Realize error!");
return false;
}
//輸出源爲mix
SLDataLocator_OutputMix outmix = {SL_DATALOCATOR_OUTPUTMIX, mix};
SLDataSink audioSink = {&outmix,NULL};
3、創建AudioPlayer並初始化
AudioPlayer對象需要通過CreateAudioPlayer
接口創建,並調用Realize
初始化
static SLObjectItf player = NULL;
const SLInterfaceID outputInterfaces[1] = { SL_IID_BUFFERQUEUE };
const SLboolean req[] = {SL_BOOLEAN_TRUE};
//audio player
(*iengine)->CreateAudioPlayer(iengine, &player, &audioSource, &audioSink, 1, outputInterfaces, req);
re = (*player)->Realize(player, SL_BOOLEAN_FALSE);
if(re != SL_RESULT_SUCCESS )
{
ALOGE("player Realize error!");
return false;
}
4、設置輸入隊列的回調函數,回調函數在數據不足時會內部調用。然後啓動播放即可。
需要通過SL_IID_ANDROIDSIMPLEBUFFERQUEUE
獲取隊列接口,並RegisterCallback
設置回調函數。
需要通過SL_IID_PLAY
獲取播放接口,並設置爲播放狀態,同時調用Enqueue
給隊列壓入一幀空數據。
需要注意的是,入隊列的數據並非立刻播放,所以不能把這個數據立刻釋放,否則會造成丟幀。
//獲取隊列接口
static SLPlayItf iplay = NULL;
static SLAndroidSimpleBufferQueueItf pcmQue = NULL;
(*player)->GetInterface(player, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &pcmQue);
if(re != SL_RESULT_SUCCESS )
{
ALOGE("get pcmQue error!");
return false;
}
//設置回調函數,播放隊列空調用
(*pcmQue)->RegisterCallback(pcmQue, PcmCall, this);
//獲取player接口
re = (*player)->GetInterface(play, SL_IID_PLAY, &iplayer);
if(re != SL_RESULT_SUCCESS )
{
ALOGE("get iplayer error!");
return false;
}
//設置爲播放狀態
(*iplay)->SetPlayState(iplay, SL_PLAYSTATE_PLAYING);
//啓動隊列回調
(*pcmQue)->Enqueue(pcmQue, "", 1);
當播放隊列數據爲空時,會調用隊列的回調函數,所以需要在回調函數中給隊列注入音頻數據
static void PcmCall(SLAndroidSimpleBufferQueueItf bf, void *contex)
{
if(pcmQue && (*pcmQue))
{
if(aFrame == NULL)
{
(*pcmQue)->Enqueue(pcmQue, "", 1);
}
else
{
//進隊列後並不是直接播放,所以需要一個buf來存,不能釋放掉
memcpy(buf, aFrame->data, aFrame->size);
(*pcmQue)->Enqueue(pcmQue, buf, aFrame->size);
}
}
}
5、播放結束後,需要調用Destroy
將各個對象釋放。
opensl es參考自:https://www.jianshu.com/p/cccb59466e99