この記事は Raspberry PiとMIDIインターフェースでデバイス開発をする のシリーズ記事です。
特に今回は、内容の一部が、前回の記事も併せて読まないと成り立ちません。
シリーズ目次
- Raspberry PiとMIDIインターフェースでデバイス開発をする
- RtMidiライブラリを使って、MIDIインターフェースを作成する
- SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む(プロトタイプ編)
- SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む(クラス編)
- Raspberry Piに外部スイッチを扱うドライバを実装する
前提
外部ライブラリとして smf_parser(v1.01) を使います。
なぜわざわざ自前で解析するのかというと、解析した結果を一部改変しながら流し込みたいからです。具体例は下記の通りです。
- ピアノの特定の弦が切れたシーンを再現したい
- 二つ以上のMIDIをスイッチしながら再生したい
下準備
SMFはMIDIイベントを列挙したファイル構造となっています。このSMFを解析した結果を格納するために次の2つの構造体を定義します。
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
になったときに再生が終了します。
コード
#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インターフェースを作成する | 作成中 SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む(クラス編) |