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再生を行うには複数のトラックをマージする必要があり、更に手間が増えますが
これもワンパターンなのでクラス化すれば問題ありません。

■参考サイト
EternalWindows MIDI / MIDI再生サンプル