ads by Amazon

2013年4月11日木曜日

9. 効果音の重要性

さて、これから波形メモリ音源の実装に入りたいところですが、音源を実装する前に「効果音発音システム」を先に実装しておきたいと思います。私はゲームにとって、「音楽」は極めて重要な要素だと思っています。一般的にゲーム音楽というとBGMをイメージされるかもしれません。しかし、ゲームにとってはBGMよりも「効果音」の方が重要度が高いです。

(1)SPACE INVADERの偉大さ
私はゲームの効果音について語るとき、必ず引き合いに出すのがスペースインヴェーダー((C)タイトー)です。私はこのゲームから、効果音の全てを学び取りました。

スペースインヴェーダーには、

  • インヴェーダーが移動する時の音
  • ショットを撃った時の音
  • インヴェーダーを撃墜した時の音
  • 自機が撃墜された時の音

の4種類の音があります。

また、インヴェーダーには大型、中型、小型、UFOの4種類が居て、全ての種類のインヴェーダーの移動音が異なるので、合計7種類の音があります。注目すべき点は、インヴェーダーの種類毎に音を変えていることです。そして、インヴェーダーは数が減ると移動速度が速くなるので、音が全体的にテンポアップすることで、「インヴェーダーが接近する臨場感」を表現しています。

スペースインヴェーダーにはBGMがありません。
しかし、効果音だけでスペースインヴェーダーというゲームの音楽が十分に表現できてしまっているのです。素晴らしい。(私はその素晴らしさを、Invader Block 2という私のゲームでインスパイアさせていただきました)

(2)波形メモリ音源の効果音発音方式
旧来の波形メモリ音源では、効果音発音に1チャネルを使っていました。
しかし、仮想ゲーム機の場合、効果音に関していえば、波形メモリ音源を使うよりも、PCMをそのまま発音させた方が、処理コストが低くなるので有利です。そこで、VGSでは、効果音はPCMをそのまま発音させる方式にしました。

以降の記事では、効果音発音システムの実装方法について、順を追って説明していきます。

8. PSG音源と波形メモリ音源の違い

厳密な言葉の定義がされている訳ではありませんが、一般的に「PSG音源」と「波形メモリ音源」というのは別のものとされています。

(1)PSG音源
General Instrument社が作成したAY-3-8190というチップが、最初に作られPSG音源です。PSGというのは、Programmable Sound Generatorの略で、その名の通り、プログラムにより音の出力を制御することが特徴の音源チップです。
AY-3-8190は、3チャネル(3声)の矩形波と、1チャネルのノイズを発生させることができます。
詳しくは、Wikipediaを参照ください。
https://ja.wikipedia.org/wiki/Programmable_Sound_Generator

(2)波形メモリ音源
このブログで取り扱うのは、PSG音源ではなく、波形メモリ音源です。
波形メモリ音源とは、音の1Hz分の波形情報をメモリにストアしておき、それを発音させる形で音を鳴らします。PSG音源との違いは、原型波形を生成させるための演算が咬まないという点です。それにより、より少ない演算コストで音を発生させることができます。その代り、メモリ消費量がPSGと比べて大きくなる欠点があります。ただし、大きくなるといってもKBオーダーの世界です。
現代では、メモリ(DRAM)はGBオーダーでも数千円程度ですが、昔のメモリは非常に高価だったので、メモリコストよりも演算コストの方が安いと考えらえた(トレードオフされた)ことにより、PSG音源の方が主流でした。
しかし、それでは過去の話しです。
現代のプログラム実行環境では、KBオーダーのメモリコストよりも、演算コストの方が遥かに高いです。そのため、VGSではPSG音源ではなく、波形メモリ音源を採用することにしました。

(3)メモリストア方式
波形メモリ音源にも幾つかの異なる考え方があります。
それは、波形情報のメモリストア方式です。
VGS以外にも波形メモリ音源を扱うプログラムに、開発室Pixelが提供しているPxtone Collageというプログラム(ピスコラ)があります。ピスコラの場合、基本となる1つの音の波形情報(基礎波形データ)をメモリにストアしておき、それをベースにして異なる音階の波形を算出する方式を採っています。私はこれを「単音ストア方式」と呼んでいます。

(4)単音ストア方式
そのメリットは、1つの波形パターン=楽器が必要とするメモリ容量が少ないということです。
それにより、沢山の種類の楽器を持つことが可能です。
単音ストア方式
その一方、音階演算による処理コストが発生するため、処理性能が悪い欠点があります。
「処理性能が悪い」とはいっても、昨今のパソコンであれば全く問題になりません。しかし、スマートフォンなどのモバイル機器は(パソコンと比べて)処理性能が悪いため、これは厄介な問題になると私は考えました。
そこで、私はVGSの波形メモリ音源は、全音ストア方式にすることにしました。

(5)全音ストア方式
これは、全ての音階の1Hz分の波形データをメモリにストアする方式です。
それにより、「周波数変換」の演算回路が不要になり、その分、処理性能が良くなります。
全音ストア方式
ただし、1つの楽器あたりの波形データの量が多くなってしまうので、あまり多くの楽器を持てないという欠点があります。
しかし、私は「ゲーム用の音源」としての波形メモリ音源であれば、単音階ストア方式にするメリット(音色数が豊富であることは)はあまり無いと判断しました。何故なら、ファミコンの場合、使用できる音色は三角波、矩形波、ノコギリ波、ノイズの4種類(VGSと同じ)しか有りませんが、それでもゲーム音楽としては十分な表現能力があると思われたので。という訳で、このブログでは今後、全音ストア方式による波形メモリ音源の実装方法について解説していきます。

2013年4月10日水曜日

7. 440Hz(A)の矩形波を鳴らし続けてみる

それでは、「4. 無音を鳴らし続ける処理の実装(Windows編)」で示したtest03.cpp(無音を鳴らし続けるプログラム)を少し改造して、440HzのA(ラ)を鳴らし続けるプログラムを作成してみたいと思います。なお、他所の解説サイトでは、サイン波を例に示している所が多いみたいですが、「サイン波よりも矩形波の方がコンピュータ向き」ということを体感して頂く為、ここでは矩形波を例に説明します。

(1)処理の考え方
test03.cppの場合、1秒の長さは22050Hzです。
つまり、出力する波形の周期は、22050Hz÷440Hzということで、だいたい50Hzのインターバルで発音してあげれば良いという計算になります。正確には50.11363636363636ですが、先ずは、そこまで厳密に考慮せず、ゆとり式に「だいたい50Hz」ということで処理を実装することにします。どうせテスト目的のものですし。なお、50Hzの矩形波(交流波)を出力するには、25Hzで「正」と「負」を交互に書き込みます。

(2)矩形波バッファリングの実装
それでは、サウンドスレッド(sndmain)に対して、だいたい440Hzの矩形波をバッファリングする処理を実装していきたいと思います。

先ずは、矩形波の算出をするための変数を宣言します。

■変数宣言の追加
    /* 矩形波バッファリング用の変数*/
    int i;
    int hz=0;
    short val=4096;

そして、memsetでバッファを0クリアしていた部分に以下の赤字のような形に変更します。

■バッファリング処理の実装
    /* 要求待ちループ */
    while(1) {
        /* READY状態の間、バッファの音を鳴らし続ける */
        while(SND_READY==_SndCTRL) {
            /* バッファを440Hzの矩形波で埋める */
            for(i=0;i<SAMPLE_BUFS/2;i++) {
                ((short*)buf)[i]=val;
                hz++;
                if(25==hz) {
                    hz=0;
                    val=-val;
                }
            }

これで、50Hz周期で、振幅4096(÷32768)の矩形波がバッファリングされるようになりました。このプログラム(test04.cpp)をコンパイルして実行すれば、だいたい440HzのAっぽい音が鳴り続けます。

6. 音程と周波数の関係

前の記事で、何気なく「440Hz」と記しましたが、これは波形(交流波)を「1秒間に440周期繰り返す」ということを意味しています。
つまり、1秒間に下図の波が440回繰り返されているということです。
この、1秒間に繰り返す周期の回数によって、音の高さ(音程)が決定します。

12平均律(ドレミファソラシド)における周波数と音程の対応関係は、下表のようになっています。
  Oct-1 Oct-2 Oct-3 Oct-4 Oct-5 Oct-6 Oct-7
C
65.4064 130.8128 261.6256 523.2511 1046.5023 2093.0045 4186.0090
C#
69.2957 138.5913 277.1826 554.3653 1108.7305 2217.4610 4434.9221
D
73.4162 146.8324 293.6648 587.3295 1174.6591 2349.3181 4698.6363
D#
77.7817 155.5635 311.1270 622.2540 1244.5079 2489.0159 4978.0317
E
82.4069 164.8138 329.6276 659.2551 1318.5102 2637.0205 5274.0409
F
87.3071 174.6141 349.2282 698.4565 1396.9129 2793.8259 5587.6517
F#
92.4986 184.9972 369.9944 739.9888 1479.9777 2959.9554 5919.9108
G
97.9989 195.9977 391.9954 783.9909 1567.9817 3135.9635 6271.9270
G#
103.8262 207.6523 415.3047 830.6094 1661.2188 3322.4376 6644.8752
A
110.0000 220.0000 440.0000 880.0000 1760.0000 3520.0000 7040.0000
A#
116.5409 233.0819 466.1638 932.3275 1864.6550 3729.3101
B
123.4798 246.9417 493.8833 987.7666 1975.5332 3951.3101
440Hzというのは、上記の表でいうところのOct-3のA(ラ)ということです。
ちなみに、1オクターブ高い音というのは、周波数を2倍にしてあげれば良いので、1オクターブ高い音であれば機械的に求めることができます。

5. 波形の種類

前の記事で記した、「無音」を鳴らすプログラムの、無音を設定している部分(memsetでbufを0クリアしている部分)に、鳴らしたい波形情報を書き込めば音が鳴る訳ですが、その実装を行う前に「波形」について、具体的な例を交えて解説しておきます。

(1)サイン波
2. 電子音のメカニズム」では、例としてサイン波の波形を図示しました。
実際に、サイン波の音を鳴らしてみて、具体的にどんな音が鳴るのか確認してみましょう。
sign440hz.mp3: VGSの音声品質で440Hzのサイン波を1秒発音(約7KB)
「丸い感じ」のイメージの音が確認できると思います。

音が丸くなる理由は、相違変化が少ないためです。
相違変化というのは、1つ前の波形位置からの変化の幅の大きさのことです。
相違変化が大きい場合、ノイジーな音になります。

(2)その他の代表的な波形
サイン波以外の代表的な波形としては、三角波、矩形波、ノコギリ波(下図)があります。
VGSの波形メモリ音源が実装している波形は、これら3種類の波形です。

(3)三角波
なお、サイン波は、だいたい三角波と同じ音です。
サイン波の形を大ざっぱにしたものが三角波なので。
ちなみに、ファミコンおPSG音源では三角波は鳴らせますが、サイン波を鳴らすことはできません。しかし、三角波のことをサイン波と呼んでいる人も結構居たりします。なので、三角波=サイン波という認識でも問題ないと思います。(音声品質が良い状態だと結構違いますが)
三角波は、サイン波よりも波形が若干尖っている分、音も若干尖った感じになります。
tri440hz.mp3: VGSの音声品質で440Hzのサイン波を1秒発音(約7KB)
高周波数(高音域)で鳴らせば笛みたいな感じの音色で、低周波数(低音域)で鳴らせばベースみたいな感じの音色になります。

(4)矩形波
いわゆる「PSG音源」と呼ばれる音源が発音できるのが矩形波です。
相違の変化回数が少ないため、とても単純な回路で波形データを生成できる特徴があるので、昔のコンピュータの音はだいたい矩形波でした。PC-9801の電源をONした時に「ピコッ」と音(beep音)が鳴りますが、それも矩形波です。
相違変化量が大きいため、三角波よりもノイジーな音になっています。
pul440hz.mp3: VGSの音声品質で440Hzの矩形波を1秒発音(約10KB)
よく、クラリネットみたいな感じの音だと言われています・・・が、個人的にはやはりコンピュータの音というイメージが強い音です。これは、マイコン世代特有の感覚かもしれませんが。最近のコンピュータ世代の方には馴染みがないと思うので。
ファミコンでは、サブメロディーや歌系のメロディーの音としてよく使われていました。

(5)ノコギリ波
実は、NOKOGI Riderというタイトルの由来は、このノコギリ波からきています。
一部、ヨルムンガンドが由来だと囁かれているようですが、作者の私が言うから間違いありません。
三角波と、矩形波を組み合わせたような特徴なので、ノイジーかつ尖った音が鳴ります。
nok440hz.mp3: VGSの音声品質で440Hzのノコギリ波を1秒発音(約9KB)
一般的には、ストリングス(弦楽器)みたいな音だと言われています。
若干、「ん?」という感じですが。
ファミコンでは、メインメロディーの音としてよく使われていました。

(6)波の形=音色のイメージ
この記事で伝えたかったことは、波の形と音色のイメージの関係を体感的に理解していただきたいということです。これらの仕組みを理解しておけば、自前で効果音を作ることができるので、非常に便利です。
よく、効果音についてはフリー素材を利用されている方が居ますが、折角オリジナルのゲームを作るのであれば、効果音もオリジナルのものを作った方が、より良いゲームが作れると思うので。効果音は、感覚的&簡単に作れることができるので、フリー素材なんかに頼る必要は全くありません。

なお、この記事で紹介しているサンプルの音は、全て効果音エディタ_Dというエディタで作りました。
http://www.geocities.jp/hirogamesoft/se_d/se_d.html
この記事で書いた知識とSE_Dがあれば、誰でもオリジナルの効果音を簡単に作ることができます。
是非、オリジナルの効果音作成に挑戦してみてください。

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をベースにして、実際に音を鳴らすプログラミング方法を解説していきたいと思います。

3. エミュレータで音を鳴らす仕組み

VGSは、エミュレータの仕組みを応用した仮想ゲーム機です。そのため、音声発音システムの実装は、エミュレータのアーキテクチャを参考にして実装しています。なので、mameのソースコードを見れば、だいたいどんな仕組みで実装すべきか分かると思います・・・ただ、それが難儀なことだから、この仕組みで実装できない人の方が多いのであろうと思います。
エミュレータの場合、細かい時系列単位(だいたい50ms~100ms間隔)で、リアルタイムな計算により音声波形データを生成して、それを音源デバイスに書き込み続ける形で音声システムが実装されています。つまり、音が全く鳴っていない状態であっても、「無音」の波形データ(0が連続した配列)を発音しています。

(1)Windows
Windowsの場合、DirectX(DirectSound)と標準のWaveMapperのどちらでも、一定時間間隔で任意の波形データを発音できる仕組みがあります。なので、どちらの仕組みを使っても実装可能ですが、VGSの場合はDirectSoundを使っているので、DirectSoundを用いたエミュレータの発音システムの実装を解説していきます。

(2)Android
Androidの場合、Android 2.3.3以降から提供されるようになったOpenSL/ESを用いることで、コールバックにより一定時間間隔で任意の波形データを発音できる仕組みを実装できます。なお、OpenSL/ESはJavaでは使えません。そのため、ゲーム本体をJavaで実装する場合であっても、音声発音の部分はC言語(JNI)で実装する必要があります。

(3)iPhone
iPhone(iOS)の場合、OpenALを用いることで、DirectSoundと似たような実装方法により、一定時間間隔で任意の波形データを発音できる仕組みを実装できます。

(4)アンマネージド・コードのプラットフォームは除外
当初、私はVGSをメトロにもポーティングしようと考えていた時期もありました。しかし、メトロの場合、XAMLでC言語での実装が可能ですが、.NET frameworkアーキテクチャみたいな感じのマネージドコードと呼ばれる仕組みの上で実装しなければなりません。
音声波形は、リアルタイムな演算により算出しなければなりません。そのため、アンマネージドコード(Nativeコード)で実装しなければ、とてもじゃないけど演算速度が追いつかないです。なので、メトロを切りました。
当然、HTML5(FireFoxOSとか?)やJavaスクリプトみたいなものも、現時点のモバイル機器では、実用に耐え得る性能を確保することは不可能です。HTML5等は、ライトゲームを作るだけなら問題無い程度ですが、並行してリアルタイム波形演算をできる程度の性能は確保できません。
ドラゴンボールのように、CPUの戦闘能力がインフレーションしていた時期(10年ぐらい前)であれば、「今はダメだけど将来は・・・」という期待を持てましたが、省電力などの関係からハードがチープ化している現状から、「マネージドコードで何でもできる」という時代は、まだまだ先のことであろうと想定しています。当面、モバイル機器対応のゲームを作るには、Nativeコードをサポートしているプラットフォームしか選択肢はありません。メトロやHTML5等の存在を否定する訳ではありません。しかし、メトロやHTML5は、少なくともこのブログで紹介する音声発音方式に耐え得るプラットフォームではありません。
Androidですら、仮に「Javaだけ」だったら対応不可能でした。(AndroidでNativeを解放したのは、当初のプラットフォーム思想からは外れた行為でしたが、結果的には良かった事だったと私は評価しています)

なので、このブログで紹介する音声システムで対応できるプラットフォームは、
・Windows(デスクトップアプリ)
・Android
・iPhone
に絞ります。