USB MIDI 1.0 の機器へ UMP の Endpoint Discovery を 6 回送りました。送信の関数は 6 回とも成功を返したのに、何も返ってきませんでした。
調べると、Core MIDI には、API の戻り値や説明だけでは、実際の動きを判断しにくい場面が、少なくとも 4 つありました。成功が返っても届いたとは限らない、一覧に出ないからといって存在しないとは限らない、という前提で、別の方法で確かめる必要があります。以下は macOS 27.0 で確かめた内容で、原因が確定していないものは、そう書いています。
1. USB MIDI 1.0 の機器へ送った Stream Message は、成功が返るのに戻ってこない
起きること。 USB MIDI 1.0 の機器の出力ポートへ、UMP の Stream Message(Message Type F。Endpoint Discovery など)を送ると、MIDISendEventList は成功(status 0)を返しますが、DIN のループバックで受信できませんでした。エラーも通知もありません。
確かめた方法。 iConnectivity mioXM のポートを使いました。MIDIServer が開いている mioXM の USB の MIDIStreaming インターフェースは、MIDI 1.0 の設定(Alternate Setting 0)だけを持つ USB MIDI 1.0 のものです。Core MIDI が報告するポートのプロトコル(kMIDIPropertyProtocolID)も 1 でしたが、これは Endpoint が使う MIDI のプロトコルを表す値で、USB の転送の形式を表すものではありません。このポートで、DIN の OUT と IN をケーブルでつないだループバックへ送り、同じポートの入力で受けました。
| 送ったもの | 戻ってきたもの |
|---|---|
| Endpoint Discovery(6 回) | なし |
SysEx F0 7D 01 02 F7 | 約 2 ms 後に戻った |
どこで消えたかを切り分けるため、MIDIServer が開いている mioXM の USB のインターフェースについて、転送に使われたメモリの記述子の累計(ioreg -l で読める UsbUserClientBufferStatistics の IOMemoryDescriptor)を、送信の前後で比べました。何もしない間は変わらず、SysEx では毎回 3 増え、Endpoint Discovery では一度も増えませんでした。この値は USB の転送ごとに増えるものと考えていて、そうであれば、Endpoint Discovery は Mac の側で捨てられた可能性が高いことになります。ただ、USB の通信そのものは記録していないので、どこで捨てられたかは確定していません。
一方、同じ Mac で作った MIDI 1.0 の仮想ポートへ送ると、Endpoint Discovery はそのまま届きました。Core MIDI のプロトコルの変換(MIDI 2.0 から 1.0)は、Stream Message を捨てていません。
原因(推測)。 USB MIDI 1.0 にも DIN にも、Stream Message を運ぶ形式がありません。MIDIServer の中で動く USB MIDI 1.0 のドライバが、UMP を USB MIDI 1.0 の形に直すときに、対応する形のない Stream Message を捨てているのだと考えています。ドライバの中は見えないので、確かめてはいません。未定義の System Real Time の FD も、同じ値が増えず、ループバックで戻りませんでした。どこで消えたかは、同じく確定していません。
回避策。 Stream Message(Endpoint Discovery、Function Block Discovery、Stream Configuration など)のやり取りは、UMP の Stream Message を運べる経路(USB MIDI 2.0 の機器、仮想ポートなど)で行います。Stream Message は、MIDI 1.0 のプロトコルの UMP でも有効な形式です。USB MIDI 1.0 の機器や DIN を通る経路で試せるのは、SysEx で運ばれる MIDI-CI までです。
2. MIDIUMPEndpointManager は、起動前につながった機器を一覧に含めない
起きること。 MIDIUMPEndpointManager.shared.umpEndpoints に、アプリの起動より前にシステムへ登録された UMP Endpoint が含まれません。UMP 1.1 の Endpoint Discovery に応答する USB MIDI 2.0 の機器でも、アプリより先につながっていれば、一覧は 0 件です。アプリの動作中に登録された機器だけが、MIDIUMPEndpointWasAddedNotification とともに一覧に入ります。SDK のヘッダーは、このマネージャーがシステム全体の一覧の写しを持つと説明していて、この動きとは合いません。
確かめた方法。 Roland A-88MKII(MIDI 2.0 モード)と、公開 API の MIDIUMPMutableEndpoint で作った Endpoint の両方で、プロセスを起動する順番を入れ替えて比べました。
| 条件 | 一覧 |
|---|---|
| 機器をつないだ後に起動したプロセス(5〜10 秒待った) | 0 件 |
| 機器の電源を入れ直す前から動いていたプロセス | 約 0.3 秒後に追加の通知が届き、一覧に入った |
MIDIUMPMutableEndpoint を作った後に起動した別のプロセス | 0 件 |
MIDIUMPMutableEndpoint を作る前から動いていた別のプロセス | 一覧に入った |
一覧に入るかどうかは、機器の種類やドライバではなく、登録されたときにそのプロセスが動いていたかで決まっていました。一方、MIDI-CI の機器の一覧(MIDICIDeviceManager.shared.discoveredCIDevices)は、後から起動したプロセスにも A-88MKII を返しました。
原因(推測)。 マネージャーの手元の一覧が、起動時に既存の機器を取り込まず、起動後の追加の通知だけで作られているのだと考えています。Core MIDI の中の処理なので、確かめてはいません。ほかの macOS の版でも同じかは調べていません。
回避策。 既存の UMP Endpoint を知る必要があるときは、umpEndpoints に頼らず、自分で Endpoint Discovery と Function Block Discovery を送って応答を読みます。私たちの検証の環境では、Core MIDI の機器とエンティティのプロパティの辞書に ump endpoint や、Group Terminal Block の First Group と Number of Groups というキーがあり、そこからも読めました。ただし、これらは SDK の公開の定数ではなく、観測したキーです。ほかの macOS の版で同じように使えるかは、確かめていません。
調べるときの小さな注意。 MIDIServer のログを見るときは、/usr/bin/log とフルパスで実行してください。zsh では組み込みの log コマンドが優先され、log show がエラーも出さずに何も表示しないことがあります。
3. MIDI 2.0 の仮想ポートは UMP Endpoint にならない。MIDIUMPMutableEndpoint でも、問い合わせにはアプリが答える
起きること。 MIDISourceCreateWithProtocol と MIDIDestinationCreateWithProtocol に MIDI 2.0 を指定して作った仮想ポートは、UMP を送受信するだけで、UMP Endpoint(Endpoint の情報、Function Block、問い合わせへの応答)にはなりません。これは SDK のヘッダーの説明どおりの仕様です。
MIDIUMPMutableEndpoint(macOS 15 以降)で作れば、Core MIDI に UMP Endpoint として登録されます。ただ、ほかのアプリが送った Endpoint Discovery や Function Block Discovery に、Core MIDI は答えません。問い合わせは、そのままアプリの受信のコールバックに届きます。
確かめた方法。 1 つのプロセスで、MIDI 2.0 の普通の仮想ポートと MIDIUMPMutableEndpoint を作り、別のプロセスから送りました。Endpoint Discovery は両方へ、Function Block Discovery は MIDIUMPMutableEndpoint へ送り、どれも問い合わせが受信のコールバックにそのまま届いて、Core MIDI からの応答はありませんでした。普通の仮想ポートへの Function Block Discovery は、試していません。MIDIUMPMutableEndpoint にだけ ump endpoint = 1 などの Endpoint のプロパティが付き、MIDIUMPEndpointManager の一覧に載りました(落とし穴 2 のとおり、登録時に動いていたプロセスにだけ)。
MIDIUMPMutableEndpoint が問い合わせに答えないのが仕様なのか不具合なのかは、ヘッダーに書かれておらず、分かりません。
回避策。 DAW などへ MIDI 2.0 の Channel Voice のメッセージを渡すだけなら、MIDI 2.0 を指定した普通の仮想ポートで足ります。Endpoint の名前や Function Block をほかのアプリへ示したい場合の対応の案は、MIDIUMPMutableEndpoint で作り、受信のコールバックで受けた問い合わせに、アプリが自分で答えることです。返すのは Endpoint Info、Device Identity、Endpoint Name、Product Instance ID、Stream Configuration、Function Block Info、Function Block Name です。ただし、アプリによる応答の実装と、それをほかのアプリ(DAW など)が正しく認識するかは、まだ検証していません。
関連して、MIDIUMPMutableEndpoint から、自分の構成と違うプロトコルの Stream Configuration Notification を送ると、MIDIServer はログに NOTICE を残し、自分の構成で Stream Configuration Request を送り返してきました。ただし、直前に要求を送っていたときなど、送ってこない場合もありました。Endpoint から送った Endpoint Discovery には、答えずに NOTICE を残すだけでした。
4. 同じ Unique ID で入力が作り直されると、古い参照の切断が -50 になる
起きること。 接続していた入力(Source)が削除され、同じ Unique ID で作り直されると、MIDIEndpointRef は別の値になります。古い参照に対する MIDIPortDisconnectSource は -50(引数が不正)を返しますが、新しい参照への接続は成功します。
確かめた方法。 検証用の仮想ポートで、次の順に試しました。
- 入力を作り、Unique ID を取得して、入力ポートから接続する。
- 入力を削除し、別の入力を作って、同じ Unique ID を設定する。
MIDIObjectFindByUniqueIDは、新しい参照を返す。- 古い参照の切断は
-50、新しい参照への接続は成功(0)。
これが OS の不具合なのか、macOS 27 で変わった動きなのかは、確かめていません。
回避策。 -50 を一律に無視しないでください。切断が -50 を返したら、Unique ID で今の入力を探します。入力が見つからなければ消えた、古い参照と別の入力が見つかれば置き換わった、と確認できるので、後始末を進めます。古い参照の入力がまだ見つかる場合や、検索そのものが失敗した場合は、本当の失敗として扱います。
共通して言えること
- 成功が返っても、届いたとは限らない。 送信の関数の戻り値だけで判断せず、ループバックで戻りを受ける、別の経路(仮想ポート)と比べる、OS が持つ転送の統計を読む、といった別の観測で確かめます。
- 一覧に出ないことは、存在しないことではない。 プロセスを起動する順番を入れ替える、別のプロセスから見る、別の API(プロパティ、MIDI-CI の一覧)と比べる、といった方法で確かめます。
- 仕様か不具合かは、分けて書く。 SDK のヘッダーの説明と合わない動きでも、ヘッダーに書かれていない部分は、仕様か不具合か決められません。私たちの記録でも、確かめた事実と推測を分けています。
何か分かったら、このページを更新します。
MIDIFabric について
MIDIFabricは、作り直された入力の切断で -50 が返ったとき、上の方法で消えたか置き換わったかを確かめてから後始末をします。機器の一覧に MIDIUMPEndpointManager は使っていません。ここに書いた動きは Core MIDI 側のもので、MIDIFabric を使っていなくても起きます。