MIDIファイルを鳴らす

つぎはMIDIです。
MIDIに関しては残念ながら sndPlaySound("bells.wav",SND_ASYNC);のような便利な関数はないので、ある程度自分で組まなければなりません。

方法は二つあって、一つはMIDIデバイスに直接データを送り込むもの、もう一つはMCIデバイスにメッセージを送る方法です。
MCIデバイスというのは、Media Control Interface のことで、デバイスに依存しないつまり付いている機器が、サウンドブラスターだろうと、MIDI音源だろうと関係なくMIDIを演奏させるためのインターフェースです。

今回は、MCIを使ってMIDIを鳴らすことにします。

MCIには、mciSendCommand(コマンドを送る)と、mciSendString (文字列を送る)があります。
C++らしく mciSendCommand を使います。

Midi関連のソースを打ち込む場合は、一式そろわないとテストできないという問題があります。

それぞれの関数がセットになっているので、デバッグもなかなか面倒です。
しかし、DirectXや、Comモデルなども同じような問題を抱えています。
ここで一度経験しておくと慣れるかもしれません。

宣言部は、CRpgView.h の class CRpgView : public CView
のパブリック部です。

BOOL MciMidiOpen();
BOOL MciMidiClose();
BOOL MciMidiSeekToStart();
BOOL MciMidiStop();

デバイスをオープン&クローズするコード
 

 ///////////////////////////////////////////////////////
 //Midiデバイスをオープンするコード(成功したらTRUEを返す)
 BOOL CRpgView::MciMidiOpen()
 {
 MCI_OPEN_PARMS prm;
 DWORD dwFlags=0;
 //デバイスにMidiを指定
 dwFlags|=MCI_OPEN_TYPE|MCI_OPEN_TYPE_ID;
 prm.lpstrDeviceType=(LPCSTR)MCI_DEVTYPE_SEQUENCER;
 
 //演奏するファイル名を指定
 dwFlags|=MCI_OPEN_ELEMENT;
 prm.lpstrElementName=m_sMidiName;
 
 //オープン
 dwFlags|=MCI_WAIT;
 MCIERROR ret=mciSendCommand(0,MCI_OPEN,dwFlags,(DWORD)&prm);
 if(ret==0){
  m_wMidiDeviceID=prm.wDeviceID;
 }else{ //エラー
  char buf[256];
  mciGetErrorString(ret,buf,sizeof(buf));
  TRACE("MciMidiOpen:%d(%s)\r\n",ret,buf);
  m_wMidiDeviceID=0;
  return FALSE;
 }
 
  return TRUE;
 }
 
 ///////////////////////////////////////////////////
 //Midiデバイスをクローズするコード(成功したらTRUEを返す)
 BOOL CRpgView::MciMidiClose()
 {
 if(m_wMidiDeviceID==0) return TRUE;
 
  MCI_GENERIC_PARMS prm;
  DWORD flag=MCI_WAIT;
  MCIERROR ret=mciSendCommand(m_wMidiDeviceID,MCI_CLOSE,flag,(DWORD)&prm);
  if(ret){
  char buf[256];
  mciGetErrorString(ret,buf,sizeof(buf));
  TRACE("MciMidiClose:%d(%s)\r\n",ret,buf);
 }
  m_wMidiDeviceID=0;
 
  return ret==0;
 }
 
Midiの演奏を開始する関数です。
 
 BOOL CRpgView::MciMidiPlay()
 {
 if(m_wMidiDeviceID==0) return FALSE;
  MCI_PLAY_PARMS prm;
  DWORD dwFlags=0;
 
  //コールバック関数を使用
  dwFlags|=MCI_NOTIFY;
  prm.dwCallback=(DWORD)GetSafeHwnd();
 
  //演奏開始
  MCIERROR ret=mciSendCommand(m_wMidiDeviceID,MCI_PLAY,dwFlags,(DWORD)&prm);
 if(ret){ //エラー
  char buf[256];
  mciGetErrorString(ret,buf,sizeof(buf));
  TRACE("MciMidiPlay:%d(%s)\r\n",ret,buf);
  MciMidiClose(); //デバイスを閉じておく
 }
  return ret==0;
 }
 

演奏中にPlayが呼ばれたら、一度演奏を止めなければいけません。
演奏を止めるための関数です。
 

 /////////////////////////////////////////////////////////
 //Midiデバイスを停止する(成功したらTRUEを返す)
 BOOL CRpgView::MciMidiStop()
 {
 if(m_wMidiDeviceID==0) return TRUE;
  MCI_GENERIC_PARMS prm;
  MCIERROR ret=mciSendCommand(m_wMidiDeviceID,MCI_STOP,0,(DWORD)&prm);
 if(ret){
  char buf[256];
  mciGetErrorString(ret,buf,sizeof(buf));
  TRACE("MciMidiStop:%d(%s)\r\n",ret,buf);
 }
  return ret==0;
 }
 
演奏を一度止めた後、先頭から再開するための関数
 
 //////////////////////////////////////////////////////////////
 //Midiデバイスの再生位置を先頭に移動する(成功したらTRUEを返す)
 BOOL CRpgView::MciMidiSeekToStart()
 {
 if(m_wMidiDeviceID==0) return FALSE;
  MCI_SEEK_PARMS prm;
  MCIERROR ret=mciSendCommand(m_wMidiDeviceID,MCI_SEEK,MCI_SEEK_TO_START,(DWORD)&prm);
 if(ret){
  char buf[256];
  mciGetErrorString(ret,buf,sizeof(buf));
  TRACE("MciMidiSeekToStart:%d(%s)\r\n",ret,buf);
 }
  return ret==0;
 }
 

ついでにCRpgView のデストラクタから MciMidiClose(); を呼ぶことにしましょう。

次はMCIにMIDIファイル名を渡して演奏する部分です。
同じく CRpgView のメンバとして定義します。
 

 ///////////////////////////////////////////////////
 //MIDIを演奏する
 //引数:曲のファイル名、ループさせるかどうか
 BOOL CRpgView::MidiPlay(LPCTSTR pName,BOOL bLoop)
 {
 m_sMidiName=pName; //演奏するMidiファイル名
 m_bMidiLoop=bLoop; //曲をループさせるかどうか
 if(m_wMidiDeviceID){
  MciMidiStop();
  MciMidiClose();
 }
 if(!MciMidiOpen()) return FALSE;
  return MciMidiPlay();
 }
 

ここでは、

m_sMidiName=pName; //演奏するMidiファイル名
m_bMidiLoop=bLoop; //曲をループさせるかどうか
m_wMidiDeviceID

というみっつのメンバ変数が使われています。
これも CRpgView のメンバ変数として宣言しないとエラーになります。
これは、プライベートで宣言した方が良いかもしれません。

ここまで出来たら、鳴らしてみましょう。
キーハンドラのUPの部分で MidPlay() を呼び出して DOWNで MciMidiClose()
を呼びます。

MIDIファイルは、ウィンドウズのディレクトリから一番短いものをプロジェクトのディレクトリにコピーしておきます。

UPキーで演奏開始、DOWNキーで演奏が終わればOKです。

ここままでは、演奏が終わると音が消えてしまうので、演奏が終わったら再び頭からループするように、しておきましょう。

CRpgView.cpp ファイルの先頭のほうに
 

/////////////////////////////////////////////////////////////////////////////
// CRpgView
 
IMPLEMENT_DYNCREATE(CRpgView, CView)
 
BEGIN_MESSAGE_MAP(CRpgView, CView)
 

という部分があります。
メッセージが来たときにどの関数を呼ぶかここで指定します。

ON_MESSAGE(MM_MCINOTIFY,CRpgView::OnMidiNotify)

そこに上の一行を追加します。
メッセージが MM_MCINOTIFY なら、OnMidiNotify が呼ばれます。

この関数は CRpgView のパブリック部に以下のように宣言します。

afx_msg LRESULT OnMidiNotify(WPARAM wParam,LPARAM lParam);

本体は以下の通り。
 

 //Midiの演奏が終わった時に呼ばれる
 afx_msg LRESULT CRpgView::OnMidiNotify(WPARAM wParam,LPARAM lParam)
 {
 if(LOWORD(lParam)!=m_wMidiDeviceID) return FALSE;
  switch(wParam){
  case MCI_NOTIFY_SUCCESSFUL:
   break;
  case MCI_NOTIFY_ABORTED:
   TRACE("MidiAbort:%d %d\n",wParam,lParam);
   return TRUE;
  default:
   TRACE("MidiNotSuccess:%d %d\n",wParam,lParam);
   MciMidiClose();
   return TRUE;
  }
 if(m_bMidiLoop){ //演奏をループさせる
  MciMidiStop();
  MciMidiSeekToStart();
  MciMidiPlay();
 }else{
  MciMidiStop();
  MciMidiClose();
 }
  return TRUE;
 }
 

関数の宣言と定義、メンバ変数の宣言などがぬけないように気をつけましょう。

メンバ変数と、メンバ関数の宣言は以下の通りです。
 

 CString m_sMidiName;
 int m_wMidiDeviceID;
 BOOL m_bMidiLoop; //演奏をループするかどうか
 BOOL MciMidiOpen();
 BOOL MciMidiPlay();
 BOOL MciMidiClose();
 BOOL MciMidiSeekToStart();
 BOOL MciMidiStop();
 BOOL MidiPlay(LPCTSTR pName,BOOL bLoop);
 afx_msg LRESULT OnMidiNotify(WPARAM wParam,LPARAM lParam);