SMFの読み込み
前回の記事でSMF(Standard Midi File)の構造について解説しました。
これを踏まえ、今回はSMFを読み込むプログラムについて解説していきます。
SMFはヘッダチャンクの後にトラックチャンクが続く構造になります。
したがって、これらを順番に読み込んでいけば良いです。
読み込み処理の大枠は以下のようになります。
// SMFデータを管理するクラス class SmfData { public: // ヘッダ情報 struct SmfHeader { U16 format; // フォーマット U16 trackNum; // トラック数 U16 timeBase; // 時間単位 }; // トラック情報 struct SmfTrack { std::vector<smfEvent> events; // イベント }; ・・・ private: SmfHeader m_header; // ヘッダ std::vector<smfTrack> m_tracks; // トラック bool m_isLoaded; // ロード済みかどうか static void ReadHeader(SmfHeader& header, std::istream& in); static void ReadTrack(SmfTrack& track, std::istream& in);
// SMFのデータをロードする void SmfData::Load(const std::string& smfName) try { if ( m_isLoaded ) throw std::runtime_error("SMFは既にロードされています。"); // SMFを開く std::ifstream smfIn(smfName, std::ios::binary); if ( !smfIn ) throw std::runtime_error("SMFのオープンに失敗しました。"); // ヘッダの読み込み ReadHeader(m_header, smfIn); // トラックの読み込み for ( UINT i = 0 ; i < m_header.trackNum ; ++i ) { SmfTrack track; ReadTrack(track, smfIn); m_tracks.push_back(track); } m_isLoaded = true; } catch ( std::exception& ) { Clear(); // 途中でエラーが発生したら読み込み途中のデータをクリア throw; }
ReadHeader()がヘッダチャンクを読み込むメソッド、ReadTrack()がトラックチャンクを読み込むメソッドです。
トラックチャンクはヘッダチャンク内のトラック数(m_header.trackNum)だけ格納されているため、トラックの数だけReadTrack()を呼び出しています。
ReadHeader()メソッドの実装は以下のようになります。
// ヘッダを読み込む void SmfData::ReadHeader(SmfData::SmfHeader& header, std::istream& in) { // チャンクタイプの読み込み char chunkType[4]; in.read(chunkType, sizeof(chunkType)); if ( !IsValidChunkType(chunkType, "MThd") ) throw std::runtime_error("SMFヘッダのチャンクタイプが不正です。"); // ヘッダ長の読み込み U32 length; in.read(reinterpret_cast<char*>(&length), sizeof(length)); if ( ToLittle(length) != 6 ) throw std::runtime_error("SMFヘッダ長が不正です。"); // ヘッダデータの読み込み in.read(reinterpret_cast<char*>(&header), sizeof(header)); header.format = ToLittle(header.format); header.trackNum = ToLittle(header.trackNum); header.timeBase = ToLittle(header.timeBase); // フォーマットとトラック数のチェック if ( header.format != 0 && header.format != 1 ) throw std::runtime_error("サポートされていないSMFフォーマットです。"); if ( header.format == 0 && header.trackNum != 1 ) throw std::runtime_error("トラック数が不正です。"); }
チャンクタイプとデータ長は決まっているため、いずれも読み込んだ後にチェックするようにしています。
読み出されるヘッダ長、フォーマット、トラック数、時間単位はビッグエンディアンであるため、リトルエンディアンに変換しています。
今回はフォーマット0と1のみ扱うことにするため、それ以外のフォーマットの場合はエラーとします。
フォーマット0の場合は単一のトラックしか存在してはいけないため、複数あった場合はエラーとします。
ReadTrack()メソッドの実装は以下のようになります。
// トラックを読み込む void SmfData::ReadTrack(SmfData::SmfTrack& track, std::istream& in) { // チャンクタイプの読み込み char chunkType[4]; in.read(chunkType, sizeof(chunkType)); if ( !IsValidChunkType(chunkType, "MTrk") ) throw std::runtime_error("トラックのチャンクタイプが不正です。"); // トラック長の読み込み U32 length; in.read(reinterpret_cast<char*>(&length), sizeof(length)); length = ToLittle(length); // イベントの読み込み SmfEvent prev, cur; do { cur = SmfEvent(); // デルタタイムの読み込み cur.deltaTime = ReadDelta(in); // イベントの読み込み ReadEvent(cur, prev.status, in); prev = cur; track.events.push_back(cur); } while ( !(cur.status == EVENT_META && cur.meta == 0x2F) ); // 終了イベントであればループを抜ける }
ヘッダチャンクの読み込み同様、まずチャンクタイプのチェックを行い、トラックのデータ長を読み込んでリトルエンディアンに変換しています。
その後にはイベントが続くため、イベントの読み込みをループ内で行っています。
イベントの末尾には終了イベントが入っているため、これが来たらループを抜けてトラックの読み込みを終了します。
イベントを読み込むループ内では、ReadDelta()でデルタタイムを読み込んだ後にReadEvent()でイベントを読み込んでいます。
ReadDelta()、ReadEvent()メソッドの実装は以下のようになります。
// デルタタイムを読み込む U32 SmfData::ReadDelta(std::istream& in) { U32 result = 0; for ( int i = 0 ; i < 4 ; i++ ) { U8 tmp = in.get(); result = (result << 7) | (tmp & 0x7F); if ( (tmp & 0x80) == 0 ) break; } return result; }
// イベントを読み込む void SmfData::ReadEvent(SmfData::SmfEvent& event, U8 prevStatus, std::istream& in) { // ステータスバイトの読み込み event.status = in.get(); // ランニングステータスか? if ( (event.status & 0x80) == 0 ) { in.putback(event.status); event.status = prevStatus; } switch ( event.status >> 4 ) { case 0x8: case 0x9: case 0xA: case 0xB: case 0xE: // データバイトを2個読み込む event.data1 = in.get(); event.data2 = in.get(); break; case 0xC: case 0xD: // データバイトを1個読み込む event.data1 = in.get(); break; case 0xF: if ( event.status == 0xF0 ) { // SysExイベントか? // イベント長の読み込み event.varLength = ReadDelta(in); // 可変長イベントの読み込み event.varEvent = new U8[event.varLength + 1]; event.varEvent[0] = 0x0F; in.read(reinterpret_cast<char*>(&event.varEvent[1]), event.varLength); ++event.varLength; } else if ( event.status == 0xFF ) { // メタイベントか? // イベントタイプの読み込み event.meta = in.get(); // イベント長の読み込み event.varLength = ReadDelta(in); // イベント長の正当性チェック U32 validLength = 0xFFFFFFFF; bool isVarLength = false; switch ( event.meta ) { case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: case 0x7F: isVarLength = true; break; case 0x20: validLength = 1; break; case 0x2F: validLength = 0; break; case 0x51: validLength = 3; break; case 0x54: validLength = 5; break; case 0x58: validLength = 4; break; case 0x59: validLength = 2; break; default: // エラー throw std::runtime_error("不正な種類のメタイベントが読み込まれました。"); } if ( !isVarLength && event.varLength != validLength ) throw std::runtime_error("不正な長さのメタイベントが読み込まれました。"); // 可変長イベントの読み込み event.varEvent = new U8[event.varLength]; in.read(reinterpret_cast<char*>(event.varEvent), event.varLength); } else { // エラー throw std::runtime_error("不正なイベントが読み込まれました。"); } break; } }
データの構造については前回の記事をご参照下さい。
注意すべきところは、ReadEvent()メソッド内でランニングステータスが来た場合の動作です。
// ランニングステータスか? if ( (event.status & 0x80) == 0 ) { in.putback(event.status); event.status = prevStatus; }
MSBが0のときはステータスバイトが省略されてランニングステータスとなるため、
in.putback()でストリームポインタをひとつ戻して以前のステータスバイトの値を採用しています。
メタイベントの読み込みでは、イベント長チェックもしっかりと行うようにしています。
上記を踏まえ、SMFを読み込むまでのプログラムを以下に記しておきます。
SmfData.h
#ifndef _SMF_DATA_H_INCLUDED_ #define _SMF_DATA_H_INCLUDED_ #include <string> #include <vector> typedef unsigned int UINT; typedef unsigned char U8; typedef unsigned short U16; typedef unsigned long U32; // SMFデータを管理するクラス class SmfData { public: // ヘッダ情報 struct SmfHeader { U16 format; // フォーマット U16 trackNum; // トラック数 U16 timeBase; // 時間単位 }; // イベント struct SmfEvent { SmfEvent() : deltaTime(0), status(0), data1(0), data2(0), meta(0), varLength(0), varEvent(NULL) { } U32 deltaTime; // デルタタイム U8 status; // ステータスバイト U8 data1; // データバイト1 U8 data2; // データバイト2 U8 meta; // メタイベントの種類 U32 varLength; // 可変長イベント長(SysEx, メタイベントの場合) U8* varEvent; // 可変長イベント }; enum { EVENT_SYSEX = 0xF0, EVENT_META = 0xFF, }; // トラック情報 struct SmfTrack { std::vector<smfEvent> events; // イベント }; SmfData(); virtual ~SmfData(); void Load(const std::string& smfName); void Clear(); const SmfHeader& GetHeader() const; const SmfTrack& GetTrack(UINT index) const; UINT GetTrackNum() const; private: SmfHeader m_header; // ヘッダ std::vector<smfTrack> m_tracks; // トラック bool m_isLoaded; // ロード済みかどうか static void ReadHeader(SmfHeader& header, std::istream& in); static void ReadTrack(SmfTrack& track, std::istream& in); static U32 ReadDelta(std::istream& in); static void ReadEvent(SmfEvent& event, U8 prevStatus, std::istream& in); static void ClearTrack(SmfTrack& track); static void ClearEvent(SmfEvent& event); static bool IsValidChunkType(char type[4], const std::string& target); template <class T> static T ToLittle(T value); }; #endif // _SMF_DATA_H_INCLUDED_
SmfData.cpp
#include "SmfData.h" #include <stdexcept> #include <fstream> // コンストラクタ SmfData::SmfData() : m_isLoaded(false) { } // デストラクタ SmfData::~SmfData() { Clear(); } // SMFのデータをロードする void SmfData::Load(const std::string& smfName) try { if ( m_isLoaded ) throw std::runtime_error("SMFは既にロードされています。"); // SMFを開く std::ifstream smfIn(smfName, std::ios::binary); if ( !smfIn ) throw std::runtime_error("SMFのオープンに失敗しました。"); // ヘッダの読み込み ReadHeader(m_header, smfIn); // トラックの読み込み for ( UINT i = 0 ; i < m_header.trackNum ; ++i ) { SmfTrack track; ReadTrack(track, smfIn); m_tracks.push_back(track); } m_isLoaded = true; } catch ( std::exception& ) { Clear(); // 途中でエラーが発生したら読み込み途中のデータをクリア throw; } // 読み込んだSMFデータをクリアする void SmfData::Clear() { for ( auto it = m_tracks.begin() ; it < m_tracks.end() ; ++it ) ClearTrack(*it); m_isLoaded = false; } // ヘッダ情報を取得する const SmfData::SmfHeader& SmfData::GetHeader() const { return m_header; } // トラック情報を取得する const SmfData::SmfTrack& SmfData::GetTrack(UINT index) const { if ( index >= m_tracks.size() ) throw std::runtime_error("不正なトラックが指定されました。"); return m_tracks[index]; } // トラック数を取得する UINT SmfData::GetTrackNum() const { return m_tracks.size(); } // ヘッダを読み込む void SmfData::ReadHeader(SmfData::SmfHeader& header, std::istream& in) { // チャンクタイプの読み込み char chunkType[4]; in.read(chunkType, sizeof(chunkType)); if ( !IsValidChunkType(chunkType, "MThd") ) throw std::runtime_error("SMFヘッダのチャンクタイプが不正です。"); // ヘッダ長の読み込み U32 length; in.read(reinterpret_cast<char*>(&length), sizeof(length)); if ( ToLittle(length) != 6 ) throw std::runtime_error("SMFヘッダ長が不正です。"); // ヘッダデータの読み込み in.read(reinterpret_cast<char*>(&header), sizeof(header)); header.format = ToLittle(header.format); header.trackNum = ToLittle(header.trackNum); header.timeBase = ToLittle(header.timeBase); // フォーマットとトラック数のチェック if ( header.format != 0 && header.format != 1 ) throw std::runtime_error("サポートされていないSMFフォーマットです。"); if ( header.format == 0 && header.trackNum != 1 ) throw std::runtime_error("トラック数が不正です。"); } // トラックを読み込む void SmfData::ReadTrack(SmfData::SmfTrack& track, std::istream& in) { // チャンクタイプの読み込み char chunkType[4]; in.read(chunkType, sizeof(chunkType)); if ( !IsValidChunkType(chunkType, "MTrk") ) throw std::runtime_error("トラックのチャンクタイプが不正です。"); // トラック長の読み込み U32 length; in.read(reinterpret_cast<char*>(&length), sizeof(length)); length = ToLittle(length); // イベントの読み込み SmfEvent prev, cur; do { cur = SmfEvent(); // デルタタイムの読み込み cur.deltaTime = ReadDelta(in); // イベントの読み込み ReadEvent(cur, prev.status, in); prev = cur; track.events.push_back(cur); } while ( !(cur.status == EVENT_META && cur.meta == 0x2F) ); // 終了イベントであればループを抜ける } // デルタタイムを読み込む U32 SmfData::ReadDelta(std::istream& in) { U32 result = 0; for ( int i = 0 ; i < 4 ; i++ ) { U8 tmp = in.get(); result = (result << 7) | (tmp & 0x7F); if ( (tmp & 0x80) == 0 ) break; } return result; } // イベントを読み込む void SmfData::ReadEvent(SmfData::SmfEvent& event, U8 prevStatus, std::istream& in) { // ステータスバイトの読み込み event.status = in.get(); // ランニングステータスか? if ( (event.status & 0x80) == 0 ) { in.putback(event.status); event.status = prevStatus; } switch ( event.status >> 4 ) { case 0x8: case 0x9: case 0xA: case 0xB: case 0xE: // データバイトを2個読み込む event.data1 = in.get(); event.data2 = in.get(); break; case 0xC: case 0xD: // データバイトを1個読み込む event.data1 = in.get(); break; case 0xF: if ( event.status == 0xF0 ) { // SysExイベントか? // イベント長の読み込み event.varLength = ReadDelta(in); // 可変長イベントの読み込み event.varEvent = new U8[event.varLength + 1]; event.varEvent[0] = 0x0F; in.read(reinterpret_cast<char*>(&event.varEvent[1]), event.varLength); ++event.varLength; } else if ( event.status == 0xFF ) { // メタイベントか? // イベントタイプの読み込み event.meta = in.get(); // イベント長の読み込み event.varLength = ReadDelta(in); // イベント長の正当性チェック U32 validLength = 0xFFFFFFFF; bool isVarLength = false; switch ( event.meta ) { case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: case 0x7F: isVarLength = true; break; case 0x20: validLength = 1; break; case 0x2F: validLength = 0; break; case 0x51: validLength = 3; break; case 0x54: validLength = 5; break; case 0x58: validLength = 4; break; case 0x59: validLength = 2; break; default: // エラー throw std::runtime_error("不正な種類のメタイベントが読み込まれました。"); } if ( !isVarLength && event.varLength != validLength ) throw std::runtime_error("不正な長さのメタイベントが読み込まれました。"); // 可変長イベントの読み込み event.varEvent = new U8[event.varLength]; in.read(reinterpret_cast<char*>(event.varEvent), event.varLength); } else { // エラー throw std::runtime_error("不正なイベントが読み込まれました。"); } break; } } // トラック情報をクリアする void SmfData::ClearTrack(SmfData::SmfTrack& track) { for ( auto it = track.events.begin() ; it < track.events.end() ; ++it ) delete[] it->varEvent; } // チャンクタイプの正当性をチェックする bool SmfData::IsValidChunkType(char type[4], const std::string& target) { if ( target.size() != sizeof(type) ) return false; return ( (type[0] == target[0]) & (type[1] == target[1]) & (type[2] == target[2]) & (type[3] == target[3])); } // リトルエンディアンに変換する template <class T> T SmfData::ToLittle(T value) { U8* p = reinterpret_cast<u8*>(&value); for ( int i = 0 ; i < sizeof(T) / 2 ; ++i ) { U8 tmp = p[i]; p[i] = p[sizeof(T) - i - 1]; p[sizeof(T) - i - 1] = tmp; } return value; }
Main.cpp
#include <windows.h> #include "SmfData.h" // メイン関数 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) try { SmfData smf; smf.Load("test.mid"); MessageBox(NULL, TEXT("SMFの読み込みに成功しました。"), TEXT("OK"), MB_ICONINFORMATION); return 0; } catch ( std::exception& e ) { MessageBoxA(NULL, e.what(), NULL, MB_ICONERROR); return -1; }
プロジェクトフォルダ上にtest.midファイルを置くとこのファイルを読み込みます。
このように、多少の手間はかかりますが、一度読み込み処理をクラス化してしまえば楽です。
MIDI再生を行うには複数のトラックをマージする必要があり、更に手間が増えますが
これもワンパターンなのでクラス化すれば問題ありません。