目次 [ Contents ]
僕がDTMを始めた頃はパソコンだけで完結できるような環境はありませんでした。MIDIという規格によって、演奏データと実際の音を分けて鳴らすのが当たり前で、接続に右往左往してました。今ではMIDIは音楽関係の機器やソフトウェアでは必ず互換機能として搭載されてますし、簡単です。端末だけで音楽を作ることもできます。時の流れは恐ろしい。
今回はArduinoを使ってMIDI音源を鳴らしてみます。
“ゲージ判定”を使った、スイッチ読み取りライブラリを作ってみました。
(2017.6.26 追記)
こんな感じです。
この記事を書くにあたりこちらを参考にさせていただきました。
準備
用意するもの
- Arduino(5V駆動)
- モーメンタリスイッチ×5個
- DIN 5ピンコネクタ(MIDIケーブル)
- 抵抗200Ω(220Ωでもいけると思います)
- MIDIインターフェース&MIDI音源
付加的パーツ(無くても問題ないです)
- LED&抵抗
- OLED
DINコネクタ
MIDIケーブルはDIN 5ピンが規格として使われていて、秋月電子やマルツパーツなどパーツショップでも手に入ります。
5つ接続ピンがありますが、結線するのはVCC、GND、通信用(TX)の3つだけです。
MIDIインターフェース・MIDI音源
信号を受信して音を鳴らすためにMIDI機器が必要になります。シンセサイザー、ドラムマシン、電子ピアノや一部のファミリーキーボードなどMIDI IN端子があれば接続できると思います。また、最近はパソコン上でシンセを再現して音を鳴らすシーケンサーソフトも多様にあり、フリーで使えるものもあるので、それで確認する方法もあります。
インストール、細かい使い方は長くなるので割愛しますが、Mac/Windowsどちらでも扱えます。フリーでここまで出来るのは中々スゴイ話ですね。上記のデモビデオの音もこれで鳴らしています。ただし、PCに接続するにはMIDIインターフェースなるものが必要になります。
MIDIとは
MIDIとは1980年代に制定された、電子楽器(主にデジタルシンセサイザーなど)のプロトコルやインターフェイスに関する規格であり、Musical Instrument Digital Interfaceの略になります。
それまでの電子楽器は、それぞれのメーカーが独自の規格で外部からコントロールするような仕様になっていました。ある程度足並みは揃えてはいたけれど、「この鍵盤ではあのシンセの音色は鳴らせない」みたいな状況は普通だったわけです。
それがデジタル機器の台頭に合わせ、そこら辺のバラバラな仕様を統一しようという動きが高まり、MIDIが共通項として誕生することになりました。以来30年以上、音楽の環境には欠かせない存在として今も活躍しています。
MIDIは一言で言えばデジタルデータ版の楽譜です。ただし、楽譜では音符一つで長さを表現しますが、MIDIでは鳴らす信号と止める信号を送って初めて一つの音符となります。そしてそのやり取りをするためのデータ書式や物理的なケーブル配線など全般も含まれます。
MIDI信号の中身
例えば、MIDI鍵盤とシンセサイザーがあったら、MIDI端子を接続するだけで鍵盤でシンセが鳴らせるようになります。これは鍵盤を押した時に「ドの音を鳴らせ」とMIDI端子を通って命令が流れ、シンセ側がそれを受信実行するから実現しています。
一般的なデジタルシンセ・ピアノはほぼサンプラー的な仕組みになっています。ものすごく単純に言うと、ピアノの鍵盤1音づつにCDデッキを用意して、押した台数だけ再生、停止をしているような状態です。なので、根本的にはオンオフの信号を渡すことができれば音が鳴らせます。
MIDIで音を出すために必要な最低限のデータは、
- チャンネル(1~16 CH)
- 音のオンかオフの指定
- 音階(ノートナンバー)
- ベロシティ(音の強弱)
になります。つまり、これらを「MIDIメッセージ」として相手の機器に送信してやればいいのです。
MIDIメッセージ
「MIDI信号の送信」なんていうと難しそうですが、実は、
31.25Kbps (±1%) の非同期方式シリアル転送
による通信なので、フォーマットに沿って「Serial」データを送るだけで実現できます(投げっぱなしの一方通行ですが)。つまり、普段Arduinoで使うSerial.printと同じような手順です。
MIDIメッセージは複数のByteデータをひとつの命令として送ります。具体的には先頭に「ステータスバイト」と呼ばれるヘッダ的な1Byteを送り、その後実データとなる「データバイト」を送ります。つまり1つのMIDIメッセージには表題と本文といった風に、最低2Byte必要になります。
また「ステータス」と「データ」を判別するために各バイトの最大ビットがステータス(表題)の場合は1、データ(本文)の場合は0という決まりになっています。つまり、
ステータスバイトは128~255
データバイトは0~127
の数値内にそれぞれ収まっていないといけません。
ステータスバイトでは下4bitでMIDIチャンネル、残り3bitでデータの種類を指定します。ここら辺は16進数が絡む話なので、わからない方はこちらを読んでみてください。
MIDIでは音階にそれぞれ番号が振り分けられて、Noteはその鍵盤位置の数値になります。
MIDIキーボードで打鍵するように音を出すには、「ノートオン」メッセージと「ノートオフ」メッセージを送ります。
ステータスバイト 1Byte目 | データバイト 2、3Byte | |||
数値範囲 | 上位3bit 8~15 |
下位4bit 0~15 |
2Byte目 0~127 |
3Byte目 0~127 |
ノートオン | 0x90 | MIDI Ch | Note | Velocity |
ノートオフ | 0x80 | MIDI Ch | Note | Velocity |
その他のMIDIメッセージの仕様を知りたい方はこちらやWeb検索で参照してください。
Arduino スケッチ
1音を出す送信
MIDI 2 チャンネルのC-3を鳴らしたければ、
#include <SoftwareSerial.h> SoftwareSerial MIDI(5, 6); // RX, TX //--- pins --- #define LED 13 void setup() { pinMode(LED, OUTPUT); MIDI.begin(31250); } void loop() { // Note on message MIDI.write(0x91); // Status "note on" & "MIDI Ch" MIDI.write(48); // note number MIDI.write(127); // velocity digitalWrite(LED, HIGH); delay(1000); // Note off message MIDI.write(0x81); // Status "note off" & "MIDI Ch" MIDI.write(48); // note number MIDI.write(127); // velocity digitalWrite(LED, LOW); delay(1000); }
ちゃんと接続されていれば、1秒ごとに「ド」の音が出るはずです。思っていた以上に簡単な話ですね。0~15の16進数に対して、MIDIチャンネルは1-16なのに注意してください。CH 2 → 0x91
MIDI信号の送信を関数にして簡略化します。
#include <SoftwareSerial.h> SoftwareSerial MIDI(5, 6); // RX, TX //--- pins --- #define LED 13 //--- MIDI --- #define MIDI_CH 2 #define VELOCITY 127 byte notes = 48; #define MIDI_ON 0x90|(MIDI_CH - 1) #define MIDI_OFF 0x80|(MIDI_CH - 1) void setup() { pinMode(LED, OUTPUT); MIDI.begin(31250); } void loop() { // Note on message SEND_MIDI(MIDI_ON, notes, VELOCITY); digitalWrite(LED, HIGH); delay(1000); // Note off message SEND_MIDI(MIDI_OFF, notes, VELOCITY); digitalWrite(LED, LOW); delay(1000); } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); }
スイッチで音のオンオフ送信
スイッチを使って音のオンオフを制御できるようにします。とりあえず1つのスイッチから。以前紹介したゲージ判定方式でスイッチを機能させます。これなら、チャタリングをプログラミングだけで回避できます。
#include <SoftwareSerial.h> SoftwareSerial MIDI(5, 6); // RX, TX //--- pins --- #define LED 13 #define SW 8 //--- Switch --- #define PUSH_SHORT 700 // judgement value of pushing counter #define SW_LIMIT 710 // counter limit //--- MIDI --- #define MIDI_CH 2 #define VELOCITY 127 byte notes = 48; #define MIDI_ON 0x90|(MIDI_CH - 1) #define MIDI_OFF 0x80|(MIDI_CH - 1) void setup() { pinMode(LED, OUTPUT); pinMode(SW, INPUT_PULLUP); MIDI.begin(31250); } void loop() { short gauge; // pushing counter task while (!digitalRead(SW)) { gauge++; // not to over limit of counter if (gauge > SW_LIMIT) gauge = SW_LIMIT; // action when pushing counter goes in range if (gauge == PUSH_SHORT) { SEND_MIDI(MIDI_ON, notes, VELOCITY); digitalWrite(LED, HIGH); } } // Note off message if (gauge >= PUSH_SHORT) { SEND_MIDI(MIDI_OFF, notes, VELOCITY); digitalWrite(LED, LOW); } } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); }
どうでしょうか?押している間だけ音が鳴るようになったと思います。押している間増加するgauge変数のカウントが、PUSH_SHORTと同じになったときだけノートオンを送ります。Main loop最後で、gauge変数がPUSH_SHORT分あるか(押したか)判定し、ノートオフを送る仕組みです。
じゃあ、このやり方でスイッチを増やそうなんて思っちゃいますが、これだと1つのスイッチしか扱えません。問題はWhile文でgaugeカウントしていることにあります。工夫が必要です。
解決法はこうです「カウントする変数の値を保持する」。
short gauge = 0; void loop() { // pushing counter task if (!digitalRead(SW)) { gauge++; // not to over limit of counter if (gauge > SW_LIMIT) gauge = SW_LIMIT; } else { // Note off message if (gauge >= PUSH_SHORT) { SEND_MIDI(MIDI_OFF, notes, VELOCITY); digitalWrite(LED, LOW); } gauge = 0; } // action when pushing counter goes in range if (gauge == PUSH_SHORT) { SEND_MIDI(MIDI_ON, notes, VELOCITY); digitalWrite(LED, HIGH); } } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); }
考え方はさっきと同じですが、メインループ内の読み取りは一回だけで、何回かの繰り返しの中でアクションを起こす仕組みに変更しています。
digitealRead判定の累計で対応を変えていることがポイントです。とにかくスイッチが押されていたらgauge変数が増加、そうでなかったらカウントリセット、その時gauge変数が貯まっていたらノートオフ送信。そして、gauge変数がPUSH_SHORT定数と均等の時だけノートオン送信。順番がややこしい感じですが、やっていることはそんな難しいことではありません。
これも関数化しちゃいます。
void loop() { byte sw_ret = BUTTON(SW); if (sw_ret == 1) { SEND_MIDI(MIDI_ON, notes, VELOCITY); digitalWrite(LED, HIGH); } else if (sw_ret == 255) { SEND_MIDI(MIDI_OFF, notes, VELOCITY); digitalWrite(LED, LOW); } } short gauge = 0; byte BUTTON(byte num) { byte sw_status = 0; // pushing counter task if (!digitalRead(num)) { gauge++; if (gauge > SW_LIMIT) gauge = SW_LIMIT; // not to over limit of counter } else { if (gauge > PUSH_SHORT) sw_status = 255; gauge = 0; } // action when switch has been pushed if (gauge[num] > PUSH_SHORT) sw_status = 2; else if (gauge == PUSH_SHORT) sw_status = 1; return sw_status; } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); }
スイッチ判定を返り値のある関数にします。押した瞬間を判定したら1、それ以外の押している間は2、離したと判定したら255を返すようにして、その数値でアクションを分けるようにしています。
複数のスイッチに対応して送信
さて、ここまで出来れば複数のスイッチを扱うことができるようになります。単純にスイッチ分だけ変数を配列にすればいいだけです。
#include <SoftwareSerial.h> SoftwareSerial MIDI(5, 6); // RX, TX //--- pins --- #define LED 13 #define PIN_NUM 5 byte pins[PIN_NUM] = {8, 9, 10, 11, 12}; //--- Switch --- #define PUSH_SHORT 700 // judgement value of pushing counter #define SW_LIMIT 710 // counter limit short gauge [PIN_NUM]; //--- MIDI --- #define MIDI_CH 2 #define VELOCITY 127 byte notes [PIN_NUM] = {48, 50, 52, 53, 55}; #define MIDI_ON 0x90|(MIDI_CH - 1) #define MIDI_OFF 0x80|(MIDI_CH - 1) void setup() { pinMode(LED, OUTPUT); for (byte i = 0 ; i < PIN_NUM ; i++) pinMode(pins[i], INPUT_PULLUP); MIDI.begin(31250); } void loop() { boolean led_stat = false; for (byte i = 0 ; i < PIN_NUM ; i++) { byte sw_ret = BUTTON(i); if (sw_ret == 1) { SEND_MIDI(MIDI_ON, notes[i], VELOCITY); } else if (sw_ret == 255) { SEND_MIDI(MIDI_OFF, notes[i], VELOCITY); } else if (sw_ret != 0) led_stat = true; } digitalWrite(LED, led_stat); } byte BUTTON(byte num) { byte sw_status = 0; // pushing counter task if (!digitalRead(pins[num])) { gauge[num]++; if (gauge[num] > SW_LIMIT) gauge[num] = SW_LIMIT; // not to over limit of counter } else { if (gauge[num] > PUSH_SHORT) sw_status = 255; gauge[num] = 0; } // action when pushing counter goes in range if (gauge[num] > PUSH_SHORT) sw_status = 2; else if (gauge[num] == PUSH_SHORT) sw_status = 1; return sw_status; } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); }
ピンの数”PIN_NUM”をdefineして、その分だけ必要な変数を配列にします。接続したスイッチのピン番号(pins)、MIDIノート番号(notes)、それぞれの判定に使うgauge変数など。PIN_NUMとピン番号の数、それに対応するMIDIノート番号の数が合致すれば、Arduinoで扱える分だけスイッチは簡単に追加できます。
ちなみにLEDの明滅を、押しているかどうかに依存するよう変更しています。
モニタリング&チューニング
最後に、一応OLEDへデータが表示できるようにします。また、スイッチのgaugeカウンターのチューニングも行います。
#include "U8glib.h" U8GLIB_SSD1306_128X32 u8g(U8G_I2C_OPT_NONE); // I2C / TWI //U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE|U8G_I2C_OPT_DEV_0); // I2C / TWI //U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0|U8G_I2C_OPT_NO_ACK|U8G_I2C_OPT_FAST); // Fast I2C / TWI //U8GLIB_SSD1306_128X64 u8g(13, 11, 10, 9, 8); // SW SPI Com: SCK = 13, MOSI = 11, CS = 10, A0 = 9 #include <SoftwareSerial.h> SoftwareSerial MIDI(5, 6); // RX, TX //--- pins --- #define LED 13 #define PIN_NUM 5 byte pins[PIN_NUM] = {8, 9, 10, 11, 12}; //--- Switch --- #define PUSH_SHORT 700 // judgement value of pushing counter #define SW_LIMIT PUSH_SHORT + 10 // counter limit short gauge [PIN_NUM]; short gauge_ref[PIN_NUM]; //--- oled display --- byte oledx, oledy; byte h_txt; //--- MIDI --- #define MIDI_CH 2 #define VELOCITY 127 byte notes [PIN_NUM] = {48, 50, 52, 53, 55}; #define MIDI_ON 0x90|(MIDI_CH - 1) #define MIDI_OFF 0x80|(MIDI_CH - 1) void setup() { // oled setup u8g.setFont(u8g_font_6x10r); u8g.setColorIndex(1); oledx = u8g.getWidth(); oledy = u8g.getHeight(); h_txt = u8g.getFontAscent(); // pins setup pinMode(LED, OUTPUT); for (byte i = 0 ; i < PIN_NUM ; i++) pinMode(pins[i], INPUT_PULLUP); //midi setup MIDI.begin(31250); Serial.begin(38400); } void loop() { SW_TASK(); DRAW(); } void DRAW() { u8g.firstPage(); do { for (byte i = 0 ; i < PIN_NUM ; i++) { u8g.setPrintPos((oledx / PIN_NUM) * i, oledy - (h_txt * 2)); if (gauge[i] != 0) u8g.print("-"); else u8g.print(notes[i]); // gauge count display u8g.setPrintPos((oledx / PIN_NUM) * i, oledy); u8g.print(gauge_ref[i]); SW_TASK(); } } while (u8g.nextPage()); } void SW_TASK() { boolean led_stat = false; for (byte i = 0 ; i < PIN_NUM ; i++) { byte sw_ret = BUTTON(i); if (sw_ret == 1) { SEND_MIDI(MIDI_ON, notes[i], VELOCITY); } else if (sw_ret == 255) { SEND_MIDI(MIDI_OFF, notes[i], VELOCITY); } else if (sw_ret != 0) led_stat = true; } digitalWrite(LED, led_stat); } byte BUTTON(byte num) { byte sw_status = 0; // pushing counter task if (!digitalRead(pins[num])) { gauge[num]++; gauge_ref[num] = gauge[num]; if (gauge[num] > SW_LIMIT) gauge[num] = SW_LIMIT; // not to over limit of counter } else { if (gauge[num] > PUSH_SHORT) { sw_status = 255; } if (gauge[num] > 0) SERIAL_MON(); gauge[num] = 0; } // action when pushing counter goes in range if (gauge[num] > PUSH_SHORT) sw_status = 2; else if (gauge[num] == PUSH_SHORT) sw_status = 1; return sw_status; } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); } void SERIAL_MON() { for (byte i = 0 ; i < PIN_NUM ; i++) { Serial.print(i); Serial.print(":"); Serial.print(gauge_ref[i]); Serial.print(" "); } Serial.println(); }
u8glibのディスプレイ設定は環境に合わせて変更します。使い方が分からなければこちらをご覧ください。また、ディスプレイを用意しなくてもシリアルモニタにも数値が出るので、そちらで確認出来ます。
ところで、急に音が出なくなったと思います。これはスケッチが長くなったため、カウンターの増加するインターバルが伸びたためです。試しに長押してみてください。大分遅れて音が出るようになっています。
#define PUSH_SHORT 700 // judgement value of pushing counter #define SW_LIMIT PUSH_SHORT + 10 // counter limit
PUSH_SHORTの定数を自分の押したカウントにあわせます。自分は10くらいでいいかなと思います。ちなみに1ケタ台はチャタリングかと思います。
//--- MIDI --- #define MIDI_CH 2 #define VELOCITY 127 byte notes [PIN_NUM] = {48, 50, 52, 53, 55};
MIDIに関する設定はここを変更できます。チャンネルはCH。残念ながらオンオフしか扱えないので、ベロシティは単一でしか変更できません。notesはオクターブC-3のドレミファソになっています。MIDIでは10CHがドラム音色と相場が決まっているので、下記のように変えれば、その音が出ると思います。
//--- MIDI --- #define MIDI_CH 10 #define VELOCITY 127 byte notes [PIN_NUM] = {36, 38, 42, 51, 49}; // kick, snare, Hi-Hat, Ryde, Crash
まとめ
ArduinoでMIDIを扱うにはMIDI用のシールドを手に入れて、MIDIライブラリを使うのが常套手段だとは思いますが、ケーブルとSoftwareSerialだけでもこれだけ出来ました。もっと深く掘り下げていけば、ひょっとしたら自作のフィジカルコントローラみたいのもできるかもしれません。
スケッチ:MIDIボタンx5
#include "U8glib.h" U8GLIB_SSD1306_128X32 u8g(U8G_I2C_OPT_NONE); // I2C / TWI //U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE|U8G_I2C_OPT_DEV_0); // I2C / TWI //U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0|U8G_I2C_OPT_NO_ACK|U8G_I2C_OPT_FAST); // Fast I2C / TWI //U8GLIB_SSD1306_128X64 u8g(13, 11, 10, 9, 8); // SW SPI Com: SCK = 13, MOSI = 11, CS = 10, A0 = 9 #include <SoftwareSerial.h> SoftwareSerial MIDI(5, 6); // RX, TX //--- pins --- #define LED 13 #define PIN_NUM 5 byte pins[PIN_NUM] = {8, 9, 10, 11, 12}; //--- Switch --- #define PUSH_SHORT 10 // judgement value of pushing counter #define SW_LIMIT PUSH_SHORT + 10 // counter limit short gauge [PIN_NUM]; //--- oled display --- byte oledx, oledy; //--- MIDI --- #define MIDI_CH 10 #define VELOCITY 127 byte notes [PIN_NUM] = {36, 38, 42, 51, 49}; // kick, snare, Hi-Hat, Ryde, Crash //byte notes[PIN_NUM] = {60, 62, 64, 65, 67}; // key C-D-E-F-G #define MIDI_ON 0x90|(MIDI_CH - 1) #define MIDI_OFF 0x80|(MIDI_CH - 1) void setup() { // oled setup u8g.setFont(u8g_font_6x10r); u8g.setColorIndex(1); oledx = u8g.getWidth(); oledy = u8g.getHeight(); // pins setup pinMode(LED, OUTPUT); for (byte i = 0 ; i < PIN_NUM ; i++) pinMode(pins[i], INPUT_PULLUP); //midi setup MIDI.begin(31250); } void loop() { SW_TASK(); DRAW(); } void DRAW() { u8g.firstPage(); do { for (byte i = 0 ; i < PIN_NUM ; i++) { u8g.setPrintPos((oledx / PIN_NUM) * i, oledy / 2); if (gauge[i] != 0) u8g.print("-"); else u8g.print(notes[i]); SW_TASK(); // switch count while drawing } } while (u8g.nextPage()); } void SW_TASK() { boolean led_stat = false; for (byte i = 0 ; i < PIN_NUM ; i++) { byte sw_ret = BUTTON(i); if (sw_ret == 1) { SEND_MIDI(MIDI_ON, notes[i], VELOCITY); } else if (sw_ret == 255) { SEND_MIDI(MIDI_OFF, notes[i], VELOCITY); } else if (sw_ret != 0) led_stat = true; } digitalWrite(LED, led_stat); } byte BUTTON(byte num) { byte sw_status = 0; // pushing counter task if (!digitalRead(pins[num])) { gauge[num]++; if (gauge[num] > SW_LIMIT) gauge[num] = SW_LIMIT; // not to over limit of counter } else { if (gauge[num] > PUSH_SHORT) { sw_status = 255; } if (gauge[num] > 0) SERIAL_MON(); gauge[num] = 0; } // action when pushing counter goes in range if (gauge[num] > PUSH_SHORT) sw_status = 2; else if (gauge[num] == PUSH_SHORT) sw_status = 1; return sw_status; } void SEND_MIDI(byte cate, byte note, byte velo) { byte data[3] = {cate, note, velo}; for (byte i = 0 ; i < 3 ; i++) MIDI.write(data[i]); }