■ 新・ゲーム開発講座




■ へっぽこプログラミング入門♪


■第34夜:BGM(MIDI)を鳴らす

効果音が鳴ったところで、今度はMIDIに挑戦してみましょう。MIDIファイルを演奏するのもマルチメディアライブラリにお任せで簡単に参ります(うーむ ^^;)。前回同様、winmm.lib mmsystem.h が必要になりますが、前回のワークスペースを流用し、従来ソースに書き足す形で進んでいきますので特に心配は要りません。ライブラリの追加方法については第33夜に記述してありますので参照して頂きたいと思います。さて、そんな訳でコマンド書式は以下のように決めて進行しましょう。ソースはこちらです。

■書式:#play_midi filename

ファイル名 filename のMIDIファイルを演奏する。


■書式:#stop_midi

MIDI演奏を停止する


■MIDI演奏について

スクリプトコマンドの実装に入る前に、MIDIファイルの扱い方を簡単に説明致します。MIDI再生には幾つかの手法がありますが、最も簡単なのは mciSendString() を使った方法でしょう。mciSendString() は、文字列の形でMCIコマンドを送る関数です。MCI (Media Control Interface ) はMIDI、CD-DA、ビデオなどを簡単なコマンド送信で扱えるようししたインタフェイスですが、ここではあまり難しく考えずに使ってみましょう。ともかく、以下のようにすればMIDIを演奏することができます。

【MIDIファイルのOPEN】
まず、MIDIファイルのOPENについてです。ABC.MID というMIDIファイルを演奏したい場合、ファイルOPENは以下のように行います。コマンドと言っても普通の英文ですね。

mciSendString("open ABC.MID type sequencer alias _MIDI_",NULL,0,NULL);


type sequencer alias _MIDI_ という部分ですが、MCIの中でMIDIファイルを _MIDI_ (←注:好きな名前を付けられます)という識別名で扱うよ、という意味です。プログラム的には曲名などはどうでも良くて、ここで名づけた _MIDI_ というシンボル名が重要です。なお、ファイルOPENに成功したかどうか確かめるには、以下のようにします。さっそく、_MIDI_ のシンボルを使っていますね♪(^^)

char buf[32];

mciSendString("capability _MIDI_ can play wait",buf,31,NULL);

capability なんたら〜・・・というのは、演奏可能かどうかの問い合わせです。このコマンド文字列を送ると、演奏可能であれば buf に "true" の文字列が返ってきます。なおここでは buf のサイズをフィーリングで32にしていますが、このサイズに特に意味がある訳ではありません。周囲の事例を見渡すと20くらいのバッファ長でプログラムしている例を多く見かけますので、もう少し短くても良いような気がします(爆 ^^)

【演奏開始】

演奏の開始は以下のようにします。文字どおりの play 命令という訳ですね。

mciSendString("play _MIDI_ notify",NULL,0,hwnd);


コマンド文字列中の "notify" というのは、「演奏が終了したら通知してくれ」という指定です。通知って何だ? ・・・と思った方、鋭いです(^^)。実は演奏が終了した、というのもイベントの一つですので、イベントハンドラ中にメッセージが降ってくるのです。そのために、最後の引数にウィンドウのハンドルを指定しておきます(ここで指定したWindowのイベントハンドラに、メッセージが降って来ます)

このメッセージの名前は MM_MCINOTIFY といい、イベントハンドラの引数 wprm の内容が MCI_NOTIFY_SUCCESSFUL だったら演奏が正常終了したことを示します。演奏終了を検知したところでもういちど演奏を開始すれば、ループ再生になります。

【演奏停止】

停止は stop の文字列を送ることで実現します。

mciSendString("stop _MIDI_",NULL,0,NULL);

なお演奏が終了したら、開いたMIDIファイルは以下のようにクローズしておきます。

mciSendString("close _MIDI_",NULL,0,NULL);

基礎的なことが分かったところで、これをスクリプトコマンドの形で実装してみましょう。ここでは以下の要領で進行することにします。


■フラグ

Mainloopで参照している構造体 Mode_stat のメンバに追加はありません。

■コマンド解析部

TextEngine.cppCommand_call() に、以下の要領でコマンド追加を行います。


void Command_call()
{

//省略

//コマンド名の評価 → 処理関数呼び出し
if( strcmp(com_name,"delay" )==0 ){ Com_delay(); flag=ON;} //遅延
if( strcmp(com_name,"wait" )==0 ){ Com_cursor_blink(); flag=ON;} //カーソルブリンク
if( strcmp(com_name,"w" )==0 ){ Com_cursor_blink(); flag=ON;} //カーソルブリンク
if( strcmp(com_name,"halt" )==0 ){ Com_halt(); flag=ON;} //終了
if( strcmp(com_name,"page" )==0 ){ Com_page(); flag=ON;} //改ページ
if( strcmp(com_name,"g_load" )==0 ){ Com_g_load(); flag=ON;} //BMPのLOAD
if( strcmp(com_name,"g_copy" )==0 ){ Com_g_copy(); flag=ON;} //BMPのCOPY
if( strcmp(com_name,"g_change")==0 ){ Com_g_change(); flag=ON;} //画面切替
if( strcmp(com_name,"flag_on" )==0 ){ Com_flag_on(); flag=ON;} //イベントフラグON
if( strcmp(com_name,"flag_off")==0 ){ Com_flag_off(); flag=ON;} //イベントフラグOFF
if( strcmp(com_name,"disp_flag")==0 ){ Com_disp_flag(); flag=ON;} //イベントフラグ表示
if( strcmp(com_name,"if_flag" )==0 ){ Com_if_flag(); flag=ON;} //フラグによる分岐
if( strcmp(com_name,"jump" )==0 ){ Com_jump(); flag=ON;} //ラベルジャンプ
if( strcmp(com_name,"file_change")==0 ){Com_file_change(); flag=ON;} //ファイル間ジャンプ
if( strcmp(com_name,"select3" )==0 ){ Com_select3(); flag=ON;} //3択
if( strcmp(com_name,"play_wav")==0 ){ Com_play_wav(); flag=ON;} //WAV演奏開始
if( strcmp(com_name,"stop_wav")==0 ){ Com_stop_wav(); flag=ON;} //WAV演奏停止
if( strcmp(com_name,"play_midi")==0 ){ Com_play_midi(); flag=ON;} //MIDI演奏開始
if( strcmp(com_name,"stop_midi")==0 ){ Com_stop_midi(); flag=ON;} //MIDI演奏停止

//省略

}

■ステータス保持用構造体

WAV演奏のときと同様に、演奏状態を保持する構造体を定義しておきます(Text_com_03.h)。音を鳴らすだけなら不要ともいえる構造体ですが、LOAD/SAVE対応を考えた場合には、演奏状態と演奏ファイル名の保持はどうしても必要になってきます。名前は _WAV_stat と同様のノリで _MIDI_stat とでも致しましょう。


struct _MIDI_stat {
int MIDI_play_stat; //MIDIを再生中か? ON=再生中 OFF=再生していない
char MIDI_fname[256]; //MIDI:再生中のファイル名
};

■演奏の開始

演奏開始の要領は↑既に解説した通りですので、素直にインプリメントして参りましょう(下記リスト参照)。コマンド処理では、まず MIDI_stat の値を調べて、既になにか演奏中ならメッセージを表示してそのまま戻るようにします。構造体のメンバはVC++では0に初期化されるので、プログラム起動時は自動的にOFF(0に#defineされています)が適用されることになり、最初に演奏開始するときにはここでは引っかかりません。

次にコマンドパラメータとしてファイル名を解析します。従来通りの処理なので特に解説は不要ですね(^^)。ファイル名を取得したら、それをバッファ str にコマンド文字列展開(OPENコマンド)して mciSendString() で送信します。ここではBGMとしての演奏を想定していますので無条件でループ再生仕様ですが、単発再生機能が欲しい方はコマンド文字列から notify を取ってください。

OPENコマンドを送信したら、"capability _MIDI_ can play wait" コマンドでそれが成功したかどうかを確認します。"true"以外の文字列が返ってきた場合には失敗なので、エラーメッセージを表示して戻ります。成功の場合は "play _MIDI_ notify" を送信し、終了通知付きで演奏を開始します。最後に MIDI_stat に「演奏中」のフラグと「MIDIファイル名」をセットして戻ります。



_MIDI_stat MIDI_stat; //MIDI演奏のステータス保持用

int Com_play_midi()
{

char str[256],f_name[256];

//既になにか演奏中か?
if( MIDI_stat.MIDI_play_stat == ON ){
msg( "別の曲を演奏中です。一端演奏停止してから再度コマンド実行してください♪","Com_play_midi()");
return 0;
}

//ファイル名解析
TEXT++;
strcpy(f_name,MIDI_PATH );
strcat(f_name,Kaiseki_TextStr());

//MIDIファイルをOPEN
sprintf( str, "open %s type sequencer alias _MIDI_", f_name );
mciSendString(str,NULL,0,NULL);

//OPENは成功したか?
MCIERROR err_code;
char err_mess[256];

err_code=mciSendString("capability _MIDI_ can play wait",str,255,NULL);
if( strcmp( str,"true" )!=0 ) {

// true以外が返ってきた→エラー文字列の取得
mciGetErrorString( err_code,(LPTSTR)err_mess,255);

sprintf( str, "MIDIファイル %s が開けません (T0T)/~~ \n\n■%s ",f_name,err_mess );
MessageBox( NULL,str,"Com_play_midi()",MB_OK );
return 0;

}

//演奏終了通知を指定しながら演奏開始♪
mciSendString("play _MIDI_ notify",NULL,0,hwnd );

MIDI_stat.MIDI_play_stat = ON; //ステータス情報の更新
strcpy( MIDI_stat.MIDI_fname,f_name ); //ファイル名のキャッシュ

return 0;

}


■演奏の終了

終了はMCIコマンドそのままでインプリメントできますが、「何も演奏していない場合」の対応を忘れずに行いましょう。サウンドカードはピンからキリまでいろいろなので、演奏状態でもないのに停止やデバイスCLOSEコマンドを送ると機種によっては何が起こるか予想がつきません(^^;)そんな訳で、まずは MIDI_stat をチェックして演奏中でない場合はなにもせずに戻るようにしておきます。おお、意味が無いかと思いきや、少しは役に立ってますねこの構造体…♪ (爆死 ^0^)

演奏中であることが確認できたら、stop コマンドを発行して演奏を停止し、さらに close コマンドでMIDIデバイスを閉じます。最後に MIDI_stat の中身をクリアして完了です。


int Com_stop_midi()
{

//何も演奏していなければそのまま戻る
if( MIDI_stat.MIDI_play_stat != ON )return 0;

mciSendString("stop _MIDI_",NULL,0,NULL); //停止
mciSendString("close _MIDI_",NULL,0,NULL); //MIDIデバイスのCLOSE

MIDI_stat.MIDI_play_stat = OFF; //演奏ステータスのフラグを解除
strcpy( MIDI_stat.MIDI_fname,"" ); //演奏ファイル名をクリアしておく

return 0;

}

■ループ演奏

ループ演奏は、終了通知を受けたところでもういちどMIDI再生を行うことで実現できる・・・と上の方で書きました。通知を受け取るのはイベントハンドラで、イベント名は MM_MCINOTIFY になります。イベントを捕まえたら、MIDI_stat のフラグを確認した上で、演奏リスタート用の関数 Com_restart_midi() を呼ぶようにしておきます。(Com_restart_midi() は Text_Com_03.cpp に記述してあります)

それにしても、イベントハンドラ WndProc() もスケルトン時代から比べると随分ゴージャスになって来ました(^^;)。ひとつひとつの機能強化は小さなものでも、塵も積もればヤマトの諸君・・・というところでしょうか。リスト全体のボリュームに惑わされないで、
肝心な部分はどこかという視点で見て頂きたいと思います。


LRESULT WndProc(HWND hwnd,UINT msg,WPARAM wprm,LPARAM lprm)
{

switch(msg) {
case WM_CREATE: //Windowが生成された
break;

case WM_DESTROY: //Windowの消去操作がされた
PostQuitMessage(0);
break;

case WM_PAINT: //描画命令が出た
//バックサーフェイスを表にコピー
BitBlt(win_hdc,0,0,640,480,Back_DC,0,0,SRCCOPY);

return DefWindowProc(hwnd,msg,wprm,lprm);
//↑DefWindowProc()にデフォルト処理を投げてやることで、
// GDI絡みの再描画指令が帳消しになってくれるそうです♪
case WM_KEYDOWN: //キーが押された
switch(wprm){
case VK_ESCAPE:
PostQuitMessage(0);
//PostMessage(hwnd,WM_CLOSE,0,0); //(ESC)を入力で終了
break;
case VK_SPACE:
case VK_RETURN:
//カーソルブリンク中ならブリンク解除
if( Mode_stat.flag_cursor_blink == ON ){
Com_cursor_blink_end();
}
//終了待ち中なら終了処理
if( Mode_stat.flag_halt == ON ){
Com_halt_end();

}
//改ページのキー待ち中なら終了処理
if( Mode_stat.flag_page2 == ON ){
Com_page2_end();
}
break;
}
break;
case WM_LBUTTONDOWN: //マウス左ボタンが押された
//カーソルブリンク中ならブリンク解除
if( Mode_stat.flag_cursor_blink == ON ){
Com_cursor_blink_end();

}
//終了待ち中なら終了処理
if( Mode_stat.flag_halt == ON ){
Com_halt_end();
}
//改ページのキー待ち中なら終了処理
if( Mode_stat.flag_page2 == ON ){
Com_page2_end();
}
Mouse_stat.time_of_L_click_old = Mouse_stat.time_of_L_click;
Mouse_stat.time_of_L_click = GetTickCount(); //時刻をメモ
goto SET_MOUSE_STAT;

case WM_LBUTTONUP: //マウス左ボタンが離された
goto SET_MOUSE_STAT;
case WM_RBUTTONDOWN: //マウス右ボタンが押された
Mouse_stat.time_of_R_click_old = Mouse_stat.time_of_R_click;
Mouse_stat.time_of_R_click = GetTickCount(); //時刻をメモ
goto SET_MOUSE_STAT;

case WM_RBUTTONUP: //マウス右ボタンが離された
goto SET_MOUSE_STAT;
case WM_MOUSEMOVE: //マウスが動いた
SET_MOUSE_STAT:
//リアルタイムでマウスの状態を保持
//※ゲームの処理はここでセットした変数の値を参照して動作します
Mouse_stat.fwkeys=(int)wprm; //ボタン状態
Mouse_stat.xpos=(int)LOWORD(lprm); //X座標
Mouse_stat.ypos=(int)HIWORD(lprm); //Y座標
break;
case MM_MCINOTIFY: //MCI系の演奏が終った
if( MIDI_stat.MIDI_play_stat == ON ) {
Com_restart_midi(); //再演奏
}
break;
default:
//その他のイベントはWindowsのシステムにお任せ(楽ちん、楽ちん)
return DefWindowProc(hwnd,msg,wprm,lprm);
}

return 0;


}


リスタートコマンド(リスタートって、漢字で書くと"再再生"?:笑) Com_restart_midi() の内容は以下の通りです。ここではMIDIデバイスを一旦クローズして再生のやりなおしをしています。我ながら、いちいち MIDI デバイスまでクローズしなくても、データの先頭にシークしなおすだけで良いような気がしているのですが・・・まあ、これで動いているのだから気にしないことにしましょうか(爆)。そのうちもう少し高度なワザを mciSendCommand() で実現したら、seek コマンドを使った例も紹介したいと思います。

int Com_restart_midi()
{

char str[256];

//一旦CLOSE
mciSendString("close _MIDI_",NULL,0,NULL);

//再度OPEN
sprintf( str, "open %s type sequencer alias _MIDI_", MIDI_stat.MIDI_fname );
mciSendString(str,NULL,0,NULL);

//OPENは成功したか?
MCIERROR err_code;
char err_mess[256];

err_code=mciSendString("capability _MIDI_ can play wait",str,255,NULL);
if( strcmp( str,"true" )!=0 ) {

// true以外が返ってきた→エラー文字列の取得
mciGetErrorString( err_code,(LPTSTR)err_mess,255);

sprintf( str, "MIDIファイル %s が開けません (T0T)/~~ \n\n■%s ",
MIDI_stat.MIDI_fname,err_mess );


MessageBox( NULL,str,"Com_restart_midi()",MB_OK );
return 0;

}

//演奏開始
mciSendString("play _MIDI_ notify",NULL,0,hwnd );

return 0;


}


■終了対策

さて、ここで重要な忘れ物があります。本来はWAV再生の項目で扱っておかなければならなかった項目なのですが、終了対策を行っておく必要があるのです。WAVやMIDI、CD−DAなどを再生したままプログラム本体を終了してしまうと、次回プログラムを起動したときにデバイスを認識しなくなったり、演奏が正しく開始できなくなる場合があるのです。

そんな訳で、いままで PostQuitMessage(0) で済ませてきたプログラムの終了処理を、専用の終了関数に置き替えます。ここではその名もズバリ quit.cpp とゆーソースファイルに、Sys_exit() という関数を新設しましょう。内容は以下の通りで、今まで のソースで PostQuitMessage(0) と記述していた部分をすべてこの関数で置き替えます。(たとえばウィンドウ右上の×ボタンを押してもちゃんと演奏終了してくれるように・・・ ^^;)


void Sys_exit()
{

//プログラム終了時、画面絡みのリソース返却はWindowsのシステムが
//やってくれるのでここでは手抜きしてなにもしません(大爆死 ^^)
//↓そんな訳で明示的に必要な部分のみ書き書き書き書き♪

//MIDIが鳴っていたら停止
Com_stop_midi();

//WAVが鳴っていたら停止
Com_stop_wav();

//Windowsシステムに終了メッセージを投げる
PostQuitMessage(0);

}

さて、これでMIDIを鳴らす環境は整いましたね。難しいようでしたら、理屈はとりあえず脇に置いておいて、まずは実際に音を鳴らして動作を確認してみて下さい。せっかくソースリストがあるのですから、いろいろ手を加えて実験しながら覚えるのが吉ではないかと思います。単純なテキスト操作と違って、音や絵が出る項目は成果が確認しやすくて覚えやすいですから…♪

なお今回もサンプルMIDI曲は
某所某氏(ただいまインドの山奥で修行中につき情報秘匿中 ^^;)に提供して頂きました。元がチャルメラだなてちょっと想像がつかない仕上がりぶりです♪ 多謝、多謝 m(_ _)m


■コラム:遅い機種とMIDIとバックバッファ転送

今回のソースですが、実は遅い機種(MMX200MHz以下くらい?)で実行すると、MIDI再生が少々スローテンポになってしまう可能性があります。理由はMIDI処理の重さと Mainloop で毎回バックサーフェイスから表画面に転送しているグラフィック処理の重さの兼ね合い具合です。いまどきのCPU事情から言えば、秋葉原で出回っている最低ランクでもPL-700MHz帯なので気にする必要がない・・・と言いきれれば良いのですが、ちょっと古いノートPCなどですとMMX-233クラスはまだまだ現役です。私の使用しているノートPCなんてMMX-166(これはさすがにロートル世代ですね ^^;)ですが、このマシンだとMIDI再生に若干の影響が出ているのが聞き取れます。安全牌を考えるなら、動作環境としては MMX200MHz 以上推奨…というのが無難なところでしょうか。

とはいえ、この現象の解決策は何もないのか・・・というとそうでもありません。画像転送によるCPU負荷を低減してやれば良いのですから、Mainloop()で毎回無条件に BitBlt() するのをやめてしまえば良いのです。バックサーフェイス → 表画面の転送は、バックサーフェイスへの描きこみが行われた直後に1度行えば充分です。ここでは分かりやすさ重視でフル転送式のソースを示していますが、実は高速化の余地はたくさん残っています。とりあえず、Mainloop() のBitBlt() をコメント文にして、同じ内容のBitBlt() をソースファイル上の各画面操作部分の直後に挿入する・・・というアイデアが浮かびます。また、文字表示などは1文字分の面積しか書き換えていないのですから、バックサーフェイス全体を表画面にコピーするのは無駄ともいえます。こんなところにも工夫の余地はありますよね(^^)。

ここではカスタマイズについてこれ以上の言及はしません。パフォーマンスを求めるのは入門レベルをクリアしてからで充分だと思うからです。「高速だけどスパゲッティ」なソースに機能追加していくのは、プログラマ本人にとっても拷問ですし、ソースを読む人にとっても拷問です。へっぽこレベルではあってもこの講座は入門者を対象にしています。その点では、なるべく素直なコードを示すのが責務だろうなぁ・・・などと考えています(本当に素直かどうかは自分では怖くて評価できませんけど:爆死 ^^;)。

旧機種対応すべきかどうかは皆さんの判断と改造意欲にゆだねようと思います。ソースは公開されており、改造、配布は自由なのですから・・・♪(^0^)/~~


<補足>
これを書いたときは何とも牧歌的な時代でしたねぇ。いまどきの数GHzあたりまえのCPU速度では、全然問題にはなりませんが…(^^;)