はじめに
ここではスタンダードMIDIファイル(.smf)のフォーマットの説明と簡単な読込サンプルについて解説しています。
スタンダードMIDIファイルについては、「smfフォーマット」などで検索するとすでに他のサイトでも解説されていますので、簡単に説明します。
このサイトでは「わいやぎのwebページ」に記載されている内容をもとに作成しています。
スタンダードMIDIファイルのフォーマット
スタンダードMIDIファイルはRIFFファイル形式に似た形式をとっています。
RIFFファイル形式は、そのファイルに必要なデータをチャンクと呼ばれるブロックに分けて保存し、チャンクにFourCCと呼ばれる4Byteの識別子(ID)を付けて管理するファイル形式です。
RIFFファイルについては下記に簡単に記載しております。スタンダードMIDIファイルは上記のRIFFファイル形式に似た形式のファイルのため、必要に応じてご参照ください。
今回作成するスタンダードMIDIファイルの読み込み関数のサンプルはこちらからダウンロードできます。 → riffsample_20231210
※スタンダードMIDIファイル以外にもwavファイルやSoundfontファイル読込サンプルなども同梱されています。
スタンダードMIDIファイルのチャンク
スタンダードMIDIファイルのチャンクと階層構造は下記となっています。各チャンクの詳細は後述します。
2つの必須チャンクがあります。
なお、スタンダードMIDIファイルはRIFFチャンクから始まっていないため、厳密にはRIFF形式のファイルではありません。
チャンク名 | タイプ | 必須 /任意 |
チャンク識別子 (ファイル/LIST識別子) |
概要 |
---|---|---|---|---|
MThdチャンク | 一般 | 必須 | MThd | スタンダードMIDIファイルのヘッダ情報(トラックフォーマット形式や分解能など)が保存されたチャンク |
MTrkチャンク | 一般 | 必須 | MTrk | 各トラックごとのMIDIデータが保存されたチャンク。複数トラックがある場合、同じチャンクID(MTrk)が複数存在することになる。 |
スタンダードMIDIファイルの各チャンクの説明
スタンダードMIDIファイルの各チャンクについて説明いたします。
MThdチャンク
概要 | スタンダードMIDIファイルのヘッダ情報(トラックフォーマット形式や分解能など)が保存されたチャンクになります | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
必須・任意 | 必須 | チャンクサイズ | 6Byte固定 | ||||||||||||||
データ部 | データ部には下記 構造体が入ります。
(WORD型はunsigned short、DWORD型はunsinged longの再定義となります。)
|
MTrkチャンク
概要 | 各トラックごとのMIDIデータが保存されたチャンクになります。 (複数トラックがある場合、同じチャンクID(MTrk)が複数存在するので注意。) |
||
---|---|---|---|
必須・任意 | 必須 | チャンクサイズ | トラック内にあるMIDIデータの内容により変わります。 |
データ部 | デルタタイム・MIDIイベント・MIDIイベントデータの3つを1つとしたMIDIデータが複数保存されます。 デルタタイムは可変長のデータで、また、MIDIイベントデータはMIDIイベントによって変わるため後述します。 |
MTrkチャンクのデータ形式
デルタタイム
(作成中)
MIDIイベントとMIDIデータ
(作成中)
スタンダードMIDファイル読み込みサンプル
スタンダードMIDファイルの読み込みの流れは
- MThdチャンクを読み込む
- MTrkチャンクを読み込み、トラック内にあるMIDIメッセージを保存する。
※トラック数に応じてMTrkの読み込みを繰り返す。
となります。
スタンダードMIDIファイルを読み込む関数のサンプルコードを記載します。
説明についてはコード内に記載しているので割愛します。
なお、スタンダードMIDIファイルの読み込みには下記のRIFFの読込を行うクラスを使用していますので、下記もご参照ください。
スタンダードMIDIファイルの読み込み関数のサンプルはこちらからもダウンロードできます。 → riffsample_20231210
※スタンダードMIDIファイル以外にもwavファイルやSoundfontファイル読込サンプルなども同梱されています。
【cmidiloader.h】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
#pragma once #include <stdio.h> #include <vector> #include <map> #include <string> using namespace std; //#include "CRiffLoaderMMIO.h" #include "criffloader.h" const FOURCC fccMThd = FourCC('M', 'T', 'h', 'd'); const FOURCC fccMTrk = FourCC('M', 'T', 'r', 'k'); constexpr BYTE MIDINoteOff = 0x80; constexpr BYTE MIDINoteOn = 0x90; constexpr BYTE MIDIPolyPress = 0xA0; constexpr BYTE MIDICC = 0xB0; constexpr BYTE MIDIProgramChange = 0xC0; constexpr BYTE MIDIChannelPress = 0xD0; constexpr BYTE MIDIPitchBend = 0xE0; constexpr BYTE MIDISystemExclusive = 0xF0; struct SMFHeader { WORD format = 0; // SMFフォーマット WORD tracknum = 0; // トラック数 WORD timedev = 0; // 時間ベース。 // 最上位ビットが0のとき、4分音符の分解能を表す。 // 最上位ビットが1のとき、絶対時間ベースとなるがWeb上にほとんど情報がなく詳細不明。 }; struct MIDIMessage { DWORD deltaTime = 0; // MIDIメッセージのデルタ時間。(前のMIDIメッセージからの時間。) DWORD absTime = 0; // MIDIメッセージの絶対時間。(曲の最初からの時間。必須ではないが扱いやすいので作成している。) BYTE event = 0; // MIDIイベント。 BYTE channel = 0; // MIDIチャンネル。 vector<BYTE> data; // MIDIイベントのデータ用。 BYTE sysexmeta = 0; // システムメタイベントのタイプ。システムメタイベントでのみ使用。 }; typedef vector<MIDIMessage> TrackMIDIMessage; class CSMFLoader { public: CSMFLoader(string filename) { criffloader.open(filename, true); initialize(); }; ~CSMFLoader() { }; void initialize() { // -------------------------------------------------------- // SMFのヘッダチャンクの処理 // SMFのヘッダチャンクのサイズを取得 DWORD chunkSize = criffloader.getChunkSize(fccMThd, 0); if (chunkSize != 6) { return; } // ヘッダチャンクのサイズは6固定。それ以外ならエラー。 // SMFのヘッダ情報を取得 criffloader.getChunkData(fccMThd, &header, chunkSize,0); // エンディアンの変更 (swqap16bitはcriffloader.hで定義) header.format = swap16bit(header.format); header.tracknum = swap16bit(header.tracknum); header.timedev = swap16bit(header.timedev); // エラー処理 if (header.format != 0 && header.format != 1) { return; } // SMFフォーマットは0か1のみ対応 if (header.tracknum < 1) { return; } // トラックがない場合は終了 if (header.timedev & 0x8000) { return; } // 絶対時間ベースは非対応とする。(Web上に情報がなく、扱い切れないため) // -------------------------------------------------------- // SMFのトラックチャンクの処理 // SMFのトラックチャンクの数を取得 int tracknum = (int)criffloader.getChunkNum(fccMTrk); if (tracknum != header.tracknum) { return; } // トラックチャンクの数がヘッダと一致しなければエラー // SMFのトラックチャンクの読込 for (int i = 0; i < tracknum; i++) { // SMFのトラックチャンクのサイズを取得 chunkSize = criffloader.getChunkSize(fccMTrk, i); if (chunkSize == 0) { continue; } // SMFのトラックチャンクのデータを取得 vector<BYTE> data(chunkSize, 0); criffloader.getChunkData(fccMTrk, &(data[0]), chunkSize, i); // SMFのトラックチャンクのデータを扱いやすいように // MIDIデータごと(MIDIMessageごと)にする TrackMIDIMessage mididata; parseTrack(data, mididata); trackdata.push_back(mididata); } // フォーマット0の場合、1つのトラックに全チャンネルが混在するので、 // チャンネルごとにトラックを分ける if (header.format == 0 && trackdata.size() == 1) { trackdata = separateTrack(trackdata[0]); } }; // SMFのトラックチャンクのデータを扱いやすいようにMIDIデータごと(TrackMIDIMessageごと)にする関数 void parseTrack(vector<BYTE>& data, TrackMIDIMessage& midimsg) { DWORD absTime = 0; MIDIMessage tmpmidimsg ,initialmidimsg; BYTE PrevMIDIEvent = 0; // ランニングステータス用 DWORD sysex_pos = 0; DWORD sysex_len = 0; // 解析状態を示す変数と各解析状態 // (SMFファイルのトラックはデータが可変長なので、1Byteずつ内容をチェックする必要がある。 // システムメタイベントのデータ長とデータの読み込み状態はシステムエクスクルーシブと共通のため、 // MIDI_SYSEXLEN, MIDI_SYSEXDATAを利用するものとする。 enum { MIDI_DELTATIME, MIDI_EVENT, MIDI_DATA1, MIDI_DATA2, MIDI_METATYPE, MIDI_SYSEXLEN, MIDI_SYSEXDATA }; int state = MIDI_DELTATIME; for (int j = 0; j < (int)data.size(); j++) { switch (state) { case MIDI_DELTATIME: // MIDIデルタタイムは、可変長のデータになる // 最上位ビットが、1の時は継続するデータがあり、0の時は継続するデータがないことを示す。 // 残りの7ビットはデータの値を示す。 // MIDIデルタタイムの読み込みが終わればMIDIデータ読込状態に映る // データの値を足し合わせる tmpmidimsg.deltaTime += data[j] & 0x7F; if (data[j] & 0x80) // 最上位ビットが1のときはまだ継続するデルタタイムデータがある。 { // 継続するデルタタイムがあるため、ビットシフトする tmpmidimsg.deltaTime = tmpmidimsg.deltaTime << 7; } else { // 次はデルタタイムではないため、状態をMIDI_EVENTに切り替える absTime += tmpmidimsg.deltaTime; tmpmidimsg.absTime = absTime; state = MIDI_EVENT; } break; case MIDI_EVENT: // MIDIイベントは8バイト固定のデータになる。 // ただし、最上位ビットが0の場合、ランニングステータスルールが適用され、 // 前のイベントを適用するとともに、読込位置を進めずに次の状態(MIDIイベントデータ1)に移る。 // MIDIイベントの読み込みが終われば次の状態に映る。 // MIDIイベントがシステムエクスクルーシブ以外はMIDIイベントデータ1読込状態に移る。 // システムエクスクルーシブの場合、下位4bitの値により、メタイベントかどうか(0x0Fかどうか)を判断し // メタイベントの場合はメタイベントタイプ読込状態に移り、 // メタイベント出ない場合はシステムエクスクルーシブデータ長読込状態に移る。 // ランニングステータスかどうかを調べる。 if (data[j] & 0x80) // 最上位ビットが1のときはランニングステータスではない。 { // 通常の処理 tmpmidimsg.event = data[j] & 0xF0; // 上位4ビットはMIDIイベント tmpmidimsg.channel = data[j] & 0x0F; // 下位4ビットはMIDIチャンネル PrevMIDIEvent = data[j]; // 次のランニングステータスのために現在の状態を保存 } else { // ランニングステータスの処理 tmpmidimsg.event = PrevMIDIEvent & 0xF0; // 上位4ビットはMIDIイベント tmpmidimsg.channel = PrevMIDIEvent & 0x0F; // 下位4ビットはMIDIチャンネル j -= 1; // データ読込位置は進めないようにする } // 状態を切り替える if (tmpmidimsg.event == MIDISystemExclusive) { // 下位バイトでシステムメタイベントかどうかを判断する。 if (tmpmidimsg.channel == 0x0F) { state = MIDI_METATYPE; } else // 本来は下位バイトが 0 か 7 のみで制限するのが望ましい。 { state = MIDI_SYSEXLEN; } // システムメタイベント・システムエクスクルーシブでのみ使用する変数をクリアする。 sysex_pos = 0; sysex_len = 0; } else { // システムメタイベント・システムエクスクルーシブではない。 state = MIDI_DATA1; } break; case MIDI_DATA1: // MIDIイベントデータ1は8バイト固定のデータになる。 // MIDIイベントデータ1の読み込みが終われば、次の状態に移る。 // MIDIイベントがプログラムチェンジやMIDIチャンネルプレッシャーであれば // 1つのMIDIデータの読込が完了となる。 // そうでなければ、MIDIイベントデータ2の読込に移る。 // MIDIイベントデータ1は特に気にせず保存する tmpmidimsg.data.push_back(data[j]); // 状態を切り替える if (tmpmidimsg.event == MIDIProgramChange || tmpmidimsg.event == MIDIChannelPress) { // プログラムチェンジとチャンネルプレッシャーはdata2がないので // 次のMIDIデータになるため、状態はMIDI_DELTATIMEに戻す。 state = MIDI_DELTATIME; // 1つのMIDIデータが終了したので、midimsg配列に追加。 midimsg.push_back(tmpmidimsg); // 一時MIDIデータ(tmpmidimsg)は再利用するのでクリアする。 tmpmidimsg = initialmidimsg; tmpmidimsg.data.clear(); } else { // プログラムチェンジとチャンネルプレッシャー以外はMIDIデータ 2つ目がある。 state = MIDI_DATA2; } break; case MIDI_DATA2: // MIDIイベントデータ2も8バイト固定のデータになる。 // MIDIイベントデータ2の読み込みが終われば、1つのMIDIデータの読み込みが完了となる。 // MIDIイベントデータ2は特に気にせず保存する。 tmpmidimsg.data.push_back(data[j]); // 次のMIDIデータになるため、状態はMIDI_DELTATIMEに戻す。 state = MIDI_DELTATIME; // 1つのMIDIデータが終了したので、midimsg配列に追加。 midimsg.push_back(tmpmidimsg); // 一時MIDIデータ(tmpmidimsg)は再利用するのでクリアする。 tmpmidimsg = initialmidimsg; tmpmidimsg.data.clear(); break; case MIDI_METATYPE: // システムメタイベントのタイプ // システムメタイベントは8バイト固定のデータになる。 // システムメタイベントは読み込みが終われば、システムメタイベント長の読込状態に移る。 tmpmidimsg.sysexmeta = data[j]; state = MIDI_SYSEXLEN; break; case MIDI_SYSEXLEN: // システムメタイベント・システムエクスクルーシブのデータ長は可変長データになる。 // 最上位ビットが、1の時は継続するデータがあり、0の時は継続するデータがないことを示す。 // 残りの7ビットはデータの値を示す。 // システムメタイベント・システムエクスクルーシブのデータ長の読み込みが終われば // システムメタイベント・システムエクスクルーシブのデータ読込状態に移る。 sysex_len += data[j] & 0x7F; if (data[j] & 0x80) // 最上位ビットが1のときはまだ継続するデータ長がある。 { // 次のデータ長さのためシフトする sysex_len = sysex_len << 7; } else { sysex_pos = 0; state = MIDI_SYSEXDATA; } break; case MIDI_SYSEXDATA: // システムメタイベント・システムエクスクルーシブのデータは可変長データになる。 // MIDI_SYSEXLENで読み込んだデータ長の分だけデータを読み込む。 // データ長の分だけ読み込みが終われば、1つのMIDIデータの読み込みが完了となる。 // システムメタイベント・システムエクスクルーシブのデータを保存する。 tmpmidimsg.data.push_back(data[j]); // システムメタイベント・システムエクスクルーシブのデータ長を // 読み込んだデータ数が超えたら終了。 sysex_pos++; if (sysex_pos >= sysex_len) { // システムメタイベント・システムエクスクルーシブでのみ使用する変数をクリアする。 sysex_pos = 0; sysex_len = 0; // 次のMIDIイベントになるため、状態はMIDI_DELTATIMEに戻す。 state = MIDI_DELTATIME; // 1つのMIDIイベントが終了したので、midimsg配列に追加。 midimsg.push_back(tmpmidimsg); // 一時MIDIイベント(tmpmidimsg)は再利用するのでクリアする。 tmpmidimsg = initialmidimsg; tmpmidimsg.data.clear(); } break; } } }; // チャンネルごとにトラックを分ける関数 vector<TrackMIDIMessage> separateTrack(TrackMIDIMessage& midimsg) { vector<TrackMIDIMessage> newtrackdata; map<int, TrackMIDIMessage> tmptrackdata; // 単純にMIDIチャンネルを分離する for (int i = 0; i < (int)midimsg.size(); i++) { if (midimsg[i].event == MIDISystemExclusive) { int id = 0; tmptrackdata[id].push_back(midimsg[i]); } else { int id = midimsg[i].channel + 1; tmptrackdata[id].push_back(midimsg[i]); } } // 分離したMIDIチャンネルのdeltatimeを修正する for (auto x : tmptrackdata) { auto tmpmidimsg = x.second; DWORD prevAbsTime = 0; for (int i = 0; i < (int)tmpmidimsg.size(); i++) { tmpmidimsg[i].deltaTime = tmpmidimsg[i].absTime - prevAbsTime; prevAbsTime = tmpmidimsg[i].absTime; } newtrackdata.push_back(tmpmidimsg); } return newtrackdata; } SMFHeader header; vector<TrackMIDIMessage> trackdata; protected: CRiffLoader criffloader; }; |
上記の関数ででスタンダードMIDIファイルを読み込むことができます。
下記のように利用することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int main() { CSMFLoader smf("test.mid"); // スタンダードMIDIファイルを読み込む // トラックごとにMIDIデータを利用して処理を行う for (int track = 0; track < smf.trackdata.size(); track++) { printf("トラック %d\n", track); int loops = smf.trackdata[track].size(); for (int i = 0; i < loops; i++) { switch (smf.trackdata[track][i].event) // MIDIイベントごとに処理 { case MIDINoteOff: //トラック内のMIDIデータ「smf.trackdata[track][i]」を使って何か処理 break; case MIDINoteOn: ~~以下略~~ } } } } |
最後に
スタンダードMIDIのファイルフォーマットとそれぞれのチャンクについての説明は以上になります。
今回作成したスタンダードMIDIファイルの読み込みクラスのサンプルはこちらからダウンロードできます。 → riffsample_20231210
※スタンダードMIDIファイル以外にもwavファイルやSoundfontファイル読込サンプルなども同梱されています。
他のファイルの読み込みなどはリンクはこちらにまとめております → メディアファイルの取り扱い
質問はコメント欄や掲示板、Twitterでいただけばとおもいます。
また、「この部分を詳しく」などの要望も掲示板やTwitterでいただければと思います。
■掲示板
■Twitterアカウント:@vstcpp URL:https://twitter.com/vstcpp