2018年12月31日 星期一

矩陣的排列順序(1)

矩陣的排列順序(1)

前言

  一個4*4矩陣是由16個float所組成,這不會有異議,但它的排列方式有兩種,常常會搞混,所以作這篇來記錄。

內容

  矩陣的兩種排列順序,如下圖
矩陣排列順序比較
如果仔細觀察可以發現這兩種排列可以透過Transpose的方式來互換,但不幸的是Direct3D與OpenGL的API只認其中一種順序,答案是"右邊",OpenGL2.0時使用的glUniformMatrix4fv()有個參數可以Transpose,所以嚴格來說沒限制順序,但Direct3D10以後開始使用ConstantBuffer與OpenGL3.1以後使用的UBO,這兩者都是直接透過記憶體將參數直接傳入GPU,沒辦法一個一個Transpose,所以才說繪圖API有認順序。

  這兩種順序在不考慮繪圖API的情形下沒有優劣之分,但由於繪圖API認定了其中一種排列方式,想想看,如果遊戲引擎採用"左邊"的排列,在Shader有matrix的變數的時候要如何寫?每次寫完後都Transpose嗎?還是每次將記憶體送到GPU時都將該變數Transpose?不論是哪一種都沒有"右邊"的排列好用,"右邊"的排列完全連改都不用改,還要想要選哪一種排列嗎?


2018年12月24日 星期一

在Atom中使用gdb

在Atom中使用gdb

前言

  在Ubuntu裡要Debug C++程式相當麻煩,Linux的C++編譯器有GCC或G++,Debugger則是GDB,GDB的教學可以參照GDB 基本操作| 教學,但用Console來控制Debugger實在是不怎麼方便,如果能像Vistual studio的方式來Debug不是很好嗎?在Ubuntu裡我用的文字編輯器是Atom,查了一下有外掛可以裝,在此把使用過程做個紀錄。

內容
  首先要先安裝兩個外掛,如下圖
安裝相關外掛
安裝"dbg"與"dbg-gdb"這兩個外掛。接著是重新編譯程式,範例如下
g++ main.cpp -o main.exe

g++ -g main.cpp -o main.exe

第一行是原本的編譯命令,請加上"-g",這樣gdb才能順利地使用break point。

  如何使用呢?在範例輸出的"main.exe"的binary檔按下右鍵,如下圖
選擇要Debug的Binary檔
按下"Debug this File"後,如下圖
設置Break pont後開始Debug
在開始Debug前,請先在需要設置break point的檔案設置,設置的方法很簡單,在需要設置的那一行按下"F9"就會有如圖的"紅點"在該行的左邊,全部設置完後,按下右下角的"Debug"就可以開始Debug,如下圖
Debug的狀況
可以在右邊看到變數狀況,左邊有控制Debug流程的UI,下方的則是Console的輸出結果,用起來就像Vistual studio一樣相當簡單。

參考資料

GDB 基本操作| 教學

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檔案格式學習心得