メインコンテンツまでスキップ

2023-12-10 Udon MIDI をデータバスとして使う試み(振り返り)

注記

このページは AI(Claude)によって翻訳されています。翻訳に誤りが含まれる可能性があります。

私は、MIDI を使って外部プロセスから Udon と通信しようと試みた、数少ないクリエイターの一人です。

試してみて学んだことを共有します。

なぜ MIDI なのか?​

外部プロセスから Udon にデータを送る方法は、(網羅的ではありませんが)それほど多くありません。

  • ストリーミング動画プレイヤーと GPU リードバック
  • String Loader のポーリング
  • 入力システム(ジョイスティックのデータ、VR の入力など…)
  • MIDI

ここで注目している MIDI はリアルタイム用途に向いています。ここでいうリアルタイムとは、遅延が数ミリ秒未満のアプリケーションを指します。

たとえば、現実世界でトラッキングしている物体の位置を、ワールドと同期させるのに使えます。

残念ながら、対応する OSC のドキュメントにはワールドへの言及があるものの、OSC は利用できません。

VRChat のコンポーネント​

Udon は VRCMidiListener コンポーネントを通じて MIDI データを受信できます。このコンポーネントは、探すデバイス名を指定する起動パラメーターが定義されていない限り、最初に見つかった MIDI デバイスを監視します。すべてのイベントは UdonBehaviour に送られます。

念のため補足すると、MIDI イベントはネットワーク同期されません。そのため、すべての処理は、MIDI データを送受信するローカルマシン上で行われます。

仮想 MIDI デバイス​

私は MIDI や音楽の経験がなく、外部プロセスから Udon へデータを送る手段の一つとして MIDI に関心があるだけです。

VRChat が MIDI メッセージを受信するには、MIDI イベントを書き込めるデバイスを作る必要があります。これは仮想 MIDI デバイスを使えば実現できます。macOS ではネイティブにサポートされているようですが、Windows には仮想 MIDI デバイスのネイティブサポートがないため、サードパーティ製のドライバーをインストールする必要があります。

インターネットを探し回りましたが、真に無料でオープンソースの解決策は見つかりませんでした。そのため、利用に一部制限のある、プロプライエタリまたはソース公開型の無料ダウンロードソフトウェアを使うことになります。いくつかはここに挙げられています:https://support.troikatronix.com/support/solutions/articles/13000081815-using-virtual-midi-ports

検索結果にはよく LoopBe1 や LoopMIDI が出てきますが、私個人は sbvmidi を使い、問題なく動きました。結果は環境によって異なる場合があります。

  • これらのソフトウェアは、入力用の MIDI デバイスと出力用の MIDI デバイスを作成します。
  • イベントは、仮想の出力 MIDI デバイスに送ります。
  • イベントは、仮想の入力 MIDI デバイスから再び出力されます。
  • VRChat が監視するのは、その仮想の入力 MIDI デバイスです。

送信側のライブラリ​

私は個人的に DryWetMIDI .NET ライブラリを使いました。これにより、VRChat SDK を使わない Unity プロジェクトからも MIDI データを送信できます。

データ型​

Udon の MIDI は 3 種類のイベントをサポートしています。

  • NoteOn
  • NoteOff
  • ControlChange

これら 3 種類のイベントには、それぞれ次のものがあります。

  • チャンネルを表す 4 ビットの数値
  • ノートを表す 7 ビットの数値
  • ベロシティまたは値を表す 7 ビットの数値

文字列のエンコード​

外部アプリから Udon に送るデータの中身は、データバッファサイズの制約(この記事の後半を参照)に合わせて、用途ごとに特別に作り込んだものにできます。ただ、処理もデバッグも簡単なので、まずは文字列から始めましょう。

問題は、バイト列を文字列にデコードするための Encoding.UTF-8 を Udon で使う簡単な方法が、工夫をしても見つからなかったことです。

そのため、文字列を UTF-8 のバイト列にエンコードする代わりに、文字列の各 Char(1 文字あたり 2 バイト)を処理することにしました。

こうすれば、文字列に ASCII 以外の文字が含まれていても、簡単に復元できます。

1 つのノートで 2 バイトを送る​

どのイベントタイプも合計 18 ビットあるので、1 つのイベントの中に Char(16 ビット)を 1 つエンコードすることも可能なはずです。

しかし、私は代わりに次のようにしました。

  • 各 Char の下位 7 ビットを、それぞれ 2 つの 7 ビット数値に格納する
  • 1 つ目の Char の最上位ビットが 0 なら、NoteOn イベントを使う
  • 1 つ目の Char の最上位ビットが 1 なら、ControlChange イベントを使う
  • 2 つ目の Char の最上位ビットが 0 なら、チャンネル 14 を使う
  • 2 つ目の Char の最上位ビットが 1 なら、チャンネル 15 を使う

これにより、エンコーダーが使うチャンネルは 2 つだけに限られます。

問題:VRCMidiListener/VRCPortMidiInput にバグがある​

内部クラス VRCPortMidiInput にある VRCMidiListener の問題さえなければ、すべてうまく動いていたはずです。

前回のフレームを処理してから受信した MIDI イベントが多すぎると、VRChat は例外を発生させます。MIDI イベントが多すぎると VRChat 内部でバッファの過剰読み取りが起こり、イベントを正しく処理できなくなります。これは、前回のフレームからおよそ 128 イベント程度でも起こりえます。

VRChat がその最大数の 8 倍を超えるイベントを処理しなければならない場合、状況によってはクライアントがクラッシュする可能性もあります。影響を受けるのは MIDI イベントを送信しているマシンだけで、MIDI イベントはそもそもネットワーク同期されないため、インスタンス内の他のプレイヤーには影響しません。

したがって、MIDI を使って VRChat に安全にデータを送るには、呼び出し側で VRChat 自体の安定性を確保する必要があります。

  • VRChat が 1 フレームで処理できる数を超えるイベントを送らないようにする。
    • これは、イベントキューまたはメッセージキューを使えば実現できます。
    • VRChat が 1 フレームで処理できる量より大きなメッセージについても、イベントをキューに入れて処理できるようにしてください。
  • 追加のイベントを送る前に、VRChat がフレームの処理を終えたことを検出する。
    • これは、FileSystemWatcher を使って VRChat の出力ログを監視することで実現できます。
    • MIDI イベントを処理する Udon の挙動の中で、そのフレームに少なくとも 1 つのノートを処理した場合、そのフレームのすべてのノートの処理が終わった後(LateUpdate を使用)に、送信側が検出できる 1 行をログに出力します。
  • VRChat が応答しなくなったときや、ユーザーがワールドから退出したときを検出する、またはタイムアウトを処理する。
    • 送信側はまずフレームの処理が完了したことを検出しなければならず、VRChat が応答していないときはそれが決して起こらないため、キューが大きくなりすぎるのを防げます。
  • VRChat が送信側より遅れている場合は、送信するデータをキューに溜めないようにする。
    • VRChat が処理し終えるころには意味を失っているメッセージで、キューが膨れ上がるのは避けたいところです。

以上です​

VRChat のフレームレートが低いほど、VRChat が受信データを処理する速度は遅くなる可能性があります。

本当の解決策は、ワールド向けの OSC ですが、現実的に考えて、作業を進める必要があるときにそれを待っていられるでしょうか?