USB-MIDI I/FをArduinoで自作する

 PCからMIDI outしたいため、安物中華USB-MIDI I/Fを買ってみたものの、届いた個体は動作しませんでした。
wave.hatenablog.com

 そこで、手元のパーツで自作できないかと思って調べてみると、Atmega 32u4を積んだArduino互換機で簡単に作れそうですので、USB-MIDI I/F(MIDI out専用/MIDI in未実装)を作ってみました。

!!警告!!
本投稿を参考に機器を製作し、直接・間接を問わずいかなる損失・障害等が発生しても私は責任を負いません。
実行する場合は全て自己責任となることをご理解ください。

 

ATmega 32u4

 マイクロコントローラにATmega 32u4を積んだArduino互換ボードは、USB HID(マウスやキーボードなどの入力装置)として動作させることができます。その目的のため過去にSparkFunのProMicro互換ボードを購入して、利用したことがあります*1
 Arduinoで(USBではない普通の)MIDIシグナルを送受するのは、MIDI自体が単なるシリアル通信なので簡単に実装できます。ですがUSBとなると、USB周りの処理や、ホスト側のPCのドライバをどうするんだとか、そもそもArduinoでできるのかと良く解りませんでした。調べてみると、どうやらATmega 32u4搭載機ならUSB HIDだけではなく、Audio Class CompliantなUSB MIDIバイスも作れるようで、そのためのArduino純正ライブラリまで存在するようです。

 とりあえず、私がしたいのはUSBからMIDI出力したい(MIDI out)だけなので、必要になるのはATmega 32u4搭載Arduino互換基板と、MIDIのDIN5ピンコネクタ、220Ω抵抗×2だけで、全て手元にストックがあったので勢いだけで作ってみました。
 

MIDIUSBライブラリ

 以下の、Arduino公式のライブラリを使用します。Arduino IDEのライブラリマネージャからインストールできます。
GitHub - arduino-libraries/MIDIUSB: A MIDI library over USB, based on PluggableUSB

 このライブラリは3つの関数で構成されていますが、ドキュメントは非常に簡素で、これだけ見てもちょっと戸惑います。
Arduino - MIDIUSB

 まず、予めMIDIそのもののデータフォーマットを理解している必要があります。MIDIバイスのマニュアルの末尾でよく見かけるアレです。
Summary of MIDI Messages

 加えて、Universal Serial Bus Device Class Definition for MIDI Devicesも理解していないと一部の実装が困難です。私はこれを知らなかったので、強引にコード書いた後に見つけて、あーそういうことだったの。みたいな感じになりました。
Universal Serial Bus Device Class Definition for MIDI Devices(PDF) - USB.org

 具体的な関数の説明を読んでみると以下のように説明されています。(※ここでのreadやsendの主語はマイクロコントローラで、PCではありません)

  • MidiUSB.read()
    ドキュメントによれば、「USB側から読み出した値を、midiEventPacket_t構造体に入れて返します。この関数でPCからノートON/OFFを受けて発音するようなMIDI INデバイスを作れます。」的な説明しか書いてありません。
  • MidiUSB.sendMIDI(midiEventPacket_t event)
    ドキュメントによれば、「MIDIエンコードされたパケットをPCに送信できます。作曲のために入力を読みだしてエンコードしたデータをソフトシンセに転送するのに使えます」的な説明だけ書かれています。
  • MidiUSB.flush()
    ドキュメントによれば、「強制的にUSBレイヤーにデータを速やかに送信させます。USBバスはリアルタイムではないため、このflush()関数をsendMIDI()関数の直後に実行しないと、正しいタイミングでデータが送信されることを保障しません。」的な説明が書かれています。

※今回はPCからMIDI outしたいだけなので、read()のみ使用し、sendMIDI()とflush()は未使用。

 midiEventPacket_t構造体の説明が無いと、さっぱりなんですが?というわけで、サンプルコードを見てみましたが、特に説明はありません。ネットで使用例を検索してみると多く見つかりますが、私がやろうとしているようにPCからMIDI outに吐き出す実装例は見つけられませんでした。
 サンプルコードから判ったことと言えば、headerにデータ有無が格納されており、そのあとにMIDIデータそのものが格納されているような雰囲気は掴めました。が、詳細が分からないので、ライブラリのソースを確認してみます。MIDIUSB.hからincludeされるMIDIUSB_Defs.hに定義がありました。

typedef struct
{
	uint8_t header;
	uint8_t byte1;
	uint8_t byte2;
	uint8_t byte3;
} midiEventPacket_t;

 header+3バイトで決め打ちされてます。MIDIメッセージは、Note On/Offなどは3バイト、プログラムチェンジなどは2バイト、MMC系のリアルタイムメッセージなどは1バイトと決まっているので、これらについては問題なくこの構造体内に格納できます。が、System Exclusiveメッセージは可変長です。GM/GS/XGリセットや、シンセサイザーのパラメータや、エフェクトパラメータのエディットに使う機器固有のメッセージ送りたいんですけど、3バイトより長いメッセージは扱えないのかと思って調べてみたところ、以下のISSUEがヒントになりました。
Receivng Sysex and Sysex Raw MIDI messages in MIDIUSB · Issue #48 · arduino-libraries/MIDIUSB · GitHub
 複数のパケットを組み合わせて、System Exclusiveメッセージを組み立てられるというコメント共に、headerと0x0Fの論理積をswitchで分岐してMIDIメッセージの種類分けをしているコードが掲載されています。この論理積が0x04の場合System Exclusiveメッセージの開始だと書かれていますが、headerについての説明もMIDIUSBライブラリのドキュメントになく、何故そうだと言えるのか(当時の私には)理解できませんでした。後に、前述のUniversal Serial Bus Device Class Definition for MIDI DevicesのTable 4-1: Code Index Number Classificationsに由来するものだと気づくことになるのですが、既に動作するコードを書いた後なので使ってません。
 というわけで、headerは中身が定かではない(と当時は思った)のでサンプルコード同様、構造体内のデータの有無の確認のみに使い、実際のデータbyte1,2,3の中身を評価して複数パケットに分割された可変長のSystem Exclusiveメッセージに対応することにしました。
 

コード

#include <MIDIUSB.h>

midiEventPacket_t midi_out;
bool flgSysExRemain = false;

void setup() {
  // Serial console for debug
  Serial.begin(38400);
  // MIDI
  Serial1.begin(31250);
}

void loop() {
  // read midi data from usb
  midi_out = MidiUSB.read();

  // send midi traffic to physical midi port
  if(midi_out.header !=0){
    // output the data 
    if (flgSysExRemain){
      if((midi_out.byte1 != 0xF7) && (midi_out.byte2 != 0xF7) && (midi_out.byte3 != 0xF7)){
        sendMIDI(midi_out.byte1, midi_out.byte2, midi_out.byte3);
      }else if((midi_out.byte1 != 0xF7) && (midi_out.byte2 != 0xF7) && (midi_out.byte3 == 0xF7)){
        sendMIDI(midi_out.byte1, midi_out.byte2, midi_out.byte3);
        flgSysExRemain = false;
      }else if((midi_out.byte1 != 0xF7) && (midi_out.byte2 == 0xF7)){
        sendMIDI(midi_out.byte1, midi_out.byte2);
        flgSysExRemain = false;
      }else if((midi_out.byte1 == 0xF7)){
        sendMIDI(midi_out.byte1);
        flgSysExRemain = false;
      }
    }
    // Channel Messages
    else if( (midi_out.byte1 >> 4 == 0b1000) // Note On
    || (midi_out.byte1 >> 4 == 0b1001)  // Note Off
    || (midi_out.byte1 >> 4 == 0b1010)  // Polyphonic AfterTouch
    || (midi_out.byte1 >> 4 == 0b1011)  // Control Change
    || (midi_out.byte1 >> 4 == 0b1110) )  // Pitch Bend
    {
      sendMIDI(midi_out.byte1, midi_out.byte2, midi_out.byte3);
    }
    else if( (midi_out.byte1 >> 4 == 0b1100) // Program Change
    || (midi_out.byte1 >> 4 == 0b1101) ) // Channel AfterTouch
    {
      sendMIDI(midi_out.byte1, midi_out.byte2);
    }
    
    // System Common Messages
    else if(midi_out.byte1 == 0xF0) // System Exclusive
    {
      if((midi_out.byte2 != 0xF7)){
        sendMIDI(midi_out.byte1, midi_out.byte2, midi_out.byte3);
        flgSysExRemain = true;
      }else{  // This pattern (F0 F7) will not occur, ManufactureID and data should be exist between F0 and F7
        sendMIDI(midi_out.byte1, midi_out.byte2);
      }
    }
    else if( (midi_out.byte1 == 0b11110001) // MIDI Time Code
    || (midi_out.byte1 == 0b11110011) )     // Song Select
    {
      sendMIDI(midi_out.byte1, midi_out.byte2);
    }
    else if(midi_out.byte1 == 0b11110010) // Song Position Pointer
    {
      sendMIDI(midi_out.byte1, midi_out.byte2, midi_out.byte3);
    }
    else if( (midi_out.byte1 == 0b11110100) // Undefined (Reserved)
    || (midi_out.byte1 == 0b11110101)       // Undefined (Reserved)
    || (midi_out.byte1 == 0b11110110)       // Tune Request
    || (midi_out.byte1 == 0b11110111) )     // End Of Exclusive (normally not match here, treated with flgSysExRemain)
    {
      sendMIDI(midi_out.byte1);
    }
    
    // System Real-Time Messages
    else if( (midi_out.byte1 == 0b11111000) // Timing Clock
    || (midi_out.byte1 == 0b11111001)       // Undefined (Reserved)
    || (midi_out.byte1 == 0b11111010)       // Start
    || (midi_out.byte1 == 0b11111011)       // Continue
    || (midi_out.byte1 == 0b11111100)       // Stop
    || (midi_out.byte1 == 0b11111101)       // Undefined (Reserved)
    || (midi_out.byte1 == 0b11111110)       // Active Sensing
    || (midi_out.byte1 == 0b11111111) )     // Reset
    {
      sendMIDI(midi_out.byte1);
    }
  }
}

// Send 3 byte MIDI message
void sendMIDI(byte statusByte, byte dataByte1, byte dataByte2) {
  Serial1.write(statusByte);
  Serial1.write(dataByte1);
  Serial1.write(dataByte2);
  
  #ifdef DEBUG
  char buf[3];
  formatByte(statusByte, buf);
  Serial.print(buf);
  formatByte(dataByte1, buf);
  Serial.print(buf);
  formatByte(dataByte2, buf);
  Serial.println(buf);
  #endif
}

// Send 2 byte MIDI message
void sendMIDI(byte statusByte, byte dataByte) {
  Serial1.write(statusByte);
  Serial1.write(dataByte);
  
  #ifdef DEBUG
  char buf[3];
  formatByte(statusByte, buf);
  Serial.print(buf);
  formatByte(dataByte, buf);
  Serial.println(buf);
  #endif
}

// Send single byte MIDI message
void sendMIDI(byte statusByte) {
  Serial1.write(statusByte);
  
  #ifdef DEBUG
  char buf[3];
  formatByte(statusByte, buf);
  Serial.println(buf);
  #endif
}

void formatByte(byte data, char *formattedChar){
  sprintf(formattedChar, "%02x", data);
}

 簡単な解説は以下の通り。

  • readして得られたmidiEventPacket_t構造体のheaderが0でなければ(MIDIデータがPCから送出されていれば)、当該データをシリアルポートに垂れ流す
  • (System Exclusiveメッセージを除き、)各MIDIメッセージのステータスバイトの種類によってデータ長が決まるので、ステータスバイトを評価して、得られた構造体の何バイト目までをシリアルポートに流すかを決定
    • (Universal Serial Bus Device Class Definition for MIDI Devicesを事前に理解していれば、ステータスバイトを直接評価せず、headerの値で処理分岐可能
  • System Exclusiveメッセージの開始(F0)を検出したら、midiEventPacket_t構造体の各byteにSystem Exclusiveメッセージの終了(F7)を検出するまで、そのままシリアルポートに垂れ流す
  • 分割されたSystem Exclusiveメッセージ処理中には、ステータスバイトに応じたデータ長決定ロジックを通らないようにフラグを立てる
    • MIDIの仕様上、System Exclusiveメッセージ中(F0~F7)にその他のMIDIメッセージが割り込むことは無い

 全てのデータパターンについてテストを行ったわけではありませんが、NoteOn/Offやコントロールチェンジ、任意バイト数のSystem Exclusiveメッセージが正常にシリアルコンソールに表示され、後述の回路でMIDI outから出力されMIDIバイスで正常に受信(発音・パラメータ変更等)されることは確認しています。
 

回路

 今回はMIDI outだけ実装していますので、外付け部品は抵抗だけの簡単な回路です。

  • ProMicroのハードウェアシリアル出力pin(TX)から220Ω抵抗を介して、DIN 5pinコネクタの5pinに接続
  • ProMicroのVccから220Ω抵抗を介して、DIN 5pinコネクタの4pinに接続
  • ProMicroのGNDから、DIN 5pinコネクタの2pinに接続
作ったもの

 ProMicro自体はこんな感じの小さな基盤ですが、これ自体を部品として使っているようなイメージです。
f:id:kachine:20200613135435j:plain 

 ユニバーサル基板にDIN5PinコネクタとProMicroを実装します。
 なお、ProMicroのmicroUSB端子は構造上、基板から外れて壊れやすいと言われています*2。そこで、コネクタに加わる応力を分散させるべく、ProMicro自体の基盤とユニバーサル基板でmicroUSBコネクタを挟み込んでみることにしました。このため、ProMicroはピンヘッダも使わず、裏返った状態で抵抗の足を使って直接はんだ付けしています。
f:id:kachine:20200613135628j:plain

 思い付きだけでProMicroの配置を決定したり、横着して抵抗の足をそのまま最短距離でDINコネクタまで引き回した結果、DINコネクタとProMicroとの配線がクロスせざるを得なくなってしまいました。このため、抵抗の足に熱収縮チューブをかぶせて短絡防止としています(熱収縮チューブが十分に縮んでいません)。
 なお、適切な配線材料が無かったので、GNDはまだ結線していませんが、相手側の機器がMIDI仕様に沿って適切にフォトカプラで絶縁されていればとりあえずは問題なく動作します。
f:id:kachine:20200613140216j:plain
 

雑感

 作ると決めてから要した時間は実質半日程度で、こんなやっつけで製作したものでも、きちんと動作しています。
 材料費は1000円前後でしょうか。半分以上はProMicroの価格ですから、安く調達できればもっと安価になります。eBayや海外通販だと安価ですが届くまでに1か月前後かかることが多いですし、Amazon.co.jpマーケットプレイスで複数まとめ売り(1個当たりの単価が安い)で国内発送の業者を選ぶといいかもしれません。

 なお、ユニバーサル基板上には広大な空きパターンが残っていますので拡張性も十分あります。MIDI IN用のフォトカプラを配置したり、ロータリーエンコーダーなどを追加してMIDIコントローラ機能を追加することも簡単にできます。
 反省としては、Universal Serial Bus Device Class Definition for MIDI Devicesを知らずにステータスバイトで分岐したり、コネクタ保護だけを優先した部品配置で結線が汚い等、よく考えてから作らないと醜いものになることをソフトウェア・ハードウェア両面から再確認したのでした。
 



以上。

*1:インターバル撮影した数百枚の画像のホワイトバランスを特定の座標を基に自動調整させるため、Adobe Lightroomで画像中の特定の座標を指定してオートホワイトバランスを作動させる処理(すなわち一連のキーボードショートカットやマウス操作を繰り返す処理)に使ったりました。

*2:まだ私のProMicroでコネクタが取れてしまったことはありませんが、他のArduino互換基板でmicroUSB端子がとれてしまい、修復してもすぐ取れるようになってしまった経験があります。