ads by Amazon

2013年4月10日水曜日

4. 無音を鳴らし続ける処理の実装(Windows編)

Windows(DirectSound)で「無音を鳴らし続ける処理」を実装する方法について、実際のコーディング例を交えながら解説していきます。

(1)ウィンドウの作成
DirectSoundの場合、音を鳴らすウィンドウのハンドルが初期化時に必要になります。
そのため、先ずは以下のようなウィンドウを表示するだけのプログラムを作ります。

test01.cpp(ウィンドウを表示するだけのプログラム)
#include <Windows.h>
#define APPNAME "DirectSound Test"

/* ウィンドウ処理 */
static LRESULT CALLBACK wproc(HWND hWnd,UINT msg,UINT wParam,LONG lParam)
{
    switch( msg ){
        case WM_DESTROY:
            PostQuitMessage( 0 );
            break ;
        default:
            return DefWindowProc( hWnd , msg , wParam , lParam );
    }
    return 0L ;
}

/* エントリポイント */
int __stdcall WinMain(HINSTANCE hIns,HINSTANCE hPIns,LPSTR lpCmd,int nCmdShow)
{
    HWND hwnd;
    MSG msg;
    WNDCLASS wc;

    /* ウィンドウクラスの登録 */
    memset(&wc,0,sizeof(wc));
    wc.lpszClassName=APPNAME;
    wc.hInstance=hIns;
    wc.style=CS_BYTEALIGNCLIENT|CS_VREDRAW|CS_HREDRAW ;
    wc.lpfnWndProc=(WNDPROC)wproc;
    if(!RegisterClass(&wc)) {
        return FALSE;
    }

    /* ウィンドウ作成 */
    hwnd=CreateWindowEx(0
        , APPNAME
        , APPNAME
        , WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_VISIBLE
        , CW_USEDEFAULT , CW_USEDEFAULT , 256 , 256
        , (HWND)NULL , (HMENU)NULL
        , hIns , (LPSTR)NULL );
    if(NULL==hwnd) {
        return FALSE;
    }

    /* メインループ */
    while(TRUE) {
        /* メッセージ処理 */
        if( PeekMessage( &msg , 0 , 0 , 0 , PM_REMOVE ) ){
            if( msg.message == WM_QUIT ) {
                break;
            }
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        Sleep(16);
    }
    return TRUE;
}
このプログラムを、Visual C++のコマンドラインから以下のコマンドでコンパイルすれば、ウィンドウを表示するだけのプログラムが作られます。
CL /MT test01.cpp user32.lib

(2)DirectSoundの初期化と解放の実装
次に、test01.cppに対して、DirectSoundの初期化処理と解放処理を入れてみます。
その前に、初期化時に音声品質に関する情報を設定する必要があるので、それらのリテラルを宣言しておきます。

■リテラル宣言
/* 波形情報リテラル */
#define SAMPLE_RATE 22050   /* 周波数 */
#define SAMPLE_BITS 16      /* ビットレート */
#define SAMPLE_CH   1       /* 1ch(モノラル) */
#define SAMPLE_BUFS 4410    /* バッファサイズ(50ms分) */
音声品質は、VGSの波形メモリ音源の音声品質と同等としておきます。

次に、DirectSoundの場合、デバイスなどのクラスオブジェクトをグローバル変数に格納しておく必要があるので、それらのグローバル変数を宣言します。

■グローバル変数宣言
/* DirectSound関連のグローバル変数 */
static LPDIRECTSOUND8 _lpDS=NULL;
static LPDIRECTSOUNDBUFFER8 _lpSB=NULL;
static LPDIRECTSOUNDNOTIFY8 _lpNtfy=NULL;
static DSBPOSITIONNOTIFY _dspn;
それぞれの変数の意味は、おいおい説明していきます。

そして、(上記のグローバル変数の)解放処理を実装します。

■DirectSound解放処理(ds_term)
/* DirectSoundを開放 */
static void ds_term()
{
    if(_lpNtfy) {
        _lpNtfy->Release();
        _lpNtfy=NULL;
    }
    if((HANDLE)-1==_dspn.hEventNotify || NULL==_dspn.hEventNotify) {
        CloseHandle(_dspn.hEventNotify);
        _dspn.hEventNotify=NULL;
    }
    if(_lpSB) {
        _lpSB->Release();
        _lpSB=NULL;
    }
    if(_lpDS) {
        _lpDS->Release();
        _lpDS=NULL;
    }
}

あとは、初期化処理を実装します。

■DirectSound初期化処理(ds_init)
/* DirectSoundを初期化 */
static int ds_init(HWND hWnd)
{
    DSBUFFERDESC desc;
    LPDIRECTSOUNDBUFFER tmp=NULL;
    HRESULT res;
    WAVEFORMATEX wFmt;

    /* デバイス作成 */
    res=DirectSoundCreate8(NULL,&_lpDS,NULL);
    if(FAILED(res)) {
        ds_term();
        return -1;
    }

    /* 強調レベル設定 */
    res=_lpDS->SetCooperativeLevel(hWnd,DSSCL_NORMAL);
    if(FAILED(res)) {
        ds_term();
        return -1;
    }

    /* セカンダリバッファの波形情報を設定 */
    memset(&wFmt,0,sizeof(wFmt));
    wFmt.wFormatTag = WAVE_FORMAT_PCM;
    wFmt.nChannels = SAMPLE_CH;
    wFmt.nSamplesPerSec = SAMPLE_RATE;
    wFmt.wBitsPerSample = SAMPLE_BITS;
    wFmt.nBlockAlign = wFmt.nChannels * wFmt.wBitsPerSample / 8;
    wFmt.nAvgBytesPerSec = wFmt.nSamplesPerSec * wFmt.nBlockAlign;
    wFmt.cbSize = 0;

    /* セカンダリバッファの記述子を設定 */
    memset(&desc,0,sizeof(desc));
    desc.dwSize=(DWORD)sizeof(desc);
    desc.dwFlags=DSBCAPS_CTRLPOSITIONNOTIFY;
    desc.dwBufferBytes=SAMPLE_BUFS;
    desc.lpwfxFormat=&wFmt;
    desc.guid3DAlgorithm=GUID_NULL;

    /* セカンダリバッファ作成 */
    res=_lpDS->CreateSoundBuffer(&desc,&tmp,NULL);
    if(FAILED(res)) {
        ds_term();
        return -1;
    }
    res=tmp->QueryInterface(IID_IDirectSoundBuffer8,(void**)&_lpSB);
    tmp->Release();
    if(FAILED(res)) {
        ds_term();
        return -1;
    }

    /* 再生終了通知を受け取るイベントを作成 */
    res=_lpSB->QueryInterface(IID_IDirectSoundNotify,(void**)&_lpNtfy);
    if(FAILED(res)) {
        ds_term();
        return -1;
    }

    /* 再生終了通知を受けれるようにしておく */
    _dspn.dwOffset=SAMPLE_BUFS-1;
    _dspn.hEventNotify=CreateEvent(NULL,FALSE,FALSE,NULL);
    if((HANDLE)-1==_dspn.hEventNotify || NULL==_dspn.hEventNotify) {
        ds_term();
        return -1;
    }
    res=_lpNtfy->SetNotificationPositions(1,&_dspn);
    if(FAILED(res)) {
        ds_term();
        return -1;
    }
    return 0;
}

最後に、初期化(ds_init)と解放(ds_term)でメッセージループをサンドイッチしてあげれば、DirectSoundを初期化・解放するプログラムが完成します。
完成版のtest02.cppはコチラです。

test02.cppは、以下のコマンドを実行すればコンパイルできます。
CL /MT test02.cpp user32.lib dsound.lib dxguid.lib

なお、dxguid.libはVisual C++ 2010をインストールしたデフォルトの状態では入っていない筈なので、別途、DirectX SDKを入手してインストールしておく必要があります。

(3)セカンダリバッファ
セカンダリバッファという言葉について、聞きなれない方も居るかもしれないので、軽く解説しておきます。DirectSoundの場合、音を鳴らす前に、音データ(パルス符号)を格納(バッファリング)してから、発音指示をする形で音を鳴らします。このバッファリングする為のバッファがセカンダリバッファです。そして、プライマリバッファは発音中のバッファだと思えば良いと思います。グラフィックスの場合、ちらつきを抑える為にダブルバッファリングの手法がよく採られますが、サウンドでも似たような仕組みだと理解してくれれば良いと思います。

(4)再生終了通知イベント
音を鳴らし続ける場合、バッファリングと発音指示を繰り返すことになります。DirectSoundの場合、その通知をWindowsイベントで受け取ることができます。つまり、以下のような処理を繰り返すことで、「発音し続けるプログラム」を作ります。
  1. バッファリング
  2. 発音指示
  3. 待機(WaitForSingleObject)
(5)サウンドスレッドの実装
上記の処理をシングルスレッドで実装するのは若干大変です。そこで、上記の処理を行う専用のスレッド(サウンドスレッド)を作ることにします。なお、iPhone(OpenAL)の場合も、DirectSoundと同様、別スレッドで実装する必要があります。
それでは、先ほど作成したtest02.cppをベースにして、サウンドスレッドで「無音を鳴らし続ける処理」を実装した形に改造したtest03.cppを作成してみたいと思います。

まず、サウンドスレッドの稼働状態を管理するフラグ(スレッド制御フラグ)を準備します。

■スレッド制御フラグ
/* スレッド制御フラグ */
#define SND_INIT    0       /* 初期状態 */
#define SND_READY   1       /* Ready状態 */
#define SND_EQ      254     /* 停止要求 */
#define SND_END     255     /* 停止状態 */
static volatile BYTE _SndCTRL=SND_INIT;
static long _uiSnd;

スレッド制御フラグは、次のような目的で利用します。
  • サウンドスレッドがready状態になったことの同期
  • サウンドスレッドへの停止要求
  • サウンドスレッドがstop状態になったことの同期
なお、スレッド制御フラグは、最適化抑止(volatile)で宣言しておかないと、コンパイラの種類によっては適切に変化を検出できなくなる恐れがあるので注意してください。
また、サウンドスレッドのスレッドIDも制御する時に必要になっておくので、宣言しておきます。
そして、スレッド制御フラグの変化=状態遷移を待機するための関数(waitstat)を準備します。

■状態遷移を待機(waitstat)
/* 状態遷移を待機する */
static int waitstat(BYTE wctrl)
{
    DWORD ec;
    while(wctrl!=_SndCTRL) {
        Sleep(10);
        if(GetExitCodeThread((HANDLE)_uiSnd,&ec)) {
            if(STILL_ACTIVE!=ec) {
                return -1; /* システム的に停止 */
            }
        } else {
            return -1; /* システム的に停止 */
        }
    }
    return 0;
}
仮にスレッドがリソース不足で起動していなかったりした場合、状態遷移が発生しなくなってしまうことでハングアップする恐れがあるため、必ず生死チェック(GetExitCodeThread)を行わなければなりません。
サウンドスレッドは、以下のような感じで実装します。

■サウンドスレッド(sndmain)
/* サウンドスレッド */
static void sndmain(void* arg)
{
    HRESULT res;
    LPVOID lpBuf;
    DWORD dwSize;
    char buf[SAMPLE_BUFS];

    /* 準備完了! */
    _SndCTRL=SND_READY;

    /* 要求待ちループ */
    while(1) {
        /* READY状態の間、バッファの音を鳴らし続ける */
        while(SND_READY==_SndCTRL) {
            /* バッファを無音状態にする(暫定) */
            memset(buf,0,sizeof(buf));
            /* セカンダリバッファへコピー*/
            dwSize=SAMPLE_BUFS;
            while(1) {
                res=_lpSB->Lock(0
                            ,SAMPLE_BUFS
                            ,&lpBuf
                            ,&dwSize
                            ,NULL
                            ,NULL
                            ,DSBLOCK_FROMWRITECURSOR);
                if(!FAILED(res)) break;
                Sleep(1);
            }
            memcpy(lpBuf,buf,dwSize);
            res=_lpSB->Unlock(lpBuf,dwSize,NULL,NULL);
            if(FAILED(res)) goto ENDPROC;
            /* 発音 */
            ResetEvent(_dspn.hEventNotify);
            res=_lpSB->SetCurrentPosition(0);
            if(FAILED(res)) goto ENDPROC;
            while(1) {
                res=_lpSB->Play(0,0,0);
                if(!FAILED(res)) break;
                Sleep(1);
            }
            WaitForSingleObject(_dspn.hEventNotify,INFINITE);
        }
        /* 要求内容が終了要求なら終了する */
        if(SND_EQ==_SndCTRL) break;
    }

    /* 正常な停止処理 */
    _SndCTRL=SND_END;
    return;

    /* 異常な停止(状態コードを変えない) */
ENDPROC:
    return;
}
まず、自前のサウンドバッファ(buf)をmemsetでゼロクリアすることで、無音状態の波形を生成しています。なお、現時点では暫定的に「無音」をバッファリングしますが、最終的には、波形のリアルタイム演算処理を実装することになります。

サウンドスレッドの処理内容を日本語で説明すると次のようになります。
  • 発音情報のバッファリングは、自前のバッファ(buf)をセカンダリバッファへコピーする形で行うこととしています。セカンダリバッファは、サーフェースなどと同様、ロック(Lock)を行うことでバッファポインタ(lpBuf)を取得できます。そして、lpBufへ波形情報をコピー(memcpy)後に、アンロック(Unlock)を行うことで内容がコミットされます。
  • イベントのリセット(ResetEvent)と再生開始位置の設定(SetPosition)をして、再生(Play)。
  • イベントをWaitForSingleObjectで待機
  • シグナル状態を検出したらまた同じことを繰り返します。
プログラムを直接見た方が分かり易いですね...
最後にWinMainにサウンドスレッドを起動・停止する処理を追加します。

■サウンドスレッド起動処理
    /* サウンド制御スレッド起動 */
    _uiSnd=_beginthread(sndmain,65536,NULL);
    if(-1L==_uiSnd) {
        ds_term();
        exit(-1);
    }
    if(waitstat(SND_READY)) {
        ds_term();
        exit(-1);
    }

■サウンドスレッド停止処理
    _SndCTRL=SND_EQ;
    waitstat(SND_END);

完成版のtest03.cppはコチラです。
コンパイル方法はtest02.cppと同じでOKなので、省略します。

これで、無音」を鳴らし続けるプログラムが完成しました。

test01.cpp~test03.cppまで実行結果が全く同じなので、動かしてもつまらないと思います。申し訳ありません。以降の記事では、このtest03.cppをベースにして、実際に音を鳴らすプログラミング方法を解説していきたいと思います。

0 件のコメント:

コメントを投稿