LoginSignup
7
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-13

この記事は Raspberry PiとMIDIインターフェースでデバイス開発をする のシリーズ記事です。

特に今回は、内容の一部が、前回の記事も併せて読まないと成り立ちません。

シリーズ目次

  1. Raspberry PiとMIDIインターフェースでデバイス開発をする
  2. RtMidiライブラリを使って、MIDIインターフェースを作成する
  3. SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む(プロトタイプ編)
  4. SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む(クラス編)
  5. Raspberry Piに外部スイッチを扱うドライバを実装する

前提

外部ライブラリとして smf_parser(v1.01) を使います。

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

MKT (2).png

下準備

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になったときに再生が終了します。

コード

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インターフェースを作成する 作成中 SMF (Standard Midi File)を解析し、MIDIインターフェースに流し込む(クラス編)
7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5