顯示具有 audio_program 標籤的文章。 顯示所有文章
顯示具有 audio_program 標籤的文章。 顯示所有文章

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

2018年12月10日 星期一

初探OpenAL

初探OpenAL

前言

  在前篇初探XAudio2裡使用了XAudio2來播放聲音,這次來使用OpenAL來播放聲音,為什麼使用OpenAL呢?因為Linux裡沒有XAudio2可以使用,在Linux有一套名為ALSA的API,但查了一下授權條款是GPL,所以決定放棄這一套API,所以在Linux使用OpenAL來實現播放聲音,在此把研究過程做個紀錄。

內容
  在開始之前,請先到OpenAL官網下載SDK,請下載下圖所示選項
下載OpenAL SDK
下載後解壓安裝即可,這樣才有OpenAL的環境與C++的Header和Lib可以用。請在專案中新增Lib路徑,路徑為"(SDK directory)/libs",檔名為"OpenAL32.lib",include路徑為"(SDK directory)/include"。

  OpenAL的範例可以在"(SDK directory)/samples"裡找到,但範例有用framework,如果需要教學網站的範例的可以參照OpenAL: Managing Playback StreamsOpenAL short example。在OpenAL的開始需要ALCdevice與ALCcontext,範例如下
#include <AL/al.h>
#include <AL/alc.h>

int main()
{
//
  ALCdevice* pDevice=alcOpenDevice(NULL);
  if(pDevice==NULL)
    return 1;
  ALCcontext* pContext=alcCreateContext(pDevice,NULL);
  if(pContext==NULL)
  {
    alcCloseDevice(pDevice);
    return 1;
  }
  if(!alcMakeContextCurrent(pContext) )
  {
    alcDestroyContext(pContext);
    alcCloseDevice(pDevice);
    return 1;
  }
  //OpenAL was ready...
  //...
  //program end...
  alcDestroyContext(pContext);
  alcCloseDevice(pDevice);
  return 0;
}

alcMakeContextCurrent()非常重要,如果這個步驟沒成功,所有的OpenAL資源將無法製造。

  接著來製造播放聲音,範例如下
#include <AL/al.h>
#include <AL/alc.h>
#include <windows.h>
ALenum GetChannelFormat(short channels,short samples)
{
  ALenum val=AL_INVAILD_VALUE;
  switch(samples)
  {
  case 16:
    if(channels>1)
      val=AL_FORMAT_STEREO16;
    else
      val=AL_FORMAT_MONO16;
    break;
  case 8:
    if(channels>1)
      val=AL_FORMAT_STEREO8;
    else
      val=AL_FORMAT_MONO8;
    break;
  }
  return val;
}
int main()
{
//
  ALCdevice* pDevice=alcOpenDevice(NULL);
  if(pDevice==NULL)
    return 1;
  ALCcontext* pContext=alcCreateContext(pDevice,NULL);
  if(pContext==NULL)
  {
    alcCloseDevice(pDevice);
    return 1;
  }
  if(!alcMakeContextCurrent(pContext) )
  {
    alcDestroyContext(pContext);
    alcCloseDevice(pDevice);
    return 1;
  }
  //OpenAL was ready...
  ALuint sourceID;
  alGenSource(1,&sourceID);
  if(alGetError()!=AL_NO_ERROR)
  {
    alcDestroyContext(pContext);
    alcCloseDevice(pDevice);
    return 1;
  }
  //Follow data load from WAV file
  WAVEFORMATEX format;
  unsigned char* pWAVRawData;
  size_t wavRawDataSize;
  //
  ALunit bufID;
  alGenBuffers(1,&bufID);
  alBufferData(bufID,GetChannelFormat(format.nChannels,format.wBitsPerSample),pWAVRawData,format.nSamplesPerSec);

  //Enqueue buffer
  alSoruceQueueBuffers(sourceID,1,&bufID);
  //Play
  alSourcePlay(sourceID);
  //wait play end
  ALint sourceState;
  alGetSourcei(sourceID, AL_SOURCE_STATE, &sourceState);
  while(sourceState==AL_PLAYING)
    ::Sleep(1);//sleep 1ms
  //
  //program end...
  alcDestroyContext(pContext);
  alcCloseDevice(pDevice);
  return 0;
}

流程的部分跟XAudio2很像,製造播放聲音的元件再推入Buffer來播放聲音,利用alGetSourcei()來確認是否播放完畢。

  最後來說Stream play的部分,XAudio2的Dequeue是自動的,但OpenAL的Dequeue的流程要手動,範例如下
ALuint tempBufferID=0;
alSourceUnqueueBuffers(sourceID,1,tempBufferID);
if(tempBufferID!=0)
{
  //Dequeue ok!
  //...
}

過程不會太難,但什麼時候會Dequeue會失敗?當這個Buffer還在播放中的話,BufferID就會錶持為0,如果你看OpenAL: Managing Playback Streams裡的話,會發現它使用alGetSourcei()透過AL_BUFFERS_PROCESSED來得知Buffer是否播放完畢,但查到這篇AL_BUFFERS_PROCESSED bug?所以我不打算用範例的方法檢測是否播完。

  整體來說XAudio2與OpenAL的用法很像,目前差異只有在Dequeue的部分,如果要整合的話相信並不困難,OpenAL可以用在Windows與Linux,但XAudio2卻只能跑在Windows,整合到底有沒有價值?只能日後再來檢驗了。

參考資料

ALSA
OpenAL官網
OpenAL: Managing Playback Streams
OpenAL short example
AL_BUFFERS_PROCESSED bug?

相關文章

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

2018年12月3日 星期一

初探XAudio2

初探XAudio2

前言

  最近要在遊戲引擎裡播放聲音,本想繼續使用以前的DirectSound來播,但查了一下DirectSound的wiki得知DirectSound已不建議使用,所以打算改成XAudio2來播放聲音,以下是學習心得,在此做個紀錄。

內容

  在使用XAudio2的開始需要一些範例,但不幸的是Windows10本身沒給範例,所以請到舊的DirectX範例裡下載,裡面有個"XAudio2"的資料夾,範例就在裡面。

  使用XAudio2時需要"IXAudio2",所有的資源都由這個COM來管理,有點類似OpenGL的context,範例如下
HRESULT hr = CoInitializeEx( nullptr, COINIT_MULTITHREADED );
if (FAILED(hr))
{
  wprintf(L"Failed to init COM: %#X\n", hr);
  return 0;
}
//
IXAudio2* pXAudio2 = nullptr;
hr = XAudio2Create( &pXAudio2 , 0);
if( FAILED( hr ) )
{
  wprintf( L"Failed to init XAudio2 engine: %#X\n", hr );
  CoUninitialize();
  return 0;
}

在CoInitializeEx()的部分並不是每次要XAudio2Create()之前都要喚起,而是整個程式只要在最開頭喚起一次就好,當然在程式最後也請喚起CoUninitialize()。XAudio2Create()可以依據需要填 入debug的flag,範例裡有示範。

  接著來製造"IXAudio2MasteringVoice",這個COM其實我並不是很了解它的作用,但一定要製造,製造的方法很簡單,範例如下
//IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice = nullptr;
if( FAILED( pXAudio2->CreateMasteringVoice( &pMasteringVoice ) ) )
{
  pXAudio2->Release();
  CoUninitialize();
  return 0;
}

  接著來製造"IXAudio2SourceVoice",這個就是在播放聲音的COM,依據需要"同時"播放幾個聲音來決定需要的數量,製造前需要決定播放的格式,這個結構是"WAVEFORMATEX",這個結構的資料可以在WAV檔裡的"fmt"的chunk裡找到,可以參考前篇WAV檔案格式學習心得,範例如下
//IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice;
WAVEFORMATEX format;
//Fill struct data
//...
IXAudio2SourceVoice* pSourceVoice = nullptr;
if( FAILED( pXaudio2->CreateSourceVoice( &pSourceVoice, &format) ) )
{
  return 0;
}

接著將WAV的raw data送到"IXAudio2SourceVoice",範例如下
//IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice;
IXAudio2SourceVoice* pSourceVoice;

unsigned char* pWAVRawData;
size_t wavRawDataSize;
XAUDIO2_BUFFER buffer = {0};
buffer.pAudioData = pWAVRawData;
buffer.Flags = XAUDIO2_END_OF_STREAM;
buffer.AudioBytes = wavRawDataSize;
if( FAILED(pSourceVoice->SubmitSourceBuffer( &buffer ) ) )
{
  pSourceVoice->DestroyVoice();
  return 0;
}
這裡要注意"IXAudio2SourceVoice"的釋放不是用Release(),而是用DestroyVoice(),接著只要使用Start()
來播放即可,要停止播放使用Stop()。

  最後來說說Stream play,請看下圖
Stream play
播放聲音時可以不斷地往後送Buffer,在XAudio2裡這個動作就是SubmitSourceBuffer(),也就是圖中的"Enqueue",圖中的"Dequeue"這個動作在XAudio2是"自動完成"的,可以利用"IXAudio2SourceVoice"下的GetState()來取得剩下的Buffer數量,這就是Stream play。這種機制幾乎是必要的,想想如果聲音檔案很長,用一般的做法(只播一個Buffer),這個Buffer就會很大,而且考量到壓縮格式的聲音檔(如Ogg),必須是解一小段播一小段(這樣很省記憶體),同樣的機制在DirectSound要實現的話相當麻煩,還必須處理Event,程式碼相當長,但XAudio2在這方面的實現就相當簡單,最後我想以後我應該會淘汰DirectSound改用XAudio2。

參考資料

DirectSound的wiki
舊的DirectX範例

相關文章

WAV檔案格式學習心得

2018年11月26日 星期一

WAV檔案格式學習心得

WAV檔案格式學習心得

前言

  最近接合Audio API到遊戲引擎上,但Audio API都不提供Parse檔案相關的功能,所以必須要自己Parse後萃取出raw data,再將raw data送入Audio API。以下是學習心得,在此做個紀錄。

內容

  WAV檔的格式是由很多的RIFF chunk所組成,檔案組成的概況如下圖
WAV檔的組成
每個RIFF chunk裡都包含了RIFF header與Content,每個RIFF header會用chunkID來表明資料的型態,而size所表明的是RIFF chunk裡的Content資料有多少Bytes。在Parse時,常常需要知道現在的RIFF chunk有多少Bytes,計算法如下
size_t riffChunkSize=riffHeader.size+sizeof(CRIFFHeader);

  接著來看看WAV檔裡的Chunk的Content,Content有多少種呢?不幸的很多種,所以這裡只列出我有遇到的種類。
chunkID是個32位元的資料,也就是4個Bytes,通常會將每個Byte表達成char,這樣就可以4個char來表達資料種類。

  第一種的chunkID為'R','I','F','F',這個很特別,它通常會是第一個RIFF chunk,特別的是它的content,一定是一個int32_t,且內容是'W','A','V','E',但是它的RIFF header裡的size的內容並不是4,而是整個WAV檔的大小減8,這個8其實就是RIFF header的大小,有點類似資料夾的概念,這個chunk包住了其它的chunk。

  第二種的chunkID為'f','m','t',' ',這裡的Content可以參考WAVEFORMATEX structure,但要注意struct裡的"cbSize",這個欄位不一定存在,所以這個chunk的size不是固定的!請依據chunk裡的size來Parsem。

  第三種的chunkID為'f','a','c','t',這個chunk的內容我並不知它的Content是什麼,我選擇直接跳過它!請注意這個chunk並不是一定存在,parse時請考慮這點。

  第四種的chunkID為'd','a','t','a',這裡就是raw data,也就是Audio API吃的資料。

  最後把前述的種類排列起來就會像下圖
Wav檔的Chunk排列

參考資料

WAVE PCM soundfile format
WAVEFORMATEX structure