SMF (Standard Midi Format)を解析し、MIDIインターフェースに流し込む（プロトタイプ編）

この記事は　**[Raspberry PiとMIDIインターフェースでデバイス開発をする](https://qiita.com/ixsiid/items/d126b1d6a073373c2143)** のシリーズ記事です。

特に今回は、内容の一部が、[前回](https://qiita.com/ixsiid/items/dfd093732d05260ed174)の記事も併せて読まないと成り立ちません。

### シリーズ目次
1. [Raspberry PiとMIDIインターフェースでデバイス開発をする](https://qiita.com/ixsiid/items/d126b1d6a073373c2143)
2. [RtMidiライブラリを使って、MIDIインターフェースを作成する](https://qiita.com/ixsiid/items/dfd093732d05260ed174)
3. ***SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む（プロトタイプ編）***
4. [SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む（クラス編）](https://qiita.com/ixsiid/items/0d45cbeffeacd7739bc9)
5. Raspberry Piに外部スイッチを扱うドライバを実装する

# 前提
外部ライブラリとして [smf_parser(v1.01)](http://wiki.joanillo.org/index.php/Fitxers_MIDI_(SMF)._Format#smf_parser) を使います。

なぜわざわざ自前で解析するのかというと、解析した結果を一部改変しながら流し込みたいからです。具体例は下記の通りです。
- ピアノの特定の弦が切れたシーンを再現したい
- 二つ以上のMIDIをスイッチしながら再生したい

![MKT (2).png](https://qiita-image-store.s3.amazonaws.com/0/256650/449b344c-54d9-19e5-61cf-641f198bfc9f.png)

# 下準備
SMFはMIDIイベントを列挙したファイル構造となっています。このSMFを解析した結果を格納するために次の2つの構造体を定義します。

```cpp
typedef struct _Smf {
	std::string filepath;
	Note * notes;
	Note * endpoint;
	Note * current;
} Smf;

typedef struct _Note {
	int time;
	unsigned char message[3];
} Note;
```

Note構造体はMIDIイベントを表しています。
`time`がそのイベントが発生する時間（ミリ秒）、`message`はMIDIメッセージを表します。

それを踏まえてSmf構造体の各メンバの意味は次の通りです。
`filepath`: 該当のSMF(.mid)ファイル
`notes`: Note構造体の配列をイベント発生時間順にソートしたもの（へのポインタ）
`endpoint`: `notes`の終端（ポインタ）
`current`: `notes`配列中の次に再生するNoteを示すポインタ

実際に再生するときは、再生開始から経過時間が`SMF.current->time`の値が大きかったら再生MIDIデバイスに`SMF.current->message`を送り、
`SMF.current++`と現在の位置をインクリメントし、`SMF.current >= SMF.endpoint`になったときに再生が終了します。

# コード
```smf.cpp
#include <iostream>
#include <cstdlib>
#include <smf.h>
#include <algorithm>
#include <chrono>
#include <thread>
#include <unistd.h>
#include "RtMidi.h"
#include "SmfParser.hpp"
#include "midi.hpp"

typedef struct _Note {
	int time;
	unsigned char message[3];
} Note;

// 解析したNoteを発生タイミング（timeメンバ）順に並び変える比較器
bool compare_event(const ch_event & left, const ch_event & right) {
	if (left.acumulate_time == right.acumulate_time) {
		if (left.event_type == right.event_type) {
			return left.par1 < right.par2;
		}
		return left.event_type < right.event_type;
	}
	return left.acumulate_time < right.acumulate_time;
}

// Note構造体のcout出力用（デバッグ用）
std::ostream & operator<<(std::ostream & Str, const Note & note) {
	if (note.message[0] == 144) {
		printf("timing %d[msec], note %d on, velocity = %d", note.time, note.message[1], note.message[2]);
	} else if (note.message[0] == 128) {
		printf("timing %d[msec], note %d off", note.time, note.message[1]);
	} else {
		printf("unknown note; %d %d %d %d", note.time, note.message[0], note.message[1], note.message[2]);
	}
	return Str;
}


typedef struct _Smf {
	std::string filepath;
	Note * notes;
	Note * endpoint;
	Note * current;
} Smf;



int main()
{
	MidiInterface * port = new MidiInterface("Multi");
	RtMidiOut *midiout = (RtMidiOut *)port->connect("FLUID", MidiDirection::OUT);
	if (midiout == NULL) {
		std::cout << "Not found target port name" << std::endl;
		exit(1);
	}

	// 読み込むファイルからSMF構造体を初期化
	// 複数のファイルを読み込めるように配列として処理しています。
	std::vector<Smf> files = {
		{"magic_music.mid", NULL, NULL, NULL}
	};

	for (int i=0; i<1; i++) {
		Smf * x = &(files[i]);
		std::cout << "parse: " << x->filepath << std::endl;

		SmfParser smf = SmfParser(x->filepath.c_str());
		smf.printcredits();
		smf.parse(0);
		smf.abstract();
		std::cout << std::endl;
		std::cout << "ticks per beat: " << smf.getTicks_per_beat() << std::endl;


		int noteCount = 0;
		for (auto y : smf.events) {
			// Note配列の確保領域を調べるため、イベントのうちtype 8, 9（Midi offとon）をカウントする
			if (y.event_type == 9 || y.event_type == 8) noteCount++;
		}

		// 複数のトラックが混在しているため、すべてのトラックのイベントを発生タイミング順に並び変える
		std::sort(smf.events.begin(), smf.events.end(), compare_event);

		x->notes = new Note[noteCount];
		int count = 0;
		int tempo = smf.getBpm();
		int tickPerBeat = smf.getTicks_per_beat();

		// 発生タイミングを時間（ミリ秒）に変換するための係数（後述）
		float k = 60000.0f / (tempo * tickPerBeat);
		std::cout << "tempo " << tempo << ", tpb " << tickPerBeat << ", k=" << k << std::endl;
		for (auto y : smf.events) {
			if (y.event_type == 9) {
				// Midi ONイベント
				// Midiメッセージはunsigned char[3]になっておりそれぞれの意味は次の通り
				// 0: イベントタイプを表す。144で固定
				// 1: 鍵盤の番号 参考 :: [MIDIノート番号と音名、周波数の対応表](http://www.asahi-net.or.jp/~HB9T-KTD/music/Japan/Research/DTM/freq_map.html)
				// 2: Velocity, 音の強さ
				x->notes[count].time = (int)(y.acumulate_time * k);
				x->notes[count].message[0] = 144;
				x->notes[count].message[1] = y.par1;
				x->notes[count].message[2] = y.par2;
				count++;
			} else if (y.event_type == 8) {
				// Midi OFFイベント
				// Midiメッセージはunsigned char[2]になっておりそれぞれの意味は次の通り
				// 0: イベントタイプを表す。128で固定
				// 1: 鍵盤の番号 参考 :: [MIDIノート番号と音名、周波数の対応表](http://www.asahi-net.or.jp/~HB9T-KTD/music/Japan/Research/DTM/freq_map.html)
				x->notes[count].time = (int)(y.acumulate_time * k);
				x->notes[count].message[0] = 128;
				x->notes[count].message[1] = y.par1;
				x->notes[count].message[2] = 0;
				count++;
			}
		}

		x->endpoint = x->notes + count;
		x->current = x->notes;

		std::cout << "count: " << count << std::endl;
	}

	// 再生は別スレッドで行う（今後のため）
	auto start = std::chrono::system_clock::now();
	// 再生するファイルを切り替える変数
	int playIndex = 0;
	std::thread thr([&files, &playIndex](RtMidiOut * midi) {
		long j = 0; // 経過時間出力制御用
		while(true) {
			int acumulate = std::chrono::duration_cast<std::chrono::milliseconds>(
				std::chrono::system_clock::now() - start
			).count();

			if (j++ > 100000) {
				// たまに状況を出力する
				std::cout << "\racumlate time: " << acumulate << "[msec]";
				fflush(stdout);
				j = 0;
			}

			while(true) {
				for (unsigned int x=0; x<files.size(); x++) {
					Smf * play = &files[x];
					if (play->current < play->endpoint && play->current->time <= acumulate) {
						if (playIndex == x) midi->sendMessage(play->current->message, 3);
						play->current++;
//						std::cout << "next: " << play -> current << std::endl;
					} else {
						break;
					}
				}
				break;
			}
			// 処理速度と合わせて、負荷軽減のためのWait処理
			// Raspberry Pi3で約3msecくらい遅れる
			// 違和感ないレベルに調整する
			usleep(1500);
		}
		std::cout << "unknown finish to play" << std::endl;
		return;
	}, midiout);
	thr.detach();

	while (true) {
		std::cout << "\r> ";
		fflush(stdout);

		int c = std::cin.get();
		if (c >= '0' && c <= '9') {
			playIndex = c - '0';
		} else if (c == 'q' || std::cin.eof()) {
			break;
		}
	}

	delete midiout;
	delete port;
	return 0;
}
```

# 解説
コード上にしていますが、コメントいただければ詳述します。

## Midiイベントの発生タイミング
SMFは、ほぼほぼ楽譜をそのままデータ化したようなものです。
各譜のタイミングは時間ではなく、テンポ（BPM, beat per minitue）と、その解像度（tick per beat）によって表されます。
実際の時間では、1分間にテンポ×解像度分のticksを処理すればいいことになります。

|前|次|
|---|---|
|[RtMidiライブラリを使って、MIDIインターフェースを作成する](https://qiita.com/ixsiid/items/dfd093732d05260ed174)|***作成中*** [SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む（クラス編）]()|