chromiumでportmidiを使ってWeb MIDI APIを使えるようにする

前フリ

先々週末にWeb Music hackathonというのがあって、遊びに行ってきた。最近はWeb MIDI APIなんていうのがあって、MIDIデバイスをブラウザから叩ける。何とも時代錯誤感のある仕様だけど、MIDIメッセージで楽器以外のデバイスを操作するというのはMaker界隈ではよくあることで、使い方はだいぶ変わってきているものの、プロトコルとしては生きているらしい。まあJavascriptの世界でハードウェアを動かしたいという人は一定数いて、その範囲にはウケるのでしょう。自作ハードウェアならUSBじゃなくてRS232Cを使っているという人もいるかもしれない。あ、ホスト側が既にいないか。

そんなわけで、hackathonの募集ページやら何やらを見ながら、本当はあんましわかってないWeb Audio APIの勉強でもしようかなとか思いつつ、試しにWeb MIDI APIのサンプルを動かしてみた。全く動かない。調べてみるとChromeのcanary buildでしか動かないという。しかもMacだけだという。これでは全くWebになっている意味がないじゃないか。仕方ない、chromiumまったくいじったことないけど、いじってLinuxでも使えるようにしよう。と思い立ったのがhackathonの1週間前くらい。*1

chromiumをビルドする

そんなわけで、ソースをチェックアウトしてビルドを試みたのだけど、まずソースのチェックアウトが鬼門だった。何やら失敗する。chromiumのチェックアウトにはgclientというスクリプトを使うのだけど(それ自体は直ちに実行できるかたちでgitリポジトリにある)、(たぶん)これの操作を誤ると、svnでもgitでも出来るソースのチェックアウトが、gitとsvnで混在して失敗してしまう。

何度かチェックアウトと設定を試みて、何とかgitベースで*2gclient syncが無事完了する。そしてchromium、ほとんどHEADがビルドしない。そんな運試しみたいな環境では作業しようがない。どうやらlkgr (last known good revision) を自動的にターゲットに出来るビルドがあるらしい。素人はコレを使わないとビルドできなそうだ。というわけでコレを設定。

そしてビルドしたものを起動するものの、すぐにエラー終了してしまう。ビルド環境がUbuntu 13.10だし、chromium的にビルドサポート環境ではないらしく、ビルドに必要なパッケージも手動で入れていたので、やっぱビルド出来ないんじゃないかと諦めた。ここまでが下準備。

そんで当日やっぱ特にやることもなかったので、アリバイ的にjsで仮想thruマシンとか作りながら、Linuxビルドを再現してぐぐらーの人に聞いてみたら、何やら --disable-setuid-sandbox をわたしてやる必要があるということだった。それでようやく自分のビルドが実行できるようになった。

MIDI APIを選ぶ

ようやく自分のコードを書けるところまで来た。既に関連ソースの在処は目星をつけていたけど、hackathon会場でgypへのファイルの追加と使いまわすソースの基礎部分を教えてもらえたので、いきなり実装から始めることが出来た。

さて、必要なのはLinux版のみなので、そのままALSAを使ってもいいのだけど、実のところALSAをいじったことは一度もない。最初からそれだと敷居が高いので、他にいじったことのあるライブラリを使ってみようと思った。具体的には以下の2つ。どちらもクロスプラットフォーム目的で書かれているMIDIライブラリだ。Jackその他chromiumに追加のdependencyを生じそうなものは今回調べていない。

# portmidi - Cで書かれている。最新のcommitは1年半くらい前だが、cmakeでビルドしようとするとJNI関連のライブラリのビルドで失敗する。porttimeというライブラリがあってtimestampも処理できる。ただし生データを直に送受信出来ない不便なライブラリ。

# RtMidi - C++で書かれている。ソースのリポジトリが無い。最新のソースアーカイブは約14ヶ月前。対応プラットフォームは幅広い(iOSまである)。char*でそのままやり取りできる。

とりあえず、RtMidiが楽だろうと思ってソースを書き始めた。hackathonの当日は、他の人たちが多分自分以外全員何かしらアプリをやっつけて発表している進行をガン無視して(あ、いや発表は聞いてたけど)、終了間際に何とかデバイスを認識させるところまでは出来た。

それから(平日はちょっと忙しくて作業が止まる)先週末にようやく再開したのだけど、RtMidiには困った制約があることに気づいた。ポートを複数開けない。そのまま制約ということにしてRtMidiでMIDIOutput#send()を処理できるところまで書こうかとも思ったけど、どうせそのままでは長続きしないだろうと思って、portmidiに乗り換えることにした。

portmidiで実装

それで、とりあえずgypをどう真面目にいじったものかわからなかったので、とりあえずmedia/midiディレクトリにそのまま必要なファイルをコピーするという暴挙に出て、さらにchromiumが前提にする厳格なビルドで失敗するportmidi本体のソースにもいくつか手を入れて*3、一応MIDIOutput#send()は簡単に処理できるようになった。portmidiはデータを単純にchar*でやりとりできないのがちょっと面倒だった。

そしてMIDIInpput#onmessage()である。これが面倒くさかった。portmidiはRtMidiと違ってコールバックを用意してくれない。じゃあここでporttimeを使って定期的にメッセージをチェックすればいいんだな、と思って初めてporttimeのAPIをチェックしたわけだけど…全部static! send()でデフォルトでporttimeを使っているので、別のタイマーを回して処理ということができない。仕方ないのでpthreadで(porttimeのコードをごく一部で使いまわしつつ)ループを自作して、何とか実装できた。メッセージ処理も、単純にchar*を渡せない問題が、こちらではもろに表面化した。chromiumのバッファは生データだし、portmidiが受信データで渡してくる可変長のSysExはどこにEOXが入ってるかわからないし(そもそも1回のメッセージ受信ではどこまで入っているかもわからないし)…といった具合だ。下手に複雑に作りこむのも考えものだ。

ともあれ、実装は何とか出来た。しかし、このままではとてもパッチとして投げられたものではない。portmidiのソースには手を入れてあるし、media/midiの下にチョクで置いてある。これを避けるためには、portmidiをTOPDIR/third_party の下に置き直して、gypの書式をちゃんと調べて、ライブラリを然るべき形で追加しないといけない。ここではたと気づいた。portmidiはJavaまわりのファイルがないと普通にビルドできない。そもそもportmidiはcmakeを使っていた。

仕方ないので、とりあえずソースはthird_party以下に置きつつ、ソースはmediaのgypに直接追加した。これはこれでincludeが見つからない罠があるわけだけど、gypの書式を調べて何とか追加できた。

成果と教訓

そんなわけでパッチが出来たので、ようやくここにビルド手順をまとめる気になった。

# https://github.com/atsushieno/portmidi をsrc/third_party 以下にcloneする。

# https://gist.github.com/atsushieno/7211044 のパッチを当てる。(追記: 自分で書いたソースをgit addし忘れてたのでupdateした)

ビルドしたchromeは、 out/Debug/chrome --disable-setuid-sandbox --enable-webmidi で起動すると、設定をいじるまでもなくwebmidiが有効になるのでシヤワセになれます。

あと教訓: クロスプラットフォームMIDI APIにはとりあえず銀の弾丸が存在しない。どれもそれぞれ何かしら足りない(というか既存アプリの用途に特化している気がする)。

とりあえずここまでできたらパッチとして投げようと思う。今はとりあえずこれ書くのでいっぱいいっぱいだったので、また後で。

あ、ちなみに動作確認はtimidityをoutputに、vmpkをinputにして行っている。全てUbuntuデスクトップ上で完結した。

*1:Windowsビルドはhackathonの場で発表があったし、そのうち使えるようになるんじゃないでしょうかね。

*2:後日聞いたところではたぶん大半の人がgitでやっているだろうということだった

*3:ちなみにpatched versionを作ったのはついさっきで、githubには無かったので適当にgithubに持ち込まれていたやつをforkした