マイコンを使ってUSB-MIDIインターフェースを作る

## 背景
先日、MacとiPhoneでStandard MIDI Fileを再生するアプリを公開しました。
その動作を確認するため、メルカリで昔所有していたRoland社のSC-88Proを見つけたので、早速購入しました。

![スクリーンショット 2025-06-16 16.37.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/8a4fc933-30a5-4821-8c52-971b3f1dad9f.png)
(島村楽器のサイトより)

Macのアプリで再生するには、USB-MIDIインターフェースを使い、SC-88ProのMIDI IN端子にケーブルを接続します。

iPhoneのアプリで再生するには、Bluetooth-MIDIインターフェースを使い、やはり同じくSC-88ProのMIDI IN端子にケーブルを接続します。

そう、ケーブルをMacとiPhoneで繋ぎかえなきゃいけないんです。
地味に面倒なんですよねぇ。こういうのって。

MIDI信号のミキサーがあればなぁ...
昔はMIDI信号をミキシングするものがありましたし、私も結構高性能なのを所有していました。

以前からこの分野には興味があり、ちょうど個人的にも自由に使える時間があるので、今のうちにやってみよう！ということで、MIDIインターフェースを作ってみることにしました。

## 手作りで感触を試す
![IMG_0781.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/15ec8cba-7be8-4bab-ac0c-3049c7d276c0.jpeg)

手持ちのSTM32F407 Discovery基板を使い、ユニバーサル基板上にLEDやディップスイッチ、MIDIコネクタを載せて、ひとまず必要なものを用意しました。

MIDIコネクタは、[秋月電子に周辺回路もついた便利なもの](https://akizukidenshi.com/catalog/g/g111112/)がありました。
これをちょこっと改造して、スタックして6IN/6OUTを実現します。

## USB-MIDIはどうやって実現する？
これが一番難しいところでした。
なかなか資料が見つかりません...探し方が悪いのかもしれません。
ネットを調べていて、GitHubにSTM32FマイコンでUSB-MIDIを実現したものを公開されている先人がいました！

- [CureMIDI8](https://github.com/keshikan/CureMIDI8)
- [midi-box-stm32](https://github.com/Hypnotriod/midi-box-stm32)

この方々のプロジェクトを見ると、STの署名が入ったMIDIのミドルウェアを使っていることがわかりました。
STのウェブサイトでは見つけられませんでしたが、もしかすると過去に配布していたのかもしれません。
これを利用してUSB-MIDIインターフェースを作ってみます。
### USBデバイスの設定〜CubeMX
USBは**Device_Only**に設定します。

![スクリーンショット 2025-06-16 16.57.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/fe46e15c-e54f-452d-93f0-40b6166a4afa.png)


ミドルウェアは**Human Interface Device Class**を選択します。
他の設定は何も変更しなくてOKです。
必要に応じてVIDを変更したりすればいいかと思います。

![スクリーンショット 2025-06-16 16.58.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/2685bd40-0331-4b82-869a-d3c8b700b4aa.png)

### MIDIミドルウェアの組み込み
**usbd_midi.c**と**usbd_midi.h**をプロジェクトに組み込みます。

![スクリーンショット 2025-06-16 17.01.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/85b5fea8-2280-4e02-a6f5-bcd95612a863.png)

コンパイラのインクルードパスにusbd_midi.hのパスを追加します。

![スクリーンショット 2025-06-16 17.03.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/24792c0f-91bc-432f-87ef-5769bbae02b4.png)

### ソースコードの修正
**usb_device.c**を修正します。

![スクリーンショット 2025-06-16 17.05.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/c183deac-72c3-4206-95f8-69baf22b279e.png)

```c:usb_device.c(ヘッダインクルードの変更)
// #include "usbd_hid.h"    //コメントアウトする

/* USER CODE BEGIN Includes */
#include "usbd_midi.h"      //インクルードヘッダを追加する
/* USER CODE END Includes */
```

```c:usb_device.c(USBD_HIDをUSBD_MIDIに変更)
  // if (USBD_RegisterClass(&hUsbDeviceHS, &USBD_HID) != USBD_OK)
  if (USBD_RegisterClass(&hUsbDeviceHS, &USBD_MIDI) != USBD_OK)

```

もう一つ、usbd_conf.cを修正します。
![スクリーンショット 2025-06-16 17.11.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/683ef4ce-a5e8-440e-8341-a2b573551b4b.png)

```c:usbd_conf.c
// #include "usbd_hid.h"     //コメントアウトする

/* USER CODE BEGIN Includes */
#include "usbd_midi.h"      //インクルードヘッダを追加する
/* USER CODE END Includes */
```

```c:usbd_conf.c(USBD_HID_HandleTypeDefをUSBD_MIDI_HandleTypeDef)に変更
  // static uint32_t mem[(sizeof(USBD_HID_HandleTypeDef)/4)+1];/* On 32-bit boundary */
  static uint32_t mem[(sizeof(USBD_MIDI_HandleTypeDef)/4)+1];/* On 32-bit boundary */

```

main.hに仮想MIDIポート数を記述します。
```c:main.h
/* Exported macro ---------------------------*/
/* USER CODE BEGIN EM */
#define MIDI_IN_PORTS_NUM              0x04
#define MIDI_OUT_PORTS_NUM             0x04

/* USER CODE END EM */
```

これでコンパイルし、USBケーブルでMacに接続すると、MIDIデバイスとして認識されます。
![スクリーンショット 2025-06-16 17.16.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/fe614769-d463-45a0-8e65-65150c31dcfd.png)

## USBから送られてくるMIDIパケット
``void USBD_MIDI_OnPacketsReceived(uint8_t *data, uint8_t len)``をオーバーライドします。
MIDIデータのポインタ(data)と、データ長(len)が引数で与えられます。
USB-MIDIでは1パケットは4バイトです。
lenは4の倍数、dataのポインタも4ずつ進めていけばOK!


パケットの詳細はUSB-IFが公開している仕様書から確認できます。
Cable NumberがMIDIの番号です。
今回作るMIDIインターフェースは4IN/4OUTなので、Cable Numberは0〜3が入ります。
それぞれに対応したMIDIポート(UART)に振り分けます。

![スクリーンショット 2025-06-16 17.25.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/36dbb530-d830-409e-8154-14b5d28168a6.png)

![スクリーンショット 2025-06-16 17.25.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/b5cd8432-9cf7-4c16-b01b-bda5f1d82d99.png)

![スクリーンショット 2025-06-16 17.25.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/639bc960-d619-4392-b62c-0ebac1365f3b.png)

## USBから送られたMIDIパケットをUARTで送る
USBから受けたタイミングでそのままUARTで送ればOKです。
単純なUSB-MIDIインターフェースの動作ですね。

さらに付加価値を高めるには、USBから送られてきたMIDIパケットをUARTで送信しつつ保持しておいて、後からいろんな機能実現のために利用します。

## USBにMIDIパケットを送る
``uint8_t USBD_MIDI_SendPackets(USBD_HandleTypeDef  *pdev, uint8_t *data, uint16_t len)``を使います。

UARTではパケットという概念がないため、MIDIデータを受けた後解析し、パケットが揃った時にUSBパケット(バイト列)を作ります。
Cable Number、Code Index NumberをUSBパケットの最初に入れ、以降にデータを入れます。

MIDIメッセージはシステム・エクスクルーシブ以外固定長なので、比較的容易に送れますね。
しかも、みんな1パケットあたり4バイト以内で収まります。
システム・エクスクルーシブは全て受け取った後にデータ数を数え、うまくパケットに分割してUSBに送ります。

MIDIと違い、**ランニング・ステータス**はサポートされません。
省略せず、必ずすべてのデータを送ります。

これらの機能を使えば、標準的なUSB-MIDIインターフェースが作れます。

## 付加価値〜MIDI-MIDI間のデータプロセッシング
USBとMIDIの間は、標準的なUSB-MIDIインターフェースでいいと思います。
素直にUSBデータをMIDIに送る、素直にMIDIデータをUSBに送る...これでいいです。
実際、USBホスト(Mac /PC)がいろんな機能を持っていたりするので、そっちに任せればいいでしょう。

MIDI-MIDI間のデータプロセッシング...一体どんなものがあるかというと、

- MIDI INとMIDI OUTのルーティング
- MIDI INからきたデータのフィルタリング
- MIDI OUTへのフィルタリング
- MIDIチャンネルの入れ替え
- MIDIデータのマージ(ミキシング)

という感じでしょうか。

これはUSBが台頭する前に行われていたMIDIインターフェースのMIDIデータプロセッシングです。
今ならMac / PCでやっちゃんでしょうが、昔はMIDIインターフェースにそういう機能を持っているものがあり、レコーディングスタジオで使われていました。

私が昔使っていた**Mark of the Unicorn**社の**MIDI TIME PIECE**が世界中で使われていました。

ファームウェアを作っているうちに、このMIDI TIME PIECEを再現しよう！
そう思うようになりました。

データ処理としては難しくなく、複雑...という感じでしょうか。
それでも地道にやっていけば実現は可能です。

この作業をしているときに、ハマったことがあるので、それを記録に残しておきます。

### ランニング・ステータス+アクティブ・センシングでデータ復元の失敗
症状
- 自作USB-MIDIでStandard MIDI Fileは正しく再生できる
- R社USB-MIDIの出力を自作インターフェースに入れて再生するとメロメロ
- R社シンセサイザーのMIDI OUTを自作インターフェースに入れて再生すると正常

このことから、

- 自作インターフェースでUSB->MIDIは正常に再生できる
- 自作インターフェースのMIDI IN->MIDI OUTに問題がある

ことは確定です。

MIDIデータプロセッシングをするため、MIDI INで受けたデータはひとまず受信バッファに貯めます。
その後、mainループの中で解析して送信先を振り分けたり、フィルタリングで捨てたりします。
MIDIデータプロセッシングのために、ランニング・ステータスで省略されたステータス・バイトも復元して、完全なパケットとして保持します。

R社のUSB-MIDIインターフェース、R社のシンセサイザーとも、MIDIデータを観測すると、両方ともランニング・ステータスを使用しており、状況に応じてステータス・バイトを省略しています。

もちろん、私もランニング・ステータスの対応はしており、ちゃんと処理はできているはず...
調べていくと、アクティブ・センシングがランニング・ステータスの途中に入ってくると、私のファームウェアが正しくランニング・ステータスを復元できないことがわかりました。

R社のシンセサイザーでは、アクティブ・センシングを出力する・しないを設定できるようになっており、アクティブ・センシングは出力しないようにしていました。
そのため、シンセサイザーのMIDI OUTを自作インターフェースに受けた場合は、正しく再生ができた...ということでした。


![ranning status.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/1aafa6b9-60d4-4fb2-84fc-0f40d7189530.png)


ステータス・バイトが来た時、内部処理ではステータス・バイトを更新していたのですが、1バイトのデータ自体はランニング・ステータスを使うことがないので、ランニング・ステータスを利用されることがありません。
ここを直したことで、R社のUSB-MIDIインターフェースで正しく再生できるようになりました。

## MIDIでパケットを詰めて送信した時に問題発生

ランニング・ステータスの処理を改善したことで正しく再生できるようになった...ように思えたのですが、**ごく稀に音が鳴りっぱなしになる**という症状が発生していました。

これは**鍵盤を離すというデータ**が欠けたときに発生します。

自作インターフェースのMIDIデータプロセッシングにミスがあって、データが欠けるのかも...と思い、自作インターフェースのMIDI出力へ送っているノート・オンとノート・オフの組み合わせが正しいかを調べ、さらにMIDI OUTをMIDI INに入れて同じようにノート・オンとノート・オフの組み合わせを調べました。
結果として、どちらもちゃんと組み合わせが正しく、ノート・オンした鍵盤はちゃんとノート・オフしています。
つまり、自作インターフェースはちゃんとデータを出していることになります。

様々なテストや検証をした結果、自作インターフェースのMIDIデータプロセッシングのタイミングによっては、MIDIパケットがつながってUARTで送信することがあります。
この時、(あくまでも想像ですが)SC-88Proの内部状態によってはパケット解析が間に合わず、ノート・オフの情報が内部的に欠落すると、音が鳴りっぱなしになるようです。

MIDIパケット間(UARTデータ間ではない)に若干の余裕が必要な可能性を考えました。
ソフトウェアでわざと時間を作る(waitのような処理)をするのは、個人的には避けたいところ。

そこで、過去の経験からUARTのストップビットを1から2へ変更してみました。
そうしたら、なんと症状が出なくなりました！

これがカラクリ
![UARTセッティング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202602/abf4b4df-8142-40f1-8b3d-b4e24369380d.png)

ストップビットが増えても論理的にはアイドルなので、受信を待つ動作になります。
ストップビットが増えたことで、パケット間に32usecの余裕が生まれます。
パケット間だけでなく、データ間にも32usecの余分な時間が入りますが、SC-88Proのデータ受信に影響はないでしょう。

SC-88Proを分解したところ、MIDIプロセッシングには日立製作所のH8が使われていました。
時代的にも連続でMIDIパケットを送りつけると、処理が間に合わない時が発生するのかもしれません。
これは仕方ないことで、歩み寄るべき事項でしょう。
幸い、ハードウェア設定で対応できたので、これはラッキーでした。

## 最後に
ひとまずUDB-MIDIの機能、MIDIデータプロセッシング、そして多くのStandard MIDI Fileの再生が正常にできるようになりました。

今は基板の修正をして、さらに使い勝手の良いものにしようと思っています。
最終版の基板が出来上がったら、報告＆レポートします。