ads by Amazon

2013年4月11日木曜日

11. 効果音処理の実装(Windows)

この記事では、「7. 440Hz(A)の矩形波を鳴らし続けてみる」で作成したtest04.cppをベースにして、
  1. 効果音データの作成
  2. 効果音データテーブル(ESLOT)の実装
  3. 効果音データの読み込み関数の実装
  4. 効果音発音フラグ設定関数の実装
  5. 効果音バッファリング関数+処理の実装
  6. 効果音データの読み込み処理の実装
  7. 効果音の発音指示の実装
までを解説します。

(1)効果音データの作成
前の記事で示したPCMデータ作成コマンド(vgswav)を使って、PCMファイルを3種類準備します。
PCMファイルは、以下のファイル名とします。
  1. eff01.pcm
  2. eff02.pcm
  3. eff03.pcm
面倒な場合、コチラから私が適当に作成したものをダウンロードできます。
効果音エディタ_Dを使えば、こういった効果音が一瞬で作れます。

(2)効果音データテーブル(ESLOT)の実装
まず、test04.cppに対して効果音のPCMデータを格納する宣言などを追加します。

■宣言追加
/* 効果音テーブル */
struct _EFF {
    unsigned int flag;      /* 再生フラグ */
    unsigned int pos;       /* 再生位置 */
    unsigned int size;      /* サイズ */
    unsigned char* dat;     /* PCMデータ */
};
static struct _EFF _eff[256];
static CRITICAL_SECTION _csEff;
今回のテストプログラム(test05.cpp)では、3つしか効果音を使いませんが、最大256個のレコードを読み込める仕様にしておきます。また、効果音の発音処理はサウンドスレッド(sndmain)で行いますが、発音の指示はメインスレッドのウィンドウプロシージャ(wproc)から行います。その為、発音指示を排他的に行うようにしないとバスエラーが発生する恐れがあるので、クリティカルセクション(_csEff)も必要です。

(3)効果音データの読み込み関数の実装
そして、PCMデータを解放する処理と、ファイルからロードする処理を行う関数を実装します。

■PCMデータの解放関数
/* PCMスロット領域の解放 */
static void freepcm(unsigned char n)
{
    if(_eff[n].dat) free(_eff[n].dat);
    _eff[n].flag=0;
    _eff[n].pos=0;
    _eff[n].size=0;
    _eff[n].dat=NULL;
}

■PCMデータのロード関数
/* PCMファイルをスロット領域へ読み込む */
static int loadpcm(unsigned char n,const char* fname)
{
    unsigned char bin[8];
    FILE* fp;

    /* ヘッダ情報の読み込み&チェック */
    if(NULL==(fp=fopen(fname,"rb"))) return -1;
    fread(bin,8,1,fp);
    if('E'!=bin[0] || 'F'!=bin[1] || 'F'!=bin[2] || '\0'!=bin[3]) {
        fclose(fp);
        return -1;
    }
    freepcm(n);

    /* サイズ情報をホストバイトオーダで設定 */
    _eff[n].size=bin[4];
    _eff[n].size<<=8;
    _eff[n].size|=bin[5];
    _eff[n].size<<=8;
    _eff[n].size|=bin[6];
    _eff[n].size<<=8;
    _eff[n].size|=bin[7];

    /* PCMデータ本体のロード */
    _eff[n].dat=(unsigned char*)malloc(_eff[n].size);
    if(NULL==_eff[n].dat) {
        freepcm(n);
        fclose(fp);
        return -1;
    }
    fread(_eff[n].dat,_eff[n].size,1,fp);
    fclose(fp);
    return 0;
}

PCMデータのファイル構造は、前の記事でも書きましたが、先頭4バイトがアイキャッチで、次の4バイトがビッグエンディアン(ネットワークバイトオーダ)のサイズ情報になっています。そこで、適切なアイキャッチが設定しているかチェックしてから、サイズをホストバイトオーダに変換します。
そして、求まったサイズ情報から、読み込みバッファをヒープ領域に確保(malloc)して、一気に読み込みます。(なお、この部分は、本当は分割ロード、リトライ処理、異常ケースに対応した実装などが本来なら必要なのですが、このブログでは、説明が煩雑になるのを避けるため、処理を簡略化しています)

(4)効果音発音フラグ設定関数の実装
効果音の発音は、_EFF構造体のメンバ変数flagの状態によって、次のように判断します。
  • 0: 発音指示が無い状態
  • 1: 発音中の状態
  • 2以上: 発音中の効果音に再度発音指示が発生した状態
フラグを0クリアする関数と、フラグをセット(インクリメント)する関数が必要になるので、それらの関数を追加します。

■フラグを0クリアする関数
/* 効果音発音フラグのクリア */
static void zflag(unsigned char n)
{
    EnterCriticalSection(&_csEff);
    _eff[n].flag=0;
    LeaveCriticalSection(&_csEff);
}

■フラグをセット(インクリメント)する関数
/* 効果音発音フラグのセット */
static void aflag(unsigned char n)
{
    EnterCriticalSection(&_csEff);
    if(_eff[n].dat) _eff[n].flag++;
    LeaveCriticalSection(&_csEff);
}
効果音の発音指示を多なうときは、aflag関数の引数にスロット番号を指定します。

(5)効果音バッファリング関数+処理の実装
効果音のバッファリング処理は次のように実装します。

■バッファリング関数(mkbuf)
/* バッファリング処理 */
static void mkbuf(char* buf,size_t size)
{
    int cs;
    int i;

    /* 無音状態にする */
    memset(buf,0,size);

    /* 優先順位の高いサウンドを検出してバッファリング */
    for(i=0;i<256;i++) {
        if(_eff[i].flag) {
            if(1<_eff[i].flag) {
                _eff[i].pos=0;
                zflag((unsigned char)i);
                aflag((unsigned char)i);
            }
            /* コピーサイズの計算 */
            cs=_eff[i].size-_eff[i].pos;
            if(SAMPLE_BUFS<cs) {
                cs=SAMPLE_BUFS;
            }
            /* コピー */
            memcpy(buf,&(_eff[i].dat[_eff[i].pos]),cs);
            _eff[i].pos+=cs;
            if(_eff[i].size<=_eff[i].pos) {
                /* 発音終了 */
                _eff[i].pos=0;
                zflag((unsigned char)i);
            }
            break;
        } else {
            _eff[i].pos=0;
        }
    }

    /* 優先順位の低いサウンドはバッファリングしない */
    /* TODO: 今後、合成を検討 */
    for(i+=1;i<256;i++) {
        if(_eff[i].flag) {
            _eff[i].pos=0;
            zflag((unsigned char)i);
        }
    }
}
このロジックが、ある意味今回記事の中核です。
なので、ちょっと詳しく解説しておきます。

■巻き戻し処理
flagが1より大きい(2以上)の場合、既に発音中の効果音に対して再び発音指示があったということを意味します。そこで、発音位置(pos)をクリアして、flagを1に戻しています。これにより、既に発音中の効果音に対して再び発音指示があった場合、巻き戻して最初から発音し直すようになります。

■バッファリング
1度にバッファリングできるサイズは、引数sizeに指定されたサイズに収める必要があります。
引数sizeには、22050Hz、16bit、1chで50ms分のデータを格納できるサイズ(4410バイト)が設定されています。発音する効果音(flagが立っている効果音)がそれ以下であれば、バッファに全てのデータを書き込み、収まりきらない場合、発音可能な分だけ書き込み、メンバ変数posに現在位置を記憶しています。
そして、発音が終了した効果音は、zflagでゼロクリアします。

■プライオリティ
今回、効果音の合成は行わず、プライオリティの高い効果音のみ発音する形にしておきます。
プライオリティは、スロット番号が小さいもの程高いものとします。そして、プライオリティが高い効果音が発音中の場合、プライオリティが低い効果音はマスクする(強制的にzflagでクリアする)ことにします。
効果音の合成については、次の記事で解説します。
なお、Invader Block 2の場合、この処理方式(合成なし)で効果音を発音しています。その方が、80年代のオールドゲームっぽい感じの雰囲気が出るので、作成するゲームの種類によっては、この方式の方が適切な場合もあります。

■mkbufの呼び出し(バッファリング処理)
test04.cppのsndmain関数中の矩形波(A)をバッファリングしている部分を、mkbufを呼び出すように置き換えます。

(6)効果音データの読み込み処理の実装
WinMainに、(3)で示したloadpcm、freepcmの呼び出し処理と、(4)で用いているクリティカルセクションの初期化と解放処理を実装します。

(7)効果音の発音指示の実装
ウィンドウプロシージャ(wproc)に効果音の発音指示を実装します。
なお、発音指示はボタン(下図)で行う仕様にします。
test05実行画面(ボタンで発音指示)
ボタンをWM_CREATEに実装し、ボタンの処理(WM_COMMAND)でボタンに対応する効果音の発音指示(aflag)を行います。

■ボタンの作成+発音指示
        case WM_CREATE:
            CreateWindow("BUTTON","eff01"
                ,WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                10,10,64,32,hWnd,(HMENU)1001,g_hIns,NULL);
            CreateWindow("BUTTON","eff02"
                ,WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                80,10,64,32,hWnd,(HMENU)1002,g_hIns,NULL);
            CreateWindow("BUTTON","eff03"
                ,WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                150,10,64,32,hWnd,(HMENU)1003,g_hIns,NULL);
            break;
        case WM_COMMAND:
            switch(LOWORD(wParam)) {
                case 1001: aflag(0); break;
                case 1002: aflag(1); break;
                case 1003: aflag(2); break;
            }
            break;
これで完成です。

なお、プライオリティの件は、eff2ボタンを押してから素早くeff1ボタンを押せば、eff2の再生がキャンセルされてeff1が発音されることを確認できると思います。逆に、eff1ボタンを押してから素早くeff2ボタンを押せば、(eff1が発音中の間は)eff2の再生がキャンセルされることを確認できると思います。

(8)全体処理 test05.cpp
ちょっと長くなりますが、test05.cppの全体像を以下に示します。
※Googleドライブだと文字化けしてしまうみたいなので...
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <math.h>
#include <dsound.h>
#define APPNAME "DirectSound Test"
HINSTANCE g_hIns;

/* 波形情報リテラル */
#define SAMPLE_RATE 22050   /* 周波数 */
#define SAMPLE_BITS 16      /* ビットレート */
#define SAMPLE_CH   1       /* 1ch(モノラル) */
#define SAMPLE_BUFS 4410    /* バッファサイズ(50ms分) */

/* DirectSound関連のグローバル変数 */
static LPDIRECTSOUND8 _lpDS=NULL;
static LPDIRECTSOUNDBUFFER8 _lpSB=NULL;
static LPDIRECTSOUNDNOTIFY8 _lpNtfy=NULL;
static DSBPOSITIONNOTIFY _dspn;

/* スレッド制御フラグ */
#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;

/* 効果音テーブル */
struct _EFF {
    unsigned int flag;      /* 再生フラグ */
    unsigned int pos;       /* 再生位置 */
    unsigned int size;      /* サイズ */
    unsigned char* dat;     /* PCMデータ */
};
static struct _EFF _eff[256];
static CRITICAL_SECTION _csEff;

/* PCMスロット領域の解放 */
static void freepcm(unsigned char n)
{
    if(_eff[n].dat) free(_eff[n].dat);
    _eff[n].flag=0;
    _eff[n].pos=0;
    _eff[n].size=0;
    _eff[n].dat=NULL;
}

/* PCMファイルをスロット領域へ読み込む */
static int loadpcm(unsigned char n,const char* fname)
{
    unsigned char bin[8];
    FILE* fp;

    /* ヘッダ情報の読み込み&チェック */
    if(NULL==(fp=fopen(fname,"rb"))) return -1;
    fread(bin,8,1,fp);
    if('E'!=bin[0] || 'F'!=bin[1] || 'F'!=bin[2] || '\0'!=bin[3]) {
        fclose(fp);
        return -1;
    }
    freepcm(n);

    /* サイズ情報をホストバイトオーダで設定 */
    _eff[n].size=bin[4];
    _eff[n].size<<=8;
    _eff[n].size|=bin[5];
    _eff[n].size<<=8;
    _eff[n].size|=bin[6];
    _eff[n].size<<=8;
    _eff[n].size|=bin[7];

    /* PCMデータ本体のロード */
    _eff[n].dat=(unsigned char*)malloc(_eff[n].size);
    if(NULL==_eff[n].dat) {
        freepcm(n);
        fclose(fp);
        return -1;
    }
    fread(_eff[n].dat,_eff[n].size,1,fp);
    fclose(fp);
    return 0;
}

/* 効果音発音フラグのクリア */
static void zflag(unsigned char n)
{
    EnterCriticalSection(&_csEff);
    _eff[n].flag=0;
    LeaveCriticalSection(&_csEff);
}

/* 効果音発音フラグのセット */
static void aflag(unsigned char n)
{
    EnterCriticalSection(&_csEff);
    if(_eff[n].dat) _eff[n].flag++;
    LeaveCriticalSection(&_csEff);
}

/* バッファリング処理 */
static void mkbuf(char* buf,size_t size)
{
    int cs;
    int i;

    /* 無音状態にする */
    memset(buf,0,size);

    /* 優先順位の高いサウンドを検出してバッファリング */
    for(i=0;i<256;i++) {
        if(_eff[i].flag) {
            if(1<_eff[i].flag) {
                _eff[i].pos=0;
                zflag((unsigned char)i);
                aflag((unsigned char)i);
            }
            /* コピーサイズの計算 */
            cs=_eff[i].size-_eff[i].pos;
            if(SAMPLE_BUFS<cs) {
                cs=SAMPLE_BUFS;
            }
            /* コピー */
            memcpy(buf,&(_eff[i].dat[_eff[i].pos]),cs);
            _eff[i].pos+=cs;
            if(_eff[i].size<=_eff[i].pos) {
                /* 発音終了 */
                _eff[i].pos=0;
                zflag((unsigned char)i);
            }
            break;
        } else {
            _eff[i].pos=0;
        }
    }

    /* 優先順位の低いサウンドはバッファリングしない */
    /* TODO: 今後、合成を検討 */
    for(i+=1;i<256;i++) {
        if(_eff[i].flag) {
            _eff[i].pos=0;
            zflag((unsigned char)i);
        }
    }
}

/* 状態遷移を待機する */
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;
}

/* サウンドスレッド */
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) {
            /* バッファリング */
            mkbuf(buf,SAMPLE_BUFS);
            /* セカンダリバッファへコピー*/
            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;
}

/* 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を初期化 */
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;
}

/* ウィンドウ処理 */
static LRESULT CALLBACK wproc(HWND hWnd,UINT msg,UINT wParam,LONG lParam)
{
    switch( msg ){
        case WM_CREATE:
            CreateWindow("BUTTON","eff01"
                ,WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                10,10,64,32,hWnd,(HMENU)1001,g_hIns,NULL);
            CreateWindow("BUTTON","eff02"
                ,WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                80,10,64,32,hWnd,(HMENU)1002,g_hIns,NULL);
            CreateWindow("BUTTON","eff03"
                ,WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
                150,10,64,32,hWnd,(HMENU)1003,g_hIns,NULL);
            break;
        case WM_COMMAND:
            switch(LOWORD(wParam)) {
                case 1001: aflag(0); break;
                case 1002: aflag(1); break;
                case 1003: aflag(2); break;
            }
            break;
        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;

    /* インスタンスハンドルを記憶しておく */
    g_hIns=hIns;

    /* クリティカルセクションの初期化 */
    InitializeCriticalSection(&_csEff);

    /* PCMデータを読み込む */
    loadpcm(0,"eff01.pcm");
    loadpcm(1,"eff02.pcm");
    loadpcm(2,"eff03.pcm");

    /* ウィンドウクラスの登録 */
    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;
    }

    /* DirectSoundの初期化 */
    if(-1==ds_init(hwnd)) {
        exit(-1);
    }

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

    /* メインループ */
    while(TRUE) {
        /* メッセージ処理 */
        if( PeekMessage( &msg , 0 , 0 , 0 , PM_REMOVE ) ){
            if( msg.message == WM_QUIT ) {
                break;
            }
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        Sleep(16);
    }

    /* 終了処理 */
    _SndCTRL=SND_EQ;
    waitstat(SND_END);
    ds_term();
    freepcm(0);
    freepcm(1);
    freepcm(2);
    DeleteCriticalSection(&_csEff);
    return TRUE;
}

0 件のコメント:

コメントを投稿