■ 新・ゲーム開発講座




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


■第37夜:MP3の再生

今回はMP3再生のリクエストがありましたので、BBSで教えていただいたMCIコマンド(大山さん感謝♪^^;)でお手軽再生を行う方法をご紹介したいと思います。方法は第34夜のMIDI再生と殆ど同じですので、アルゴリズムの詳細に関しては第34夜を読んでいただくこととして、エッセンスの部分を中心に行きたいと思います。スクリプトのコマンドとしては以下の書式で実装することに致しましょう。ゲームのBGM再生を想定していますのでMIDI同様にループ再生専用とし、またMP3ファイルの置き場所はMIDIファイルと同じフォルダにします(同じ音楽ファイルとゆーことで ^^)。

■書式:#play_mp3 filename

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


■書式:#stop_mp3


MP3演奏を停止する


■MP3演奏について

既に第34夜で解説したMIDIと同様に、MP3もマルチメディアライブラリの使用で演奏可能です。winmm.libmmsystem.h をプロジェクトに加えればOKです。ここでは第34夜と同様に mciSendString() 関数でMCIコマンド文字列を送出することによって演奏を行います。要領は以下の通りです。

【MP3ファイルのOPEN】

まず、MP3ファイルのOPENについてです。ABC.MP3 というMP3ファイルを演奏したい場合、ファイルOPENは以下のように行います。



mciSendString("open ABC.MP3 type mpegvideo alias _MP3_",NULL,0,NULL);


MIDIの場合は typesequencer でしたが、MP3 はなんと mpegvideo の範疇に入るようです。MP3 = Mpeg Layer 3 だそうですのでMCIコマンドではそういう扱いになっているようです。なお上記コマンド文字列中の _MP3_ は演奏するファイルを内部的に識別するためのシンボル名で、好きな名前に変更可能です。またファイルOPENに成功したかどうか確かめるには、以下のようにします(MIDIと同じですね)。成功していればバッファ buf に "true" の文字列が入ります。

char buf[32];

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



【演奏開始】

演奏の開始はMIDIと同じ要領で play 命令で行います。下の例ではループ演奏を前提に、演奏終了通知指定(notify)つきでコマンドを発行しています(ループ演奏しない場合は "notify" の文字をとってしまえばOKです)。

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


notify オプションをつけた場合は、演奏終了時に hwnd で指定したウィンドウのイベントハンドラに MM_MCINOTIFY というイベントメッセージが降ってきます。イベントハンドラの引数 wprm の内容が MCI_NOTIFY_SUCCESSFUL だったら演奏が正常終了したことを示します。演奏終了を検知したところでもういちど演奏を開始すればループ再生になるのはMIDIと一緒です。

ただし、MM_MCINOTIFY というイベント名は演奏がMIDIだろうがMP3だろうがCD-DAだろうが全部一緒(このへんのセンスのなさがMS的とゆーか何と言うか・・・ ^_^;) なので、MIDIと同居させるときは注意が必要です。プログラム側で「何のデバイスを演奏していたのか」という情報を把握していないときちんとしたループ再生はできません。

【演奏停止】

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



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


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


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


要領としてはMIDIとほとんど同じなので、第34夜の記事を理解できればそれほど難しくはないと思います(^^)。


■実装

さて、基礎的なことが分かったところで、これをスクリプトコマンドの形で実装してみましょう。MIDI演奏と要領はまったく一緒なので、ここでは第34夜で作成したMIDI演奏関数をコピー&ペーストしてMP3版に改造することにします(なんて安易 ^^;)。なおソースはこちらになります。曲を提供して頂いた某所某氏(最近はアフガンで神の声を聞いているそうですが… ^^;)に感謝の意を表しつつ、以下の解説を参考にしながら内容を確認してみてください。


【改造する前に】

さてMIDI演奏部分を改造するのは良いのですが、注意すべき点がひとつあります。演奏終了通知メッセージが MIDI も MP3 も一緒なので、ループ演奏(ゲームには必須の機能) を行うためにはあらかじめ演奏内容がどちらなのか判別する仕掛けをつくっておく必要があるということです。そこで、MIDI構造体を拡張して判別用のフラグを作っておきます(Text_Com_03.h)。判別用のシンボルは、とりあえず _MIDI _MP3 にしておきましょうか。



#define _MIDI 0
#define _MP3 1

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



そしてMP3演奏とごちゃまぜにならないよう、あらかじめMIDI演奏部にこのフラグのチェック/操作部分を設けておきます(Text_Com_03.cpp)。


//-----------------------------------------------------------
// MIDIを再生する
//
// 書式:#play_midi f_name
//-----------------------------------------------------------

_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 ); //ファイル名のキャッシュ
MIDI_stat.type = _MIDI; //再生ファイルのタイプを記録

return 0;

}

//-----------------------------------------------------------
// MIDIファイルのループ再生(イベントハンドラから呼ぶ)
//-----------------------------------------------------------

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

//再生ファイルのタイプを記録
MIDI_stat.type = _MIDI;

return 0;

}

//-----------------------------------------------------------
// MIDI演奏の停止
//-----------------------------------------------------------

int Com_stop_midi()
{

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

//演奏しているのがMIDIファイルでなければ戻る
if( MIDI_stat.type != _MIDI ) 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演奏部分の改造(・・・というほどのものでもないですが:笑)が終わったら、これらの3関数をコピー&ペーストしてしまいます。面倒ですので同じソースファイル Text_Com_03.cpp の末尾に貼り付けちゃいましょう。そして各関数名の Com_play_midi() Com_stop_midi() Com_stop_midi() をそれぞれ Com_play_mp3() Com_stop_mp3() Com_stop_mp3() に変更します。関数の内容を変更する箇所は コマンド文字列 とフラグ設定くらいで、あとはコメント部分を MIDI → MP3 に直すくらいです。うーん、簡単ですね(^0^)


//-----------------------------------------------------------
//
MP3を再生する
//
// 書式:#play_
mp3 f_name
//-----------------------------------------------------------

int Com_play_mp3()
{

char str[256],f_name[256];

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

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

//MP3ファイルをOPEN
sprintf( str, "open %s type
mpegvideo alias _MP3_", f_name );
mciSendString(str,NULL,0,NULL);

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

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

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

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

}

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

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

return 0;


}

//-----------------------------------------------------------
//
MP3ファイルのループ再生(イベントハンドラから呼ぶ)
//-----------------------------------------------------------

int Com_restart_mp3()
{

char str[256];

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

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

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

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

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

sprintf( str, "MP3ファイル %s が開けません (T0T)/~~ \n\n■%s ",MIDI_stat.MIDI_fname,err_mess );
MessageBox( NULL,str,"Com_restart_mp3()",MB_OK );
return 0;

}

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

//再生ファイルのタイプを記録
MIDI_stat.type =
_MP3;

return 0;

}

//-----------------------------------------------------------
//
MP3演奏の停止
//-----------------------------------------------------------

int Com_stop_mp3()
{

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

//演奏しているのがMP3ファイルでなければ戻る
if( MIDI_stat.type !=
_MP3 ) return 0;

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

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

return 0;

}



さて、安直に関数本体をコピー&ペーストで作ったら、この関数を呼び出す部分を作成しましょう。TextEngine.cpp のコマンド処理呼び出し関数 Command_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演奏停止
if( strcmp(com_name,"play_mp3")==0 ){ Com_play_mp3(); flag=ON;} //MP3演奏開始
if( strcmp(com_name,"stop_mp3")==0 ){ Com_stop_mp3(); flag=ON;} //MP3演奏停止

if( strcmp(com_name,"set_font_size")==0 ){ Com_set_font_size(); flag=ON;} //フォントサイズ変更
if( strcmp(com_name,"set_text_area")==0 ){ Com_set_text_area(); flag=ON;} //テキストエリア変更
if( strcmp(com_name,"set_text_wait")==0 ){ Com_set_text_wait(); flag=ON;} //テキストウェイト変更
if( strcmp(com_name,"set_text_color")==0 ){ Com_set_text_color(); flag=ON;} //テキストカラー変更

//省略


}


さらに、ループ再生を行うためにイベントハンドラ WndProc() を以下のように書き換えます(main.cpp)。いままでMIDI用のループ再生関数を呼んでいた部分を、フラグによって MIDI/MP3 の判定をしながら適切な処理を呼び出すように変更する訳ですね。


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

switch ( msg ) {

//省略

case MM_MCINOTIFY: //MCI系の演奏が終った
if( MIDI_stat.MIDI_play_stat == ON ) {
switch( MIDI_stat.type ) {
case _MIDI: Com_restart_midi(); //MIDI再演奏
break;

case _MP3: Com_restart_mp3(); //MP3再演奏
break;

default: MessageBox(NULL,"BGMのタイプが変だよ〜ん","WndProc()",MB_OK);
break;
}

}
break;

default:
//その他のイベントはWindowsのシステムにお任せ(楽ちん、楽ちん)
return DefWindowProc(hwnd,msg,wprm,lprm);
}

return 0;



}


そして仕上げに終了処理関数 Sys_exit()MP3の後始末を書き加えます(quit.cpp)。この関数はプログラムの終了時に呼び出されるもので、演奏デバイスをOPENしたまま強制終了することを防ぐ意味合いがあります。(詳細はMIDI再生の項を参照願います)


void Sys_exit()
{

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

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

//MP3が鳴っていたら停止
Com_stop_mp3();

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

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

}


さて、如何でしたでしょうか。MCIコマンドでこんなに簡単にMP3の演奏が出来るとは私も知らなかったので、自分でも感心することしきりです(笑)。統一した手法でマルチメディアを扱えるように、というMCIの設計意図はたしかに恩恵があるわけですね。願わくばこーゆー(誰もが関心を持つであろう)情報はHELPで簡単に解説が出てくるようにして欲しいのですが、残念ながらコンパイラ付属のCD-ROMでは検索しても情報は得られませんでした。・・・まあ、MSの商魂を考えれば 「別途リファレンスマニュアル(途方も無く高額)を買え」 とゆーことなのかも知れませんが♪(爆 ^0^)

とゆーことで、今回はこのへんで・・・♪