MIDIファイルから各イベントを解析し、ノートオンだけを取り出すソースコードを紹介しています。
このソースコードは、音屋FES【踊-Odori-】で使用した「MidTimeConv」での各MIDIトラック解析に使用しています。
[st-card id=201]
Contents
「MidTimeConv」について
「MidTimeConv」は任意のMIDIファイルを読み込み、それを解析することでノートオン情報だけを取得し、曲頭からそのノートオンまでの絶対時間を算出するアプリです。
ソースコード紹介
以下はMIDIファイルの情報からノートオンのみを抽出し、デルタタイムを算出、格納する関数です。プログラム中では、1トラックごとにこの関数を呼び出すつくりになっています。C言語とWin32APIを用いていますので、Visual StudioのExpress版に転用が可能です。参考になれば幸いです(一部、公開していない関数や変数を使っていますので、そこは補ってください)
//—————————————————————————————//
// トラックデータ取得
// HANDLE hFile : ファイルハンドル
// WORD wTrackNo : トラック番号
// WORD wTimeBase : タイムベース
// DWORD dwSeekTrkSize : 直前までのトラックサイズ
// DWORD *pdwTrackSize : 今回のトラックサイズ
//—————————————————————————————//
BOOL GetMidFileTrackData(HANDLE hFile, WORD wTrackNo, WORD wTimeBase, DWORD dwSeekTrkSize, DWORD *pdwTrackSize)
{
//1. ——————————————————-
// 各トラックのヘッダ読み取り
BYTE bufTrkHead[SIZE_MIDI_TRACK_HEADER] = {0}; // ヘッダ文字列
DWORD dwReadSize = 0; // 使わないとしても、読み取ったサイズは受け取らなくてはならない
// bufに読み込む
::SetFilePointer( hFile, dwSeekTrkSize, 0, FILE_BEGIN );
::ReadFile( hFile, bufTrkHead, sizeof(bufTrkHead), &dwReadSize, NULL );
// ヘッダチェック
if( CheckMidiHeader( bufTrkHead, ‘M’, ‘T’, ‘r’, ‘k’ ) == FALSE ) {
return FALSE; // MIDIファイルフォーマットではないので終了
}
// 次のトラックのためにトラックサイズを書き出す
*pdwTrackSize = SIZE_MIDI_TRACK_HEADER + BYTODWORD(bufTrkHead[4], bufTrkHead[5], bufTrkHead[6], bufTrkHead[7]);
//データ本体の読み取り
//可変長変数
BYTE *bufTrk;
try {
bufTrk = new BYTE[*pdwTrackSize];
}
catch(bad_alloc) {
// newの割付に失敗した場合
return FALSE;
}
DWORD dwSeekFile = 0; // シーク用カウンタ
DWORD dwTempo = 0; // テンポ
DWORD dwData = 0; // データ
DWORD dwDeltaTime = 0;// デルタタイムは最大4バイトなのでDWORD型で扱う
WORD wNo = 0; // 配列の添え字
BYTE byRunState = 0;// ランニングステータス用
//2. ——————————————————-
::SetFilePointer( hFile, dwSeekTrkSize + SIZE_MIDI_TRACK_HEADER, 0, FILE_BEGIN );
::ReadFile( hFile, bufTrk, *pdwTrackSize, &dwReadSize, NULL );
// ループしてデータを取り込んでいく
while( dwSeekFile < dwReadSize ) {
//前のイベントからデルタタイム分だけ待って次のイベントに進む
//デルタタイムが0ならば同時(和音)ということ
for( int i = 0; i < MAX_DELTA_SIZE; i++ ) {
// デルタタイムの長さ
dwData = (( dwData ) << 7 ) | ( bufTrk[dwSeekFile] & 0x7F );
// 7ビット目がゼロならばデルタタイムはそこで終わり
// デルタタイムは最大4バイト
if(( bufTrk[dwSeekFile] & 0x80 ) == 0x00 ) {
dwSeekFile++;
break;
}
// 次の位置に進める
dwSeekFile++;
}
// 先頭からの位置
dwDeltaTime = dwDeltaTime + dwData;
//3. ——————————————————-
// デルタタイムの次のデータをチェック
if( bufTrk[dwSeekFile] < 0x80 ) {
// ランニングステータス
if(( byRunState > 0xC0) && ( byRunState > 0xDF )) {
// プログラムチェンジとチャンネルプレッシャーは2byte
dwSeekFile = dwSeekFile + 1;
}
else {
// それ以外は3byte
dwSeekFile = dwSeekFile + 2;
}
}
//4. ——————————————————-
else if(( bufTrk[dwSeekFile] >= 0x80 ) && ( bufTrk[dwSeekFile] <= 0x8F )) {
// ノートオフ情報(3byte)
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + 3; // 次に進める
}
else if(( bufTrk[dwSeekFile] >= 0x90 ) && ( bufTrk[dwSeekFile] <= 0x9F )) {
// dwSeekFile + 2がゼロならノートオフ
// ノートオン情報
// 9n kk vv (3byte)
if( bufTrk[dwSeekFile + 2] != 0x00 ) {
// メモリ破壊につながらないようにデータの幅チェック
wNo = (wNo > MAX_BUF_SIZE) ? MAX_BUF_SIZE : wNo;
// 各トラックの先頭から順番にデルタタイムを入れる
s_dwTimeBaseList[wTrackNo][wNo] = dwDeltaTime;
// ノートのキー
s_dwKeyList[wTrackNo][wNo] = bufTrk[dwSeekFile + 1] % 12;
// トラックのデータ数
s_dwTrkDataNum [wTrackNo]++;
wNo++; // 次の箱へ
}
dwSeekFile = dwSeekFile + 3;
}
//5. ——————————————————-
else if(( bufTrk[dwSeekFile] >= 0xA0 ) && ( bufTrk[dwSeekFile] <= 0xAF )) {
// ポリフォニックキープレッシャー
// An kk vv (3byte)
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + 3;
}
else if(( bufTrk[dwSeekFile] >= 0xB0 ) && ( bufTrk[dwSeekFile] <= 0xBF )) {
// コントロールチェンジ
// Bn cc vv (3byte)
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + 3;
}
else if(( bufTrk[dwSeekFile] >= 0xC0 ) && ( bufTrk[dwSeekFile] <= 0xCF )) {
// プログラムチェンジ情報
// Cn pp (2byte)
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + 2;
}
else if(( bufTrk[dwSeekFile] >= 0xD0 ) && ( bufTrk[dwSeekFile] <= 0xDF )) {
// チャンネルプレッシャー情報
// Dn vv (2byte)
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + 2;
}
else if(( bufTrk[dwSeekFile] >= 0xE0 ) && ( bufTrk[dwSeekFile] <= 0xEF )) {
// ピッチベンド情報
// En mm ll (3byte)
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + 3;
}
else if(( bufTrk[dwSeekFile] == 0xF0 ) || ( bufTrk[dwSeekFile] == 0xF7 )) {
// SysExイベント
// データの最後は必ずF7で終了する
byRunState = bufTrk[dwSeekFile]; // ランニングステータス用
dwSeekFile = dwSeekFile + bufTrk[dwSeekFile + 1] + 1;
}
//6. ——————————————————-
else if( bufTrk[dwSeekFile] == 0xFF ) {
// メタデータ
// メタデータ(0xFF)は「0xFF イベント データの長さ 実際のデータ」の順で並んでいる
//——————————————————-
// FF 00 02 ssss シーケンス番号
// FF 01 len text テキスト
// FF 02 len text 著作権表示
// FF 03 len text シーケンス名(曲タイトル)・トラック名
// FF 04 len text 楽器名
// FF 05 len text 歌詞
// FF 06 len text マーカー
// FF 07 len text キューポイント
// FF 08 len text プログラム名 (音色名) RP-019
// FF 09 len text デバイス名 (音源名) RP-019
// FF 20 01 cc MIDIチャンネルプリフィックス v1.0
// FF 21 01 pp ポート指定 SMF非標準
// FF 2F 00 — トラック終端
// FF 51 03 tttttt テンポ設定
// FF 54 05 hr mn se fr ff SMPTEオフセット
// FF 58 04 nn dd cc bb 拍子の設定
// FF 59 02 sf mi 調の設定
// FF 7F len data シーケンサ特定メタイベント
//——————————————————-
// FFの次にあるデータを見てイベントの種類が何かを調べる
switch(bufTrk[dwSeekFile + 1]) {
case META_FILE_END:
// 2Fの場合→ファイルの終わり FF 2F 00
// newの配列を消すのを忘れない
delete bufTrk;
return TRUE;
case META_TEMPO:
// 51の場合→テンポ情報 FF 51 03 tt tt tt
// 時間あたりの拍数ではなく1拍あたりの時間でテンポを表現
// 4分音符の長さ(マイクロ秒単位)
TempoList[s_nTempoCnt].dwTempo = bufTrk[dwSeekFile + 3] * 256 * 256 + bufTrk[dwSeekFile + 4] * 256 + bufTrk[dwSeekFile + 5];
// 先頭からのタイムベース
TempoList[s_nTempoCnt].dwTimeBase = dwDeltaTime;
// 末端には適当な最大値を入れておく
TempoList[s_nTempoCnt + 1].dwTimeBase = 999999999;
// テンポ情報の回数
s_nTempoCnt++;
// 先に進める
dwSeekFile += SIZE_META_TEMPO;
break;
default:
// ほかは今回は必要ないのでデータの分だけ先に進める
// 後続するデータサイズとFF・命令の分だけ加算
// データ長が0だったらその分1を加算
dwSeekFile = dwSeekFile + bufTrk[dwSeekFile + 2] + 2 + 1;
BYTE bys = bufTrk[dwSeekFile];
break;
}
}
dwData = 0; // 使ったら片付ける
}
// newの配列を消すのを忘れない
delete bufTrk;
return TRUE;
}
ソースコード解説
ソースコード解説 1. 冒頭部分
まず、関数先頭から、whileのループまでは、トラックのチェックとデータ読み取りの準備をしています。MIDIファイル自体は関数外で開いており、この関数にはファイルハンドルだけを渡しています。MIDIファイルの各トラックは、先頭4バイトに’M”t”r”k’の文字があり、次の4バイトでトラックのバイトサイズであるため、これらの値をチェックして、MIDIファイルとしての整合性チェックとデータ読み取り用の配列準備を行っています。なお、本解説ではMIDIファイルフォーマットについて扱わないので、後述の参考サイト様を参照してください。
次に、whileのループの中では、まずデルタタイムを読み取り、次にMIDIのパラメータを読み取りつつ、シークしていくという方法をとっています。この部分について、以下に述べます。
ソースコード解説 2. デルタタイム
デルタタイムは、ノートオンなどのあるイベントから次のイベントまでの相対的な間隔を示す方法で、最長4バイトのデータ長を持ちます。各1バイトのうち、最上位の7bit目が、次のバイトにデルタタイムが続くか否かを示しており、他7bitが実際のデルタタイムの長さを示します。デルタタイムとタイムベースとテンポの値を使用すれば、音符間の絶対的な時間を算出できます。
今回の例では、for文内の最初の処理でデルタタイムを加算し、その後デルタタイムの最上位ビットを検証し、次に続かないならばループを抜ける、という処理にしています。
ソースコード解説 3. ランニングステータスとは
同じイベントが連続する場合には、ステータスバイトを省略することができ、これをランニングステータスと呼びます。ここでは、ノートオンの情報だけが欲しい為、ランニングステータスについては、単にシークするだけとしています。なお、ランニングステータスのパラメータは、前のループ時にステータス記録用変数byRunStateに記録し、それと比較するという方法をとっています。
ソースコード解説 4. ノートオン情報
今回の関数では、ノートオン情報が欲しいため、ノートオンを見つけたら、それをスタティックな配列に格納しています。s_dwTimeBaseList[wTrackNo][wNo] がデルタタイム、s_dwKeyList[wTrackNo][wNo] が音階、そしてs_dwTrkDataNum [wTrackNo]が本トラックでのデータ数を管理しています。
ソースコード解説 5. ポリフォニックキープレッシャー~SysExイベント
上述の通り、ノートオン以外のデータは不要な為、各ステータスに応じてバッファをシークさせています。
ソースコード解説 6. メタデータ
メタデータの中にはテンポとトラックの終わりに関する情報も含まれる為、これらのデータを見逃さないようにします。テンポは、一般的に四分音符で示される形式ではなく、1拍あたりの絶対時間になります。曲中でテンポの変わる曲は、テンポに関するメタデータが多数出現する為、配列で記録する必要があります。
トラックの終わりのデータに当たったら、そこで関数を抜けます。
ソースコードライセンス
転用、改変等ご自由に行いください。記事そのものの転載等はご遠慮ください。
コメント