背景
先日、MacとiPhoneでStandard MIDI Fileを再生するアプリを公開しました。
その動作を確認するため、メルカリで昔所有していたRoland社のSC-88Proを見つけたので、早速購入しました。
Macのアプリで再生するには、USB-MIDIインターフェースを使い、SC-88ProのMIDI IN端子にケーブルを接続します。
iPhoneのアプリで再生するには、Bluetooth-MIDIインターフェースを使い、やはり同じくSC-88ProのMIDI IN端子にケーブルを接続します。
そう、ケーブルをMacとiPhoneで繋ぎかえなきゃいけないんです。
地味に面倒なんですよねぇ。こういうのって。
MIDI信号のミキサーがあればなぁ...
昔はMIDI信号をミキシングするものがありましたし、私も結構高性能なのを所有していました。
以前からこの分野には興味があり、ちょうど個人的にも自由に使える時間があるので、今のうちにやってみよう!ということで、MIDIインターフェースを作ってみることにしました。
手作りで感触を試す
手持ちのSTM32F407 Discovery基板を使い、ユニバーサル基板上にLEDやディップスイッチ、MIDIコネクタを載せて、ひとまず必要なものを用意しました。
MIDIコネクタは、秋月電子に周辺回路もついた便利なものがありました。
これをちょこっと改造して、スタックして6IN/6OUTを実現します。
USB-MIDIはどうやって実現する?
これが一番難しいところでした。
なかなか資料が見つかりません...探し方が悪いのかもしれません。
ネットを調べていて、GitHubにSTM32FマイコンでUSB-MIDIを実現したものを公開されている先人がいました!
この方々のプロジェクトを見ると、STの署名が入ったMIDIのミドルウェアを使っていることがわかりました。
STのウェブサイトでは見つけられませんでしたが、もしかすると過去に配布していたのかもしれません。
これを利用してUSB-MIDIインターフェースを作ってみます。
USBデバイスの設定〜CubeMX
USBはDevice_Onlyに設定します。
ミドルウェアはHuman Interface Device Classを選択します。
他の設定は何も変更しなくてOKです。
必要に応じてVIDを変更したりすればいいかと思います。
MIDIミドルウェアの組み込み
usbd_midi.cとusbd_midi.hをプロジェクトに組み込みます。
コンパイラのインクルードパスにusbd_midi.hのパスを追加します。
ソースコードの修正
usb_device.cを修正します。
// #include "usbd_hid.h" //コメントアウトする
/* USER CODE BEGIN Includes */
#include "usbd_midi.h" //インクルードヘッダを追加する
/* USER CODE END Includes */
// if (USBD_RegisterClass(&hUsbDeviceHS, &USBD_HID) != USBD_OK)
if (USBD_RegisterClass(&hUsbDeviceHS, &USBD_MIDI) != USBD_OK)
// #include "usbd_hid.h" //コメントアウトする
/* USER CODE BEGIN Includes */
#include "usbd_midi.h" //インクルードヘッダを追加する
/* USER CODE END Includes */
// 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ポート数を記述します。
/* 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デバイスとして認識されます。
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)に振り分けます。
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を自作インターフェースに受けた場合は、正しく再生ができた...ということでした。
ステータス・バイトが来た時、内部処理ではステータス・バイトを更新していたのですが、1バイトのデータ自体はランニング・ステータスを使うことがないので、ランニング・ステータスを利用されることがありません。
ここを直したことで、R社のUSB-MIDIインターフェースで正しく再生できるようになりました。
MIDIでパケットを詰めて送信した時に問題発生
ランニング・ステータスの処理を改善したことで正しく再生できるようになった...ように思えたのですが、ごく稀に音が鳴りっぱなしになるという症状が発生していました。
これは鍵盤を離すというデータが欠けたときに発生します。
自作インターフェースのMIDIデータプロセッシングにミスがあって、データが欠けるのかも...と思い、自作インターフェースのMIDI出力へ送っているノート・オンとノート・オフの組み合わせが正しいかを調べ、さらにMIDI OUTをMIDI INに入れて同じようにノート・オンとノート・オフの組み合わせを調べました。
結果として、どちらもちゃんと組み合わせが正しく、ノート・オンした鍵盤はちゃんとノート・オフしています。
つまり、自作インターフェースはちゃんとデータを出していることになります。
様々なテストや検証をした結果、自作インターフェースのMIDIデータプロセッシングのタイミングによっては、MIDIパケットがつながってUARTで送信することがあります。
この時、(あくまでも想像ですが)SC-88Proの内部状態によってはパケット解析が間に合わず、ノート・オフの情報が内部的に欠落すると、音が鳴りっぱなしになるようです。
MIDIパケット間(UARTデータ間ではない)に若干の余裕が必要な可能性を考えました。
ソフトウェアでわざと時間を作る(waitのような処理)をするのは、個人的には避けたいところ。
そこで、過去の経験からUARTのストップビットを1から2へ変更してみました。
そうしたら、なんと症状が出なくなりました!
ストップビットが増えても論理的にはアイドルなので、受信を待つ動作になります。
ストップビットが増えたことで、パケット間に32usecの余裕が生まれます。
パケット間だけでなく、データ間にも32usecの余分な時間が入りますが、SC-88Proのデータ受信に影響はないでしょう。
SC-88Proを分解したところ、MIDIプロセッシングには日立製作所のH8が使われていました。
時代的にも連続でMIDIパケットを送りつけると、処理が間に合わない時が発生するのかもしれません。
これは仕方ないことで、歩み寄るべき事項でしょう。
幸い、ハードウェア設定で対応できたので、これはラッキーでした。
最後に
ひとまずUDB-MIDIの機能、MIDIデータプロセッシング、そして多くのStandard MIDI Fileの再生が正常にできるようになりました。
今は基板の修正をして、さらに使い勝手の良いものにしようと思っています。
最終版の基板が出来上がったら、報告&レポートします。