この記事はArduino Advent Calender 2017の18日目の記事1で、同時にQiita初投稿記事でもあります。
Arduinoを音楽系の工作に使いたい、電子楽器/DTMと使いたいという場合はArduino + MIDIの組み合わせはかなり有力な候補です。というのも、MIDIインターフェースは、依然として電子音楽の世界のデファクトスタンダードになっているからです。
こうした需要に応えるArduinoのライブラリとして、そのものずばり"Arduino MIDI Library"はあるのですが、日本語で参照できるこのライブラリの説明があまり充実していません。簡単な使い方・使ってみた系であればこことかこことかこことかこことかあるのですが、使い方について詳しく説明されてはいません。送信側はまだマシですが、受信側、特にCallback周りの説明は壊滅的に少ないというより見たことがありません(loop
関数内で定数比較して全処理を並べたようなものばかり)2。
こういった状況下できちんと使いこなそうとすると、公式の資料から直接ライブラリの関数一覧を参照しに行くか、英文読んでもよくわからず最悪ライブラリのソースコードを読む必要が出てきます。地味に送信側と受信側で変数順が違ったり関数のくくりが違ったりととイラッとくる部分があったりして(本稿最終節参照)、こういったものを参照せずに開発するのは不可能です。結果、Arduino MIDI Libraryの能力が十分に発揮されていない状況になっているのではないかと思われます。
ということで、Arduino MIDI Library (ver. 4.3.1)についての日本語解説を書いてみることにしました。Libraryの使用上必要な、ほぼすべての機能について解説したつもりです。
なお、
- (12/21追記)Arduino MIDI Libraryでどんなことができるのか、基本的なイメージができていたほうが恐らく読みやすいです(本稿では詳しい機能紹介をすることを主眼においているため、初心者には複雑かもしれません)。そういった意味で、Arduino MIDI LibraryもMIDIも全く知らない方は、本稿を読む前に先ほど挙げたこことかこことかこことかここあたりを参照する、Arduino MIDI Libraryを少し触ってみてから読むなどされると良いかと思います。
- MIDI自体の解説等はあまり詳しくやりません。1. に書いたことに加え、MIDI自体に関しては既にWeb上に良い解説がいくつもあるためです。簡単な説明は載せますが、それ以上の知識が必要な場合はそれらの記事(例えばこことかここなど、SMF(MIDI ファイル ) ではなく MIDI自体の解説を探してください)を参照してください。
- 本稿は、1年前に筆者が所属する某大学のエレクトーンサークルの部内誌に投稿した文章を再編集したもの3で、書き直すのも面倒なので文体が変わります。
- 部内誌に書いた文章はここの内容の1/2くらいで、残りの内容の一部(エレクトーンとの連携によるArduino MIDIの活用事例)は12/21のAdvent Calender記事として投稿予定です。
適宜不要なところ・わかりにくいところは読み飛ばしてください(特に「補足」及び「MIDIの概略」箇所)。多分問題ありません。また、間違い・疑問点あればコメントにてご指摘ください。質問も同様です。
MIDIの概略
特にこの節は読み飛ばして良く、適当に読んでArduino MIDI Libraryの説明本編に進んでいただきたい。必要な場合は、Web上のわかりやすい解説を参照すべきである。
MIDI
MIDI(Musical Instrument Digital Interface)は、電子楽器間の演奏情報を相互にデジタル通信するために策定された共通規格である(規格書: 日本語, 英語)。従って、大半の電子楽器はこのMIDI信号をやりとりする手段を持っている。多くの (特に古い場合)電子楽器はドイツ工業品標準規格で策定されているDIN-5ピンコネクタを使うが、最近の機器ではUSB 端子のみついていることもある。このように、 MIDI の通信規格において物理層に当たる部分は近年の技術発展に伴って使われなくなる場合も出てきているが、それより上位のプロトコル部分については現役であり、USBやFirewire、EthernetやBLE(Bluetooth Low Energy) などの他の通信規格にラッパーされた形のMIDIの標準規格(英語)も策定されている。
MIDIの通信
画像(回路図)は「MIDI1.0 規格書」より引用。
最も古典的なMIDIの通信は、上記の回路図で示すような、入力側と出力側がフォトカプラによって絶縁された、5 V I/OのUART(Universal Asynchronous Receiver/Transmitter, Arduinoで言うSerial通信)で行われる。ただし、以下の点には注意が必要。
- 正確には電圧5 Vである必要はなく、フォトカプラをドライブ出来るするために5 mAの電流が流せれば良い。従って、3.3 V I/Oでも一応問題はなく、そのための仕様も策定されている。
- 通信速度は31.25(±1%) kbpsである。これが地味に厄介で、Arduinoだと問題ないが、例えばRaspberry Piでは標準的にはこの速度で通信出来ない。
- MIDIの通信用には専用の回路が必要である。Arduinoを使う場合は、MIDIシールドなどを用いれば良い。
こういった点から、Arduino上でナイーブ4にMIDI信号を送信するには、例えば次のようにすれば良い。
byte noteOn = {0x90, 0x60, 0x7f};
byte noteOff = {0x90, 0x60, 0x00};
void setup(){
Serial.begin(31250);
}
void loop(){
Serial.write(noteOn, sizeof(noteOn));
delay(1000);
Serial.write(noteOff, sizeof(noteOn));
delay(1000);
}
これで、チャンネル1のドの音を、1秒ごとに鳴らして止めてを繰り返すことが出来る。
が、全てのコマンドについてメッセージの数字を調べてきて16進数に変換してSerial.writeで送るように書いて…というのはあまりに手間である。また、送る方は比較的楽であるが受け取る側は解釈が非常にめんどくさく、実装時にバグを埋め込まないようにするのが大変である。それを避けるために("Don't repeat yourself")Arduino MIDI Libraryを使うのが良いだろう。
Arduino MIDI Libraryの仕事は、内部でメッセージの変換を行った上で31.25 kbpsの通信速度でSerial write/readを行っているということがこの実験から推測され、実際そのとおりである。
MIDIメッセージ
MIDIでやりとりされるMIDIメッセージは、複数バイトから構成されることが主であり、その各バイトは、次に示す画像の通りに分類することが出来る。ステータスバイトはメッセージの開始を表すとともに、以後に何バイトのメッセージが続くか、それらはどういった意味を持つかを表現する。データバイトは、ステータスバイトに従属する具体的なパラメータを表す。
このように多くの分類を持つが、主要なものについてその機能を説明すると(「MIDI チャンネル メッセージ」)
メッセージ | 説明 |
---|---|
ノートオン | ノートを演奏する。 |
ノートオフ | ノートの演奏を停止する。 |
コントロール チェンジ | ペダル、レバー、その他のデバイスからのデータを使って楽音を変更する。ボリュームやバンク セレクトなど、さまざまなコントロールにも使われる。 |
プログラム (パッチ) チェンジ | パッチ番号を割り当てることにより、チャンネルの音色を選択する。 |
アフタータッチ | キーのアフタータッチに従って、個別のノートまたはチャンネルのすべてのノートを変更する。 |
ピッチ ベンド チェンジ | チャンネルで演奏されるすべてのノートのピッチを変更する。 |
である。 |
システムエクスクルーシブ
MIDIメッセージは基本的に固定長のメッセージだが、拡張性を持たせるため、より長い可変長メッセージも送信できるようになっており、それがシステムエクスクルーシブである。システムエクスクルーシブは、必ずF0Hに始まりF7Hに終わり、その間は全てデータバイトである。
こいつの存在によって、MIDIの受信側の取り扱いが難しいことになるが、Arduino MIDI Libraryではこれを気にする必要はそれほどない。
Arduino MIDI Libraryにおけるシステムエクスクルーシブメッセージの具体的な取り扱いについては、アドベントカレンダー次回の記事に回そうと思う[^next]。
Arduino MIDI Libraryの入手
Arduino MIDI Libraryは、ここから入手できる。
他のArduino Library同様に導入すれば良い(例えばここなどを参照)。
Arduino MIDIを使用するための基板
画像はMIDIシールド - Switch Science社より引用。
ArduinoでMIDI通信をするには専用の回路、ないしはArduinoのシリアル通信用AVRのファームウェア書き換えによるUSB-MIDI化が必要である。専用の回路としては、先にも述べたようなMIDIシールド(上図)などを用いれば良い。USB-MIDIが良ければ、例えばここなどを参照のこと。
Arduino MIDI Library の example を読んで見る
Libraryを使う時の基本は、Libraryについているソースコードを読んでみることだろう。ここでは、ついてくるexampleのうち、Libraryの機能を理解するために適する3つについて読んでみる(残りはこれらの延長にすぎない)。理解を促すため、ソースコード中にもともと英語で書かれているコメントを、日本語に訳して書いてある。
これらの解説を通して、Arduino MIDI Libraryの殆どの機能・使い方を網羅する。
MIDI_Basic_IO
まず、Arduino MIDI Libraryについてくるexample中でも最も基本的なソースコードである"MIDI_Basic_IO"を見てみる。
#include <MIDI.h> //MIDIライブラリ使用のためのヘッダファイル読み込み
// MIDIメッセージ送受信の方法についての簡単なチュートリアル
// チャンネル4にメッセージが入ってきたら、1秒間ArduinoのLEDが点灯し、その間ノートが再生される。
MIDI_CREATE_DEFAULT_INSTANCE(); // MIDIクラスのインスタンスとして"MIDI"を生成する。
#define LED 13 // Arduino Uno用のLEDピンの設定
void setup()
{
pinMode(LED, OUTPUT);
MIDI.begin(4); // MIDIインスタンスの初期化、その際チャンネル4のみをlisten
}
void loop()
{
if (MIDI.read()) // (チャンネル4に)メッセージが入ってきたら
{
digitalWrite(LED,HIGH);
MIDI.sendNoteOn(42,127,1); // ノートオン(pitch 42, velo 127 on channel 1)
delay(1000); // 1秒待機
MIDI.sendNoteOff(42,0,1); // ノートオフ
digitalWrite(LED,LOW);
}
}
MIDIライブラリのもっとも基本となる使い方である。上から順番に見ていく。
ヘッダファイル読み込み・インスタンス生成
まず、MIDIライブラリを使う際には最初の2行が必要不可欠である。 #include <MIDI.h>
によってMIDIライブラリの関数を使えるようにする。
次に、MIDI_CREATE_DEFAULT_INSTANCE();
によってArduinoにおけるMIDI通信用に必要なセットアップ(MIDIクラスのインスタンスMIDI
を生成する、詳細は後で)を自動で行う。
この2行はおまじないとして、MIDIライブラリを使用する際は必ず書く必要がある。
MIDI.begin
次にMIDI関係の記述を見ると、MIDI.begin(4);
がある。MIDIの処理を始める際はまず、このMIDI.begin();
が必要であると同時に(つまりこれもおまじない)、この時beginがとる引数(この場合は4)のチャンネルのメッセージがMIDI IN端子から入ってくるのを受け付けて次のMIDI.read
で処理する、という意味も含まれている。MIDI.begin()
の引数は、次のどれかになる。
-
MIDI_CHANNEL_OMNI
5: 全てのチャンネルのメッセージを受信 -
MIDI_CHANNEL_OFF
5: 全てのチャンネルメッセージを受信しない -
1 - 16
の数字どれか: そのチャンネルのメッセージのみを受信・処理する- e.g.)
MIDI.begin(1)
: チャンネル1のメッセージのみを処理
- e.g.)
- 引数なし:
MIDI.begin(1)
と同じ
(補足)MIDI.setInputChannel
setInputChannel
関数を用いると、listenする対象のチャンネルをMIDI.begin()
で設定したチャンネルから変更できる。MIDI.setInputChannel(変更先ch)
(取れる引数はMIDI.begin()
と同じ)で変更可能。
MIDI.read
以上のセットアップのもと、loop()
関数内で実際の処理が行われる。
まず、MIDI.read()
が呼び出されている。MIDI INで受信するメッセージを使用する場合は、必ずloop()
関数内にこれをおく必要がある。MIDI.read()
関数は、まだ処理されていない(MIDI.begin()の引数で指定したチャンネルの)MIDIメッセージがMIDI INに入っていればtrue
、なければfalse
を返す。従って、このexampleのそれ以後の記述は、MIDI INにチャンネル4を対象としたMIDIメッセージが入っていれば処理される内容となる。
(補足) なお、必要があればMIDI.read
の引数に直接チャンネルを指定することで読み込みを行う対象のチャンネルを初期設定から変えられる。MIDI_CHANNEL_OMNI
等も使用可。
e.g.) MIDI.read(1)
のように呼ぶことで、MIDI.begin()
での設定にかかわらずチャンネル1のメッセージのみを処理。
MIDI.sendNoteOn/Off
チャンネル4を対象としたMIDIメッセージが入ってきたあと、1秒間音がなるようなメッセージを送信している。MIDI.sendNoteOn(byte note, byte velocity, byte channel})
関数では、チャンネルchannnel
番、note
の高さの音を、ベロシティvelocity
で鳴らすようなノートオンメッセージをMIDI OUTから送る。MIDI.sendNoteOff()
も同様である。なお、MIDI.sendNoteOff()
においてベロシティ0を指定した場合は、ノートオンメッセージがノートオフ用途で送信される。基本的にMIDI OUTからMIDIメッセージを送信する場合は、それぞれに対応するsend関数群が存在するのでそれを使えば良く、それらの関数は本稿の最後にまとめておく。
MIDI_Callbacks
#include <MIDI.h>
MIDI_CREATE_DEFAULT_INSTANCE();
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
// ノートの鳴り始めにさせたいことを何でも書いて
// できるだけコールバックの中は短くするように、
// でないとloop関数が遅くなり、リアルタイム性能に悪影響が出てしまう
}
void handleNoteOff(byte channel, byte pitch, byte velocity)
{
// ノートの鳴り終わりにさせたいことを何でも
// ベロシティ0のノートオンメッセージもNoteOffとして解釈される(つまりここで処理される)点に注意
}
void setup()
{
// handleNoteOn関数をMIDIライブラリに結びつけ、NoteOn時に呼び出されるようにする
MIDI.setHandleNoteOn(handleNoteOn); // 引数にはハンドラの「関数名」を書く
// NoteOffにも同様
MIDI.setHandleNoteOff(handleNoteOff);
// 全チャンネルを読む形でMIDIインスタンスを初期化
MIDI.begin(MIDI_CHANNEL_OMNI);
}
void loop()
{
// リアルタイム性の確保のため、MIDI.readをできるだけ早く呼ぶように
MIDI.read();
}
ハンドラ
このexampleでは、具体的な処理自体は書かれていないが、ハンドラによる呼び出しの例を示している。MIDIメッセージを受信して処理するタイプのデバイスを作りたい場合、このハンドラを使用したほうがわかりやすい。
MIDIの受信時の処理を書きたい場合、MIDI.read()
を呼び出した際のデータを別の関数で読み込んで来て処理することもできる(次のexampleはそうやって処理をしているし、ネットに落ちている多くのナイーブな受信系ソースコードもそうしている)が、大抵非常で、次のexampleのような例でも無い限り6大変面倒であるためハンドラを使用したほうが良い。ハンドラは、自分が担当するメッセージがMIDI.read()によって読み込まれた時に呼び出される。MIDI_Callbacksでは、ノートオンメッセージ及びノートオフメッセージ時に読み出される関数が宣言されている。したがって、音の鳴り始め(ノートオン)で何をするかをhandleNoteOn
関数の処理として、音の鳴り終わり(ノートオフ)で何をするかをhandleNoteOff
関数の処理として書いてやることで、MIDIメッセージ受信時に、必要な処理をわかりやすく記述することが出来る。なお、これらの関数名はhandleHogeHogeの形である必要はなく、自由につけて良い。
setHandle
ハンドラは、サンプルからもわかるように、setup()関数内において(loop関数内でも良いが)、MIDIメッセージHogehogeに対応するMIDI.setHandleHogehoge(ハンドラ名);
のような、setHandle関数群を使用することで使用出来る。setHandle関数群の引数は、対応する引数の型を持つようなハンドラの"関数名"^pointerである。全てのハンドラの引数の型とそれぞれの役割は、本稿の最後に列挙する。
(補足)コールバックの解除
コールバックを解除し、ハンドラの使用をやめたい場合はdisconnectCallbackFromType
関数を用いる。この関数は引数として、本稿末尾にも記載があるMidiType
の定数を用いる。これは例えば、disconnectCallbackFromType(midi::NoteOn)
のようにmidi::
をつけてやれば良い(あるいは本稿末尾の欄のように、最初の方にUSING_NAMESPACE_MIDI
を記載し、midi
名前空間を使用する)。
MIDI受信処理を行う際の注意
この手の受信して処理するタイプのデバイスを作る際の注意すべき点として、全体的に処理を軽く少なくするよう心がける必要がある。これは、ハンドラ内、loop()
内両方に言えることである。長い処理を書いてしまうと、次のメッセージが届いても処理するまでに遅延が発生し、リアルタイム性が損なわれてしまうためである。
MIDI_DualMarger
#include <MIDI.h>
// MIDIクラスのインスタンスを2つ以上生成し、Dual Margerを作るためのexample.
// midiAとmidiBの2つのMIDI I/Oがあり、midiAとmidiBがそれぞれ入力をそのままTHRUしつつ、
// もう片方からの入力も追加して出力されるようにしている。結果として:
// A out = A in + B in
// B out = B in + A in
#ifdef ARDUINO_SAM_DUE
MIDI_CREATE_INSTANCE(HardwareSerial, Serial, midiA);
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, midiB);
#else
#include <SoftwareSerial.h>
SoftwareSerial softSerial(2,3);
MIDI_CREATE_INSTANCE(HardwareSerial, Serial, midiA);
MIDI_CREATE_INSTANCE(SoftwareSerial, softSerial, midiB);
#endif
void setup()
{
// 全チャンネルをlistenする状態でmidiA, midiB両方を初期化
midiA.begin(MIDI_CHANNEL_OMNI);
midiB.begin(MIDI_CHANNEL_OMNI);
}
void loop()
{
if (midiA.read())
{
// midiA INからの入力は自動的にmidiA OUTにスルーされ、送信される。
// midiBへも同様に同じデータを送信する。
midiB.send(midiA.getType(),
midiA.getData1(),
midiA.getData2(),
midiA.getChannel());
}
if (midiB.read())
{
// midiB INからの入力は自動的にmidiB OUTにスルーされ、送信される。
// midiAへも同様に同じデータを送信する。
midiA.send(midiB.getType(),
midiB.getData1(),
midiB.getData2(),
midiB.getChannel());
}
}
ここでは、MIDIの入出力が1つではない場合などの取り扱いについて、特にMIDI_CREATE_INSTANCE()
マクロについて説明する。
MIDI_CREATE_INSTANCE
通常MIDIライブラリ使用時の関数の頭につくMIDI.
は、MIDIライブラリで定義されているクラスのインスタンスを表している7。このインスタンスは、内部で入出力先に関する情報も持っているため、2つ以上の別々のMIDI IN/OUTが存在する場合、別々にインスタンスを生成する必要がある。
こういった場合に用いるのがMIDI_CREATE_INSTANCE()
マクロで、このマクロの3つの引数は順番に
- ハードウェアシリアル
HardwareSerial
を使用するか、SoftSerialライブラリによって使えるようになるソフトウェアシリアルSoftwareSerial
を使用するか - シリアルポート先のインスタンス。通常のシリアルポートであれば
Serial
、Megaなどに付属するそれ以上のポートはSerial1
,Serial2
,Serial3
, ソフトウェアシリアルであれば生成したそのインスタンス名(通常softSerial
)。 - 生成するMIDIインスタンス名。この例では
midiA
,midiB
である7。
なお、MIDI_CREATE_DEFAULT_INSTANCE()
はMIDI_CREATE_INSTANCE(HardwareSerial, Serial, MIDI)
と等価である(そのように定義されている)。
このようにしてMIDIインスタンスを生成した場合、MIDIメッセージを送受信する際もMIDI.
の代わりに定義したインスタンス名、例えばmidiA.
やmidiB.
を使って関数を使用する必要がある7。MIDI.begin();
も同様に、それぞれのインスタンスについてmidiA.begin();
及びmidiB.begin();
のように全てsetup
内で書かないと使えないことに注意。
(補足)MIDI_CREATE_CUSTOM_INSTANCE
MIDI_CREATE_INSTANCE
以上に詳しい設定ができるインスタンス生成マクロとして、MIDI_CREATE_CUSTOM_INSTANCE(Type,SerialPort,Name,Settings)
がある。これは、最初の3つはMIDI_CREATE_INSTANCE
と同じであるが、最後の1つに詳しい設定をする構造体が入れられる。このSettingsは、以下のような変数からなる構造体である。
型 | 変数名 | 初期値 | 説明 |
---|---|---|---|
static const bool | UseRunningStatus | false |
(送信時)ランニングステータスを用いるか。一部のAVRでバグがあるためtrue にする際は注意。 |
static const bool | HandleNullVelocityNoteOnAsNoteOff | true |
ベロシティ0のノートオンメッセージをノートオフメッセージとして使うかどうか。true でノートオフ化させる |
static const bool | Use1ByteParsing8 | true |
MIDI.read()関数の1回の呼び出しあたり、マイコン自体ののシリアルバッファから1度に1 Byteのみ読み込むようにするか否か。trueで呼び出し1回あたり1 Byteのみの読み込み、falseで、呼び出し時にバッファ内部に入っている最初の1連のMIDIメッセージ全体の読み込み8 |
static const long | BaudRate | 31250 | シリアル通信のボーレート |
static const unsigned | SysExMaxSize | 128 | システムエクスクルーシブで処理できる最大サイズ |
以下のようにすることで、この設定を書き換えてインスタンスを生成できる。
struct MySettings : public midi::DefaultSettings
{
static const unsigned SysExMaxSize = 1024; // Accept SysEx messages up to 1024 bytes long.
};
MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial2, midi, MySettings);
MIDI.read 後の処理
なお、この例のようにMIDI.read()
で読んできたデータはMIDI.getData1()
などで直接値を取り出すことができるが、その後の処理の取り扱いが複雑な場合はコールバック関数を使用したほうが良いのは先に説明したとおりである。
ちなみに、このexampleのままだとシステムエクスクルーシブが異なる送信先に送られない。対処するためには、ハンドラを追加するなどしないといけない。
(補足)MIDI THRU関係
ところで、これはDual "Marger"であって"Swapper"ではないにも関わらず、midiA IN => midiA OUT, midiB IN => midiB OUTの(はいってそのまま出す)処理については明示的に示されていない。コメントにもあるように、MIDIライブラリではINから入ってきたものはそのままOUTに出て行く(thruされる)ようになっている。
もし、明示的にこれを止める必要がある場合にはMIDI.turnThruOff();
をsetup
内で呼び出さなくてはならない。なお、一度OffにしたあとOnにしたければMIDI.turnThruOn();
。
このThruする対象は、setThruFilterMode
関数を使うともう少し詳しく設定することができ、次の表に示された定数9を、例えばFull
ならMIDI.setThruFilterMode(midi::Full)
のようにして呼び出してやることで設定できる(midi名前空間を使用することを宣言していれば、midi::
は不要)。
値 | 説明 |
---|---|
Off | Thruしない |
Full | 全てThruする |
SameChannel | MIDI.begin()で設定されたチャンネルのメッセージ+チャンネル無関係のシステムメッセージのみThruする |
DifferentChannel | MIDI.begin()で設定されたチャンネル以外のチャンネルのメッセージ+チャンネル無関係のシステムメッセージをThruする |
(補足)ライブラリに含まれる関数・定数
本文中に、"後で示す"と書いた関数・定数の一覧を示す。
send関係
それぞれの関数・引数の役割は、名前から推測されたい。sendRealTime
を除いて十分理解可能であろうと思われる(後に補足する)。
関数名(引数の型) |
---|
void sendNoteOn (DataByte inNoteNumber , DataByte inVelocity , Channel |
void sendNoteOff (DataByte inNoteNumber , DataByte inVelocity , Channel inChannel ) |
void sendProgramChange (DataByte inProgramNumber , Channel inChannel ) |
void sendControlChange (DataByte inControlNumber , DataByte inControlValue , Channel inChannel ) |
void sendPolyPressure (DataByte inNoteNumber , DataByte inPressure , Channel inChannel ) |
void sendAfterTouch (DataByte inPressure , Channel inChannel ) |
void sendAfterTouch (DataByte inNoteNumber , DataByte inPressure , Channel inChannel ) |
void sendPitchBend (int inPitchValue , Channel inChannel ) / (double inPitchValue , Channel inChannel ) |
void sendSysEx (unsigned inLength , const byte * inArray , bool inArrayContainsBoundaries =false) |
void sendTuneRequest (void) |
void sendTimeCodeQuarterFrame (DataByte inTypeNibble , DataByte inValuesNibble ) / (DataByte inData ) |
void sendSongPosition (unsigned inBeats ) |
void sendSongSelect (DataByte inSongNumber ) |
void sendRealTime (MidiType inType ) |
void beginRpn (unsigned inNumber , Channel inChannel ) |
void sendRpnValue (unsigned inValue , Channel inChannel ) / (byte inMsb , byte inLsb , Channel inChannel ) |
void sendRpnIncrement (byte inAmount , Channel inChannel ) |
void sendRpnDecrement (byte inAmount , Channel inChannel ) |
void endRpn (Channel inChannel ) |
void beginNrpn (unsigned inNumber , Channel inChannel ) |
void sendNrpnValue (unsigned inValue , Channel inChannel ) / (byte inMsb , byte inLsb , Channel inChannel ) |
void sendNrpnIncrement (byte inAmount , Channel inChannel ) |
void sendNrpnDecrement (byte inAmount , Channel inChannel ) |
sendRealTime
のみ補足する。これは、本稿末尾に記載されている定数群のうちMidiType
として定義されている定数のなかから、さらにリアルタイムメッセージとして適したものを引数として取る。使用時はsendRealTime(midi::Start);
のように、midi::
を頭に付して(あるいは末尾に示すようにUSING_NAMESPACE_MIDI
を宣言しておいて)使用する。
receive関係
read()
MIDI.read()
は、次の順序で処理を行っている(MIDI.hpp 651 - 670行目)。
-
MIDI.read
がOffにされていないか(MIDI_CHANNEL_OFF
ではないか)を確認 - 1つのMIDIメッセージが最後まで届いているかを確認(parse()関数呼び出し)
- MIDIメッセージが読み出し対象と一致しているか確認、一致していればCallback実行、ハンドラの呼び出し
- THRUの実行
従って、ThruとしてMIDI OUTに出力されるよりも前にCallbackが実行されるという順序である。
callback関係
send関係同様、それぞれの引数の役割は、名前から推測されたい。十分理解可能であろうと思われる。なお、変数名を変えてハンドラを作ることは可能であるが、変数の名前だけ順序を入れ替えても役割の順序は変わらないことによく注意せよ。例えばhandleNoteOn(byte note, byte velocity, byte channel)
なんてものを定義してしまうと、note
がチャンネル番号、velocity
が音の高さ(ノート)、channel
が音の強さ(ベロシティ)を意味するカオスで大変見苦しい関数になる。
ところで、ここで定義される関数をSend用の関数と比較して欲しい。変数順もかなり違い、ものによっては関数名すら変えてくる(SystemExclusiveとSysEx)。なぜかこちらはリアルタイムメッセージ別に関数が用意されているし。どうしてこうなったのか。使用するたびキレそうになる。
Callback 関数の設定用関数 | ハンドラ関数の型(引数の型) |
---|---|
void setHandleNoteOff( funcname ) | void handler(byte channel , byte note , byte velocity ) |
void setHandleNoteOn( funcname ) | void handler(byte channel , byte note , byte velocity ) |
void setHandleAfterTouchPoly( funcname ) | void handler(byte channel , byte note , byte pressure ) |
void setHandleControlChange( funcname ) | void handler(byte channel , byte number , byte value ) |
void setHandleProgramChange( funcname ) | void handler(byte channel , byte number ) |
void setHandleAfterTouchChannel( funcname ) | void handler(byte channel , byte pressure ) |
void setHandlePitchBend( funcname ) | void handler(byte channel , int bend ) |
void setHandleSystemExclusive( funcname ) | void handler(byte * array , unsigned size ) |
void setHandleTimeCodeQuarterFrame( funcname ) | void handler(byte data ) |
void setHandleSongPosition( funcname ) | void handler(unsigned beats ) |
void setHandleSongSelect( funcname ) | void handler(byte songnumber ) |
void setHandleTuneRequest( funcname ) | void handler(void) |
void setHandleClock( funcname ) | void handler(void) |
void setHandleStart( funcname ) | void handler(void) |
void setHandleContinue( funcname ) | void handler(void) |
void setHandleStop( funcname ) | void handler(void) |
void setHandleActiveSensing( funcname ) | void handler(void) |
void setHandleSystemReset( funcname ) | void handler(void) |
システムエクスクルーシブ関係
Arduino MIDI Libraryで扱う中では(MIDI一般に於いても)、唯一システムエクスクルーシブのみが可変長メッセージであり、この扱いが多少特殊である。
send
Arduino MIDI Libraryにおけるシステムエクスクルーシブのsend関数はvoid sendSysEx (unsigned inLength , const byte * inArray , bool inArrayContainsBoundaries =false)
である。inLnegth
が配列長、inArray
が実際の送信するシステムエクスクルーシブ命令の配列データである。重要かつ微妙にややこしいのが最後のフラグ変数で、こいつが
-
true
:inArray
は、システムエクスクルーシブの境界を表す、冒頭のF0H, 末尾のF7Hには含まれているものとして扱う。 -
false
:inArray
は、システムエクスクルーシブの境界を表す、冒頭のF0H, 末尾のF7Hには含まれていないものとして扱う。
のであり、しかも指定しない場合の標準値がfalse、含まれていないものである。この点が実はcallbackと噛み合っていない。
callback
Arduino MIDI Libraryにおけるシステムエクスクルーシブの
- ハンドラ関数の設定用関数は
void setHandleSystemExclusive( funcname )
である。sendと違い略さずSystemExclusive
であるから注意。 - ハンドラ関数自体は
void handler(byte * array , unsigned size )
である。array
に受信したエクスクルーシブメッセージの値が、size
に受信したエクスクルーシブメッセージのサイズが入る。
である。ここで重要なのは、ハンドラに渡されるメッセージについて、境界を含むか含まないか、つまりarray[0] == 0xf0
となるかどうかである。実はarray
は境界を含み、array[0] == 0xf0
となる。従って、このハンドラで受け取ったシステムエクスクルーシブメッセージを送信する場合、
void handleSysEx(byte *array, unsigned size){
//Do anything
MIDI.sendSysEx(size, array, true);
}
のように、sendSysEx
関数に渡す際、第三引数にtrue
を忘れずにセットする必要があるし、また受け取ったデータが何かであるかを判断する際にも、境界を含んでいることを忘れず処理する必要がある。。
定数
MidiType
MIDIメッセージの種類を表す定数。 SendRealTime()で使用可能なのは、このうちSystem Real Time(とコメントでも記されている)のもの。
enum MidiType
{
InvalidType = 0x00, ///< For notifying errors
NoteOff = 0x80, ///< Note Off
NoteOn = 0x90, ///< Note On
AfterTouchPoly = 0xA0, ///< Polyphonic AfterTouch
ControlChange = 0xB0, ///< Control Change / Channel Mode
ProgramChange = 0xC0, ///< Program Change
AfterTouchChannel = 0xD0, ///< Channel (monophonic) AfterTouch
PitchBend = 0xE0, ///< Pitch Bend
SystemExclusive = 0xF0, ///< System Exclusive
TimeCodeQuarterFrame = 0xF1, ///< System Common - MIDI Time Code Quarter Frame
SongPosition = 0xF2, ///< System Common - Song Position Pointer
SongSelect = 0xF3, ///< System Common - Song Select
TuneRequest = 0xF6, ///< System Common - Tune Request
Clock = 0xF8, ///< System Real Time - Timing Clock
Start = 0xFA, ///< System Real Time - Start
Continue = 0xFB, ///< System Real Time - Continue
Stop = 0xFC, ///< System Real Time - Stop
ActiveSensing = 0xFE, ///< System Real Time - Active Sensing
SystemReset = 0xFF, ///< System Real Time - System Reset
};
MidiControlChangeNumber
本文中特に触れなかったが、コントロールチェンジの種別ごとの定数も次のように定義されており、定数名でもって使用することが出来る。
enum MidiControlChangeNumber
{
// High resolution Continuous Controllers MSB (+32 for LSB) ----------------
BankSelect = 0,
ModulationWheel = 1,
BreathController = 2,
// CC3 undefined
FootController = 4,
PortamentoTime = 5,
DataEntry = 6,
ChannelVolume = 7,
Balance = 8,
// CC9 undefined
Pan = 10,
ExpressionController = 11,
EffectControl1 = 12,
EffectControl2 = 13,
// CC14 undefined
// CC15 undefined
GeneralPurposeController1 = 16,
GeneralPurposeController2 = 17,
GeneralPurposeController3 = 18,
GeneralPurposeController4 = 19,
// Switches ----------------------------------------------------------------
Sustain = 64,
Portamento = 65,
Sostenuto = 66,
SoftPedal = 67,
Legato = 68,
Hold = 69,
// Low resolution continuous controllers -----------------------------------
SoundController1 = 70, ///< Synth: Sound Variation FX: Exciter On/Off
SoundController2 = 71, ///< Synth: Harmonic Content FX: Compressor On/Off
SoundController3 = 72, ///< Synth: Release Time FX: Distortion On/Off
SoundController4 = 73, ///< Synth: Attack Time FX: EQ On/Off
SoundController5 = 74, ///< Synth: Brightness FX: Expander On/Off
SoundController6 = 75, ///< Synth: Decay Time FX: Reverb On/Off
SoundController7 = 76, ///< Synth: Vibrato Rate FX: Delay On/Off
SoundController8 = 77, ///< Synth: Vibrato Depth FX: Pitch Transpose On/Off
SoundController9 = 78, ///< Synth: Vibrato Delay FX: Flange/Chorus On/Off
SoundController10 = 79, ///< Synth: Undefined FX: Special Effects On/Off
GeneralPurposeController5 = 80,
GeneralPurposeController6 = 81,
GeneralPurposeController7 = 82,
GeneralPurposeController8 = 83,
PortamentoControl = 84,
// CC85 to CC90 undefined
Effects1 = 91, ///< Reverb send level
Effects2 = 92, ///< Tremolo depth
Effects3 = 93, ///< Chorus send level
Effects4 = 94, ///< Celeste depth
Effects5 = 95, ///< Phaser depth
// Channel Mode messages ---------------------------------------------------
AllSoundOff = 120,
ResetAllControllers = 121,
LocalControl = 122,
AllNotesOff = 123,
OmniModeOff = 124,
OmniModeOn = 125,
MonoModeOn = 126,
PolyModeOn = 127
};
midiクラス内で定義された定数の使い方
定数は、
- スケッチのソースコード、
#include <MIDI.h>
後にUSING_NAMESPACE_MIDI
と記述しておく -
midi::NoteOff
のような形でmidi::
を頭に付ける
ことで使うことが出来る。
-
18日目 本来は17日目だったが、完成させられず公開しない間に他の人の記事になりました。遅くなり申し訳ありません。期限は守りましょう。 ↩
-
見たことがありません これらの記事自体を批判するつもりは一切なく、むしろこれらの記事の目的には良くあっている良い記事だと思っています。批判的になっているように見えるのは、あくまで本稿の立ち位置の違いを明確にするためです。似たようなことは、例えば東京大学の清水明教授の『熱力学の基礎』(学部1年の時の教科書だった)などでもされていて、ある意味参考にしています。 ↩
-
再編集したもの といいつつ、かなり加筆しました。 ↩
-
ナイーブ naive: (悪い意味で)素朴な、ばか正直な。naiveに本来"良い"意味はなく、つまり某ボディーソープの名前はダメ。 ↩
-
MIDI_CHANNEL_OMNI/OFF こいつらは、midi_Defs.h内部で
#define MIDI_CHANNEL_OMNI 0
,#define MIDI_CHANNEL_OFF 17 // and over
として定義されている。 ↩ ↩2 -
次のexampleのような例でも無い限り DualMargerがなぜ許されるかというと、これはメッセージの種別に関わらず、判断を挟むこと無くそのまま送信してしまうためハンドラを設定するよりもよっぽど楽だからである。これがシンセを作るみたいに入ってくるメッセージ種別ごとに処理が異なるというような場合、ハンドラを使わないと何をやってるのかわけがわからないコードが生成されかねない(大抵されている)。 ↩
-
インスタンス C言語しか知らない人はクラスを知らないと思われるが、よくよく考えるとインスタンスがそもそもわからないから無意味な記述かもしれない。midiA, midiBのように違う名前のものを作った場合は、
MIDI.
で呼び出す関数の名前がmidiA.
midiB.
から始まるという程度に捉え直せば概ね問題ない。 ↩ ↩2 ↩3 -
Use1ByteParsing 公式Referenceでの、変数の説明だけ読んでもわからない。~~キレそうになった。~~この説明でもわからない場合、MIDI.hpp内 675 - 952行目の
parse()
関数の定義を参照せよ。 ↩ ↩2 -
次の表に示された定数 MIDI Thru用の定数は、midi_Defs.h内部で
enum MidiFilterMode
として定義され、Off = 0, Full = 1, SameChannel = 2, DifferentChannel = 3
である。 ↩