S-JIS[1998-12-05/1999-10-23]

mciSendCommandを使ったMIDIの鳴らし方

音楽を演奏する方法は色々あるのでしょうが、 たぶん一番簡単なのはMIDIファイルをそのまま演奏する事です。
MFCにはそういう機能は用意されていませんが、 APIにはmciSendCommandというMIDIを演奏できる関数があります。
これは、CDの演奏やその他よく分からない物(総称してデバイスと呼ぶ)にも使えるらしいです。


MCIの使い方は、おおまかに言って次の様になります。

  1. デバイスをオープンする。
  2. デバイスを再生する。
  3. デバイスをクローズする。

う〜ん、シンプル(笑)
他にも途中で演奏を止めたり、ポーズをかけたり、演奏開始位置を変えたりする事もできます。


mciSendCommandはデバイスにメッセージを送るAPIなので、 メッセージループが回っていなければなりません。
また、デバイスの再生時にコールバック関数を指定できますが、 その為にはウィンドウハンドルを必要とします。 したがってMFCからmciSendCommandを使う場合、 CMainFrameまたはC〜Viewクラスのメンバー関数にするのがいいと思います。
このページの例ではCMainFrameに書きます。


  1. 演奏するには、まずデバイスをオープンします。
    MIDIの場合は、ファイル名を指定する必要があります。

    MainFrm.h: 後述

    MainFrm.cpp:

    
    #include "MMsystem.h"
    //Midiデバイスをオープンする(成功したらTRUEを返す)
    BOOL CMainFrame::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;
    }
    

    m_sMidiNameはMIDIファイルのファイル名です。 これはこの関数を呼び出す前にセットしておきます。
    m_wMidiDeviceIDはデバイスのIDです。 MCI_OPENで返されたIDを再生や停止などに使います。


  2. デバイスIDを取得したら、再生するだけです。

    MainFrm.h: 後述

    MainFrm.cpp:

    //Midiデバイスを再生する(成功したらTRUEを返す)
    BOOL CMainFrame::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);
    		MidiMciClose();	//デバイスを閉じておく
    	}
    	return ret==0;
    }
    

    dwFlagsにMCI_NOTIFYを指定すると、演奏が終わった時に MM_MCINOTIFYメッセージが指定したウィンドウに送られます。


  3. 使わないかも知れませんが 一応、演奏を停止する関数も載せておきます。

    MainFrm.h: 後述

    MainFrm.cpp:

    //Midiデバイスを停止する(成功したらTRUEを返す)
    BOOL CMainFrame::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;
    }
    

  4. 演奏が終わった曲を再び再生したい時は、 再生位置を先頭に戻してからMCI_PLAYを実行します。

    MainFrm.h: 後述

    MainFrm.cpp:

    //Midiデバイスの再生位置を先頭に移動する(成功したらTRUEを返す)
    BOOL CMainFrame::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;
    }
    

  5. 一度デバイスをオープンした後に 次の曲を演奏しようと思ったら、 一旦現在のデバイスをクローズする必要があります。

    MainFrm.h: 後述

    MainFrm.cpp:

    //Midiデバイスをクローズする(成功したらTRUEを返す)
    BOOL CMainFrame::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;
    }
    

  6. あとは、演奏が終わった時のMM_MCINOTIFYメッセージを処理する関数を用意します。
    MM_MCINOTIFY自体はMCI_OPENやMCI_PLAYの実行が終了した時に送られるのですが、 それはmciSendCommandの呼び出し時にMCI_NOTIFYを指定した時だけに限られます。 今回はMCI_PLAYでしか指定していないので、演奏が終わった時に送られて来るのです。

    MainFrm.h: 後述

    MainFrm.cpp:

    〜
    BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    	ON_MESSAGE(MM_MCINOTIFY,CMainFrame::OnMidiNotify)
    END_MESSAGE_MAP()
    〜
    //Midiの演奏が終わった時に呼ばれる
    afx_msg LRESULT CMainFrm::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){	//演奏をループさせる
    		MidiMciStop();
    		MidiMciSeekToStart();
    		MidiMciPlay();
    	}else{
    		MidiMciStop();
    		MidiMciClose();
    	}
    	return TRUE;
    }
    
    switch〜case内の意味はヘルプを見て下さい。よく知らないっす。

    MCIでのMIDIの演奏では曲のループ機能は無いので、 曲が終わった時に再び再生することで擬似的にループさせます。
    m_bMidiLoopはループさせるかどうかの値を持たせています。事前に値をセットしておきます。


  7. さて、最後にこれらのMCIコマンドを使用して曲を演奏する関数を用意しておきます。

    MainFrm.h: 後述

    MainFrm.cpp:

    //MIDIを演奏する
    //引数:曲のファイル名、ループさせるかどうか
    BOOL CMainFrame::MidiPlay(LPCTSTR pName,BOOL bLoop)
    {
    	m_sMidiName=pName;	//演奏するMidiファイル名
    	m_bMidiLoop=bLoop;	//曲をループさせるかどうか
    
    	if(m_wMidiDeviceID){
    		MciMidiStop();
    		MciMidiClose();
    	}
    	if(!MciMidiOpen()) return FALSE;
    	return MciMidiPlay();
    }
    
    //MIDIの演奏を停止する
    BOOL CMainFrame::MidiStop()
    {
    	if(!MciMidiStop()) return FALSE;
    	return MciMidiClose();
    }
    
    アプリケーションからは、MidiPlay("なんちゃら.mid",TRUE);の様にして呼び出します。
  8. 最後に、これらの関数に関するヘッダー部分を載せておきます。
    またこの他に、MMsystem.hをincludeし、WinMM.libを ライブラリに追加する必要もあります。
    あと、念のためにコンストラクタでm_wMidiDeviceIDを0に初期化しておく事をお勧めします。

    MainFrm.h:

    class CMainFrame : public CFrameWnd
    {
    〜
    public:	//音楽関連
    	BOOL MidiPlay(LPCTSTR pName,BOOL bLoop=TRUE);
    	BOOL MidiStop();
    	BOOL IsMidiPlay() const { return m_wMidiDeviceID!=0; }
    	afx_msg LRESULT OnMidiNotify(WPARAM wParam,LPARAM lParam);
    private:
    	BOOL MciMidiOpen();
    	BOOL MciMidiPlay();
    	BOOL MciMidiStop();
    	BOOL MciMidiSeekToStart();
    	BOOL MciMidiClose();
    	CString m_sMidiName;	//演奏するファイル名
    	BOOL m_bMidiLoop;	//演奏をループするかどうか
    	WORD m_wMidiDeviceID;	//演奏中のデバイスID
    〜
    };
    

冒頭にも書いた通り、mciSendCommandはデバイスにメッセージを送るAPIなので メッセージループがちゃんと回っている必要があります。
タイマー割り込みのAPIであるtimeSetEventで指定され呼び出されたコールバック関数(TimerProc)の中で mciSendCommandを使うとうまく鳴りません。ウィンドウがかたまります。
MFCの場合、素直にCWnd::SetTimerで割り込みを発生させてCWnd::OnTimerの中でmciSendCommandを呼び出すと大丈夫です。

また、CDを演奏する場合等も使い回せる部分は結構あるので(MCI_STOPやMCI_CLOSE等) うまくクラス化する事も出来ると思います。


[1999-10-23]
timeSetEvent内から呼び出す場合には、mciSendCommandを呼び出すための関数を別に用意し 独自のメッセージ(WM_USER等)を割り当て、PostMessage(WM_USER);とすれば一応鳴るみたいです。

MainFrm.h:

class CMainFrame : public CFrameWnd
{
〜
public:	//音楽関連
	BOOL MidiPlay(LPCTSTR pName,BOOL bLoop=TRUE);
	afx_msg LRESULT DoMidiPlay(WPARAM wPrm,LPARAM lPrm);
〜
};

#define	WM_DO_MIDI_PLAY	(WM_USER+0)
MainFrm.cpp:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
	//{{AFX_MSG_MAP(CMainFrame)
〜
	//}}AFX_MSG_MAP
	ON_MESSAGE(WM_DO_MIDI_PLAY,CMainFrame::DoMidiPlay)
END_MESSAGE_MAP()

LRESULT CMainFrame::DoMidiPlay(WPARAM wPrm,LPARAM lPrm) 
{
	if(m_wMidiDeviceID){
		MidiMciStop();
		MidiMciClose();
	}
	MidiMciOpen();
	MidiMciPlay();

	return 0;
}

BOOL CMainFrame::MidiPlay(LPCTSTR pName,BOOL bLoop)
{
	m_sMidiName=pName;	//演奏するMidiファイル名
	m_bMidiLoop=bLoop;	//曲をループさせるかどうか

	PostMessage(WM_DO_MIDI_PLAY);

	return TRUE;
}
ただし上記の実装のやり方では、MidiPlay()から帰った時点で演奏が本当に成功したかどうか 分からないところが欠点です。
VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま