MIDIストリームによるSMFの演奏

概要:MIDIデータを演奏する

MIDIデータを演奏する最も基本的な方法は、ショートメッセージとロングメッセージを送り続ける事です。
もちろん時間管理は正確に行わなければなりません。
これは面倒というか、正確な時間管理など(私の技術では)不可能に近いです。

一般にMIDIデータを演奏する場合はMIDIストリームを利用します。
MIDIストリームには時間管理をしてくれるという御利益の他にも
複数のメッセージをまとめて処理してくれるという良いような悪いような特典付きです。
まとめて処理するので、リアルタイム処理には不向きです。
ま、そういう高度な事は DirectX に任せておけばよいでしょう。

MIDIストリームは既存のデータを演奏する事を主眼に設計されたようですが、
癖があって、かなり使い難いです。
しかも、過去にいろいろバグがあったらしい。

ここで用いるMIDIデータは前節で作成(変換)したSMFフォーマット0データです。

■メッセージとイベント

メッセージとイベントは同じ情報であっても、
○MIDIケーブル上を流れる情報を「メッセージ」
○メモリやハードディスク等に記録された情報を「イベント」
と呼び、互いを区別します。

■MIDIストリーム

MIDIストリームの処理手順は以下の通りです。

○再生準備

1.midiStreamOpen関数
2.MIDIデータのセット
3.midiOutPrepareHeader関数
4.midiStreamOut関数

○再生

5.midiStreamRestart関数

○終了

6.midiOutReset関数
7.midiOutUnprepareHeader関数
8.midiStreamClose関数

処理の中心はMIDIデータのセットです。
midiStreamOut関数が望む形でセットしなければなりません。
詳しくは後述します。

■SmfPlayクラス

SmfPlayクラスはSmfFormatクラスをpublicに継承しています。

HMIDISTRM m_hStream はMIDIストリームのハンドルです。
MIDIHDR *m_pMidiHdr はMIDIHDR構造体のアドレスです。
BYTE m_bufsum は m_pMidiHdr の個数です。
DWORD m_bufsize は m_pMidiHdr->lpData のバイト数です。
DWORD m_offset はMIDIの先頭から次に読み込みを開始する位置までのバイト数です。
 最後まで読み込んだ場合はMIDIのバイト数に一致します。
SMFPLAYCONDITION m_condition は演奏状態を表す列挙体です。
 停止中なら STOP 、再生中なら PLAY 、一時停止中なら PAUSE です。
 この列挙体は SmfPlay クラス内部でのみ使用するので非公開とします( SmfPlay クラスの非公開領域で宣言)。
HANDLE m_hThread はコールバックスレッドのハンドルです。

詳しくは後述します。

class SmfPlay : public SmfFormat{
    // 列挙体
    enum SMFPLAYCONDITION{ STOP,PLAY,PAUSE };

    // メンバ変数
    HMIDISTRM m_hStream;
    MIDIHDR *m_pMidiHdr;
    BYTE m_bufsum;      // m_pMidiHdr の個数
    DWORD m_bufsize;    // m_pMidiHdr->lpData のバイト数
    DWORD m_offset;     // MIDIの先頭から次に読み込みを開始する位置までのバイト数
    SMFPLAYCONDITION m_condition;    // 演奏状態
    HANDLE m_hThread;   // コールバックスレッドのハンドル
    // メソッド
    DWORD Sysexc(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written);
    DWORD Meta(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written);
    DWORD ShortEvent(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written);
    DWORD EndOfTrack(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written);
    DWORD TrackChunk(BYTE *p,MIDIHDR *pMidiHdr);
    void MsgBox(MMRESULT result,char *caption);
    void StreamOut(void);
    void Initialize(void);
    // 静的メソッド
    static DWORD WINAPI ThreadProc(void *lpParameter);    // コールバックスレッド
public:
    SmfPlay(char *rscName,char *rscType);    // リソース
    SmfPlay(char *fileName);                 // ファイル
    SmfPlay(BYTE *pData);                    // データ(SMFのデータ形式に従っている必要があります)
    ~SmfPlay(void);
    void Play(void);    // 再生開始します(一時停止を解除します)
                        // 最後まで演奏した場合は自動的に停止します(再生開始位置を先頭にシークする)
    void Stop(void);    // 停止します(再生開始位置を先頭にシークする)
    void Pause(void);   // 一時停止します
    MMRESULT GetCurrentPosition(LPMMTIME lpmmt,UINT cbmmt);    // midiStreamPosition 関数を呼び出します
    double GetEndTime(void);    // 終了時刻(秒単位)を取得します
};

■SMFの読み込み

SmfPlayクラスはSmfFormatクラスを継承しているので、
SmfPlayクラスのコンストラクタが処理するより先に、SmfFormatクラスのコンストラクタが処理されます。
従って、SmfPlayクラスのコンストラクタが実行される時には
SMFフォーマット0(ランニングステータス補完)に変換されたデータが完成しており、
そのアドレスは m_pMidi メンバ変数に保持されています。

初期化処理は Initialize メソッドで行います。

// リソース
SmfPlay::SmfPlay(char *rscName,char *rscType):SmfFormat(rscName,rscType)
{
    if(m_pMidi==NULL) return;    // MIDIの読み込みに失敗している

    Initialize();
}

// ファイル
SmfPlay::SmfPlay(char *fileName):SmfFormat(fileName)
{
    if(m_pMidi==NULL) return;    // MIDIの読み込みに失敗している

    Initialize();
}

// データ(SMFのデータ形式に従っている必要があります)
SmfPlay::SmfPlay(BYTE *pData):SmfFormat(pData)
{
    if(m_pMidi==NULL) return;    // MIDIの読み込みに失敗している

    Initialize();
}

■初期化

Initialize メソッドの最初の処理は midiStreamOut 関数に渡すバッファの作成ですが、
バッファは MIDIHDR 構造体に関連付けるので、まずはバッファの個数だけ MIDIHDR 構造体を用意します。
バッファのアドレスは MIDIHDR 構造体の lpData メンバに格納します。

バッファは64KB未満でなくてはなりません(合計で64KB以上は可能)
また、4バイトの倍数でなくてはなりません
バッファの容量によっては一度に全てのデータをセットする事ができないので、マルチバッファリングは必須です。
今回は24000バイトのバッファを2個作りました。
これらバッファにはメッセージやデルタタイムを書き込んでいきます。

マルチバッファリングとは、再生を開始する前に2個以上のバッファを再生待ちキューに入れておく事で
先に書き込んだバッファが処理済みになっても、次のバッファの処理が間断なく継続されるので、
一瞬の無音も発生させない技術です。
あるバッファが処理済みになったら、他のバッファを処理している間に
新しいデータを書き込み、再び再生待ちキューに入れます。

処理済みになった事を知る最良の方法はメッセージを発生させて、コールバックで受け取る事です。
コールバックにはウィンドウ、関数、スレッドなどがあります。
関数は割り込みに相当するので、実行できる関数にかなりの制限があり、
バッファを再生待ちキューに入れる事もできません。
ウィンドウは表示させない(見えない)ウィンドウを作成するという方法もありますが、
個人的には綺麗な解決策じゃないと思います。
というわけで、スレッドを採用します。
スレッドには関数のような制約もなく、ウィンドウと同じように処理する事ができます。
ただし、スレッドはメッセージを受け取れるようにしておかなければなりません
スレッドにはオブジェクトのポインタを渡します。
詳しくは後述します。

MIDIストリームをオープンしたらTPQN(時間単位)を設定します。
TPQNは MIDIPROPTIMEDIV 構造体にセットして、midiStreamProperty 関数で設定します。
TPQNの設定は再生前に実行しておく必要があります。

Initialize メソッドの最後では StreamOut メソッドを呼び出して、一つのバッファを再生待ちキューに入れておきます。
実際に再生を開始する前にもう一つのバッファを再生待ちキューに入れる事でマルチバッファリングが実現されます。
再生の時にまとめて処理するのではなく、予め一つのバッファを処理する理由は
要求があった時により速く再生できるようにする為です。
また、MIDIストリームは一時停止の状態でオープンされるので、まだ再生しません。

// 初期化します
void SmfPlay::Initialize(void)
{
    m_bufsum=2;    // バッファの個数(マルチバッファリングするので複数個以上必要)
    m_pMidiHdr=(MIDIHDR*)calloc(m_bufsum,sizeof(MIDIHDR));

    m_bufsize=24000;    // バッファのバイト数(64KB未満 & 4バイトの倍数でなければならない)
    for(BYTE k=0;k<m_bufsum;k++){    // 必要なメンバだけ初期化
        m_pMidiHdr[k].lpData=(LPSTR)malloc(m_bufsize);
        m_pMidiHdr[k].dwBufferLength=m_bufsize;
        m_pMidiHdr[k].dwUser=k;
    }

    DWORD dwThreadID;
    m_hThread=CreateThread(NULL,0,SmfPlay::ThreadProc,this,0,&dwThreadID);
    while(!PostThreadMessage(dwThreadID,WM_APP,0,0)) Sleep(1);    // スレッドのメッセージキュー作成

    UINT dev=MIDI_MAPPER;
    MMRESULT result=midiStreamOpen(&m_hStream,&dev,1,dwThreadID,NULL,CALLBACK_THREAD);
    MsgBox(result,"open");

    // TPQN(TickPerQuarterNote)の設定
    WORD division=(m_pMidi[12]<<8)+m_pMidi[13];
    MIDIPROPTIMEDIV proptime;
    proptime.cbStruct=sizeof(MIDIPROPTIMEDIV);
    proptime.dwTimeDiv=division;
    result=midiStreamProperty(m_hStream,(BYTE*)&proptime,MIDIPROP_SET | MIDIPROP_TIMEDIV);
    MsgBox(result,"property");

    m_offset=22;    // トラックチャンクのデータセクションの先頭を指す
    m_condition=STOP;

    StreamOut();    // 必要な時に直ぐ再生できるよう予めバッファリングしておく(再生はしない)
}

■エラー通知メッセージボックス

MsgBox メソッドはマルチメディア関数のエラーをメッセージボックスで通知します。

// マルチメディア関数のエラーをメッセージボックスで通知します
void SmfPlay::MsgBox(MMRESULT result,char *caption)
{
    TCHAR text[512];

    if(result!=MMSYSERR_NOERROR){
        mciGetErrorString(result,text,sizeof(text)/sizeof(text[0]));
        MessageBox(NULL,text,caption,MB_OK);
    }
}

■データを読み込んで出力する

変換したデータを読み込んでバッファに書き込む処理は TrackChunk メソッドで行います。
TrackChunk メソッドに渡す引数は「次に読み込みを開始するアドレス」と「現在のバッファのアドレス」です。
戻り値は変換したデータを読み込んだバイト数です。
何度も TrackChunk メソッドを実行する事で、いつかは m_offset メンバ変数の値が
変換したデータのバイト数と等しくなります。
等しくなった場合は全てのデータを読み込んだという事になります。

現在のバッファの番号は bufnum 静的変数で管理します。
一つのバッファを書き込んだらバッファの番号を更新します。

また、内部仕様の問題なのでプログラマが気を付けていれば問題ない事ですが、
全データを読み込んだにもかかわらず StreamOut メソッドを呼び出した場合は何もしないでメソッドを終了します。
同じく、現在全てのバッファを使い切っているにもかかわらず StreamOut メソッドを呼び出した場合も
何もしないでメソッドを終了します。
バッファの状態は MIDIHDR 構造体の dwFlags メンバを参照すればわかります。
○処理未了 MHDR_PREPARED | MHDR_INQUEUE | MHDR_ISSTRM
○処理終了 MHDR_DONE | MHDR_PREPARED | MHDR_ISSTRM

// データを読み込んで出力する(再生はしない)
void SmfPlay::StreamOut(void)
{
    static bufnum=0;        // バッファの番号

    BYTE *p=m_pMidi;
    DWORD midiSize=22+(p[18]<<24)+(p[19]<<16)+(p[20]<<8)+p[21];
    if(m_offset==midiSize) return;    // 全データを読み込んだ

    // 全バッファ使用中なら何もしない
    if(m_pMidiHdr[bufnum].dwFlags & MHDR_INQUEUE) return;      // バッファは再生のためにキューに入れられている

    m_offset+=TrackChunk(p+m_offset,&m_pMidiHdr[bufnum]);      // データを読み込んでバッファに書き込む

    MMRESULT result=midiOutPrepareHeader((HMIDIOUT)m_hStream,&m_pMidiHdr[bufnum],sizeof(MIDIHDR));
    MsgBox(result,"prepare");

    result=midiStreamOut(m_hStream,&m_pMidiHdr[bufnum],sizeof(MIDIHDR));
    MsgBox(result,"out");

    bufnum=(bufnum+1)%m_bufsum;
}

■データを読み込んでバッファに書き込む

TrackChunk メソッドは演奏処理の中核を担うメソッドです。
実際の書き込み処理は下請けメソッドに任せていますが、
イベントの種類を判別して、適切な下請けメソッドを呼び出しています。

TrackChunk メソッドはステータスの省略が無い事を前提に処理しています。
ランニングステータスについて考慮する場合は TrackChunk メソッドで管理する事になります。
そして、下請けメソッドにステータスを渡す構造になるでしょう(省略の有無にかかわらず)。

下請けメソッドは、書き込み先の先頭からのバイト数を保持する変数 written を書き換える事に注意して下さい。

バッファが尽きた場合はコールバックフラグを設定して終了します。
コールバックメッセージは MEVT_F_CALLBACK フラグを設定したイベントが処理された時に発生します
バッファが尽きているのですから、一つ前のイベントにフラグを設定する事に注意して下さい。
イベントのバイト数は不定なので、予め取得しておく必要があります。

バッファに書き込むサイズは4バイトの倍数でなくてはなりません。
バッファそのもののサイズも4バイトの倍数でなくてはならない他に、
書き込むサイズにも同じ制約がある事に注意して下さい。

// データを読み込んでバッファに書き込む
// BYTE *p 次に読み込みを開始するアドレス
// MIDIHDR *pMidiHdr 現在のバッファのアドレス
DWORD SmfPlay::TrackChunk(BYTE *p,MIDIHDR *pMidiHdr)
{
    LPSTR pMidiBuf=pMidiHdr->lpData;
    memset(pMidiBuf,0,m_bufsize);    // バッファの全内容クリア

    DWORD offset=0,written=0;
    DWORD befWri=0,*befEvent;    // befWri は前回書き込んだバイト数
    while(1){
        BYTE nt;    // 読み込んだ時間のバイト数
        DWORD deltaTime;
        nt=Decode(p+offset,&deltaTime);    // デルタタイムを読み込む

        // 前回のイベントのアドレス取得
        befEvent=&((MIDISTRMEVENT*)(pMidiBuf+written-befWri))->dwEvent;
        befWri=written;

        DWORD ne;    // 読み込んだイベントのバイト数
        if(p[offset+nt]==0xF0 || p[offset+nt]==0xF7){    // システムエクスクルーシブイベント
            ne=Sysexc(deltaTime,p+offset+nt,pMidiBuf,&written);
        }else if(p[offset+nt]==0xFF){    // メタイベント
            if(p[offset+nt+1]==0x2F && p[offset+nt+2]==0x00){    // End of Track
                ne=EndOfTrack(deltaTime,p+offset+nt,pMidiBuf,&written);
                if(ne) offset+=(nt+ne);    // 成功
                else (*befEvent) |= MEVT_F_CALLBACK;    // バッファが尽きた
                break;     // while
            }else{
                ne=Meta(deltaTime,p+offset+nt,pMidiBuf,&written);
            }
        }else{    // MIDIイベント
            ne=ShortEvent(deltaTime,p+offset+nt,pMidiBuf,&written);
        }

        if(ne){    // 成功
            offset+=(nt+ne);
            befWri=written-befWri;
            continue;    // while
        }else{    // バッファが尽きた
            (*befEvent) |= MEVT_F_CALLBACK;    // 前回のイベントにコールバックフラグ設定
            break;    // while
        }
    }

    pMidiHdr->dwFlags=0;
    pMidiHdr->dwOffset=0;
    memset(pMidiHdr->dwReserved,0,sizeof(pMidiHdr->dwReserved));
    pMidiHdr->lpNext=0;
    pMidiHdr->reserved=0;
    pMidiHdr->dwBytesRecorded=written;    // 4の倍数でなければならない

    return offset;    // データセクションのバイト数
}

■ショートイベント

ショートイベントの処理は ShortEvent メソッドで行います。

ショートイベントは MIDISTRMEVENT 構造体に格納します。
これは私が定義した構造体ですが、この形式で書き込む事になっています。

DWORD dwDeltaTime にはデルタタイムを格納します。
DWORD dwStreamID には常に 0 を格納します。
DWORD dwEvent にはショートイベントを格納します。

ショートイベントにはMIDIイベント、セットテンポイベント等があります。
どのイベントであるかを判別する為に
dwEvent メンバの最上位バイトにフラグを指定します。
○MIDIイベント MEVT_F_SHORT フラグ、
○セットテンポイベント MEVT_TEMPO フラグ
MEVT_TEMPO フラグは1バイト型なのでシフトします。

dwEvent メンバは下位バイトから格納していく事に注意して下さい。
理由はビックエンディアンで解釈されるからです。

MIDISTRMEVENT 構造体のサイズは12バイトです。
ShortEvent メソッドの最初では12バイト書き込むだけバッファが残っているか調べて、
不足していれば何もしないで、バッファ不足を知らせる戻り値 0 を返します。

// ショートイベントを格納する
typedef struct tagMIDISTREAMEVENT{
    DWORD dwDeltaTime;
    DWORD dwStreamID;
    DWORD dwEvent;
}MIDISTRMEVENT;

// ショートイベントを読み込んでバッファに書き込む
// DWORD deltaTime デルタタイム
// BYTE *p 読み込み元データの先頭アドレス
// LPSTR pMidiBuf 書き込み先バッファの先頭アドレス
// DWORD *written 書き込み先の先頭からのバイト数
DWORD SmfPlay::ShortEvent(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written)
{
    if((*written)+sizeof(MIDISTRMEVENT) > m_bufsize) return 0;    // バッファ不足

    MIDISTRMEVENT shortEvent;
    shortEvent.dwDeltaTime=deltaTime;
    shortEvent.dwStreamID=0;

    DWORD offset;
    if(p[0]==0xFF && p[1]==0x51){    // セットテンポイベント
        shortEvent.dwEvent=(MEVT_TEMPO<<24) | (p[3]<<16) | (p[4]<<8) | p[5];
        offset=6;
    }else{    // MIDIイベント
        if((p[0] & 0xF0)==0xC0 || (p[0] & 0xF0)==0xD0){
            shortEvent.dwEvent=MEVT_F_SHORT | (p[1]<<8) | p[0];
            offset=2;
        }else{
            shortEvent.dwEvent=MEVT_F_SHORT | (p[2]<<16) | (p[1]<<8) | p[0];
            offset=3;
        }
    }
    memcpy(pMidiBuf+(*written),&shortEvent,sizeof(MIDISTRMEVENT));
    (*written)+=sizeof(MIDISTRMEVENT);    // *written を書き換える事に注意して下さい
    return offset;    // 読み込んだデータのバイト数
}

■メタイベント

メタイベントはファイル固有の情報なので、基本的には無視します(演奏には必要ありません)。
ただし、セットテンポイベントだけは ShortEvent メソッドを呼び出して処理します。

// メタイベントを読み込んでバッファに書き込む(セットテンポイベント以外は読み飛ばす)
// DWORD deltaTime デルタタイム
// BYTE *p 読み込み元データの先頭アドレス
// LPSTR pMidiBuf 書き込み先バッファの先頭アドレス
// DWORD *written 書き込み先の先頭からのバイト数
DWORD SmfPlay::Meta(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written)
{
    if(p[0]==0xFF && p[1]==0x51){    // セットテンポイベント
        return ShortEvent(deltaTime,p,pMidiBuf,written);    // 戻り値は読み込んだデータのバイト数
    }else{    // 無視するメタイベント
        DWORD len;
        DWORD offset=Decode(p+2,&len);    // イベントサイズ(可変長)とイベントデータ(可変長)のバイト数を取得
        return 2+offset+len;    // 読み飛ばしたデータのバイト数
    }
}

■ロングイベント

ロングイベントは MIDIEVENT 構造体に格納します。

typedef struct midievent_tag
{
  DWORD dwDeltaTime;  /* Ticks since last event */
  DWORD dwStreamID;   /* Reserved; must be zero */
  DWORD dwEvent;     /* Event type and parameters */
  DWORD dwParms[1];   /* Parameters if this is a long event */
} MIDIEVENT;


MIDISTRMEVENT 構造体の最後に DWORD dwParams[1] を付け加えただけです。
途中まで同じである、という事を TrackChunk メソッドにおける「以前のイベントを取得する」処理で利用しています。

dwParams[1] は可変長に扱います。
ロングイベントは可変長なので、必要なだけ拡張するのです(必要なだけメモリ領域を確保する)。

dwParams メンバは下位バイトから書き込みます。
使用しなかったバイトは 0 で埋め尽くします
dwParams メンバに書き込むのはステータスとイベントデータです。
イベントサイズは書き込みません

dwEvent メンバの最上位バイトにはロングイベントである事を示す MEVT_F_LONG フラグを格納します。
下位3バイトには dwParams メンバ変数に実際に書き込んだバイト数を格納します。
使用しなかったバイトはカウントしません。
つまり、参照元のステータスとイベントデータのバイト数です(イベントサイズのバイト数は含まない)。

midiStreamOut 関数に渡すデータは MIDISTRMEVENT 構造体(の形式)、
または MIDIEVENT 構造体の連続である事が決められています

// システムエクスクルーシブイベントを読み込んでバッファに書き込む
// DWORD deltaTime 相対時間
// BYTE *p 読み込み元データの先頭アドレス
// LPSTR pMidiBuf 書き込み先バッファの先頭アドレス
// DWORD *written 書き込み先の先頭からのバイト数
DWORD SmfPlay::Sysexc(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written)
{
    DWORD offset=1;    // ステータスのバイト数
    DWORD len;         // イベントのバイト数
    offset+=Decode(p+offset,&len);    // イベントサイズ(可変長)とイベントデータ(可変長)のバイト数を取得
    len++;     // ステータスのバイト数を加算
    DWORD nParam=(len/4) + (DWORD)((len%4)?1:0);

    if((*written)+sizeof(MIDIEVENT)+(nParam-1)*sizeof(DWORD) > m_bufsize) return 0;    // バッファ不足

    MIDIEVENT *pLongEvent;
    pLongEvent=(MIDIEVENT*)malloc(sizeof(MIDIEVENT)+(nParam-1)*sizeof(DWORD));
    memset(pLongEvent,0,sizeof(MIDIEVENT)+(nParam-1)*sizeof(DWORD));

    pLongEvent->dwDeltaTime=deltaTime;
    pLongEvent->dwStreamID=0;
    pLongEvent->dwEvent=MEVT_F_LONG | len;
    pLongEvent->dwParms[0]=p[0];
    for(DWORD k=0;k<len-1;k++){
        DWORD index=(k+1)/4;
        BYTE shift=(BYTE)(8*((k+1)%4));
        pLongEvent->dwParms[index] |= (p[offset+k]<<shift);
    }
    offset+=(len-1);    // ステータスを除くイベントのバイト数を加算

    memcpy(pMidiBuf+(*written),pLongEvent,sizeof(MIDIEVENT)+(nParam-1)*sizeof(DWORD));
    (*written)+=sizeof(MIDIEVENT)+(nParam-1)*sizeof(DWORD);    // *written を書き換える事に注意して下さい

    free(pLongEvent);
    return offset;    // 読み込んだデータのバイト数
}

■End of Track イベント

End of Track イベントは再生の終了を知らせるイベントとして送出します。
基本的にはショートイベントの処理に同じですが、
フラグは MEVT_F_CALLBACK だけを格納しました。

コールバックは End of Track イベントを受け取る事で、再生の終了を知ります。

// 終了イベントとして End of Track を送る
// DWORD deltaTime 相対時間
// BYTE *p 読み込み元データの先頭アドレス
// LPSTR pMidiBuf 書き込み先バッファの先頭アドレス
// DWORD *written 書き込み先の先頭からのバイト数
DWORD SmfPlay::EndOfTrack(DWORD deltaTime,BYTE *p,LPSTR pMidiBuf,DWORD *written)
{
    if((*written)+sizeof(MIDISTRMEVENT) > m_bufsize) return 0;    // バッファ不足

    MIDISTRMEVENT shortEvent;
    shortEvent.dwDeltaTime=deltaTime;
    shortEvent.dwStreamID=0;
    shortEvent.dwEvent= MEVT_F_CALLBACK | (p[2]<<16) | (p[1]<<8) | p[0];

    memcpy(pMidiBuf+(*written),&shortEvent,sizeof(MIDISTRMEVENT));
    (*written)+=sizeof(MIDISTRMEVENT);    // *written を書き換える事に注意して下さい
    return 3;    // 読み込んだデータのバイト数
}

■コールバックスレッド

スレッドはオブジェクトごとに作るので、プロシージャはメソッドにします。
スレッドのプロシージャをメソッドにする場合はstaticメソッドである必要があります
staticメソッドから非staticメンバを参照する事はできないので、
スレッドを作ったオブジェクトのポインタ( this ポインタ)を渡してもらいます。

MEVT_F_CALLBACK フラグで発生するメッセージは MM_MOM_POSITIONCB です。
MEVT_F_CALLBACK フラグ以外で発生するメッセージはありません。
実際には何か来ていますが、正体不明です。

メッセージは GetMessage 関数で取得します。
このスレッドはイベント駆動なので、PeekMessage 関数は不向きです。

メッセージの WPARAM にはメッセージを発生させた HMIDIOUT が格納されています。
MM_MOM_POSITIONCB のリファレンスは間違っているので注意して下さい(ちゃんとした本で間違いを指摘していた)。

メッセージの LPARAM にはメッセージを発生させたイベントを含む MIDIHDR 構造体のアドレスが格納されています。
MIDIHDR 構造体の dwOffset メンバはメッセージを発生させたイベントのオフセットなので、
これらからメッセージを発生させたイベントの特定が可能です。

もし End of Track イベントなら Stop メソッドを呼び出して停止します。
Stop メソッドでは midiStreamStop 関数を呼び出していますが、
midiStreamStop 関数を呼び出さない限り再生は継続されていると解釈されるので、
現在時刻の取得などで正しい値が得られません。

そうではないにしても、全データを読み終えている状態なら何もしません。

まだ読み込んでいないバッファが有る場合だけ StreamOut メソッドを呼び出します。
本来なら midiOutUnprepareHeader 関数を実行してから呼び出すべきですが、
「メディアデータの再生中にこの操作を実行することはできません。
デバイスをリセットするか、またはデータの再生が終了するまで待って下さい。」
というエラーが発生する事があるので、やめました。
データの再生は終了しているハズなんだけどなー?
そもそもエラー発生条件がよくわからん。
ま、midiOutUnprepareHeader 関数を実行しなくても他は正常に動作するんですから大丈夫でしょう。

スレッドはオブジェクト誕生から消滅まで存在し続ける事にします。
従って、再生が終了したからといってスレッドを破棄してはいけません
スレッドの破棄はオブジェクトのデストラクタで行います。

また、このスレッドプロシージャはstaticメソッドなので、複数のオブジェクトから参照される事に注意して下さい。
内部でstatic変数を使う場合は、それが意図した動作をするのか良く検討する必要があります。
でも基本的には使わない方が良いでしょう。

// コールバックスレッド
DWORD WINAPI SmfPlay::ThreadProc(void *lpParameter)    // staticメソッド
{
    SmfPlay *sp=(SmfPlay*)lpParameter;    // スレッドを作成したオブジェクト

    MSG msg;
    BOOL bRet;
    while((bRet=GetMessage(&msg,NULL,0,0))!=0){    // スレッドはオブジェクト消滅まで終了させない
        if(bRet==-1) break;

        MIDIHDR *pMidiHdr;
        MIDISTRMEVENT *pEvent;

        BYTE *p;
        DWORD midiSize;

        if(msg.message==MM_MOM_POSITIONCB){
            pMidiHdr=(MIDIHDR*)msg.lParam;

            pEvent=(MIDISTRMEVENT*)&(pMidiHdr->lpData[pMidiHdr->dwOffset]);    // メッセージを送ったイベント
            if((pEvent->dwEvent & 0x00FFFFFF)==0x00002FFF){    // End of Track
                sp->Stop();    // 停止して、再生開始位置を先頭にシークする
                continue;
            }

            p=sp->m_pMidi;
            midiSize=22+(p[18]<<24)+(p[19]<<16)+(p[20]<<8)+p[21];
            if(sp->m_offset==midiSize) continue;    // 全データを読み込んだ

            // まだ読み込んでいないバッファが有る
            sp->StreamOut();
        }
    }
    return 0;
}

■再生、一時停止解除

MIDIストリームは一時停止の状態でオープンします。
一時停止を解除するには midiStreamRestart 関数を呼び出します。
midiStreamRestart 関数は単に再生を開始する目的でも使います。

再生処理では、まだ読み込んでいないバッファがある場合は SreamOut メソッドを呼び出します。
もしかしたら一つのバッファで全データを読み終えたかもしれない事に注意して下さい。
ま、StreamOut 関数には全データを読み込んだにもかかわらず
呼び出した場合の処理を組み込んであるから大丈夫ですが。

// 再生開始します(一時停止を解除します)
// 最後まで演奏した場合は、Stopメソッドを呼び出します
void SmfPlay::Play(void)
{
    if(m_pMidi==NULL) return;        // MIDIの読み込みに失敗している

    if(m_condition==PLAY) return;      // 再生中は何もしない

    MMRESULT result=midiStreamRestart(m_hStream);
    MsgBox(result,"restart");

    if(m_condition==PAUSE){     // 一時停止していた場合は一時停止解除だけして終了
        m_condition=PLAY;
        return;
    }
    m_condition=PLAY;

    BYTE *p=m_pMidi;
    DWORD midiSize=22+(p[18]<<24)+(p[19]<<16)+(p[20]<<8)+p[21];
    if(m_offset==midiSize) return;     // 全データを読み込んだ

    // まだ読み込んでいないバッファが有る
    StreamOut();
}

■停止

midiStreamStop 関数を呼び出します。
また、再生開始位置を先頭にシークします。
実際には m_offset メンバ変数の値を変更します。

そして、次の再生の為に SreamOut メソッドを呼び出し、バッファリングしておきます。

// 停止します(再生開始位置を先頭にシークする)
void SmfPlay::Stop(void)
{
    if(m_pMidi==NULL) return;    // MIDIの読み込みに失敗している

    MMRESULT result=midiStreamStop(m_hStream);
    MsgBox(result,"stop");

    m_condition=STOP;

    m_offset=22;
    StreamOut();    // 必要な時に直ぐ再生できるよう予めバッファリングしておく(再生はしない)
}

■一時停止

midiStreamPause 関数を呼び出します。

// 一時停止します
void SmfPlay::Pause(void)
{
    if(m_pMidi==NULL) return;    // MIDIの読み込みに失敗している

    if(m_condition!=PLAY) return;    // 再生中ではない

    MMRESULT result=midiStreamPause(m_hStream);
    MsgBox(result,"pause");

    m_condition=PAUSE;
}

■時間取得

現在時刻は midiStreamPosition 関数で得られます。
現在時刻は midiStreamStop 関数を呼び出さない限りカウントし続けます。
midiStreamStop 関数の呼び出し後から midiStreamRestart 関数の呼び出し前までは 0 です。

終了時刻は SmfFormat クラスが計算してくれているので、
計算結果が格納されている m_endTime メンバ変数を参照するだけです。

// midiStreamPosition 関数を呼び出します
MMRESULT SmfPlay::GetCurrentPosition(LPMMTIME lpmmt,UINT cbmmt)
{
    if(m_pMidi==NULL) return 1;    // MIDIの読み込みに失敗している
    // 1 は未定義の外部エラー

    return midiStreamPosition(m_hStream,lpmmt,cbmmt);
}

// 終了時刻(秒単位)を取得します
double SmfPlay::GetEndTime(void)
{
    if(m_pMidi==NULL) return 0;    // MIDIの読み込みに失敗している

    return m_endTime;
}

ちなみに、現在時刻の設定は非常に困難です。
途中から始めた場合でも、それ以前から鳴り続けている音の処理もしなければならないからです。
つまり、先頭から検索しつつ、必要なイベントについては処理していかなければならないのです。
そもそも、秒単位で指定した場合は
現在のテンポに注意しつつチックに変換する必要があります。

多大な労力を払ってそこまでする価値があるのか疑問です。
そこまで高度な事がしたい場合は DirectX を利用した方が良いでしょう。

■デストラクタ

スレッドを破棄して、MIDIストリームの終了処理を行います。

SmfPlay::~SmfPlay(void)
{
    if(m_pMidi==NULL) return;        // MIDIの読み込みに失敗している

    if(m_hThread) CloseHandle(m_hThread);

    MMRESULT result=midiOutReset((HMIDIOUT)m_hStream);
    MsgBox(result,"unprepare");
    for(BYTE k=0;k<m_bufsum;k++){
    result=midiOutUnprepareHeader((HMIDIOUT)m_hStream,&m_pMidiHdr[k],sizeof(MIDIHDR));
        MsgBox(result,"unprepare");
    }
    result=midiStreamClose(m_hStream);
    MsgBox(result,"close");
    for(k=0;k<m_bufsum;k++){
        if(m_pMidiHdr[k].lpData) free(m_pMidiHdr[k].lpData);
    }
    free(m_pMidiHdr);
}

★☆ ダウンロード ☆★


戻る / ホーム