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以上に重要な存在なので、効果音データは必ず事前にストア済みの情報を使わなければなりません。