Android 播放音頻(PCM)的兩種方法--AudioTrack/OpenSL ES使用簡介

本文主要介紹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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章