SMFのフォーマット変換(1から0)

概要:SMFのフォーマット変換とランニングステータス補完、おまけに終了時間計算

標準MIDIファイル(SMF)のフォーマット1からフォーマット0への変換アルゴリズムについて解説します。
ここでは変換にのみ着目するので、イベント(メッセージ)一覧などは提示しません。
基本的な知識は他で勉強された方が良いでしょう。
また、変換の目的は演奏なので、演奏しやすいようにランニングステータスは補完します。
フォーマット0についても同一の処理を施す事になります。
最初は遅くなってしまいますが、他の処理が簡単になるメリットの方が大きいと判断しました。
演奏については次節で解説します。

■変換しなければならない理由

SMFにはフォーマット0/1/2がありますが、殆ど1です。
また、2は滅多に使われないので無視します。
フォーマット0と1の違いはトラックチャンクの個数です。
フォーマット0は一つのトラックチャンクしか持ちませんが、
フォーマット1は1〜65535個のトラックチャンクを持つ事ができます。
ただし、各トラックチャンクは一つのチャネルにしか対応する事ができません(フォーマット1の場合)
各トラックチャンクは楽器ごとに分割されている事が多く、
複数のトラックチャンクが一つのチャネルに対応している事もあります。

演奏する時は時間順に並んでいる必要があるのですが、
フォーマット1の場合は複数のトラックチャンクを一つに統合して時間順に再配置してやる必要があります。
そしてトラックチャンクを一つだけ持ち、時間順に配置されているのがフォーマット0です。
演奏するだけが目的なら不要な情報を省いたネイティブフォーマットを採用する事もできますが、
今回は全ての情報を残し、フォーマット0に従うものとします。

演奏するだけなら時間順に並んでいるフォーマット0の方が便利なのに、
わざわざ分割して保存している理由は何でしょうか?
それは編集のしやすさにあります。
楽器ごとに分割しておいた方が編集しやすいですよね。

■SMFの構造

SMFは一つのヘッダーチャンクと一つ、または複数のトラックチャンクから構成されます。

0 ヘッダーチャンク
+14 トラックチャンク
トラックチャンクが続く(フォーマット1)

二番目以降のトラックチャンクの開始アドレスは一番目のトラックチャンクのサイズに依存します。

ヘッダーチャンクの構造は次表の通りです。

0 1 2 3 4 5 6 7 8 9 A B C D
"MThd" 0006 format nTracks division

0006 は format , nTracks , division の合計バイト数です。
 現在は6に固定されていますが、将来どうなるかは保証されていません。
format はフォーマットの番号です。
nTracks はトラックチャンクの個数です。
division は時間単位です。詳しくは後述します。

各数値はビッグエンディアンで記述されている事に注意して下さい。

トラックチャンクの構造は次表の通りです。

0 1 2 3 4 5 6 7 8 〜
"MTrk" データサイズ データセクション

データサイズはデータセクションのバイト数です。
 トラックチャンクのバイト数は「データサイズ+8」です。

データセクションに格納されているデータは
デルタタイムとイベントの組み合わせを一つの単位として、これが数珠繋ぎになったものです。

【デルタタイム】【イベント】【デルタタイム】【イベント】【デルタタイム】【イベント】……

デルタタイムは一つ前のイベントから現在のイベントまでの相対時間です。
デルタタイム、イベントについて詳しくは後述します。

■SmfFormatクラス

SMFのフォーマット1からフォーマット0への変換処理を SmfFormat クラスにまとめました。
各メソッドの解説は順次していきますが、メンバ変数についてはきちんと頭に入れておいて下さい。

BYTE *m_pMidi はMIDIの先頭アドレスです。
 変換が完了したデータを格納する新たなメモリ領域の先頭アドレスです。
doube m_endTime は終了時刻(秒単位)です。
 終了時刻は SmfFormat クラスには不要なものなので、取得するメソッドを用意しませんでしたが、
 ここで計算しておいた方が簡単なので、計算だけして m_endTime メンバ変数に格納しておきます。
 m_endTime の値を取得するメソッドは SmfFormat クラスを継承するクラスが備える必要があります。

MIDI 構造体は絶対時間とイベント、そして次のリストへのポインタを持つ線形リストです。
変換の過程で使用しますが、詳しくは後述します。

class SmfFormat{
    // メソッド
    MIDI** ConvertStage1(BYTE *pData,DWORD *nSumEvent);
    MIDI** ConvertStage2(BYTE *pData,MIDI **midiList,DWORD nSumEvent);
    BYTE* ConvertStage3(BYTE *pData,MIDI **midiArray,DWORD nSumEvent);
    BYTE* Convert(BYTE *pData);    // フォーマット1をフォーマット0(ランニングステータス補完)に変換する
    BYTE* Initialize(BYTE *pData);
    void ComputeEndTime(BYTE *pData,MIDI **midiArray,DWORD nSumEvent);
protected:
    // メンバ変数
    BYTE *m_pMidi;    // MIDIの先頭アドレス
    double m_endTime;    // 終了時刻(秒単位)
    // メソッド
    BYTE Encode(BYTE *variable,DWORD fixed);
    BYTE Decode(BYTE *variable,DWORD *fixed);
public:
    SmfFormat(char *rscName,char *rscType);    // リソース
    SmfFormat(char *fileName);                 // ファイル
    SmfFormat(BYTE *pData);                    // データ(SMFのデータ形式に従っている必要があります)
    ~SmfFormat(void);
    void CreateSMF(char *fileName);    // 標準MIDIファイル(SMF)を作成する(ランニングステータスを補完したフォーマット0)
                                       // 拡張子も付けて下さい
};

■SMFの読み込みと初期化

SMFはリソースから読み込む場合を主に想定しています。
というのも、MCIはファイルからの読み込みにしか対応していないからです。
SmfFormatクラスは勿論、ファイルやデータからの読み込みにも対応していますが、
ファイルならMCIを使った方が良いでしょう。
データの場合は暗号化したファイルを読み込んで、復号化したデータを渡すという特殊な用途にも使えます。
ただしデータはSMFの構造に従っている必要がある事に注意して下さい。

// リソース
SmfFormat::SmfFormat(char *rscName,char *rscType)
{
    m_pMidi=NULL;

    HRSRC hrs=FindResource(NULL,rscName,rscType);
    if(hrs==NULL){
        MessageBox(NULL,"失敗しました\n指定したリソースは存在しません",rscName,MB_OK);
        return;
    }
    HGLOBAL hg=LoadResource(NULL,hrs);
    BYTE *pData=(BYTE*)LockResource(hg);

    BYTE *p=Initialize(pData);
    if(p) m_pMidi=p;    // 作成したデータを指す
}

// ファイル
SmfFormat::SmfFormat(char *fileName)
{
    m_pMidi=NULL;

    HANDLE fh=CreateFile(fileName,GENERIC_READ,0,NULL,
        OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if(fh==INVALID_HANDLE_VALUE){
        MessageBox(NULL,"失敗しました\nファイルが開けません",fileName,MB_OK);
        return;
    }
    DWORD fileSize=GetFileSize(fh,NULL);
    BYTE *pData=(BYTE*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,fileSize);
    DWORD readSize;
    ReadFile(fh,pData,fileSize,&readSize,NULL);
    CloseHandle(fh);

    BYTE *p=Initialize(pData);
    if(p) m_pMidi=p;    // 作成したデータを指す
    HeapFree(GetProcessHeap(),0,pData);
}

// データ(SMFのデータ形式に従っている必要があります)
SmfFormat::SmfFormat(BYTE *pData)
{
    m_pMidi=NULL;

    BYTE *p=Initialize(pData);
    if(p) m_pMidi=p;    // 作成したデータを指す
}

// データチェックと初期化
BYTE* SmfFormat::Initialize(BYTE *pData)
{
    if(strncmp((char*)pData,"MThd",4)){
        MessageBox(NULL,"標準MIDI(SMF)を指定して下さい","非対応データ",MB_OK);
        return NULL;
    }
    DWORD dataSize=(pData[4]<<24)+(pData[5]<<16)+(pData[6]<<8)+pData[7];
    if(dataSize!=6){
        MessageBox(NULL,"標準MIDI(SMF)を指定して下さい","非対応データ",MB_OK);
        return NULL;
    }
    WORD format=(pData[8]<<8)+pData[10];
    if(format==2){
        MessageBox(NULL,"指定したMIDIはフォーマット2です","非対応フォーマット",MB_OK);
        return NULL;
    }else if(format>2){
        MessageBox(NULL,"フォーマット0または1のみ対応しています","非対応フォーマット",MB_OK);
        return NULL;
    }
    if(strncmp((char*)&pData[14],"MTrk",4)){
        MessageBox(NULL,"標準MIDI(SMF)を指定して下さい","非対応データ",MB_OK);
        return NULL;
    }

    return Convert(pData);    // ランニングステータスを補完したフォーマット0データを作成する
}

Initialize メソッドでは様々なデータチェックを施して、全て合格した場合に Convert メソッドを呼び出します。
Convert メソッドは成功した場合は新たに作成した(変換した)データのアドレスを返します。
失敗した場合は NULL を返します。
そして、m_pMidi メンバ変数に作成した(変換した)データのアドレス、
または NULL を指定する事で、以降の処理に用います。
変換前のリソースやファイル、データは直ちに解放する事ができます。

■変換

変換処理は三段階から構成されます。

1.相対時間から絶対時間に変換し、ランニングステータスを補完する
  変換したデータは線形リストに格納します。

2.時間の早い順にポインタ配列で線形リストの各リストを指す
  時間順に並び替える必要がありますが、線形リスト(それも複数ある)のソートは困難なので、
  ポインタ配列で時間の早い順に各リストのアドレスを格納していきます。

3.絶対時間から相対時間に変換し、フォーマット0に変換する
  時間順に並び替えられたデータを元にフォーマット0に変換します。

これらの処理は本来一つですが、
あまりにプログラムが長くなってしまったので、分割しただけです。

// フォーマット1をフォーマット0(ランニングステータス補完)に変換する
// BYTE *pData 変換前のデータのアドレス
// 戻り値は変換後のデータのアドレス
BYTE* SmfFormat::Convert(BYTE *pData)
{
    WORD nTrack=(pData[10]<<8)+pData[11];    // 変換前のトラック数

    // 相対時間から絶対時間に変換し、ランニングステータスを補完する
    DWORD nSumEvent;
    MIDI **midiList=ConvertStage1(pData,&nSumEvent);

    // 時間の早い順にポインタ配列で線形リストの各リストを指す
    MIDI **midiArray=ConvertStage2(pData,midiList,nSumEvent);

    // 絶対時間から相対時間に変換し、フォーマット0に変換する
    BYTE *pSmf0=ConvertStage3(pData,midiArray,nSumEvent);

    // 不要になったバッファを解放する
    for(DWORD k=0;k<nTrack;k++){
        MIDI *pMidi=midiList[k];    // 線形リストの先頭から解放していく
        while(pMidi!=NULL){
            MIDI *pNext=pMidi->pNext;    // 次のリストのアドレスを一時保存
            free(pMidi);
            pMidi=pNext;
        }
    }
    if(midiList) free(midiList);
    if(midiArray) free(midiArray);    // ポインタ配列は線形リストを指している

    return pSmf0;
}

線形リストを用いる理由はイベントの長さが可変だからです。
そして、それを格納する為には各要素の長さが可変の配列を実現する必要があります。
実際にはそんな配列は存在しないので、線形リストを用いる事になるというわけです。

線形リストの構築には MIDI 構造体を用います。

// 線形リストを構築する
typedef struct tagMIDI{
    tagMIDI *pNext;     // 次のリスト
    DWORD dwSumTime;    // 絶対時間
    BYTE dwEvent[1];    // 可変長に確保する
}MIDI;

tagMIDI *pNext は次のリストのアドレスです。
DWORD dwSumTime は絶対時間です。
 SMFには相対時間で記録されていますが、
 それではソートのしようがないので絶対時間を順次計算していきます。
BYTE dwEvent[1] はイベントです。
 イベントの長さに合わせて可変長に確保します。
 sizeof(MIDI) は正しい値が得られるとは限りません。

■可変長数値のデコード

MIDIには可変長数値が多く登場します。
これはデータ容量を減らす為ですが、受信側ではデコードするする必要があります。

可変長数値は各バイトの最上位ビットをフラグとして使用しています。
最上位ビットが1であれば、次のバイトも有効、
最上位ビットが0であれば、そのバイトで終了
である事を表しています。
ただし最大4バイトです。
ここで、MIDIはビックエンディアンである事を忘れないで下さい。
デコードする時は最上位ビットを調べつつ、下位7ビットの数値を連結していきます。

また、今度の為にも何バイト読み進めたかは重要な情報なので、戻り値として返しましょう。

// 可変長から固定長数値に変換
BYTE SmfFormat::Decode(BYTE *variable,DWORD *fixed)
{
    (*fixed)=0;
    for(BYTE k=0;;k++){    // 最大4バイト
        if(k>=4) return 0;    // Error
        (*fixed)+=(variable[k] & 0x7F);
        if(!(variable[k] & 0x80)) break;
        (*fixed)<<=7;
    }
    return k+1;    // 可変長数値のバイト数
}

■可変長数値のエンコード

エンコードはデコードと全く逆の事をすれば良いだけです。
次のバイトも有効である場合は最上位ビットに1をセットする事を忘れないで下さい。
ここでも何バイトに格納したかは重要な情報なので、戻り値として返しましょう。

// 固定長から可変長数値に変換
BYTE SmfFormat::Encode(BYTE *variable,DWORD fixed)
{
    if(fixed<128){
        variable[0]=(BYTE)fixed;
        return 1;    // 戻り値は可変長数値のバイト数
    }else if(fixed<16384){
        variable[0]=(BYTE)((fixed>>7) | 0x80);
        variable[1]=(BYTE)( fixed & 0x7F);
        return 2;
    }else if(fixed<2097152){
        variable[0]=(BYTE)((fixed>>14) | 0x80);
        variable[1]=(BYTE)((fixed>> 7) | 0x80);
        variable[2]=(BYTE)( fixed & 0x7F);
        return 3;
    }else if(fixed<268435456){    // 最大値 268435455
        variable[0]=(BYTE)((fixed>>21) | 0x80);
        variable[1]=(BYTE)((fixed>>14) | 0x80);
        variable[2]=(BYTE)((fixed>> 7) | 0x80);
        variable[3]=(BYTE)( fixed & 0x7F);
        return 4;    // 最大4バイト
    }
    return 0;    // Error
}

■データセクション

SMFにはMIDIイベント、システムエクスクルーシブイベント、メタイベントの三種類のイベントが記録されています。
各イベントの一覧などは提示しないとして、重要なのはその長さです。

○MIDIイベント

MIDIイベントは1バイトのステータスと1バイトまたは2バイトのデータ(従属情報)から構成されています。

ステータス データ データ
1バイト 1バイト 1バイト

ステータスは 0x8n 〜 0xEn です。
n はチャネル番号です。
0xCn と 0xDn は1バイトのデータを、
それ以外は2バイトのデータを用います。

○システムエクスクルーシブイベント

システムエクスクルーシブイベントは 0xF0 または 0xF7 のステータスを持ちます。

0xF0 イベントサイズ イベントデータ 0xF7
1バイト 可変長 可変長 1バイト

イベントサイズはイベントデータと0xF7の合計バイト数です。
0xF7 はシステムエクスクルーシブイベントの終わりを表します。

0xF7 イベントサイズ イベントデータ
1バイト 可変長 可変長

イベントサイズはイベントデータのバイト数です。

このように二種類あるので、イベントの終端はイベントサイズより計算します。

○メタイベント

メタイベントはリアルタイムの世界には無いファイル固有の情報です。

0xFF イベントタイプ イベントサイズ イベントデータ
1バイト 1バイト 可変長 可変長

メタイベントは演奏に必要ない情報も含みますが、
ここでは変換処理に専念する事にして、全てのイベントを残します。
ただし、End of Track イベント( 0xFF2F00 )はトラック統合により一つだけになるので、
一番時間の遅いイベントだけを残します。
一番遅いと言っても、他のイベントの一番遅い時間が End of Track イベントの時間と
一致するとは限らない事に注意して下さい。

ステータスは以前のステータスと同じ場合は省略されて記録される事があります
これをランニングステータスと言います。
複数のトラックチャンクが存在する場合は、各トラックチャンクにおいて、
以前のステータスと同じなら省略する事もできる、という事になります。
これは一つのトラックチャンクに統合する際にかなりやっかいな問題です。
時間順に並び替えたら順番が変わってしまい、省略できなくなってしまう事が多々あるからです。
従って、ランニングステータスは補完してしまいます。
もし省略したい場合は一度補完した後に再び省略するという手順で処理するのが簡単でしょう。

省略されているかどうかはステータスがあるはずのバイトの最上位ビットを調べればわかります。
ステータスの最上位ビットは1、データの最上位ビットは0と決まっているからです。
ステータスがあるはずのバイトで最上位ビットが0なら省略されていると判断できます。
この時、ステータスの有無によって次に読むべき場所が変わる事に注意して下さい。

■相対時間から絶対時間に変換し、ランニングステータスを補完する

変換したデータは線形リストに格納していきます。
線形リストはトラックチャンクの個数だけ用意します
これを配列として表現すれば、要素一つが線形リスト一つに相当します。

各線形リストの先頭はダミーです。
最初はイベントサイズが不明なので、とりあえず最小サイズのリストを作成しておき、
次のリストからイベントサイズに一致するメモリ領域を確保していきます。

線形リストは一方通行なので、元のポインタに上書きしてはいけません
先頭のアドレス情報が失われ、後で困る事になります。

各線形リストの最後は次のリストが NULL である事で判断します。

一番時間の遅い End of Track イベントは最後の線形リストの末尾に追加します。
最後の線形リストにする理由は、
ソートにおいて時間が同じなら番号の小さいトラックチャンクを優先する仕様にしているからです。
End of Track イベントはトラックチャンクの最後になくてはならないので、その為の仕掛けというわけです。

イベントの数も重要な情報です。
いちいち有効な線形リストの個数を数えるのは面倒なので、
ここで計算しておき、この値を後々まで用います。

メソッドの戻り値は線形リスト配列のアドレスです。

// 相対時間から絶対時間に変換し、ランニングステータスを補完する
// BYTE *pData 変換前のデータのアドレス
// DWORD *nSumEvent イベントの数
MIDI** SmfFormat::ConvertStage1(BYTE *pData,DWORD *nSumEvent)
{
    BYTE *p=pData;
    WORD nTrack=(p[10]<<8)+p[11];

    MIDI **midiList=(MIDI**)calloc(nTrack,sizeof(MIDI*));    // 線形リスト
    for(DWORD k=0;k<nTrack;k++){    // 先頭のバッファ確保(先頭はダミー)
        midiList[k]=(MIDI*)calloc(1,sizeof(MIDI));
    }

    (*nSumEvent)=0;

    DWORD offset=14;        // MIDIの先頭からのバイト数
    DWORD lastEndTime=0;    // 一番遅い End of Track の時間

    for(k=0;k<nTrack;k++){
        MIDI *pMidi=midiList[k];
        offset+=8;          // チャンクタイプとデータサイズを読み飛ばす
        DWORD sumTime=0;    // 各トラックの絶対時間
        BYTE befSta=0;      // 各トラックの一つ前のイベント
        while(1){
            DWORD deltaTime;    // 相対時間
            offset+=Decode(p+offset,&deltaTime);
            sumTime+=deltaTime;

            BYTE status;
            if(!(p[offset] & 0x80)){    // 真ならステータスが省略されている
                status=befSta;    // ステータスを補完
            }else{
                status=p[offset];
                offset++;    // ステータスのバイト数だけ進める
            }
            befSta=status;

            // p+offset はステータスの次を指している事に注意

            DWORD dataSize,len=0;    // len はステータスを含めないバイト数
            if(status==0xFF){    // メタイベント
                if(p[offset]==0x2F && p[offset+1]==0x00){    // End of Track
                    if(sumTime>lastEndTime) lastEndTime=sumTime;    // 一番遅い End of Track の時間
                    pMidi->pNext=NULL;    // 各トラックの最後
                    offset+=2;    // ステータスを含めないイベントのバイト数だけ進める
                    break;    // while
                }else{    // 他のメタイベント
                    len=1;    // イベントタイプのバイト数だけ進める
                    len+=Decode(p+offset+1,&dataSize);    // イベントサイズ(可変長)のバイト数だけ進める
                    len+=dataSize;    // イベントデータ(可変長)のバイト数だけ進める
                }
            }else if(status==0xF0 || status==0xF7){    // システムエクスクルーシブイベント
                len=Decode(p+offset,&dataSize);    // イベントサイズ(可変長)のバイト数だけ進める
                len+=dataSize;    // イベントデータ(可変長)のバイト数だけ進める
            }else{    // MIDIイベント
                if((status & 0xF0)==0xC0 || (status & 0xF0)==0xD0) len=1;
                else len=2;
            }
            pMidi->pNext=(MIDI*)malloc(sizeof(MIDI)+len);
            pMidi=pMidi->pNext;    // 線形リストを一つ進める
            pMidi->dwSumTime=sumTime;
            pMidi->dwEvent[0]=status;
            memcpy(&pMidi->dwEvent[1],p+offset,len);

            (*nSumEvent)++;
            offset+=len;    // ステータスを含めないイベントのバイト数だけ進める
        }
    }
    // 一番時間が遅い End of Track を最後のトラックに書き込む
    for(MIDI *pMidi=midiList[nTrack-1];pMidi->pNext!=NULL;pMidi=pMidi->pNext);    // トラックの最後を探す
    pMidi->pNext=(MIDI*)malloc(sizeof(MIDI)+2);
    pMidi=pMidi->pNext;
    pMidi->dwSumTime=lastEndTime;
    pMidi->dwEvent[0]=0xFF;
    pMidi->dwEvent[1]=0x2F;
    pMidi->dwEvent[2]=0x00;
    pMidi->pNext=NULL;    // 新しい(トラックの)最後
    (*nSumEvent)++;

    return midiList;
}

■時間の早い順にポインタ配列で線形リストの各リストを指す

複数の線形リストから時間の早い順に選択し、統合するには
全ての線形リストの先頭から参照して、現在のリストの中から一番時間の早いものを選び、
選ばれた線形リストだけを次に進める
事を繰り返します。

選ばれた線形リストの現在のリストのアドレスはポインタ配列に格納します。
ポインタ配列には時間の早い順にリストのアドレスを格納していきます
ただし線形リストの先頭にあるダミーリストは最初から除外してしまいます。
ポインタ配列には有効なリストのみを格納していくのです。
完成後は配列要素の先頭から参照する事で、時間の早い順に各リストにアクセスできます。

線形リストの最後には注意して下さい。
現在のリストのアドレスが NULL なら、その線形リストの全てのリストを参照し終わった事を表しています。
ただし他の線形リストはまだ参照し終わってないかもしれないので、
参照し終わった線形リストを除外しつつ参照を続けます

時間が同じ場合は番号の小さいトラックチャンクを優先します。
End of Track イベントは最も番号の大きいトラックチャンクの末尾にあるので、
自動的に最後に配置される事になります。

ここでの処理が完了すれば時間順に参照できるので、終了時刻も計算してしまいましょう。
終了時刻の計算については後述します。

// 時間の早い順にポインタ配列で線形リストの各リストを指す
// MIDI **midiList ConvertStage1メソッドの戻り値
MIDI** SmfFormat::ConvertStage2(BYTE *pData,MIDI **midiList,DWORD nSumEvent)
{
    WORD nTrack=(pData[10]<<8)+pData[11];

    MIDI **list=(MIDI**)calloc(nTrack,sizeof(MIDI*));
    for(DWORD n=0;n<nTrack;n++){
        list[n]=midiList[n]->pNext;    // 線形リストの先頭はダミー
    }
    // 線形リストの有効な各要素を指すポインタ配列
    MIDI **midiArray=(MIDI**)calloc(nSumEvent,sizeof(MIDI*));

    for(DWORD k=0;k<nSumEvent;k++){
        for(DWORD min=0;min<nTrack && list[min]==NULL;min++);    // 有効な一番時間が早いイベントを探す
        if(min==nTrack) continue;    // 全てのイベントを参照し終わった(k==nSumEvent)
        for(n=min+1;n<nTrack;n++){
            if(list[n]==NULL) continue;
            if(list[n]->dwSumTime < list[min]->dwSumTime) min=n;
        }
        midiArray[k]=list[min];
        list[min]=list[min]->pNext;    // 選択されたトラックの線形リストを次に進める
    }
    free(list);

    ComputeEndTime(pData,midiArray,nSumEvent);    // 終了時刻(秒単位)を計算する

    return midiArray;
}

■絶対時間から相対時間に変換し、フォーマット0データを作成する

絶対時間から相対時間に変換し、さらに可変長数値へと変換します。
そして相対時間(デルタタイム)とイベントの数珠繋ぎを一つの新たなメモリ領域に作成していきます。

【デルタタイム】【イベント】【デルタタイム】【イベント】【デルタタイム】【イベント】……

相対時間は一つ前から現在までの時間です。

新たなメモリ領域は最終的なサイズが不明なので、逐次拡張していきます。
realloc 関数は別の領域に割り当てる事もあるので注意して下さい。
戻り値が新しい領域のアドレスなので、必ず戻り値を受け取って下さい。

// 絶対時間から相対時間に変換し、フォーマット0データを作成する
// MIDI **midiArray ConvertStage2メソッドの戻り値
BYTE* SmfFormat::ConvertStage3(BYTE *pData,MIDI **midiArray,DWORD nSumEvent)
{
    BYTE *midi0=(BYTE*)malloc(22);
    BYTE info[18]={'M','T','h','d',0,0,0,6,0,0,0,1,pData[12],pData[13],'M','T','r','k'};
    memcpy(midi0,info,18);

    DWORD written=22;    // フォーマット0データのバイト数
    DWORD befTime=0;     // 一つ前のイベントの絶対時間

    for(DWORD k=0;k<nSumEvent;k++){
        DWORD deltaTime=midiArray[k]->dwSumTime - befTime;
        befTime=midiArray[k]->dwSumTime;

        BYTE varlenTime[4];
        BYTE num=Encode(varlenTime,deltaTime);
        midi0=(BYTE*)realloc(midi0,written+num);
        memcpy(midi0+written,varlenTime,num);    // デルタタイム
        written+=num;

        BYTE status=midiArray[k]->dwEvent[0];
        DWORD eventSize,len;
        if(status==0xFF){    // メタイベント
            num=Decode(&midiArray[k]->dwEvent[2],&len);
            eventSize=2+num+len;
        }else if(status==0xF0 || status==0xF7){    // システムエクスクルーシブイベント
            num=Decode(&midiArray[k]->dwEvent[1],&len);
            eventSize=1+num+len;
        }else{    // MIDIイベント
            if((status & 0xF0)==0xC0 || (status & 0xF0)==0xD0) eventSize=2;
            else eventSize=3;
        }
        midi0=(BYTE*)realloc(midi0,written+eventSize);
        memcpy(midi0+written,midiArray[k]->dwEvent,eventSize);    // イベント
        written+=eventSize;
    }
    DWORD trackDataSize=written-22;    // トラックチャンクのデータサイズ
    midi0[18]=(BYTE)(trackDataSize>>24);
    midi0[19]=(BYTE)(trackDataSize>>16);
    midi0[20]=(BYTE)(trackDataSize>> 8);
    midi0[21]=(BYTE) trackDataSize;

    return midi0;
}

■ファイルに書き出す

作成した(変換した)データはSMF(フォーマット0)そのものなので、
全てのデータを書き出すだけで正しいSMFを作成できます。
ファイル容量を節約したい場合はランニングステータスを利用しても構いませんが、
たかだか数KBの節約にどれほどの価値があるのか疑問ではあります。

// 標準MIDIファイル(SMF)を作成する(ランニングステータスを補完したフォーマット0)
// char *fileName 拡張子も付けて下さい
void SmfFormat::CreateSMF(char *fileName)
{
    if(m_pMidi==NULL) return;    // MIDIの読み込みに失敗している

    DWORD nByte=22;    // MIDIのバイト数
    nByte+=(m_pMidi[18]<<24)+(m_pMidi[19]<<16)+(m_pMidi[20]<<8)+m_pMidi[21];

    HANDLE fh=CreateFile(fileName,GENERIC_WRITE,0,NULL,
        CREATE_NEW,FILE_ATTRIBUTE_NORMAL,NULL);
    if(fh==INVALID_HANDLE_VALUE) MessageBox(NULL,"失敗しました\n同名のファイルが存在します",fileName,MB_OK);

    DWORD dwWriteSize;
    WriteFile(fh,m_pMidi,nByte,&dwWriteSize,NULL);
    CloseHandle(fh);
}

■終了時刻(秒単位)を計算する

SMFの時間単位はヘッダーチャンクの division で決まります。
最上位ビットが0ならチック、1なら秒です。
基本的にはチックであり、秒は滅多に使われないので無視します。

ここではチックを秒に変換する方法を解説します。

チックを秒に変換するには division とテンポが関係します。
division の最上位ビットが0(チック単位)の場合は
下位15ビットに「チック / 4分音符」が記録されています。

テンポは「マイクロ秒 / 4分音符」です。
テンポはセットテンポイベントに記録されており、セットテンポイベントを処理する事で更新されていきます。
有効になるのはセットテンポイベントが出現した以降です。
また、初期値として時刻 0 の位置に置く必要がありますが、
一つも指定されなかった場合、シーケンサは 500000 を仮定します。

このメソッドは絶対時間が格納されている線形リストを
時間で昇順にソートしたポインタ配列が完成した以降に呼び出す事ができます。
次節で解説する演奏処理の時ではなく、変換処理の過程で終了時刻を計算するのは
せっかく計算した絶対時間の情報を有効活用する為です。
演奏の時点で終了時刻を計算しようとした場合は再び相対時間から絶対時間を計算しなければなりません。
また、その時は簡単な配列ではなく、一つのメモリ領域から
可変長のデータ群を適切に処理していかなければならずかなり面倒です。

チックを秒に変換する場合はテンポが更新される事に注意して下さい。
各テンポが有効な区間の秒を計算して、加算していくのです。
新しいセットテンポイベントが出現した時に、現在までに有効だったテンポは一つ前のテンポです。
従って、現在のテンポと絶対時間を保存しつつ次のセットテンポイベントを探していく事になります。
最後の時間は End of Track イベントが出現した時間です。
フォーマット1の場合は一番時間が遅い End of Track イベントの時間ですが、
フォーマット0の場合は一つしか存在しないので、その事を考慮する必要ありません。

変換式は
 相対時間 : チック
 現在のテンポ : マイクロ秒 / 4分音符
 division : チック / 4分音符
から導けます。

// 終了時刻(秒単位)を計算する
// ConvertStage2メソッド以降に呼び出して下さい
void SmfFormat::ComputeEndTime(BYTE *pData,MIDI **midiArray,DWORD nSumEvent)
{
    m_endTime=0;
    WORD division=(pData[12]<<8)+pData[13];

    DWORD befTempo=500000;    // 一つ前のテンポ(現在有効なテンポ)
                              // 500000 はセットテンポイベントが1つも指定されなかった場合の仮定値
    DWORD befTime=0;    // 一つ前のセットテンポイベントが発生した時の絶対チック
    for(DWORD k=0;k<nSumEvent;k++){
        MIDI *p=midiArray[k];
        DWORD deltaTime;
        if(p->dwEvent[0]==0xFF){
            if(p->dwEvent[1]==0x51 && p->dwEvent[2]==0x03){    // セットテンポイベント
                deltaTime=p->dwSumTime - befTime;
                m_endTime+=deltaTime*(befTempo/1000000.0)/division;    // 相対時間を加算していく
                befTempo=(p->dwEvent[3]<<16)+(p->dwEvent[4]<<8)+p->dwEvent[5];
                befTime=p->dwSumTime;
            }else if(p->dwEvent[1]==0x2F && p->dwEvent[2]==0x00){    // End of Track
                deltaTime=p->dwSumTime - befTime;
                m_endTime+=deltaTime*(befTempo/1000000.0)/division;    // 相対時間を加算していく
            }
        }
    }
    // deltaTime : チック
    // befTempo  : マイクロ秒 / 4分音符
    // division  : チック / 4分音符
}

■終了処理

デストラクタでは作成した(変換した)データを解放します。

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

    free(m_pMidi);
}

★☆ ダウンロード ☆★


戻る / ホーム