蛇腹楽器型MIDIコントローラーV2 ～ USB MIDIデバイスの作り方

[前回紹介した蛇腹楽器型MIDIコントローラー](https://qiita.com/tomoto335/items/92580af26b5c171dc1c0)をV2に進化させた。

1. Plug-and-play、つまりデバイスをUSBでPCにつなぐだけでMIDIデバイスとして見え、PC上で特別なソフトウェアを動かすことなくDAWから直接使うことができるようにした。いわゆるUSB MIDI class compliantである。

2. ミュートや感度設定ための操作盤を付けた。PC上のサポートアプリケーションがなくなって「感度調整はコードを書き換えてくれ」「ミュートは画面上のボタンを押してくれ」といった手は通用しなくなったため、その代替手段である。

3. 外気圧測定用センサーを付けた。内圧用センサーのみだとどうしても演奏中にキャリブレーションがずれてくるので、外気圧を常に測定して内圧との差を使うようにした。

外見:
<img src="https://lh3.googleusercontent.com/pw/AL9nZEXCbEcDx-McY3_UK4pV-s96ERzuaAbXqOLV0pv3qz2Xn05kcqLkEX4New0XQY4HCzZfdCAMDrQWzowxAAo4bhPDy682Om1xhrsqp3mtf5a2siBzIE_2vCE-dzPlnCx_dllVGxZtR9wccsYPcMQi8u6_aQ=w640-h628-no" />
<img src="https://lh3.googleusercontent.com/pw/AL9nZEXIbX34WAjwTn_HjbeXBtcQ6Du9_5-mTuQG7ykDocY1K2nJuRxLGnc6baAyo3P_KlAdSGcC-weYGPpkXYEwmWZOD4YPGwNPD2IZunLdP3JUMFVXMOWqGY9IDca0JgnJ2yI3e4gqdrPGVqbWT09PUsb_ng=w640-h373-no" />

操作盤のデモ:
<iframe width="560" height="315" src="https://www.youtube.com/embed/DyZMNsaLk7w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

1点目と2点目については(ドライバや設定用のソフトウェアを必要としない)完全スタンドアロンのMIDI楽器を作ってみるという技術的チャレンジであり、見た目がかっこいいとかアガるとかいった気分的な点を除けば、V1に対する機能的な優位性は実はほとんどない。できることや性能はほぼ同じで、自分で使う分にはPC上で専用ソフトウェアを走らせることはまったく問題にならないので、むしろデバイス側が複雑になって問題が起こりやすい・対処しづらいというデメリットの方が大きいくらいである。感度調整が音を出しながら楽器上のつまみでできるというのは意外と便利、くらいだろうか。

とはいえ、技術チャレンジとしては興味深いものだったので、得られた知見をシェアすれば人の役に立つこともあろうと、本記事では1点目のUSB MIDIデバイスの作り方にフォーカスして解説してみたい。検索しても作例をあまり見ないし、ハマりどころも結構あったので、どこかの誰かの参考になれば幸いである。

# USB MIDIデバイスの作り方

## ESP32-S2の購入

筆者は当初「USBコネクタの付いているArduinoボードは、簡単に任意のUSBデバイス(例えばキーボードのようなHID, Human Interface Device)としてプログラムできる」となんとなく思っていたが、これは間違いだった。確かに、例えば[M5StickCの回路図](https://docs.m5stack.com/en/core/m5stickc)を見るとUSBコネクタはUSB-UART変換チップにハードワイヤされている、つまりこのUSBコネクタはUSBシリアルポートとしてしか動作しないよう運命づけられていることがわかる。[さらに調べると](https://esp32.com/viewtopic.php?t=22114)、ESP32で任意のUSBデバイスを作るにはUSBコントローラーというものが搭載されている機種が必要で、ESP32-S2やS3といったものがそれに当たるということがわかった。

とりあえずやってみなければわかるまい、というわけで[$6程度のESP32-S2ボード](https://www.amazon.com/dp/B0B291LZ99)をAmazonでゲット。
<img src="https://www.wemos.cc/en/latest/_static/boards/s2_mini_v1.0.0_1_16x16.jpg" width=160>
調べるとWemos社のLolin S2 Mini相当品らしく、

https://www.wemos.cc/en/latest/s2/s2_mini.html

ArduinoのESP32ボードマネージャの最新版にも選択肢が存在した。よき。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/256f9903-08cf-1c2a-bcdf-70f8ad8b8033.png)

## プログラムの書き込み

手始めに`Serial`に何かを出力するhello, world的なプログラムを動かしてみよう。USBをシリアルポート(CDC, Communication Device Class)として設定する処理は、ボード設定で "USB CDC On Boot" をEnabledにしてあれば暗黙に行われ、自分で書く必要はないようだ。なのでこれだけ↓でよい。

```
void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.printf("hello, world %d\n", millis() / 1000);
  delay(1000);
}
```

プログラムを書き込むには、ボード上の0ボタンを押しながら(GPIO0をLOWにした状態で)リセットし、プログラム書き込みモードで起動する必要がある。筆者は[昔ESP8266を生で使っていた](https://tomoto335.hatenablog.com/entry/esp-wroom-02/7)のでお馴染みの方法だ。

するとWindows上でﾋﾟﾎﾟｯとデバイス接続音がして、デバイスマネージャーで見ると下図(a)のように "ESP32-S2" が不明なデバイスとして見える。何かドライバが足りないのかと一瞬焦るのだが、よく見るとシリアルポートの方にも(b)が増えており問題ない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/cc4e6d94-7389-e0c9-8d64-0cc21b64d9af.png)
Arduino開発環境から(b)を書き込み先として指定してプログラムを書きこんでみると、このようになるはずだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/cd0f2019-d34f-a2bf-f74c-6e1450f10660.png)
書き込みは成功しているのにエラーメッセージが出ていることに注意。「自動リセットできないのでアプリを実行するには手でリセットしろ」と警告が出ているのでそのせいかもしれない。ここは騙されず、書き込みが100%まで到達し、成功メッセージ "Hash of data verified" が出ているかをいちいち確認する必要がある。地味にウザいが、直す方法を探し当てるのも面倒な微妙な問題である。

メッセージの指示通りに手でリセットボタンを押すと、またﾋﾟﾎﾟｯとデバイス切断音と接続音がして、プログラム書き込み用シリアルポートとは別のアプリケーション用ポートが表れる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/a39da48a-1ad5-300e-65c3-cc86e314da31.png)
このポートを端末ソフトウェアで開き、アプリケーションの出力が表示されば最初の一歩は成功である。(もしArduinoのシリアルモニタやシリアルプロッタを使うのなら、ボードの種別をちゃんとLolin S2 miniに設定しておいた方がよい。USBシリアルにボード種別など関係ないはずだとは思うのだが、筆者の環境ではM5StickCを選んだままだとデータが見えなかったことがある。)

## MIDIデバイスの作成

USBをシリアルポート以外のデバイスとして見せるには、TinyUSBというライブラリを使う必要がある。みんな大好き[Adafruitによる移植版](https://github.com/adafruit/Adafruit_TinyUSB_Arduino)がよいだろう。他に[EspTinyUsb (ライブラリ名はESP32TinyUSB)](https://github.com/chegewara/EspTinyUSB)というのもあるが、うまく動かなかった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/e4bd53d8-6f3c-6ab8-eef1-26967aea2baa.png)
MIDI機能は[Forty Seven EffectsのMIDI Library](https://github.com/FortySevenEffects/arduino_midi_library)に依存しているのでそちらも入れる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/e5c26031-b418-5886-1a82-62ce095cc80f.png)
MIDIデバイスを作る公式サンプルはこちら。

https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/master/examples/MIDI/midi_test/midi_test.ino

ただ、このサンプルからはわかりにくいポイントが幾つかあるので、MIDIメッセージ送出に機能を絞ってそれらのポイントを補足したものがこちら。

```c++
#include <Adafruit_TinyUSB.h>
#include <MIDI.h>

// ESP32ではデバイス情報はUSB MIDIオブジェクトの構築前に`USB`に対して設定しておく必要がある
// https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/master/README.md
// に書いてあるようだがわかりにくい
struct USBInitializer {
  USBInitializer() {
    USB.productName("My awesome MIDI device");
    USB.manufacturerName("My awesome company");
  }
} usbInitializer;

// USB MIDIオブジェクトの構築
// Adafruit_USBD_MIDI型のusbdMIDIがトランスポート層で、これをバインドして
// MIDIという名前のAPIオブジェクトを作る、というノリのおまじない
Adafruit_USBD_MIDI usbdMIDI;
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usbdMIDI, MIDI);

void setup() {
  Serial.begin(115200); // デバッグ用途等でUSBシリアルも使いたければbeginしておく
  
  MIDI.begin(MIDI_CHANNEL_OMNI);
  while (!TinyUSBDevice.mounted()) {
    delay(10);
  }
}

int count = 0;

void loop() {
  int note = 64 + count / 2;
  int velocity = (count % 2) ? 0 : 100;
  int channel = 1; // チャンネルは1始まりであることに注意(0はMIDI_CHANNEL_OMNIに使われている)
  count = (count + 1) % 24;
  MIDI.sendNoteOn(note, velocity, channel);
  Serial.printf("n:%d,v:%d,c:%d\n", note, velocity, channel);
  delay(500);
}
```

* USB MIDIオブジェクトの構築処理は謎の呪文っぽくてわかりにくいが、意味を読み解くとこんなノリである。
    * まず`Adafruit_USBD_MIDI`というトランスポートの実装([Adafruit TinyUSB Libaryにより提供](https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/master/src/arduino/midi/Adafruit_USBD_MIDI.h))を作り、
    * それをバインドしてインタフェースオブジェクト([MIDI Libraryにより提供](https://github.com/FortySevenEffects/arduino_midi_library/blob/master/src/MIDI.h))を作れ、
    * ちなみにそのインタフェースオブジェクトの変数名は `MIDI` にせよ
* MIDIチャンネルの指定は1始まりである。MIDI仕様に慣れた人は0始まりが自然だと思うが、このライブラリでは0はOMNIモードに割当たっているようだ。
* MIDIデバイス名はデフォルトでボード名 "LOLIN-S2-MINI" になってしまい非常にダサいのだが、好きな名前にするためデバイス情報を設定するAPIを呼んでもうまくいかず、[README](https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/master/README.md)をよく見たらESP32版ではこれらのAPIには効果がないということがわかりにくく書いてあった。試行錯誤の末、[`USB`というオブジェクト](https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/USB.h)にデバイス情報を設定してからUSB MIDIのオブジェクトを作れば意図通りに名前を変えられることがわかり、上のようなコード(静的オブジェクトの構築前に処理を差し込むためダミーの静的オブジェクトを作ってコンストラクタに処理を書く)で解決した。ちなみにこれをやってもデバイスマネージャー上で見える名前は変わらないので、DAWや[MIDIberry](http://newbodyfresher.linclip.com/)から確認すること。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/caaeca6f-bb64-46f7-8059-72fb01372eac.png)
* `Serial.begin`を呼べばUSBシリアルを並行して(同じ物理的なUSB接続上で)使うことができる。デバイスマネージャーで見るとMIDIとシリアルポートの両方のインタフェースを持つコンポジットデバイスに見える。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/76adecce-2482-a2f4-6a49-116cb1075e04.png)

## 以後の工程

以上がUSB MIDIデバイスを作成する方法の基本である。蛇腹楽器型MIDIコントローラーを完成させるには、センサー値をコントロールチェンジの値に変換する処理を作る(V1でPC上で行っていた処理の移植に相当)、操作盤で感度等のパラメタを変更できるようにする、といった工程が必要であるが、本記事のスコープ外とする。後日気が向いたら書くかもしれない。(追記: 書いた→https://qiita.com/tomoto335/items/b7f913a5a9f855a304e6)

# 評価(V1方式との比較)

PC上にカスタムアプリケーションと仮想MIDIデバイスを必要とするV1方式に対して、ドライバ不要のplug-and-play MIDIデバイスを作ることができたことは確かな技術的成果である。一方、このデバイスをパッケージ製品として頒布する野望でもない限り、(PC上であれこれソフトウェアを動かすことに抵抗のない筆者の個人使用においては)実用性の面ではそれほど大きなメリットはなかった。つまり「どちらでも変わらないので、簡単な方でよかったんじゃね？」という結論である。

* 筆者の環境で100メッセージ/秒程度のボリューム情報を流してみたところ、V1方式でもV2方式でも明確な優劣は認められなかった。当初「仮想MIDIデバイスを挟むV1方式よりもV2方式の方が遅延やスループットの面で優れた特性を持つのではないか」と予想したのだが、実際にはそのような違いは見て取れなかった。
* MIDIデバイスが認識されなくなることがときどきある。デバイスのリセットやUSBケーブルの差し直しで復活するが、原因は不明。USBシリアルを用いるV1方式の方がこの手のトラブルは起こりにくい。

一方、操作盤やセンサーの追加には意義があったのだが、これは本記事で説明していないので評価は別の機会に譲ることとする。

# リファレンス一覧

* [M5StickCドキュメント](https://docs.m5stack.com/en/core/m5stickc) (M5Stack)
  回路図(Schematic)を見るとUSBコネクタがUSB-UART変換チップにハードワイヤされていることがわかる。
* [ESP-WROOM-02(Arduino)によるWiFiネットワーキング (7) ~ 自作アプリケーションのための基本回路](https://tomoto335.hatenablog.com/entry/esp-wroom-02/7) (筆者)
  筆者がESP8266で電子工作を始めた頃の記事で、この当時はプログラム書き込み時にはボタンを押してモードを切り替えて起動するのが当然だった。
* [EspTinyUSB for ESP32-C3](https://esp32.com/viewtopic.php?t=22114) (Expressif)
  ESP32でUSBのMIDIデバイスを作りたければ対応したボードが必要というお話。
* [Adafruit TinyUSB Library for Arduino](https://github.com/adafruit/Adafruit_TinyUSB_Arduino) (Adafruit)
  ちゃんと動いたTinyUSBスタック。
* [Arduino MIDI Library](https://github.com/FortySevenEffects/arduino_midi_library) (Forty Seven Effects)
  Adafruit TinyUSB Libraryと組み合わせて使うMIDIライブラリ。
* [Arduino ESP32のUSB関連機能](https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/USB.h) (Expressif)
  Adafruit TinyUSB Libraryの説明に "ESP32 port relies on Espressif's esp32-hal-tinyusb.c for building usb descriptors which requires all descriptors must be specified in usb objects declaration i.e constructors." と書いてあってよくわからなかったのだが、結局のところ「Adafruit TinyUSBライブラリはそのへんの面倒は見ないのでExpressifのUSB機能の方を使って設定しておけよ」という話だったようだ。
* [EspTinyUsb](https://github.com/chegewara/EspTinyUSB) (chegewara)
  動かなかった方のTinyUSBスタック。最初はMIDIデバイスとして認識すらされず、https://github.com/chegewara/EspTinyUSB/issues/115 に従って `setBaseEP(3)` したら見えるようになった。しかし結局PCにMIDIメッセージが届くことはなく、ソースを見るとMIDIの実装はスカスカだし、https://github.com/chegewara/EspTinyUSB/issues/52 によればそもそも作者がMIDIにあまり関心なさそうである。
* [MIDIberry](http://newbodyfresher.linclip.com/) (newbodyfresher)
  前回は「Windows上でBLE MIDIデバイスと対話するためのアプリケーション」と紹介したが、本来は汎用の仮想MIDIパッチベイである。「自作MIDIデバイスからやってきたMIDIメッセージをソフトウェアシンセに流す」みたいなことがDAWのような大げさなものを使わずにできる。