Raspberry Pi PicoでMIDIモニタを作ってみた


PicoのSDKにはTinyUSBライブラリが含まれており、これを使って比較的簡単にUSB デバイスを作ったり、USB デバイスを接続することができます。

今回はこれを使って、USB経由で受信したMIDIメッセージをUARTに出力するMIDIデバイスを作ってみました。TinyUSBにはサンプルコードも色々入っており、MIDIデバイスも含まれています。サンプルコードはMIDIメッセージをUSBへ出力するシーケンサーのようなデバイスでしたので、それとは違うものにしました。

ちなみに、現在TinyUSBがサポートしているUSBデバイスは、ドキュメントによれば以下の通りです。

●デバイス
・USBオーディオクラス2.0 (UAC2) (開発継続中)
・Bluetoothホストコントローラインタフェース(BTH HCI)
・Communication Device Class(CDC)
・Device Firmware Update(DFU): ランタイムのみ
・Human Interface Device (HID): 汎用(入力と出力)、キーボード、マウス、ゲームパッド等
・マスストレージクラス(MSC): 複数のLUNをサポート
・MIDIデバイス
・RNDIS, CDC-ECMによるネットワーク(開発継続中)
・USB Test and Measurement Class (USBTMC)
・汎用の入出力エンドポイントを持つベンダ固有のクラス。INFファイル無しでwinUSBドライバをロードするためにMS OS 2.0互換デスクリプタと組み合わせることが可能。
・ベンダ固有のクラスを用いるWebUSB
●ホスト
・Human Interface Device (HID): キーボード、マウス、汎用
・マスストレージクラス(MSC)
・ハブは1段のみサポート

今回作成したのはMIDIデバイスです。ソフトウェアをPicoに書き込み、PC に接続すると、PicoがMIDIデバイスとして認識されます。PC からMIDIメッセージを送ると、その内容がUARTにテキストで出力されます。

コードは以下のリポジトリにアップロードしてあります。

https://github.com/boochow/pico_test_projects/tree/main/usb-midi

最初にMIDIについて簡単に説明しておきます。

MIDIは楽器を制御するための通信プロトコルで、通常1~3バイトのメッセージを送受信します。メッセージの先頭バイトが機能を表し、2バイト目以降はパラメータです。
先頭バイトはMSBが常に1であり、2バイト目以降はMSBが常に0です。
例外としてシステムエクスクルーシブ(SysEx)メッセージでは、先頭が0xF0、末尾が0xF7、途中はMSBが0の不定長のバイト列となっています。
MIDI規格の詳細は以下のドキュメントが公開されています。

https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message
MIDI1.0規格書

MIDI over USBでは、MIDIメッセージは全て4バイトのデータにエンコードされます。先頭バイトがデータ種別を表し、2バイト目以降にMIDIメッセージが入っています。不定長である SysExメッセージも3バイト以下に分割されて上記のフォーマットでエンコードされます。もっとも、このデータはTinyUSBがデコードしてくれますので、ユーザプログラムは意識する必要はありません。

TinyUSBの使い方はシンプルです。
tusb_config.hとusb_descriptors.cの二つのファイルが必要ですが、これはTinyUSBの付属のサンプルからコピーしてきて若干の編集をすれば使えます。

tusb_config.hは、その名の通りTinyUSBのコンフィグファイルです。ポイントは

#define CFG_TUD_MIDI 1

です。その他、バッファサイズの設定(CFG_TUD_MIDI_RX_BUFSIZE、CFG_TUD_MIDI_TX_BUFSIZE)も必要になります。

usb_descriptors.cはデバイス種別によって異なりますので、TinyUSB付属のUSB-MIDIのサンプルのものをコピーしてきます。編集の必要はほとんどありませんが、PCに接続したとき、デバイスの名称が表示されますが、その名称はこのファイルの中で定義されています。

あとは

tusb_init();

で初期化したあとメインループの中で

tud_task();

をひたすら呼び出し、

uint32_t tud_midi_n_read(uint8_t itf, uint8_t jack_id, void* buffer, uint32_t bufsize)

でMIDIメッセージを読みだして処理します。ここでitfはインタフェース、jack_idは仮想的なMIDIジャックですが、今回はどちらも1つしかないので0です。

jack_idなどを含むUSB-MIDIにおけるMIDI機器のモデル化については、詳細はUSB MIDIデバイスクラスの仕様書(下記)の3章で解説されています(私もちゃんと読んでいませんが。)

USB Class Definition for MIDI Devices v2.0 | USB-IF

また、あわせてUSBデバイス自体のモデルについても見ておくとよいかと思います。

USBの基本アーキテクチャ

実際にやることはMIDIメッセージを受け取って処理する(今回はメッセージ種別に応じた文字列をprintfでUARTに出力する)ことだけですので、プログラムそのものはシンプルです。
出力は以下のような感じになります。左から
生データ | Ch:MIDIチャンネル | MIDIイベント : パラメータ
の順に並んでいます。

904565 | Ch: 00 | Note On   : key=69 velocity=101
804540 | Ch: 00 | Note Off  : key=69 velocity=64
904023 | Ch: 00 | Note On   : key=64 velocity=35
804040 | Ch: 00 | Note Off  : key=64 velocity=64
903e33 | Ch: 00 | Note On   : key=62 velocity=51
803e40 | Ch: 00 | Note Off  : key=62 velocity=64
b01402 | Ch: 00 | Control   : num=20 value=2
b01407 | Ch: 00 | Control   : num=20 value=7
b0140b | Ch: 00 | Control   : num=20 value=11

今回はUARTに出力しているだけですが、前回のシンセサイザ等と組み合わせれば、USB-MIDIで動作するシンセサイザをPicoで作ることができるでしょう。

コメント