2018年12月17日 星期一

初探OpenSLES

初探OpenSLES

前言

  在Android平台系統提供的音效API為OpenSLES,不過可以在GitHub找到OpenAL in Android,在評估過後,我還是決定使用系統提供的,在此把學習過程做個紀錄。

內容

  在評估在Android時到底要用OpenAL還是OpenSLES時,由於擔心OpenAL的部分不是官方所提供的,如果有問題很難得到支援,所以就選了OpenSLES,但OpenSLES的用法和OpenAL或XAudio2相近嗎?是的,至少在Stream play的流程是一樣的,整合起來不會有太大的問題。

  OpenSLES的範例並不多,目前的參考範例為NDK的範例。在開始Stream play前,和OpenAL與XAudio2一樣要初始化,具體的範例如下
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
int main()
{
  //
  //...  
  //
  SLObjectItf engineObject;
  SLEngineItf engineInterface;
  SLObjectItf outputMixObject;
  SLEngineOption engineOption[]=
  {
    {
      (SLuint32)SL_ENGINEOPTION_THREADSAFE,
      (SLuint32)SL_BOOLEAN_TRUE
    }
  };
  SLresult res=slCreateEngine(&engineObject,1,engineOption,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->Realize(engineObject,SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  //...
  //
  (*outputMixObject)->Destroy(outputMixObject);
  (*engineObject)->Destroy(engineObject);
  return 0;
}

先來說說OpenSLES的API使用習慣,SLObjectItf的物件在經過Create後,一定要Realize()才會生效,這點一定要注意!釋放的部分就比較簡單,直接喚起Destroy()即可,接著是介面物件,可以想像是物件的Public funciton,不過必須透過GetInterface()來取得,介面物件並不需要做釋放的動作。EngineObject的Create是透過slCreateEngine()來建立,建立後喚起Realize(),接著取得該物件的介面,也就是EngineInterface,接著透過engine interface來Create OutputMixObject,一樣,OutputMixObject在Create後馬上喚起Realize(),這樣就初始化完成了。釋放的部分相當簡單,對個別物件喚起Destroy()即可。

  接著來說明Stream play的實現,先看看範例
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
bool g_IsPlaying=false;
void PlayerPlayCallback(SLPlayItf caller,void* pContext, SLuint32 iEvent)
{
  // play finish
  if(iEvent==SL_PLAYEVENT_HEADATEND)
  {
    //On play end...
    g_IsPlaying=false;
  }
}
void PlayerBufferQueueCallBack(SLAndroidSimpleBufferQueueItf playerBufferQueueInterface,void* pContext)
{
  //
  //Enqueue next buffer...
}
int main()
{
  //
  //...  
  //
  SLObjectItf engineObject;
  SLEngineItf engineInterface;
  SLObjectItf outputMixObject;
  SLEngineOption engineOption[]=
  {
    {
      (SLuint32)SL_ENGINEOPTION_THREADSAFE,
      (SLuint32)SL_BOOLEAN_TRUE
    }
  };
  SLresult res=slCreateEngine(&engineObject,1,engineOption,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->Realize(engineObject,SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  //Follow data load from WAV file
  WAVEFORMATEX format;
  unsigned char* pWAVRawData;
  size_t wavRawDataSize;
  //
  SLObjectItf playerObject;
  SLPlayItf playerPlayInterface;
  SLAndroidSimpleBufferQueueItf playerBufferQueueInterface;
  SLDataLocator_AndroidSimpleBufferQueue locatorBufferQueue= 
  {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
  SLuint32 tagBitsPerSample=waveFormat.wBitsPerSample*waveFormat.nChannels;
  SLDataFormat_PCM formatPCM= 
  {
    SL_DATAFORMAT_PCM,
    waveFormat.nChannels,
    waveFormat.nSamplesPerSec*1000,
    tagBitsPerSample,
    tagBitsPerSample,
    SL_SPEAKER_FRONT_CENTER, 
    SL_BYTEORDER_LITTLEENDIAN
  };
  SLDataSource audioSource={&locatorBufferQueue,&formatPCM};
  SLDataLocator_OutputMix locatorOutmix={SL_DATALOCATOR_OUTPUTMIX,outputMixObject };
  SLDataSink audioSink={&locatorOutmix,NULL};
  SLInterfaceID interfaceIDList[]={SL_IID_BUFFERQUEUE,SL_IID_VOLUME, SL_IID_EFFECTSEND};
  SLboolean requiredList[] = {SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE};
  res=(*engineInterface)->CreateAudioPlayer(engineInterface,&playerObject,&audioSource,&audioSink,2,interfaceIDList,requiredList);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerObject)->Realize(playerObject,SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerObject)->GetInterface(playerObject,SL_IID_PLAY,&playerPlayInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerPlayInterface)->RegisterCallBack(playerPlayInterface,PlayerPlayCallback,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerPlayInterface)->SetCallbackEventsMask(playerPlayInterface,SL_PLAYEVENT_HEADATEND);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerObject)->GetInterface(playerObject,SL_IID_BUFFERQUEUE,&playerBufferQueueInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerBufferQueueInterface)->RegisterCallback(playerBufferQueueInterface,PlayerBufferQueueCallBack,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*playerBufferQueueInterface)->Enqueue(playerBufferQueueInterface,pWAVRawData,wavRawDataSize);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  g_IsPlaying=true;
  res=(*playerPlayInterface)->SetPlayState(playerPlayInterface,SL_PLAYSTATE_PLAYING);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  while(g_IsPlaying)
    ::Sleeping(1)//sleep 1ms
  //
  (*playerObject)->Destroy(playerObject);
  (*outputMixObject)->Destroy(outputMixObject);
  (*engineObject)->Destroy(engineObject);
  return 0;
}

範例碼很長,但其實是因為OpenSLES的使用習慣造成看起來很長,和OpenAL與XAudio2一樣Create一個播放元件,OpenSLES的播放元件是"AudioPlayer",初始化的參數很多,要注意的只有SLDataFormat_PCM,裡面設定的是播放資料的格式,其餘的請照抄 ,AudioPlayer在Create後一樣要Realize(),接著是取得PlayerPlayInterface與PlayerBufferQueueInterface,PlayerPlayInterface控制的是播放與停止,還要注意RegisterCallBack()與SetCallbackEventsMask(),這兩個步驟,這兩個步驟是要在播放器播完聲音後喚起起一個CallBack,範例的話就是PlayerPlayCallback(),PlayerBufferQueueInterface控制的是Stream play的queue,和XAudio2一樣不需要自己Dequeue,但要透過RegisterCallback()來註冊CallBack,這個CallBack會在每處理完一個Buffer後喚起,在範例的話就是PlayerBufferQueueCallBack(),PlayerPlayCallback()與PlayerBufferQueueCallBack()都可以看到"pContext"的參數,也可以看到RegisterCallback()的最後一個參數,範例是都傳NULL,這個部分可以依據需要傳入物件的指標,在CallBack的時候再將pContext做轉型來做到傳遞物件,範例在要Equeue next buffer的部分是空的,請依據需要傳入物件指標來完成,Enqueue的部分可以用PlayerBufferQueueInterface來實現。

  整體來說,OpenSLES的程式碼會看起來很長,因為使用習慣的關係,在Stream play的部分,有一些麻煩,就是要規劃Enqueue next buffer的規格與規劃播放結束的處理。加上先前的初探XAudio2初探OpenAL來參考,就可以寫出整合這3套API的Stream play。

參考資料

OpenAL in Android
OpenSLES官網

相關文章

WAV檔案格式學習心得
初探XAudio2
初探OpenAL

沒有留言:

張貼留言