Open5

MIDI 2.0 UMPとByte Order

Atsushi EnoAtsushi Eno

さいきん自分のAndroidオーディオプラグインフレームワークのMIDIメッセージング部分をMIDI 2.0化したり、それをAndroid MidiDeviceService経由で受け取って処理する仕組みを作っていて、バイトオーダーの不整合でハマったので書いておこうと思う。

前提となる技術的な事情を大まかに書いておくと、MIDI 2.0をOSレベルでサポートしているのはAppleのOSくらいしかなくて、現状でMIDI 2.0のメッセージのやり取りはMIDI 1.0のコネクションを確立したポートに単なるバイトストリームとしてデータを送ることができる、というレベルでしかない。ただ、UMPを送るだけならそれで十分だし、この前提ならどんなOSでもおよそ問題ないはずだ。AndroidのMidiDeviceServiceでは問題なくできている。

Atsushi EnoAtsushi Eno

それならMIDI 1.0の頃と大差なくメッセージングできるはずだ、となりそうだけど、MIDI 2.0の場合はひとつMIDI 1.0で問題にならなかったことが問題になる。データのバイト順序だ。

UMPは32ビット(ユーティリティ、システム、MIDI 1.0)、64ビット(Sysex7、MIDI 2.0)、128ビット(Sysex8, MDS)のデータがやり取りされるので、データを32ビット整数で持っておくのが一般的になると思う。これをバイトストリームに放り込むと、環境によってはリトルエンディアンになったりビッグエンディアンになったりする。32ビット整数から8ビット整数の配列にする方式をきちんと理解しておく必要がある。

(MIDI 1.0の時代には可変長のSysexメッセージがあったので、32ビット整数を前提としたパケットの格納はできなかった。)

MIDI 2.0 UMP仕様では、これは「規定しない」となっている:

2.1.1.1 Scope of Bit, Byte, and Word Order Guidance

Although UMP 32-bit words can be converted to and from byte streams for storage or transmission, the
formats of such byte streams, including the byte order to be used for such transport and storage, are outside the scope of this specification. Per Section 1.1, it is expected that separate transport specifications will defineformats and byte orders for each particular transport, and separate file format specifications addressing the UMP Format will define byte orders for each particular file format.

For the internals of any given implementation, a device or system may use any desired format, including
native-endian 32-bit words.

Atsushi EnoAtsushi Eno

今回ハマったのは、自分のMidiDeviceServiceが期待通りのUMPを自作のMIDIアプリケーション(MIDI入力クライアント)から受け取れていないという問題だった。自作のMIDIデバイス(のフレームワーク)も、自作のMIDIアプリも、MIDI 1.0とMIDI 2.0の両方についてサポートしたりしなかったりで、実際にはもっとややこしいのだけど、ここでは説明を簡単にするためにどちらもMIDI 2.0で繋がっていることまでは前提とする。

自分のプログラムの場合、まずKotlinとCで別々のライブラリを作っていた。

Cライブラリはオーディオプラグインの内部でMIDI 2.0メッセージをリアルタイム処理できるように、アロケーションフリーのheader only libraryにしてあって、Kotlinライブラリは言語的な制約からリアルタイム性を求めても意味がないので、プラットフォームのMIDIアクセスやSMF…や、MIDI 2.0についてはSMFに相当する楽曲データ…の読み書きを可能にするライブラリにしていた。

目的も実装も大半は別々だけど、UMPの整数値をパラメータから生成するAPIは、ほぼcmidi2からktmidiに移植してきた。なので生成される32ビット整数での内部表現は食い違わない。

このライブラリの機能を使って書いたアプリでは、データ生成で食い違いは生じなかった。一方で、自作アプリに仮想MIDIキーボードアプリ https://github.com/atsushieno/kmmk があって、これはUMP生成APIを使わずに自力でストリームに変換していた。これが自分のAndroid環境では(ほとんどがそうだと思う)プラットフォームのlittle endianと整合せずに不正なデータを生み出していた。

この問題を修正したコードはこんな感じになる。一応Kotlin MPPを前提にしているので、java.nioのAPIは使わず、Ktorを追加でインポートしてByteOrderを取得しなければならなくなった。

https://github.com/atsushieno/kmmk/commit/55cdd4ecc2d4d8cf8bdc058ced19b4f911229fbb#diff-da9aeef0f9af661847bb88c213a03c309d0ea093e16dd39f8622535453999f6cR30

Atsushi EnoAtsushi Eno

一方で cmidi2 に後付けで適当に実装してテストも雑にやっていた「パラメーター取得」系のAPIもバグっていた。

https://github.com/atsushieno/cmidi2/commit/1e22b5283a785418ec6c6ee774000b81c3a0cff2

これは最初からint32_tで扱っていれば良かったものを、void*からuint8_tを2つ含む構造体でUMPのmessage type, group, status, channelを扱うように設計していたせいだ。これはAPI設計を参考にしたLV2 AtomのAPIがそうなっていたからなのだけど、構造体のバイトオーダーはシンプルに定義順(というかコンパイラ実装依存)だったので、bit endianなデータしか処理できなかったというわけだ。自分のアプリは当初はまだ先のMIDIキーボードアプリから送信されたものをこのAPIで受け取って処理するくらいしか無かったので、問題に気付かなかったというわけだ。

これらの問題を修正したら、少なくともUMPのやり取り部分についてはきちんと処理できるようになった。

今回はどちらも自分のローカル端末の範囲内でやり取りしているので、トランスポート層におけるバイトオーダーの違いを意識する必要は無かったけど、MIDIデバイスに接続した先はBid Endianだった(IBM S390だったりBig Endianで設定されたARM CPUだったり)という場合のことを考えたら、トランスポートがきちんとbyte orderを意識して処理するように設計されている必要がある。