#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;
};