ads by Amazon

2013年4月12日金曜日

16. オペレーション・レコード

この記事では、VGS音源の音源ドライバ・音源モジュール間のオペレーション仕様を示します。

(1)モジュール間I/F
13. 基礎アーキテクチャ」でも解説しましたが、VGSの場合、音源ドライバが音源モジュールに対して、オペレーションを入力し、音源モジュールが発音処理を行います。
音源ドライバ・音源モジュール間のI/F

(2)オペレーション
オペレーションとは、「発音せよ」「消音せよ」といった感じの指令です。
実際、VGSのオペレーションには幾つかの種類がありますが、先ずは最低限必要な発音(KEY-ON)消音(KEY-OFF)待機(WAIT)のオペレーションだけ実装することを目標にします。

(3)WAITオペレーション
楽譜記号のオタマジャクシには、全音符(白い丸)、半音符(白抜きの♩)、四分音符(♩)、八分音符(♪)などの種類があります。その名の通り、半音符は全音符の1/2の長さ、四分音符は全音符の1/4の長さ、八分音符は全音符の1/8の長さという具合です。全音符の長さはテンポにより異なります。
つまり、WAITオペレーションというのは、音や無音(休符)の長さを待機する為のパラメータです。
「それならば、KEY-ONオペレーションに長さのパラメータを持たせれば良いのでは?」と思われるかもしれませんが、「逐次オペレーション」で複数のチャネル(声部)のKEY-ONとKEY-OFFを並列に管理する場合、WAITオペレーションを分離しておいた方が、シンプルに実装できます。(この理屈は、現段階では理解できなくても問題ありません)

(4)音源ドライバの状態遷移
音源ドライバは、下図のような状態遷移をします。
音源ドライバの処理の状態遷移
WAIT以外のオペレーションが発生した場合、連続逐次的にオペレーションの処理を行い、WAITオペレーションが発生した時は待機状態になるようにします。つまり、WAITオペレーションが状態遷移のキーになります。

(5)オペレーション・レコードの設計
それでは、オペレーション・レコードの構造体について考えてみます。
レコード設計をする場合、主キーとそれに関連するプロパティが何かを考えます。
主キーは「オペレーションの種類」です。
そして、オペレーションの種類毎に必要なプロパティ情報(=オペランド)は、次の通りです。
  • Wait: ①時間(周波数と同じとする:1秒=22,050)
  • KEY-ON: ①チャネル番号(0~5)、②音程(0~84)
  • KEY-OFF: ①チャネル番号(0~5)
上記を踏まえて、次のような構造体でオペレーション・レコードを宣言することにします。
/* オペレーションレコード */
#define NOTE_WAIT       0       /* WAIT */
#define NOTE_KEYON      1       /* KEY-ON */
#define NOTE_KEYOFF     2       /* KEY-OFF */
struct _NOTE {
    unsigned char type;         /* 種別 */
    unsigned char op1;          /* オペランド1 */
    unsigned char op2;          /* オペランド2 */
    unsigned char op3;          /* オペランド3 */
    unsigned int val;           /* 値 */
};
256未満で収まるオペランドはop1、op2、op3に持たせ、大きな数値(時間)はvalに持たせる仕様です。なお、1秒=22050で、int=2の32乗-1なので、ひとつの音符または休符の最大長は194,783秒という制約が生じることになりますが、バッソ・コンティヌオでもそんな長い音符はこの世に存在しないので、問題ありません。

15. 蛙の歌を歌わせてみる

前の記事で作成したtoneテーブル(tone.c)のテストを兼ねて、test06.cppをベースにして蛙の歌をBGMとして再生するプログラム(発音テスト)を実装してみることにします。なお、この段階では、まだオペレーションを実装していないので、ダイレクトにテーブルの波形情報をバッファリングする形で実装します。

(1)toneテーブルのextern宣言
まず、tone.cで宣言しているtoneテーブル(TONE1、TONE2、TONE3、TONE4)をextern宣言でアクセスできるようにします。
/* トーンテーブル */ extern "C" { extern short* TONE1[85]; /* 三角波 */ extern short* TONE2[85]; /* ノコギリ波 */ extern short* TONE3[85]; /* 矩形波 */ extern short* TONE4[85]; /* ノイズ */ } 

(2)発音処理の実装
mkbuf関数の、効果音を発音後の部分に、次の発音テストコードを実装します。
/* 音源エミュレータテスト */ { static int hz; static int ply[7]={39,41,43,44,43,41,39}; static int p1; // 発音位置 static int p2; // 波形位置 for(i=0;i<size;i+=2) { bp=(short*)(&buf[i]); wav=*bp; wav+=TONE3[ply[p1]][1+p2]*64; if(32767<wav) wav=32767; else if(wav<-32768) wav=-32768; (*bp)=(short)wav; p2+=2; if(TONE3[ply[p1]][0]<=p2) p2=0; hz++; if(11025<=hz) { p1++; if(7<=p1) p1=0; p2=0; hz=0; } } }

上記のテストコードでは、11025Hzの間隔(=0.5秒間隔)で、「ドレミファミレド」の発音を繰り返しています。別にフルコーラスでも良いのですが、単なるテストコードなので簡単に。なお、TONE3(矩形波)のテーブルを使っていますが、この部分をTONE1にすれば三角波、TONE2にすればノコギリ波、TONE4にすればノイズが奏でられるようになります。

これで実装完了です。
簡単ですね。

この実装を施したtest07.cppはコチラからダウンロードできます。
test07.cppのコンパイルは、次のコマンドでOKです。
CL /MT test07.cpp tone.c user32.lib dsound.lib dxguid.lib

BGM(蛙の歌)を鳴らした状態で、効果音の発音をすることができることも確認してください。
これで、BGMと効果音の両方を鳴らすことができました。

まぁ、ここまでは簡単です。
本格的に難しくなるのは、ここから先です

・・・ここからが地獄だ・・・

恐らく、ここまでの記事では脱落者は少ないものと推測しています。そもそも、このブログで扱っているネタに興味を持たれる時点で、それなりに高いプログラミング能力をお持ちの方が大半だろうと思われるので。しかし、ここから先は、脱落者続出になるかもしれません。

誰も読まないブログ?それもまた一興。

14. toneテーブルの準備

8. PSG音源と波形メモリ音源の違い」で解説したように、これから作成する音源モジュールは、全音ストア方式の音源モジュールです。そこで、楽器毎に全音階分の基礎波形データの音色テーブル(toneテーブル)を準備する必要があります。

(1)テーブル形式
基礎波形データのテーブル形式は、音階毎に1Hz分のパルス符号数と符号付16bit整数のパルス符号を持つ可変長テーブルとします。ごちゃごちゃ言ってますが、要するにshort型配列の塊です。
1Hz分のパルス符号数(n)は、音階によって異なります。
音は、低い音の周期(n1)が長く、高い音の周期(n2)が短い特性があります。
つまり、配列サイズはn2<n1となります。
したがって、可変長配列として準備するのが妥当だと言えます。

(2)テーブル作成作業
それでは、上記の理論を踏まえて、三角波、矩形波、ノコギリ波、ノイズのtoneテーブルを手書きで作成・・・などと愚かなことをやる人は、このブログの読者には居ないと思います。我々プログラマは、そういったバカバカしい作業を効率的にこなすのが得意なので。

私の場合、下記のプログラムを組んで、toneテーブルを一瞬で作成しました。
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc,char* argv[]) { const char *n1[12]={"A","A#","B","C","C#","D","D#","E","F","F#","G","G#"}; const char *n2[12]={"A","AS","B","C","CS","D","DS","E","F","FS","G","GS"}; float cnt[85]={ 55.0000 /* A 0 /00 */ , 58.2705 /* A# 0 /01 */ , 61.7354 /* B 0 /02 */ , 65.4064 /* C 1 /03 */ , 69.2957 /* C# 1 /04 */ , 73.4162 /* D 1 /05 */ , 77.7817 /* D# 1 /06 */ , 82.4069 /* E 1 /07 */ , 87.3071 /* F 1 /08 */ , 92.4986 /* F# 1 /09 */ , 97.9989 /* G 1 /10 */ , 103.8262 /* G# 1 /11 */ , 110.0000 /* A 1 /12 */ , 116.5409 /* A# 1 /13 */ , 123.4798 /* B 1 /14 */ , 130.8128 /* C 2 /15 */ , 138.5913 /* C# 2 /16 */ , 146.8324 /* D 2 /17 */ , 155.5635 /* D# 2 /18 */ , 164.8138 /* E 2 /19 */ , 174.6141 /* F 2 /20 */ , 184.9972 /* F# 2 /21 */ , 195.9977 /* G 2 /22 */ , 207.6523 /* G# 2 /23 */ , 220.0000 /* A 2 /24 */ , 233.0819 /* A# 2 /25 */ , 246.9417 /* B 2 /26 */ , 261.6256 /* C 3 /27 */ , 277.1826 /* C# 3 /28 */ , 293.6648 /* D 3 /29 */ , 311.1270 /* D# 3 /30 */ , 329.6276 /* E 3 /31 */ , 349.2282 /* F 3 /32 */ , 369.9944 /* F# 3 /33 */ , 391.9954 /* G 3 /34 */ , 415.3047 /* G# 3 /35 */ , 440.0000 /* A 3 /36 */ , 466.1638 /* A# 3 /37 */ , 493.8833 /* B 3 /38 */ , 523.2511 /* C 4 /39 */ , 554.3653 /* C# 4 /40 */ , 587.3295 /* D 4 /41 */ , 622.2540 /* D# 4 /42 */ , 659.2551 /* E 4 /43 */ , 698.4565 /* F 4 /44 */ , 739.9888 /* F# 4 /45 */ , 783.9909 /* G 4 /46 */ , 830.6094 /* G# 4 /47 */ , 880.0000 /* A 4 /48 */ , 932.3275 /* A# 4 /49 */ , 987.7666 /* B 4 /50 */ , 1046.5023 /* C 5 /51 */ , 1108.7305 /* C# 5 /52 */ , 1174.6591 /* D 5 /53 */ , 1244.5079 /* D# 5 /54 */ , 1318.5102 /* E 5 /55 */ , 1396.9129 /* F 5 /56 */ , 1479.9777 /* F# 5 /57 */ , 1567.9817 /* G 5 /58 */ , 1661.2188 /* G# 5 /59 */ , 1760.0000 /* A 5 /60 */ , 1864.6550 /* A# 5 /61 */ , 1975.5332 /* B 5 /62 */ , 2093.0045 /* C 6 /63 */ , 2217.4610 /* C# 6 /64 */ , 2349.3181 /* D 6 /65 */ , 2489.0159 /* D# 6 /66 */ , 2637.0205 /* E 6 /67 */ , 2793.8259 /* F 6 /68 */ , 2959.9554 /* F# 6 /69 */ , 3135.9635 /* G 6 /70 */ , 3322.4376 /* G# 6 /71 */ , 3520.0000 /* A 6 /72 */ , 3729.3101 /* A# 6 /73 */ , 3951.3101 /* B 6 /74 */ , 4186.0090 /* C 7 /75 */ , 4434.9221 /* C# 7 /76 */ , 4698.6363 /* D 7 /77 */ , 4978.0317 /* D# 7 /78 */ , 5274.0409 /* E 7 /79 */ , 5587.6517 /* F 7 /80 */ , 5919.9108 /* F# 7 /81 */ , 6271.9270 /* G 7 /82 */ , 6644.8752 /* G# 7 /83 */ , 7040.0000 /* A 7 /84 */ }; float fhz; int hz; int i; int tone=0; float n,m; printf("/*\n"); printf(" *--------------------------------------------------\n"); printf(" * 三角波(SANKAKU)\n"); printf(" *--------------------------------------------------\n"); printf(" */\n"); for(tone=0;tone<85;tone++) { fhz=88200/cnt[tone]; hz=(int)fhz; printf("/* #%02d: tone %s oct %d */\n",tone,n1[tone%12],1+tone/12); m=64.0f / (fhz/4); printf("short TRI_%s%d[%d]={%d",n2[tone%12],1+tone/12,hz+1,hz); for(n=0,i=0;i<hz/4;i++,n+=m) { printf(",%d",(int)n); } for(i=0;i<hz/2;i++,n-=m) { printf(",%d",(int)n); } for(i=0;i<hz/4;i++,n+=m) { printf(",%d",(int)n); } printf("};\n\n"); } printf("short* TONE1[85]={"); for(tone=0;tone<85;tone++) { if(tone)printf(","); printf("TRI_%s%d",n2[tone%12],1+tone/12); } printf("};\n\n"); printf("/*\n"); printf(" *--------------------------------------------------\n"); printf(" * ノコギリ波(NOKOGIR)\n"); printf(" *--------------------------------------------------\n"); printf(" */\n\n"); for(tone=0;tone<85;tone++) { fhz=88200/cnt[tone]; hz=(int)fhz; printf("/* #%02d: tone %s oct %d */\n",tone,n1[tone%12],1+tone/12); m=64.0f / (fhz/2); printf("short NOC_%s%d[%d]={%d",n2[tone%12],1+tone/12,hz+1,hz); for(n=0,i=0;i<hz/2;i++,n+=m) { printf(",%d",(int)n); } for(n=-64.0f;i<hz;i++,n+=m) { printf(",%d",(int)n); } printf("};\n\n"); } printf("short* TONE2[85]={"); for(tone=0;tone<85;tone++) { if(tone)printf(","); printf("NOC_%s%d",n2[tone%12],1+tone/12); } printf("};\n\n"); printf("/*\n"); printf(" *--------------------------------------------------\n"); printf(" * 矩形波(KUKEI)\n"); printf(" *--------------------------------------------------\n"); printf(" */\n\n"); for(tone=0;tone<85;tone++) { fhz=88200/cnt[tone]; hz=(int)fhz; printf("/* #%02d: tone %s oct %d */\n",tone,n1[tone%12],1+tone/12); printf("short KUK_%s%d[%d]={%d",n2[tone%12],1+tone/12,hz+1,hz); for(i=0;i<hz/2;i++) { printf(",%d",48); } for(;i<hz;i++) { printf(",%d",-48); } printf("};\n\n"); } printf("short* TONE3[85]={"); for(tone=0;tone<85;tone++) { if(tone)printf(","); printf("KUK_%s%d",n2[tone%12],1+tone/12); } printf("};\n\n"); printf("/*\n"); printf(" *--------------------------------------------------\n"); printf(" * ノイズ\n"); printf(" *--------------------------------------------------\n"); printf(" */\n\n"); for(tone=0;tone<85;tone++) { fhz=88200/cnt[tone]; hz=(int)fhz; printf("/* #%02d: tone %s oct %d */\n",tone,n1[tone%12],1+tone/12); printf("short NIZ_%s%d[%d]={%d",n2[tone%12],1+tone/12,hz+1,hz); for(i=0;i<hz/2;i++) { printf(",%d",rand()%64); } for(;i<hz;i++) { printf(",%d",-(rand()%64)); } printf("};\n\n"); } printf("short* TONE4[85]={"); for(tone=0;tone<85;tone++) { if(tone)printf(","); printf("NIZ_%s%d",n2[tone%12],1+tone/12); } printf("};\n\n"); return 0; } 
上記プログラムをコンパイルして実行すれば、音色テーブルの配列を宣言するC言語のソースコードが標準出力されるので、それをリダイレクトで保存します。(一種のCGIプログラムです)

保存したファイル(tone.c)はコチラに置いておきます。

(3)tone.c
このソースコードでは、TONE1、TONE2、TONE3、TONE4というshort型配列に、三角波、ノコギリ波、矩形波、ノイズの1Hz×85音階分の配列を宣言しています。各音階配列は、第一要素がパルス符号の数で、第二要素以降がパルス符号データになっています。

(4)補足1
なお、上記の説明を読んでいて、「基礎波形のパルス符号はshort型よりsigned char型の方が良いのでは?」という点に気づけた方が居るとすれば、素晴らしいです。その方がメモリ容量を1/2に節約できるので、品質(効率性)が良くなります。
しかし、波形情報は基本的に16bitで計算するので、基数変換のコストを削るために、short型にしておきました。ついでに、配列の最初の要素にパルス符号数を格納しているので、char型だと収まりきらないという事情があり、short型にしておきました。
でも、結局計算する時は32bitにしている訳だし、後者の件は構造体にすれば問題無いので、signed charの方が良いかもしれません。

(5)補足2
あと、実際の音源モジュールのサンプリング周波数は22050Hzですが、波形データはそれより細かい粒度で作成しています。これは、周波数の誤差を少なくするために必要な処置です。(割り切れない数値が誤差となり、誤差が大きいと若干音痴になってしまうため、基礎波形は粒度を細かくすることで、誤差を少なくしています)

13. 基礎アーキテクチャ

前の記事までの解説で、「音を鳴らす仕組み」についてご理解いただけたものと思います。
この記事からは、いよいよ波形メモリ音源を実装する為のアーキテクチャについて解説していきます。

(1)音源モジュール (H/W)
以前の記事で既に解説しましたが、VGSは、下図のように全音ストア方式の音源モジュール(エミュレータ)が、波形情報をPCMデバイスへ書き込むことで、音声の発音を行います。
発音機構

(2)オペレーション機構
ハードウェア(エミュレータ)は、それ単体では「無音」を発音し続けるだけです。
つまり、何も動作しません。
ハードウェアを動作させるには、必ずオペレーションが必要になります。
ハードウェアへのオペレーションを代替するプログラムのことを一般的に、「ドライバ」と呼びます。

(3)音源ドライバ (S/W)
音源モジュールを制御するには音源ドライバが必要になります。音源ドライバは、メモリ上に記憶されているオペレーションセットから、逐次的にオペレーションを取り出し、それを音源モジュールに伝えることで、ハードウェアを制御します。
音源ドライバ

(4)ハードウェアとソフトウェア
音源モジュールはハードウェア(H/W)で、音源ドライバはソフトウェア(S/W)です。
VGSは、ハードウェア(仮想ハードウェア)ですが、「ハードウェア機能」として音源ドライバもエミュレータの一部として実装しています。そのため、VGSを利用する場合、プログラマが音源ドライバを作成する必要はありません。VGSは、そういった低レベルプログラミングの負担をプログラマに負わせないことで、ゲームの作り易い環境を提供することを、設計思想の基礎に据えています。その反面、プログラマから「自前の音源を作成する」という自由度を奪ってしまっている訳ですが。

自前の音声システムを作るには、その両方についての理論と実装をマスターしなければなりません。以降の記事では、音源モジュールと音源ドライバの理論と実装について解説していきます。

2013年4月11日木曜日

12. 波形合成

前の記事では、波形合成を行わず、スロット毎(効果音の種類毎)にプライオリティを設けて、ひとつの効果音だけ発音する効果音の発音システムを実装しました。しかし、昨今のゲームは通常、複数の効果音を同時に発音することができます。

この記事では、前の記事で作成したtest05.cppをベースにして、複数の効果音を同時に発音するプログラムを作成してみたいと思います。

(1)波形合成の考え方
複数の効果音を同時に発音することは、波形の合成を行うことで実現できます。
波形の合成というと、何やら難しい定理でも登場しそうな気がするかもしれませんが、波形の合成は、単純に鳴らす全ての波形データを足し算するだけで実現できます。(合成された波形を分解するには、フーリエ解析が必要になるので若干難しいですが、合成するのは簡単です)

(2)オーバフロー対策
波形データを足し算する場合、オーバフロー対策が必要になります。
波形データは16bitの整数(-32,768~32,767)ですが、波形の足し算を行った結果、-32,769以下または32,768以上になるとオーバーフローしてしまいます。そこで、合成を行う時は32bitの数値に合成し、バッファリングする時に16bitの範囲に丸める必要があります。

(3)実装
以上の2点を踏まえて、mkbuf関数を次のように改造します。
/* バッファリング処理(合成版) */
static void mkbuf(char* buf,size_t size)
{
    int i,j;
    int cs;
    int wav;
    short* bp;

    /* 無音状態にする */
    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(size<(size_t)cs) {
                cs=(int)size;
            }
            /* バッファリング */
            for(j=0;j<cs;j+=2) {
                bp=(short*)(&buf[j]);
                wav=*bp;
                wav+=*((short*)&(_eff[i].dat[_eff[i].pos+j]));
                if(32767<wav) wav=32767;
                else if(wav<-32768) wav=-32768;
                (*bp)=(short)wav;
            }
            /* ポジション・チェンジ */
            _eff[i].pos+=cs;
            if(_eff[i].size<=_eff[i].pos) {
                /* 発音終了 */
                zflag((unsigned char)i);
            }
        } else {
            _eff[i].pos=0;
        }
    }
}
これで完成です。簡単ですね。
一応、完成版のtest06.cppはコチラからダウンロードできます。

理論はこんなに簡単であるにも関わらず、効果音の合成が長らく実現できていない時期があった原因は、test05.cppのバッファリング処理(memcpy)と比べて演算コストが高いためです。

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;
}

10. wavファイルの構造と変換

効果音用のPCMデータは、wavファイルから取得することにします。

(1)wavファイルのフォーマット
↓のサイトに分かり易く記述されています。
http://www.kk.iij4u.or.jp/~kondo/wave/

(2)wavファイルはそのまま使ってはならない
wavファイル(RIFF構造の一種)は、非常に汎用性の高いフォーマットです。
汎用性が高いということは、逆説すれば無駄(冗長)な情報が多いということです。
例えば、周波数、ビットレート、チャネル数などの情報は、VGS音源の場合は22050Hz、16bit、1chに固定されているので、ファイルに持たせるのはナンセンスです。そこで、wavファイルから必要な情報を抜き取り、独自ファイル(PCMファイル)のフォーマットに変換したものをリソースとして持つことにします。
なお、VGS音源にとって必要な情報は、以下の2種類です。
  • パルス符号の数
  • パルス符号データの塊
(3)変換コマンド(vgswav)
VGSでは、22050Hz、16bit、1chのwavファイルを独自形式(PCM)に変換するvgswavというコマンドを提供しています。vgswavコマンドのソースコードは次のような感じになっています。

■vgswav.c
/* WAVEを独自形式のPCMデータに変換する */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 情報ヘッダ */
struct DatHead {
    char riff[4];
    unsigned int fsize;
    char wave[4];
    char fmt[4];
    unsigned int bnum;
    unsigned short fid;
    unsigned short ch;
    unsigned int sample;
    unsigned int bps;
    unsigned short bsize;
    unsigned short bits;
    char data[4];
    unsigned int dsize;
};

int main(int argc,char* argv[])
{
    FILE* fpR=NULL;
    FILE* fpW=NULL;
    int rc=0;
    struct DatHead dh;
    char* data=NULL;
    char mh[4];

    /* 引数チェック */
    rc++;
    if(argc<3) {
        fprintf(stderr,"usage: vgswav input(wav) output(pcm)\n");
        goto ENDPROC;
    }

    /* 読み込みファイルをオープン */
    rc++;
    if(NULL==(fpR=fopen(argv[1],"rb"))) {
        fprintf(stderr,"ERROR: Could not open: %s\n",argv[1]);
        goto ENDPROC;
    }

    /* 情報ヘッダを読み込む */
    rc++;
    if(sizeof(dh)!=fread(&dh,1,sizeof(dh),fpR)) {
        fprintf(stderr,"ERROR: Invalid file header.\n");
        goto ENDPROC;
    }

    /* 形式チェック */
    rc++;
    if(0!=strncmp(dh.riff,"RIFF",4)) {
        fprintf(stderr,"ERROR: Not RIFF format.\n");
        goto ENDPROC;
    }
    rc++;
    if(0!=strncmp(dh.wave,"WAVE",4)) {
        fprintf(stderr,"ERROR: Not WAVE format.\n");
        goto ENDPROC;
    }
    rc++;
    if(0!=strncmp(dh.fmt,"fmt ",4)) {
        fprintf(stderr,"ERROR: Invalid format.\n");
        goto ENDPROC;
    }
    rc++;
    if(0!=strncmp(dh.data,"data",4)) {
        fprintf(stderr,"ERROR: Invalid data.\n");
        goto ENDPROC;
    }

    printf("Header of %s:\n",argv[1]);
    printf(" - Format: %d\n",dh.fid);
    printf(" - Channel: %dch\n",dh.ch);
    printf(" - Sample: %dHz\n",dh.sample);
    printf(" - Transform: %dbps\n",dh.bps);
    printf(" - Block-size: %dbyte\n",(int)dh.bsize);
    printf(" - Bit-rate: %dbit\n",(int)dh.bits);
    printf(" - PCM: %dbyte\n",(int)dh.dsize);


    rc++;
    if(22050!=dh.sample) {
        fprintf(stderr,"ERROR: Sampling rate is not 22050Hz.\n");
        goto ENDPROC;
    }
    rc++;
    if(1!=dh.ch) {
        fprintf(stderr,"ERROR: Sampling channel is not 1(mono).\n");
        goto ENDPROC;
    }
    rc++;
    if(16!=dh.bits) {
        fprintf(stderr,"ERROR: Sampling bit rate is not 16bit.\n");
        goto ENDPROC;
    }
    rc++;
    if(dh.sample*2!=dh.bps) {
        fprintf(stderr,"ERROR: Invalid transform-rate(byte/sec).\n");
        goto ENDPROC;
    }

    /* 波形データを読む込む領域を確保する */
    rc++;
    if(NULL==(data=(char*)malloc(dh.dsize))) {
        fprintf(stderr,"ERROR: Memory allocation error.\n");
        goto ENDPROC;
    }

    /* 波形データを読み込む */
    rc++;
    if(dh.dsize!=fread(data,1,dh.dsize,fpR)) {
        fprintf(stderr,"ERROR: Could not read PCM data.\n");
        goto ENDPROC;
    }

    /* 書き込みファイルをオープン */
    rc++;
    if(NULL==(fpW=fopen(argv[2],"wb"))) {
        fprintf(stderr,"ERROR: Could not open: %s\n",argv[2]);
        goto ENDPROC;
    }

    /* ヘッダ書き込み */
    rc++;
    strcpy(mh,"EFF");
    if(4!=fwrite(mh,1,4,fpW)) {
        fprintf(stderr,"ERROR: Could not write header.\n");
        goto ENDPROC;
    }

    /* サイズ情報書き込み(Big-endian) */
    rc++;
    mh[0]=((dh.dsize & 0xFF000000) >> 24) & 0xFF;
    mh[1]=((dh.dsize & 0x00FF0000) >> 16) & 0xFF;
    mh[2]=((dh.dsize & 0x0000FF00) >> 8) & 0xFF;
    mh[3]=dh.dsize & 0xFF;
    if(4!=fwrite(mh,1,4,fpW)) {
        fprintf(stderr,"ERROR: Could not write size.\n");
        goto ENDPROC;
    }

    /* PCM書き込み */
    if(dh.dsize!=fwrite(data,1,dh.dsize,fpW)) {
        fprintf(stderr,"ERROR: Could not write data.\n");
        goto ENDPROC;
    }

    rc=0;

    /* 終了処理 */
ENDPROC:
    if(data) {
        free(data);
    }
    if(fpR) {
        fclose(fpR);
    }
    if(fpW) {
        fclose(fpW);
    }
    return rc;
}
このコマンドを実行すれば、wav形式のファイルが、先頭32bit(4バイト)にアイキャッチ、次の32bit(4バイト)にサイズ、残りがPCMデータという非常にシンプルなデータ構造に変換されます。ちなみに、サイズ情報をビッグエンディアンに変換しているのはナンセンスです。私の自前のツールの仕様の関係で、そういう仕様にしています。(PCMデータそのものはリトルエンディアンです)

(4)効果音はストアすること
AndroidやiPhoneの標準のAPIでは、wavファイルをそのまま発音する機能があります。
しかし、効果音というのは何回も繰り返し再生されるものなので、再生の都度、ファイルを読み込むというのは、処理効率が悪すぎます。
VGSの場合、全リソースデータ(ROMファイル)の内容を、起動時に一括でメモリ領域に展開していて、以降はメモリ領域を参照することでデータアクセスを行っています。これにより、リアルタイム性の高い効果音の発音処理を実現しています。ゲームにとっての効果音は、BGM以上に重要な存在なので、効果音データは必ず事前にストア済みの情報を使わなければなりません。

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
に絞ります。

2. 電子音のメカニズム

(1)仕組み
電子音(コンピュータの音)は、時系列に変化するパルス符号と呼ばれる数値データ(PCM; Pulse Code Modulation)を、音源モジュールに入力することで振動により音波を発生させることで発音します。その音波が空気を媒介して鼓膜に刺激として伝わり、鼓膜に伝わった刺激が神経で電気信号に変換され、その電気信号が脳に伝わることで、人は「音」を認識します。

(2)音の属性
音には、「大きさ」と「高さ」という属性があります。
音の大きさは、音波の振幅の大きさによって決まります。
そして、音の高さは、音波の波形(交流波)パターンが1秒間に何回繰り返されたか=周波数(Hz)によって決まります。
最も基本的な音波の波形パターンは、サイン波(パルス波)と呼ばれる形状(下図)のものです。サイン波とは、その名の通り、sin(T)という計算式で求めることができる単純な波形です。

(3)音声品質
音声の品質は、パルス符号の大きさ(bitレート)、1秒間に送るデータ量(標本化周波数)、音波の数(チャネル数)によって決まります。
   bitレート
パルス符号の値が0の場合、音は無音になります。そして、0からの絶対値の大小が音の大小(振幅)ということになります。
なお、bitレートが高いほど大きい音が鳴らせるという訳ではありません。bitレートが高い場合、無音から最大音までの粒度が細かい音を鳴らすことができるようになります。
   標本化周波数
標本化周波数は、単位時間(1秒間)当たりに音波の標本化を行う回数(Hz)です。
   チャネル数
チャネル数は、音の発信源の数です。
一般的に1ch(モノラル)と2ch(ステレオ)の2種類が良く使われていますが、映画館で上映する映画の場合、3ch以上のチャネルを使用している場合があります。

正確には、振動を発生させる装置の抵抗の大きさ(インピーダンス)などの要素も影響しますが、音のデータとしての品質を決定する要素は、その3つです。

(4)VGSの音声品質
VGSの音声品質は、以下のように規定しました。
品質の構成要素VGSの仕様
bitレート16bit
標本化周波数22,050Hz
チャネル数1ch (モノラル)

音声の事に限らず、「最善」の品質の商品とは何でしょうか?

単純に「品質が高い商品である」と考えている方が居るかもしれません。
しかし、その考え方は、間違っています。重要なのは、何処の品質を重点的に求めるか=観点です。つまり、最善の品質の商品とは、“適切な観点”で品質が高い商品のことを指します。
要するに、最高の品質の商品は、最善の品質の商品ではありません。

ただし、仮に間違った観点で品質が高い商品があったとしても、その商品の利用者にとって、一見すると何ら不便は有りません。何故なら、必要な品質が確保されていなければ不便ですが、必要以上の品質があっても不便は無いので。しかし、品質とは常にコストと表裏一体の存在です。そのため、高い品質を得ようとする場合、それなりのコストが生じます。そして、間違った部分に対して注ぎ込まれたコストは、最終的に利用者が負担することになります。そのため、最良な品質の商品は、利用者に対して「無駄なコストを支払う」という不便を与えます。そのため、賢明な利用者が求める商品は、最高の品質の商品ではなく、最善の品質の商品です。もちろん、利益度外視で間違った観点の品質を求めることは、しばしば有るかもしれませんが。

ゲームにはゲームに適した音楽が求められます。

それが、ゲームの音楽にとって必要な品質を追求する上での観点となります。
なお、コスト=お金と思っている方も居るかもしれませんが、「時は金なり」という諺があるように、時間=お金なので、コスト=時間でもあります。音声品質が高いほど、CPU演算の量が増えるため、無駄に「CPU時間」を喰うことになります。これも一種のコストです。上記に示した音声品質は、コストを勘案した上で、私がベストだと考えたゲーム音楽の音声品質です。

1. はじめに

(1)ゲームで音楽を鳴らす方法
一般体なWindows、Android、iPhoneのプログラムの場合、MP3、OGG、WMAなどの圧縮された音ファイルに記録されたPCMデータをループ再生させる形で音楽を鳴らしています。しかし、この方式では、VGSの音声品質(22050Hz/16bit/1ch)なら1分あたり約2MB、CDの音声品質(44010H/16bit/2ch)なら1分あたり8MB程度の容量が必要になります。

現代のコンピュータならともかく、嘗てのファミコンなどでは、それだけ多くのメモリを積むことは不可能でした(昔のメモリはビット単価が高価でした)ので、その仕組みを採ることはできません。その仕組みが最初に採用されるようになったのは、PCエンジンのCD-ROM2ぐらいからです。嘗てのゲーム機は、プログラマブルな音源モジュール(PSG音源、波形メモリ音源、FM音源など)を搭載したマイクロチップを、プログラムが制御することで、ゲーム音楽を奏でていました。

(2)現代における価値
現代のコンピュータなら、チップチューン音源が無くても、MP3等でどんな音楽でも奏でることができます。敢えてチップチューン音源の価値を挙げるなら、「音楽データの容量を低く抑えることができる」という点ぐらいです。ネットワークがブロードバンド化し、ディスクやメモリの単価が年々安くなっていった現代、その価値は極めて低いものとなりました。

しかし、スマートフォンの登場により状況が変わりました。

スマートフォンの場合、帯域制限の大きい無線ネットワークを使用することになります。また、本体に搭載されたメモリサイズは、PCと比較して遥かに小さく、iPhoneなら本体容量の拡張はできません。Androidなら外付けのSDカードで容量の拡張は可能ですが、本体のメモリサイズは極めて小さい(私が所有しているXperia rayの場合300MBぐらいしかない)です。
つまり、スマートフォンではまだまだ、「容量の小さいアプリ」に対するニーズが大きいといえます。
実際、Appleの開発の手引書にも「アプリのサイズは可能な限り小さくすること」がガイドされています。

(3)仮想化
スマートフォンには、プログラマブルな音源チップは搭載されていません。
しかし、ネイティブコードの処理性能であれば、音源チップのエミュレーションは可能です。
そこで、私は、エミュレータの仕組みを応用して、スマートフォンでも使用できる仮想音源チップを開発し、それを私の仮想ゲームマシンVGSに搭載しました。

(4)本ブログの目的
このブログは、VGSで実装している波形メモリ音源システムの実装に関する詳細な実装方法を解説することを目的に設置してみました。このブログに書かれた記事を全部読んで頂ければ、Windows、Android、iPhone全てのプラットフォームで共通の動作をする、音声発音システムの実装が可能になると思います。
私は、VGSという枠組みで、波形メモリ音源を用いた音声発音システムを、プログラマが容易に実装できる形で提供しています。そして、私はVGSの利用を促進していくことが最良の手段であると考えています。
しかし、独自のプログラムでその仕組みを実装したいというプログラマ諸氏の熱意に応えるため、この仕組み(内部実装)を詳細に解説することにしました。なお、既に本(右側のVGS book)で解説している内容が幾つかあるかもしれません。「読んだことあるぞ?」という記事は、適宜読み飛ばしてください。