安定的で合理的なデバッグ作業を実現するApply Changes @ C96

ひさしぶりに技術同人誌に寄稿しました。TechBoosterのC96(夏コミ)新刊です。4日目南ナ49abのようです。boothで予約も出来るようです。

techbooster.booth.pm

今回はAndroid Studio 3.5でInstant Runの代替機能のように紹介されて登場したApply Changesと呼ばれる機能について、掘り下げて解説しています。とは言ったものの、実のところ20ページ弱あって長過ぎると思い、ソースコードまで追っかけた部分をバッサリ切り落としたので、このエントリでは補遺としてそれをまとめておこうと思います。

本エントリはあくまで補遺なので、上記同人誌の本文を参照できる人でないと読み進めるのは辛いと思います。Apply Changesについて後述するProject Marbleの投稿を読んでいて理解している人なら大丈夫かもしれません(一応)。

Instant Runについておさらいしたい場合は、2016年にわたしがまだQiitaを使っていた頃にまとめてあるので、そちらを見てください。

Instant Runはデバッグビルドに特化した技術でしたが、Apply Changesは実のところデバッグも意識しつつデバッグビルドに特化しないデプロイメント技術である、と言ったほうが適切かもしれません。今回の草稿の情報源の多くはAndroid Studio開発チーム?の外郭チームっぽいProject Marbleのメンバーによるブログ投稿なのですが、実はこの投稿だけではInstant Runが実現したビルドの省力化のうち、Apply Changes以外の部分については言及していないんですね。なので「じゃあInstant Runで実現していたアレやコレはどーなったの?」という話も実はあるのですが(たとえばターゲットと無関係なABIやリソースをスキップするビルドの最適化部分とか)、その辺はまだ文章としての情報が無いので言及していません。

と、前置きしたところで、本題に入ります。

(1) Apply ChangesはAndroid Studioの機能の一部として存在しています。Android Gradle Pluginではありません。Instant Runがビルドの最適化を中心とした機能であったのに対し、Apply Changesはあくまでビルドされたアプリケーションの更新適用("apply" "changs")に本質があって、これは主としてGradleで操作するものではありません。いずれにしろ、AOSPで参照すべきブランチはstudio-master-devです。

(2) Apply Changesの主要な実装部分は、AOSPでいえばtools/base/deployに含まれています。ここにApply Changesにおけるdeployerの実装、差分のジェネレーター、Androidターゲット側で動作するdeployagentとの通信スタックやそこで使われるprotocol bufferの定義ファイルなどが含まれています。

(3) 一方で、Apply Changesの実装をカバーしているように見えるけど、多分Instant Runの改良部分なのではないかと思われるコードも、tools/adt/ideaの中のandroid/src/com/android/tools/idea/以下、特にstats, deploy, runにいくつかあります。runディレクトリにはアプリケーションのデプロイメントについて特に詳しく解説したREADME.mdが含まれているのですが、ここにはApply Changesへの言及はひとつもなく、どうやらInstant Run時代のデプロイメントについてのみまとめた内容であるようです。

(4) 本稿ではAndroid 8.0でARTのRuntime Instrumentationのために新しく実装されたJVMTIによってhot code replaceapply code changesが実現しているということを説明していますが、具体的にはUpdateMethodsCode()という関数がこれを実現しています(AOSPページでpermalinkを取得する方法がわからなかったのでmasterへのリンクです…!)。JVMTIはOracleのhot code replaceで使われている技術で、Java Platform Debugger Architectureと呼ばれる一連のデバッガー関連仕様の一つです。標準的なコード置換技術なので、JVMTIをサポートしているどんなIDEでも原理的にはデバッガーからコードの差し替えが可能になっているかもしれません。

(5) なお、これと関連して、tools/adt/idea/android/src/com/android/tools/idea/fd/actionsでは、JVMTIが「有効になっていれば」Instant Runは実行されないようになっています。先に言及したQiita記事で軽く言及していますが、パスの途中にあるfdというのはInstant Runの機能の一部となるfast deploymentの略であると考えられます(Instant Runランタイムのjarにも含まれている名前です)。

(6) ちなみにApply Changesと関係があるのかはわからないのですが、adbにもfastdeployという機能?が新しく追加されていて、この中にdeployagent、deploypatchgeneratorといったツールのソースがいくつかあります。これらは「Androidターゲット上で動作する実行ファイル」であり、Apply Changesで使われているものと一致するかもしれません。

…というわけで、Apply Changesについてわたし以上に興味のある人は、この辺りから深入りできるのではないかと思いますので、ぜひソースコードリーディングにチャレンジしてみてください。

CircleCI(など)でJUCEアプリケーションをビルドする

JUCE GUIアプリケーションをCircleCIでビルドしてテスト実行するにはいろいろ面倒事を解決しないといけないのでまとめておく。CircleCIでやっているのはたまたまなので、他にも任意のDocker Imageを使用してビルドできるCI環境があれば応用がきくはず。実行環境は今回はUbuntuを想定している(パッケージ名等を調整すれば任意のLinuxイメージで動くはず)。

アプリケーションを起動してテストを実行しないとしても、Projucerによるビルドファイル生成が含まれている場合は、結局依存パッケージが必要になるので、およそ面倒事は避けられないのではないかと思う。リポジトリに生成済みのビルドファイルまで含めておくのであれば、この限りではない。

docker image

今回は ubuntudesktop/gnome-3-28-1804 を使っている。JUCE GUIアプリケーションはX11で動いており、そのために必要な環境があらかじめ設定されているイメージが望ましい。ただ、依存パッケージの中にlibwebkitgtk-4.0-devがあり、これは実のところかなり多数の追加依存パッケージを巻き込むはずなので、標準的なubuntuイメージでも十分かもしれない。

依存パッケージが増えすぎるとCI実行時間が無駄に長くなるのでなるべく避けたいところではあるけど、CircleCI上で40秒くらいだったので、まあ…仕方ないかな…という気持ちでそれ以上は追求していない。気になるようであれば独自のdocker imageを公開しておいたほうがよいかもしれない。

package deps

依存パッケージは現状これくらい含まれている。

echo y | apt-get install xvfb wget unzip libc6 libcurl3-gnutls-dev  \
    libfreetype6-dev libgcc1 libjpeg-dev libpng-dev libstdc++6 \
    libwebkit2gtk-4.0-dev libx11-6 libxext6 zlib1g  make g++ \
    mesa-common-dev libasound2-dev

Projucerの実行時にlibcurl3-gnutlsとlibfreetype6が必要になるが、その後juce_gui_basicsのモジュールをビルドする際にlibwebkitgtk-4.0-devとその依存関係が必要になる。juce-audio-basicsをLinuxでビルドするにはlibasound2-devも別途必要になる。

JUCEのダウンロード

Ubuntu bionic以降にはjuce-toolsのパッケージが含まれているので、それをそのまま使うという手もあるが、バージョンは5.4.1など多少古いものになる(現時点で5.4.3)。JUCEのAPIはバージョンによって割と変更されていくることがある(筆者は最近juce-coreのXmlAPIが5.4.3+でだいぶ変わってきていることに気づかずにハマった)。公式サイトのリンクから取得するのが安牌ではないかと思う。

xvfbを使用してProjucer, make, アプリケーション(テスト)を実行する

CircleCIのような環境でGUI依存のコードを実行しようとすると、おなじみのcannot open DISPLAY:0のエラーになって終了することになる。chromeなどであればheadlessビルドを使用してテストまで実行できることもあるが、GUIを使用するコードをそのまま実行できるようになってほしい、というのは割と一般的な需要だろう。

Chromiumをheadlessではない状態でビルドして実行するやり方をまとめているブログがあったので、そのやり方を真似してみた。

xvfb-run -a --server-args="-screen 0 1280x800x24 -ac -nolisten tcp -dpi 96 +extension RANDR" 実行したいコマンド

Projucer --set-global-search-path

Projucerには--resaveというオプションがあるので、これを実行すれば*.jucerファイルからLinuxMakefileを生成できるのだけど、ここには一つ罠があって、Projucerはローカル環境で設定されているグローバルパスを必要とする。これがないとJUCEモジュールの参照を適切に設定できずにファイル生成に失敗する。

このグローバル設定は--set-global-search-pathというオプションで指定できる。このオプションは実行するOSと対象パスを引数として要求する。JUCEのパスと、ビルドするコードやモジュール設定によってはVST3 SDKなどのパスも必要になる。

注意すべきは、このオプションと--resaveは排他的だということだ。つまり、先に--set-global-search-path等を別途実行してから、--resaveを実行すればよい。

サンプルconfig.yml

version: 2
jobs:
  build:
    docker:
      - image: ubuntudesktop/gnome-3-28-1804

    steps:
      - checkout

      - run:
          name: update repos
          command: apt-get update

      - run:
          name: install packages
          command: echo y | apt-get install xvfb wget unzip libc6 libcurl3-gnutls-dev libfreetype6-dev libgcc1 libjpeg-dev libpng-dev libstdc++6 libwebkit2gtk-4.0-dev libx11-6 libxext6 zlib1g make g++ mesa-common-dev libasound2-dev

      - run:
          name: get JUCE 5.4.3
          command: wget https://d30pueezughrda.cloudfront.net/juce/juce-5.4.3-linux.zip

      - run:
          name: unzip JUCE
          command: unzip juce-5.4.3-linux.zip

      - run:
          name: build and run tests
          command: JUCE_DIR=`pwd`/JUCE xvfb-run -a --server-args="-screen 0 1280x800x24 -ac -nolisten tcp -dpi 96 +extension RANDR" run.sh

自分のモジュールでは、run.shの中に、Projucerの実行などをまとめている。

$JUCE_DIR/Projucer --set-global-search-path linux defaultJuceModulePath $JUCE_DIR/modules
$JUCE_DIR/Projucer  --resave 自分のコードの.jucer

cd Builds/LinuxMakefile && make && cd ../..

Builds/LinuxMakefile/build/アプリケーション (実行して自動的にテストを実行して終了するオプション)

ビルドだけで用事が済むのであれば、最後のステップは不要だ。

6月のmusic tools hacks, あるいはLV2 for Android

もっと早くに実現していたはずだったのですが、体調不良などに引きずられて今頃になって台湾に渡ってフラフラ過ごしています。人に会う用事が毎日のようにあって、日本の引きこもり生活とは雲泥の差が…

無駄話はさておき、5月にAndroid Native MIDI APIについて書いたときに、こんなことを書いていました。

Google I/Oでこの方面でなんか発表してほしいなあというお気持ちの表明だったのですが、どうやら何も出てきませんでした。まあGoogleがやらないのであれば自分でやるしかない感じですよね…

とはいえ、何もないところから仕組みを作るのはいささか心もとないので、まずは5月にいじっていたLV2やlilvをAndroidに移植するところから着手しました(ホントはこれウソ入ってるんですが、まあ今全部書かなくてもいいでしょう)。

Androidサポートとは具体的にはFILEからの読み込みをAndroid Assetからの読み込みに変更するとか、direntを使ったディレクトリベースのプラグイン検索を無効化するとか(今回この実装を書いていて初めて気付いたのですが、Androidのassetはディレクトリ構造になっているくせにディレクトリのiteratorは無いんですね…)、ちまちまとした変更ではあります。しかし、元のライブラリが慣れないRDFやTurtleを扱うものなので勝手がわからない、慣れないCコード、ビルドがcerbero経由でしかなくgithubに確認なしでcommitしないとpullしてビルドできない、lv2の有用そうなやつをビルドするにはlibsndfileやcairoもビルドしなければならない、lv2まわりではcerberoが想定していないwafが使われている…といった諸事情もあって、割と手間取りました。たぶんここまでAndroid NDKでビルドできるようにしたやつ、cerberoのAndroid方面担当者以外では自分くらいしかいないのでは…

ソースはだいたいこの辺にあります。

LV2には、ドキュメントでも言及されている公式サンプルと、vst3sdk用のサンプルであるmdaというこの方面では割とポピュラーなもの(?)を移植したものがあり、今回Android向けにビルドした際にはこれらもビルドしています(正直プラグインはコードまで確認していません。どうせやっていることはオーディオバッファの加工くらいだろうし)。

今試験的に作っているサンプルアプリ(これはまだソース公開していない)をDeployGateで公開しているのですが(ってこのリンクもしかしたら無効なのかも…ログインしないと見られないのですが、わたしは自分のアカウントでしかログインできないので確認できない…)追記: やっぱ開けなさそうなのでとりあえずdropboxに一時うpしました、UIがまだメチャクチャで…まあそのうち使えるようになると思います(!?)。アプリ内のプラグインのスイッチを入れるとそのプラグインが適用されたwavの波形が描画されて再生もできるという感じです(再生は加工前のwavにもできるのですがwav表示がおかしい…)。また、このアプリ、諸般の事情からminSdkVersion 29なので、動作させることが出来る端末は世の中に1%も無いという…このへんは本番が出せる頃には改善したいと思っています(!?)。

また、これらのLV2プラグインをサービスとして列挙する仕組みまでは作ったのですが、それ以上は現状何もできていません。サービス側のスイッチを入れてもダミーのforeground serviceが開始されるだけです。

そんなわけで来月もこの辺のコードを書くことになると思いますが、忙しくなりそうなので手が回らないかも…とりあえず、LV2がAndroidで動く、というだけでも(当然プラットフォーム別に実装されるGUIのサポートは無くても)割と面白いんじゃないかと思います。ただもう少し一般化したいところではあります。LV2ほとんど利用例が無いので。

ちなみに、似たようなことをvst3sdkでも出来ないかと思ってビルドを試みたのですが、(X11なvstguiは論外として)Android NDK r20で実装されていないstd::experimental::filesystemLLVMではlibc++fs)に依存していたり、GCC固有の__gnu_cxx::__**atomic**_add()に依存していたり(Android NDKにはもうGCCが含まれていないのです)と、まだビルドできる状況ではないということが分かりました…(atomicはstd::**atomic**_fetch_add()で書き換えられそうではありますが)。幸いlibc++fsはNDK r21を目標に実装されそうなので、待っておこうと思っています。

オーディオプラグインとプロセス分離に関する覚書

ここ数日ばかり、DAWのオーディオエンジンがあまりプラグインをプロセス分離でロードしていないのは何でだろう?というのが気になっていて、いろいろ調べていたので、覚書としてまとめてみる。

プロセス分離モデル

DAWというものは安定して動作させるのが難しいソフトウェアの類型である。その最大の理由はオーディオプラグインを動作させる方式にかかる制約にある。

DAWは、オーディオプラグインのホストとして、さまざまな開発者によって提供されるオーディオプラグインを、エンドユーザーのさまざまなプラットフォーム上でロードして実行する。オーディオプラグインの開発者の視点で言えば、エンドユーザーはさまざまなDAWを使用して、さまざまなプラットフォーム上でそのプラグインをロードして実行する。

一般的に、このようなプラグイン機構においては、プロセスを分離して、プラグインのプロセスがクラッシュしても、ホスト側はアプリケーションを継続して実行できるようにするほうが安全だ。このようなモデルはアウトオブプロセス実行と呼ばれる(アウトプロセスと呼ぶ分野もあるようだ)。Webブラウザプラグイン機構やIDEプラグイン機構には、このモデルを採用するものが少なくない。実際には、諸条件を考慮して同一プロセスの中でライブラリを動的にロードして実行することもある。このようなモデルはインプロセス実行と呼ばれる。

リアルタイム制約

オーディオプラグインの場合には、問題を複雑化する要因がある。オーディオプラグインのパイプラインはリアルタイム優先度で処理されなければならないので、オーディオプラグイン自体がリアルタイム優先度で処理を完了できなければならないし、パイプライン処理にも時間をかけすぎることはできない。

しかし現実には、DAWはかなりの数のオーディオプラグインを処理しなければならない。オーディオトラックが50、100とあるような本格的な楽曲になると、オーディオプラグインが数百件処理されることにもなる。AppleWWDC 2019では最新のMac ProのデモでLogic Pro上で1000トラック作成してKontaktを割り当てて再生するものがあったらしいが、1000トラックもあればさすがに一般的な音楽制作では十分すぎるほどだ。しかし1000という数字が圧倒的ということは、ひと桁減らして100トラックではデモとして足りないということでもある。いずれにしろ、DTMは業務用チーズシュレッダーを使うことができない一般大衆のためにあるべきだ。実際の制作作業ではトラックのフリーズ等の処理を施しながら作業することでパフォーマンス上の問題を回避するのが一般的だと思われるが、オーディオプラグインのパイプラインにシビアな処理速度が求められることに変わりはない。

インプロセスでオーディオパイプラインを処理する場合は、単にリストを辿りながら単純なループでプラグインの処理を実行するだけで足りる。しかしアウトオブプロセスで処理する場合は、呼び出しと応答をプロセス間通信のようなかたちで連結しなければならないことになる。単純なプロセス間のコンテキストスイッチでもそれなりのコストがかかるし、IPCのフレームワークも厳しく選択しなければならない。

CLAPというオーディオプラグイン機構が考案されたときにhacker newsに寄せられたコメントのひとつで、このプロセス分離とパフォーマンスの両立が難しいという問題が挙げられていて、これがやや詳しく解説していたので、参考までに言及しておきたい。1プラグインプロセスの切り替えにつき6-7μsかかるというのはそれなりのインパクトがありそうだ。

リアルタイム処理で重要なのは、遅延が無いという意味での安定的な処理モデルだが、処理速度の観点を無視できるということはなく、パフォーマンスが良いほどより多くのオーディオプラグインを処理できるという利点はある(前述のとんでもない処理速度をもつMac Proの利点はここにある)。

これがもしネットワーク越しに行われるとなるとさらに大規模な遅延が生じうる。YAMAHAのNetDuettoなどはこれを実現しているということになろうが、一般的には同じマシンで動作していることがほぼ前提であると考える必要がありそうだ。Web経由でこれを処理できるようになる状況は、まだまだ期待できなそうに思えてしまう。

2019/06/08追記: Bitwig Studio 2.5には、オーディオプラグインごとにプロセス分離モデルを選択できるオプションがある。

f:id:atsushieno:20190608001806p:plain

アプリケーションの制限されたリソースアクセス

デスクトップPCの環境においては、まだアプリケーションの動作環境に対する制約は小さい。DAWなどのオーディオプラグインホストとVSTなどのオーディオプラグインは別々のベンダーが開発するものであり、ホストのプロセス内部にオーディオプラグインのライブラリをロードして実行するのはごく一般的だ。

しかしモバイル環境におけるリソースのアクセスは、デスクトップ環境ほど自由ではない。一般的には、アプリケーションのそれぞれには、コンテナのようにアクセス制限が課されていて、他のアプリケーションのリソースには自由にアクセスできない。DAWのアプリケーションが、他のオーディオプラグインのアプリケーションに含まれるライブラリを動的にインプロセスにロードして実行することができないとなると、必然的にオーディオプラグインとのやり取りもアウトオブプロセスで行われることになる。

また、デスクトップOSであっても、macOSにはアプリケーションのサンドボックス実行という概念があり、主にMac App Storeで配信されるアプリケーションに課せられる制約だが、一般的には、サンドボックス下にあるアプリケーションには、PC上のファイルやリソースに無制限にアクセスすることはできない。他所のアプリケーションの共有ライブラリをロードして呼び出すことも無条件にはできないし、ライブラリだけをロードできたとしても、別のアプリケーションのリソースにアクセスすることはできない。

この関係で面白いのはmacOSiOSを同様にサポートしているAppleのAudioUnit v3(以下単にAU、v3が前提)なのだけど、長くなるので節を改めて続ける。

AudioUnit v3のアプローチ

AUAppleプラットフォームで(プラットフォームのレベルで)サポートされているオーディオプラグインのための仕様だ。OSの一部なのかよ!というのが多分正しいリアクションだが、OSの機能が拡張される理由の一因になっている側面はありそうだ。というのは、macOSのセキュリティ機構の一部であるアプリケーション サンドボックスと、オーディオプラグインのようなプログラムは、先に軽く言及したとおり相性が悪いので、何かしらの対応策が必要になったからだ。

AUホストとなるアプリケーションをMac App Storeで配布する場合にはサンドボックス環境をサポートする必要がある。サンドボックス環境では、外部のアプリケーションとなる従来のAUではロードできない。このため、AUはv3で任意のアプリケーションでロードできるApp Extensionとして実装することになった。

App Extensionの動作原理については、ある程度のレベルまではUnderstand How an App Extension Worksというドキュメントに詳しくまとめられている。また、AU v3については、WWDC 2015におけるセッション資料が詳しい。

AUホストは、AUをアウトオブプロセスで動作させることもインプロセスで動作させることもでき、アウトオブプロセスで動作する場合は、XPCと呼ばれるプロセス間通信の仕組みに基づいてメッセージングが行われるようだ。ホスト側はkAudioComponentInstantiation_LoadInProcessを、AU側はAudioComponentBundleの内容を調整するかたちで設定する。アウトオブプロセスで動作する場合は、レンダリング処理1つあたり40μsくらいまでのレイテンシーが加わるようだ(macOSでの話と思われる)。

インプロセスで動作させることが可能になっているというのは、App Extensionという特殊な機構に基づいているがゆえの特権である、といえそうだ。Appleは自らGarage BandやLogic Proもリリースしているので、低レイテンシーオーディオにOSのレベルで積極的にコミットしていて、AU v3の仕様はこの現れであるといえる。

AUホストやAUプラグインの開発については、特にサンドボックスとの関連も含めて、以下のドキュメント群が詳しい。

もっともプロセス分離モデルに関する詳細な解説は見当たらなかった。インプロセスのAUがどのようにサンドボックスを実現しているのか、あるいはリソースにアクセスできてしまうのか、といった疑問は解決されなかった(試せばわかることだとは思うが、普段Mac上で活動していない筆者にはあまり本気で調べる意思が無い)。

AUサポートに関しては、macOSiOSで大きな違いは無さそうだ。OSによるセキュリティ機構としてもおそらく大きな違いは無いのだろう。

VST3とsandboxing

ちなみに、こういったmacOSの状況を鑑みると、「ということはVST3もAUみたいな機構を実装しているのか…?」という疑問も浮かんでくる。AUサンドボックスの都合でApp Extensionとなることを強いられたのだとしたら、VST3も同様にインプロセスにアクセスできるApp Extensionとして設計される必要があるはずだ(これがユーザーランドで可能なのかどうかはわからないが)。

どうなっているのか…と思って"App" "Extension" でvst3sdkのソースツリーを検索してみたが、出てきたのがAddVST3AuV3.cmakeというcmakeモジュールくらいであり、これはiOSビルドにのみ適用されるようだ。

sandboxingせずにsandboxed hostでプラグインとして使用することは出来ないだろうし、sandboxed hostで使えるようにすることは諦めているのだろうか…と思ったところでふと気付いてApp StoreDAWを検索してみると、Logic Pro以外はほぼ無いという状況だった。音楽アプリケーションがApp Storeで公開されていないのであれば、いくらApp Storeでsandboxingを強制しても無駄なことだ。Logic ProではもともとAUしか使えないわけだし、MacをサポートしているオーディオプラグインはほとんどVSTにもAUにも対応しているし、VSTホスト(DAW)のベンダーにとってはVSTをサポートしているだけでLogic Proに対して優位にあるといえるし、App Storeに置けないことにはほぼデメリットがない。

Androidのアウトオブプロセスによる処理の可能性

iOSと同様にアプリケーションごとに異なるLinuxユーザーアカウントを作成しているAndroid環境で、サンドボックスAUのようなことは実現できるのか。筆者が現時点で思いつく範囲では、以下のような課題がある。

(1) オーディオプラグイン同士のインタラクション: オーディオパイプラインとプラグインホストの間で渡されるオーディオバッファへの読み書きが問題なく行えなければならない。これは共有メモリへの読み書き(shm, ashmemなど)でおこなれそうだ。ashmemはandroid-15以降でサポートされている(Java APIのSharedMemoryは27以降のみ)。

(2) リアルタイム処理可能なIPCのサポート - Androidは8.0でBinder IPCのリアルタイム優先度を実現しているようだ(ただしユーザーに開放されているかは未確認)。リアルタイム優先度のbinderメッセージングは、native MIDI APIの実現のためには必須だったと考えられるが、AAudioにおけるlow latencyモードのオーディオ処理でも必要だったと考えるのが妥当そうだ。

いずれにしろ、この辺りの課題をクリアできれば、AndroidでもAUのようなアウトオブプロセスのプラグイン機構が実現できるかもしれない。

6/12追記: ELK(Linuxベースのオーディオ用リアルタイムOS)の開発者が今年になって書き始めた一連のブログ投稿(現時点で未完成)で、今回のネタに関連するLinux方面での取り組みなどがよくまとめられている。

5月のmusic tooling hacks

あたし5月って苦手なのよね…無駄に爽やかだから…あと天気が変わりすぎて肌荒れしちゃうし。

戯言はさておき、5月に読み書きしていたコードの話を雑に書きます。

JUCE/VST3

VST3まわりをいじって音が出せるようになっていたJUCEですが、とりあえず試しにtracktion_engineで使われているやつでパッチビルドを使ってみたらちゃんとPlaybackDemoで再生するところまでいけたので、これだけでも使いたい人はいるかもしれんと思って結局PRにして投げるだけ投げました(ROLIはPRを受け取らないと表明しているはずですが)。

VSTまわりを少し調べてわかったのですが、AudioPluginHostでmda-~なプラグインのUIが表示されるのは、VSTGUIをサポートしていない場合にJUCEがVST3プラグインのパラメーター情報などをもとに最低限?のUIを表示しているようです。おそらく同じことが「VST3をサポートしている」Bitwig Studioについても言えるのではないでしょうか。VST3で作成されたプラグインとしてLinux上でGUI表示までサポートしているものを知らないので(何しろVST3をサポートしているホストを日常的に使っていないので!)、VST3に深入りすることがあればもう少し調べてみるか…みたいなステータスです。個人的にはGUIに行くより前に調べることがいろいろあるので…

LV2, lilv, lilv-sharp

VST3方面が(主にupstreamが)あまり芳しくないので、5月は少し方向性を変えてLV2を調べたりしていました。LV2というのはLADSPA v2のことです。要はLinuxネイティブで動かせるプラグインに手を出しておこうと思ったわけです。実のところLV2はLinux方面でもそんなに使われていないので、こっちに手を出す意味がどれくらいあるかは怪しいところですが、今知っておけば日本の第一人者に近い立ち位置になれるなーというお気持ちで調べたりしています。

http://lv2plug.in/

個人的には、Chrome OSLinuxALSAをサポートし始めた2019年から、米国の教育機関ではマーケットシェア60%にまで至るこのOSで動作するオーディオプラグインをサポートする流れは確実に出来ていくだろうと思っているので、この辺に手を出すなら早いほうがいいと思っています。

そんなわけでLV2ですが、(おそらく日本で知ってる人はほとんどいないと思うので)ゼロから説明しないと分かってもらえないところだとは思うのですが、これを真面目に説明しようと思うと中で使われているRDFやらTurtleやらいろいろめんどくさいところから始めなければならず(この辺が参入障壁を無駄に高くしている要因だと思う)、このエントリでそこまでする気には到底なれないので、いずれ暇を見て何か書くと思います。

とりあえず、いま興味があるのはオーディオプラグインホスティング方面なので、LV2のホスティング実装であるところのlilvで遊んでいます。lilvは純粋なCライブラリなので(LV2のエコシステム自体が割とそうなっている)、C#バインディングを作ったりしていました。P/Invoke部分は例によってnclangを使ってほぼ自動生成です(ちょいちょいコマンドラインオプションでごまかしている部分がある)。

github.com

nclang/generated-natives

lilv-sharpを作っているときにnclangをいじっていて気付いたのですが、MSにもClangSharpというclangのバインディングがあって(これ自体は知っていたのですが)、nclangと同様にPInvoke自動生成ツールが付いているんですね。ClangSharpはそれ自体がlibclangのバインディングをコレで生成していて、dogfoodingとしてはなかなか良いと思うのですが、nclangでもそういうブランチを作って移行してみようと思った結果「これはあんま意味ないな…」となって放置しています。libclang自体はAPI後方互換性があるので現状ほとんど困らないですし。むしろClangSharpは自動生成APIを用意しているだけで、生成されたAPIが.NETらしく便利に使えるものになっているとは言い難いので、ClangSharpは使わずにnclangを使い続けようと思いました。

.NET開発はほぼ継続不能状態

ただ、最近はそもそも.NET開発をほとんど中止している状態です。というのはmsbuildLinuxパッケージングが全然まともにメンテされていなくて使い物にならないからです。実際にはdotnet/source-build(dotnet関連プロジェクトのビルドに使われているビルドツール)の細かい問題と、msbuildスクリプトが参照できないアセンブリを見に行って失敗する問題と、msbuildのビルドにバンドルされているRoslynがちょっとしたプロジェクトのビルドでも簡単にクラッシュしてビルド自体が続行できないようなレベルになっている問題など、いろいろ累積的で、.NET Coreチームがまともに仕事していないことに起因しているのですが、とりあえず諸方面で状態を問い合わせるだけでうんざりしたので、ビルドが正常化するまで放置することにしました。MSBuildが開発のエコシステムに食い込んでいなければMSBuildを捨てればいいだけの話なのですが、Riderとか使っているとビルド時にはMSBuildを呼び出すんですよね…

https://github.com/atsushieno/managed-midi/issues/42

あと開発を単純に放置できる話であればよいのですが、MIDI関連ツールはわたしが日常的に使っているものなので、こんなリスクをかかえたまま開発を継続することはできないので、他の作業が落ち着いたらC++にでも移植することになると思います。managed-midiの一部はKotlinに移植済なのですが、unsignedがまだ標準でサポートされていないKotlinで開発を継続するのはイマイチだなと思って躊躇しているところです。CLionのKotlin/Native・MPPサポートも特にライブラリ開発に関しては現状ほぼ無いので。

幸いなことに、modern C++はそれなりに面白そうなので、C#とあまり変わらないノリで開発できるんじゃないかという気もしています。C#でもP/Invokeとの絡みでリソース管理を細かく気にするようなコードばかり書いていたので。むしろC#のほうがいろいろpinnedにしないといけない分かえってめんどくさかったりして。lilvのPoCコードを書いているときも、lilv-sharpではこの関連で無駄にいろいろハマりました。lldb + monobt.pyでデバッグしてもいろいろ実行時情報不足だったりするし…。この辺C/C++でPoC書いたら1日で済んだので、最初からこっちでやろうと思うようになりました。

反省と展望

そんなわけで今後はC/C++方面に進んでいくんじゃないかなと思っています。知らんけど。

6月こそはもう少し音楽打ち込み作業に時間を確保したい…(フラグ)

Android AMidi (Native MIDI) APIについて

概要

Android Qで新しく追加されるNative MIDI APIは、Android NDKを利用してビルドされるネイティブコードで、Androidシステム上に登録されている「MIDIバイス」にアクセスするためのAPIだ。ネイティブコードで記述することで、ある種のMIDI関連タスクの処理負荷が軽減され、リアルタイム性が向上することが期待される(「リアルタイム性」についてはこの過去記事などを参照)。

Native MIDI APIはC APIとして公開されており、amidi/AMidi.hをインクルードして、libamidi.soを共有ライブラリとして参照してビルドする。毎回Native MIDI APIと書くのは長いので、以降はAMidiと記す。

Android MIDI API (Java) のおさらい

Android MIDI APIAndroid 6.0(Marshmallow)で追加された。AndroidMIDIバイスには、USBデバイス、BLEデバイス、ユーザー/アプリケーション実装によるMIDIバイスサービスの3種類がある。よくiOSMIDIバイスとして販売されているBLE製品は、Appleの仕様に基づいてMMAMIDI Manifacturers Association)で規定されたMIDI-BLE仕様を実装しているAndroidでも使えるはずだ。

JavaAndroid MIDI APIandroid.media.midiパッケージにある。Android上でシステムに登録されたMIDIバイスを利用するためには、Androidのシステムサービスとして登録されたMidiManagerにアクセスして、Javaオブジェクトとして存在するMidiDeviceを取得する必要があり、ART上のJavaオブジェクトを経由せずにAndroidシステムが管理するMIDIバイスにアクセスすることはできない(C/C++のコードだけで完結するとしてもJNIEnvオブジェクトやMidiManagerオブジェクト、MidiDeviceオブジェクトを扱う必要がある)。

Androidのシステム登録と無関係にMIDIバイスを接続するのであれば、システムが実装しているUSB接続やBLE接続に相当するコードを自分で用意しなければならない。仮想MIDIバイスなら特に難しいことはないだろう。*1

機能

AMidiはJavaandroid.media.midiパッケージのAPIに相当する機能の全てを提供しているわけではない。AMidiの機能の範囲は、次のように、ほぼMIDIアプリケーション(クライアント)側の、入出力ポートにおけるMIDIメッセージの送受信に限定されている。

  • MIDIバイスをあらわす構造体AMidiDeviceの入出力ポートAMidiInputPortAMidiOutputPortを開閉する
  • ポートの数は取得できるが、ポート情報の詳細は一切取得できない
  • MIDIバイスの詳細情報は一切取得できない
  • 入力ポートAMidiOutputPortからMIDIメッセージを受け取る
  • 出力ポートAMidiInputPortMIDIメッセージを送る

AMidi.hで宣言されている型は以下の3つ(内容は未公開)、関数は以下の13件しかない。

型:

typedef struct AMidiDevice AMidiDevice
typedef struct AMidiInputPort AMidiInputPort
typedef struct AMidiOutputPort AMidiOutputPort

関数:

media_status_t AMIDI_API AMidiDevice_fromJava(
    JNIEnv *env, jobject midiDeviceObj, AMidiDevice **outDevicePtrPtr)
media_status_t AMIDI_API AMidiDevice_release(
    const AMidiDevice *midiDevice)
int32_t AMIDI_API AMidiDevice_getType(
    const AMidiDevice *device)
ssize_t AMIDI_API AMidiDevice_getNumInputPorts(
    const AMidiDevice *device)
ssize_t AMIDI_API AMidiDevice_getNumOutputPorts(
    const AMidiDevice *device)
media_status_t AMIDI_API AMidiOutputPort_open(
    const AMidiDevice *device, int32_t portNumber, AMidiOutputPort **outOutputPortPtr)
void AMIDI_API AMidiOutputPort_close(
    const AMidiOutputPort *outputPort)
ssize_t AMIDI_API AMidiOutputPort_receive(
    const AMidiOutputPort *outputPort, int32_t *opcodePtr, uint8_t *buffer, size_t maxBytes,
    size_t* numBytesReceivedPtr, int64_t *outTimestampPtr)
media_status_t AMIDI_API AMidiInputPort_open(
    const AMidiDevice *device, int32_t portNumber, AMidiInputPort **outInputPortPtr)
ssize_t AMIDI_API AMidiInputPort_send(
    const AMidiInputPort *inputPort, const uint8_t *buffer, size_t numBytes)
ssize_t AMIDI_API AMidiInputPort_sendWithTimestamp(
    const AMidiInputPort *inputPort, const uint8_t *buffer, size_t numBytes, int64_t timestamp)
media_status_t AMIDI_API AMidiInputPort_sendFlush(
    const AMidiInputPort *inputPort)
void AMIDI_API AMidiInputPort_close(
    const AMidiInputPort *inputPort)

バイス詳細情報やポート詳細情報など、文字列が関連してくるAPIについては、C/C++のコードでどのようなデータ表現を用いるのか、定まった解がなく、AMidi APIはこれらをうまいこと回避しているともいえる。

ひとつ重要なことを指摘しておく必要があろう。MidiDeviceインスタンスは、アプリケーション側であるMidiManagerを経由してのみ取得できるものなので、MidiDeviceServiceの実装でこれを取得することはできない。つまり、MidiDeviceServiceではAMidiは使用できないということである。後述するfluidsynth-midi-serviceでこれを利用できる可能性はゼロだ。

Javaコードとの棲み分け

AMidiは処理速度が重要になるMIDIメッセージの送受信に特化したネイティブライブラリであり、それ以外の機能についてはJavaAPIが使われることが想定される。 AMidiのエントリーポイントはAMidiDevice_fromJava()関数であり、AMidiを使うためには少なくともJava側でandroid.media.midi.MidiDeviceインスタンスを取得する必要がある。

そこから先は、MIDIアプリケーションの目的によって、利用できるAPIの向きが変わる。

  • (1) 外部MIDIバイスからのMIDIメッセージを受け取って処理するアプリケーションでは、MIDI入力ポートからのメッセージの受け取りをandroid.media.midi.MidiOutputPortからではなくAMidiOutputPort_receive()で受け取って処理するようにすると、Native MIDI APIによってMIDIメッセージの送受信が軽量化される可能性がある。(MIDI音源と接続したり仮想MIDI音源を操作するタイプのMIDIバイスサービス実装では、MidiDeviceインスタンスを取得できないので、この可能性は無い。)

  • (2) 外部MIDI音源MIDIメッセージを送るアプリケーションでは、MIDI出力ポートへのメッセージの送信をandroid.media.midi.MidiInputPortではなくAMidiInputPort_send()あるいはAMidiInputPort_sendWithTimestamp()で処理するようにすると、内部的に軽量化される可能性がある。(カスタムMIDIバイスと接続するMIDIバイスサービス実装では、MidiDeviceインスタンスを取得できないので、この可能性は無い。)

いずれの場合も、従来のJavaandroid.media.midiAPIでは、対象MIDIバイスとの接続にはAndroidのServiceの仕組みに基づいてメッセージがParcelにシリアライズされ、Parcelとして送受信され、Parcelから元のデータがデシリアライズされる必要があった。この部分がAMidiで最適化されている可能性はある。

この節で何度も「可能性がある」と留保付きの表現を用いているのは、ここはプレビュー時点でソース未公開のAndroid Qのフレームワーク実装の部分なので情報がないためである。一般的には異なるプロセス同士でメモリエリアを共有することはできないが、Parcelが内部的に使用しているbinderの仕組みでサービス側とメモリを直接やり取りしている可能性はある。Android Qで新たに追加された機能ということは、フレームワークのレベルで改良される必要があったということがうかがわれる。

Android 8.0から、binderのドライバーがrealtimeの優先度で動作できるようになっている。これがMidiDeviceServiceを経由するMIDIメッセージのやり取りに使用されている可能性もある、と筆者は想像している。

実用未遂: fluidsynth mdriverのAMidiサポート

筆者は(実のところさほど明確な目的もなく)Fluidsynthにパッチを送り続けているのだが、今回もFluidsynthにAMidiを応用するMIDIドライバーを作ってみた。自前のgithub fork上で公開しており、公式にもissueとして登録してあった。(ただ、前述のとおりMidiDeviceServiceで使える目処が立たなかったので、このissueは閉じてある。)

Fluidsynthは、その起源はLinux用のソフトウェア・シンセサイザーだが、クロスプラットフォームで動作するように、プラットフォーム固有の「ドライバー」の上に音声合成の共通コードが乗っかっているかたちになっている。ドライバーには、プラットフォームのオーディオAPIにアクセスするオーディオドライバー(以降ソースコードの名前に合わせてadriver)と、MIDI APIにアクセスしてそこから受け取ったMIDIメッセージをそのまま合成エンジンに高速に渡すことができるMIDIドライバー(以降mdriver)がある。mdriverは、fluidsynth(.exe)で内部的に利用できるオプション機能だ(使わなくてもよい)。

mdriverを使わない場合、合成エンジンに対してnote on, program change, control changeなどの機能を実装するC APIを呼び出すかたちになる。この場合、MIDIメッセージとして受信したバイト列をアプリケーション側が自前で解析して、対応するAPIを呼び出す必要がなる。

mdriverはnew_fluid_midi_driver()という関数で生成するが、このときコールバック関数としてhandle_midi_event_func_tを渡すことになる。この中で、別途生成したsynthエンジン(fluid_synth_t)で直接合成させてもよいし(fluidsynth(.exe)が行っているのはこれだ)、別の処理を挟んでもよい。Android上では、OpenSLESやOboeが有効になっていないsynthオブジェクトにMIDIドライバーが受け取ったMIDIメッセージを処理させても、あまり意味がない(MIDIメッセージのフィルタリングくらいはできる)が、幸いなことに最新のfluidsynth masterには筆者のOboe/OpenSLESドライバーが含まれているので、恩恵を受けることが出来る。

今回考えたのはNative MIDI APIを応用して、AMidiのmdriverを実装する、というものである。Android MIDI API経由でFluidsynthのMidiReceiverが受信したMIDIメッセージを、JNIマーシャリングコストなしでfluidsynthの合成エンジンが処理できるか試してみる、という感じである。しかしMidiDeviceServiceからAMidiへのエントリーポイントが全く存在しないため、実現可能性は無かった。

潜在的な需要: オーディオプラグイン機構のサポート

前述した話だが、Android 8.0ではリアルタイム優先度のbinderドライバーが利用可能になっている。これによって潜在的に大きな恩恵を受けることが出来るのがオーディオルーティングを伴うアプリケーションだ。オーディオ編集アプリケーションが、オーディオエフェクトをサービスとして実装するアプリケーションを、オーディオプラグインとして利用できたら、なかなかおもしろいことになるのではないかと筆者は想像している。(前節でも重ねて「可能性がある」と書いてきたのと同じで、まだソースが公開されていないので、ここは想定でしかない。)

Android 8.0未満の環境では、binderを経由してデータを渡して処理させたら、リアルタイム優先度を維持することが不可能だった。リアルタイム優先度を維持できないと、オーディオ処理に遅延が生じることになる。それを受け容れるというのも一つのあり方だが、レコーディング時やライブコーディング時など、限りなく「絶対に」に近いかたちで遅延を回避したい状況というのはあるだろう。

オーディオ? Native MIDI APIMIDIAPIではないのか? という疑問が湧いてくると思うが、音楽制作で一般的に使われるVSTAU (Audio Unit)、LADSPAなどオーディオプラグインの世界では、一般的にMIDIメッセージがプラグインの制御に使われている(もう少し正確にいえば、MIDIメッセージを変換して各オーディオプラグインのパラメーターを制御するようになっている。オーディオプラグインの制御範囲はMIDIの表現できる範囲に限定されない)。このメッセージング機構がリアルタイム優先度で行われるようになれば(もちろんオーディオ処理のほうがはるかに大きな制約になるわけだが)、オーディオホスト/プラグイン機構を実現するための壁がひとつ取り払われることになるのではないか、と思っている。

ついでに自分の予言者ごっこtweetも貼っておこう。

既存プラットフォームをサポートしつつamidiサポートを追加する

AMIDIサポートをネイティブコードで実装してするとなると、amidi.soをshared libraryとしてビルド時に参照することになる。これは、実行時にAndroid P以前のプラットフォームでは「使わない」ようにする必要がある。Javaのコードであれば、android.os.Build.VERSIONなどを使って条件分岐すれば足りるが、ネイティブの共有ライブラリの場合はそこまで単純ではないので注意が必要だ。

dlopen()で共有ライブラリがロードされるとき、デフォルトではそのライブラリが参照している共有ライブラリが連動してロードされる。Cの呼び出しを直接制御しているのであれば、dlopenの引数にRTLD_LAZYを指定すれば、シンボルの解決だけは先送りすることができるが、参照ライブラリのロード自体は即時に行われるので、Build.VERSIONの比較によって「実行されない」ようにコードを書いていても、JavaでいうところのUnsatisfiedLinkErrorが生じることは避けられない。

また、筆者のfluidsynthjnaのように、JNAeratorのような自動バインディング機構を活用していると、シンボル解決もロード時に全て行われる実装になるため、RTLD_LAZYにも効果がない。ダミーとなるライブラリを用意するとしても、シンボル自体が全て存在していなければならないことになる。

実のところこの問題には先例となるソリューションがある。GoogleはOboeでaaudioをサポートしているが、aaudioをサポートしたoboeがAndroid 27以前の端末でもUnsatisfiedLinkErrorを起こさずに実行できるのは、共有ライブラリとしてlibaaudio.soにリンクすることなく、dlopen()を使ってAAudioの関数を動的にロードしているためである。

https://github.com/google/oboe/blob/c278091a/src/aaudio/AAudioLoader.h#L67

古いプラットフォームでも動作するようにamidiを部分的にサポートするライブラリをビルドしようと思ったら、同様の仕組みを実現しなければならない。というわけで、自分で実装してみた。ただし一度も使っていないコードなので動作しない可能性が割とある。

https://github.com/atsushieno/fluidsynth-fork/blob/d92cbc5/src/drivers/fluid_android_amidi.c#L40

*1:たとえば、Google/music-synthesizer-for-AndroidにはMIDIメッセージを処理する機能があるが、Android MIDI APIとは無関係だ。

4月のmusic tooling hacks

4月というか平成も終わろうとしている今日この頃ですが、相変わらず時代をまたぐ無職生活エンジョイ勢です。相変わらず謎の方向性でコードを書いています。そんなわけでこのひと月くらい?のコーディング活動を雑にまとめます。

JUCE/VST3/Linuxの高い壁

楽曲制作環境の改善手順として、SMFベースのMIDIプレイヤーからオーディオプラグイン中心の音源ドライバーに移行しようと思っているのですが、2019年はLinuxデスクトップ開発者にとってたいへん難易度の高い年です(断言)。

昨年からSteinbergがVST2 SDKを廃止してダウンロードできない状態にしてしまい、VST3 SDKのみが入手可能な状態になったのですが、VST3 SDKGPLで入手できるものの、各種のネイティブVST*1プラグインDAW等のホストアプリケーションが追従していない状態です。VST2とVST3は根本的に別物に近いんですよね。

特にJUCEを使っているプロジェクトが、ホストもオーディオプラグインも全滅です。JUCEはLinux版VST3に対応しておらず、公式フォーラムに「おいおいLinux用VSTプラグインを作れると思って使ってきたけど詰んでるじゃん?」というスレッドが立っている状態です。 割と新鮮なUI設計で面白いhelioシーケンサーも、Waveformで使われているtracktion_engineも、Googleのmusic-synthesizer-for-AndroidベースのDX7互換FMシンセサイザーDexedも、サウンドフォントが使えるjuicysfpluginも動きません。*2

JUCEのプラグインならLADSPAでビルドすれば動くんじゃん?とか思うわけですが、dexedみたいにJUCE 5.2.1くらいのソースを取り込んでいるやつではProjucerでちょいちょいっとjuce_audio_processorsをいじってもVST2が無いだけでビルドエラーになるみたいな理不尽な状態なので、VST3でちゃんとビルドできるようになっていないとメンテナンス出来ない状態になるなあと思います。

そういうわけでたまにJUCEのソースを読みながら、Linux上でVST3を使えるようなブランチを作ろうとしているのですが、ちゃんと動くものが出来るまでには時間がかかりそうな気がしています。その間に本家で対応していてほしいけど、先のissueにsurprisingly difficultなどという供述があり…。JUCE、外部からのcontributionを受け付けない伽藍プロジェクトなので、今後こういうフレームワークに乗っかっていくことにはLinuxデスクトップユーザーとしては危機感があります。

オーディオプラグインフレームワークとしては、VSTの他にもLADSPA V2 (LV2) などがあって、各種ホストでこっちが標準でサポートされるようになってほしいところです。ちなみに今のところVST3をLinux上のDAWで使おうと思ったらBitwig StudioかArdour5訂正: 対応していると思っていたけどLXVSTのみでしたという選択肢しか無いと思います。Bitwig Studioえらい。

f:id:atsushieno:20190430211123p:plain

このスクショはJUCEのextraにあるAudioPluginHostなのですが、リストアップされているオーディオプラグインの中にVST3が含まれているのがミソです。juicysfpluginはguiが表示できなくて詰むのですが、mdaほげほげの類は音も出てGUIも表示され、パッチを作っていた自分がまずビックリでした…

fluidsynth-midi-service-j

github.com

先月までずっとfluidsynthのAndroidビルド用forkを本体にマージするための開発にかかっていたわけですが、ひと区切りついたので、Androidアプリケーション側の開発にシフトしました。まだ最初だけノイズが乗るみたいな問題があるのですが(ゴミのあるオーディオバッファが再生されている、だけでもなさそう…)、それなりに使える状態のアプリにはなってきたと思います。proof of conceptレベルでもちゃんとJava/Kotlinでアプリ開発するのはx年ぶり(思い出せない)なので*3なので、AACやらdata-bindingやらconstraint layoutやらをごく当たり前のように使ったり、DeployGateにも数年ぶりにうpしたりと、今らしいスタイルで開発できるのは新鮮です。これはこれで楽しい〜

先月の時点では単に和音を鳴らすだけのボタンがあるMainActivityしか無かったのですが、fluidsynth初期化設定をUIで調整できるようにして、Oboe/AAudioのLowLatencyモードやExclusiveモードも試せるようになったので、この辺に興味のある人にとってはちょっと楽しい感じです。

あと先月ノリでmanaged-midi (C#) から移植したKotilnのktmidiが、主にbyteのsigned/unsignedまわりとIntへの型変換を含めた比較で大爆死だったので、ちゃんと動くように全面的に手を加えて、SMFもちゃんと再生できるようになっています。KotlinにはexperimentalでunsignedTypesというのがあるのですが、仮にそれがkotlincまでシームレスに統合されるようになっても、Int定数との比較などでIntへの型変換などが必要になってくると、常にunsignedなのか否かを意識したコードを書かなければならず、KotlinでMIDIアプリケーションを安全に書くのは極めて難しいのではないかと思うようになりました(experimentalが外れてUByteの定数が書けるようになれば、この問題は少し緩和されることでしょう)。少なくとも、MMLコンパイラをKotlinに移植しようという気持ちはなくなりました。

あとアプリで実践的にfluidsynthを使うようになった関係で、ちゃんとSF3 (compressed soundfonts) をサポートするためにlibsndfileをビルドできるように本家fluidsynthに変更を加えたり、bitriseやローカルのdockerでビルドを確認できるようにしたり、といったこともやっていました。Android用fluidsynthがビルドできないという問い合わせ、地味によく来ていたので…(しかもなぜかメールが多い)。

あとこの方面の問い合わせに連動して北米方面のDAWの開発会社からうちで仕事しないか?みたいなのが来て、ktkr! とか思って前向きに返事したのですが、「よっしゃ連絡するわ」という返事を最後に通信途絶した()ので相変わらず無職です。(どうでもいい)

英語では他にもいろいろ書いたのですが(Android Native MIDI APIの話とか)、めんどくさくなったのでこっちでは省略します(何)。

soundfont-player-cs

github.com

MIDI中心からオーディオプラグイン中心の制作環境に移行するためには、スムーズにリズムパートを打ち込めるような環境と音源が必要です(前回のイベントで頒布した作品では、実はリズムパートが皆無に近いんです)。これまでもMMLで打ち込んできていて、ノートベロシティを踏まえた打ち込みは簡単だし、DAWでやると面倒くさい3連符もごく簡単に打ち込めるし、MMLでの打ち込みで続けたいのですが、手元にあるSC-8820にある音は限られていて、リズムパートを希望通りの音色で打ち込むのは無理ではないかと思うようになりました。

オーディオプラグインであればSerumとかEZDrummerとか、手元にあるだけでもOmnisphereとかBatteryとかあるわけですが、Linux環境で使えるものがほしいわけです。ドラムパートくらいならサウンドフォントでもいけるのではないか(加工系のプラグインならLADSPA等でいろいろあるし)と思ったのですが、サウンドフォント自体は大量に手に入る時代とはいえ、それぞれのサウンドフォントにどんな音があるのかを簡単に知る術がないぞ…と思ったわけです。

とりあえず、GUI上に手持ちのサウンドフォントにある音色(プリセット = プログラム / バンク)のリストをありったけ表示して、選ばれたやつを広大な鍵盤リストのようなものに表示して、どこでも叩けるようにしたら、音色の確認は簡単にできるようになるのではないかと思って、Xwt, Fluidsynth, NAudioを組み合わせてざっくり作りました。意外とこういうツールが無いんだよな…

f:id:atsushieno:20190430204056p:plain

やっている事自体は難しくないので、それこそ仕組みだけならWeb Audioとかでも作れなくはないのですが、サウンドフォントはファイルがでかいのでWebには向いていません。サウンドフォントに限らず、音楽系ツールは膨大なデータを伴うことが多いので、全般的にWebには向いていないですね。ローカルに各種リソースがあることが前提になると思います。

今回はたまたま C# で組みましたが、既にUIにかなり不満があって(特にListViewあたり)、これを解消するにはもう言語ごと他所に引っ越すしか無いかなと思っているのですが、なかなか自分の需要に合うものが少なそうです。とりあえずQML.NETあたりを試してみようかなとは思っています(これもnugetパッケージ版のLinuxビルドがおかしくて試せなかった)。

managed-midi: CoreMIDI on .NET Core, etc.

もうひとつ、managed-midiの話なのですが、ひとつ長い間どうにかしたいと思っていた問題が解決しました。.NET Frameworkや.NET Coreのプロジェクトでmanaged-midiを使うと、Macで使えるネイティブバックエンドが無いという問題です。

MIDIアクセスはプラットフォーム固有のAPIアクセスが必要になるのですが、Mac上のCoreMIDIを使うにはXamarin.Macに依存するしか無かったわけです。そうするとプロジェクトそのものがXamarin.MacのProjectTypeGuidを前提とするものになり、デスクトップ用のmonoを使ったり(.NET Frameworkが全体的にdeprecatedになりそうなので忘れられているかもしれませんが、.NET Coreの範囲を超えるデスクトップ用アプリを作ろうと思ったらmonoを使うことになるのです)、.NET Coreを使ったりということを考えると、Xamarin.Macは難しくなってきます。Xamarin.Macにはfull profileがあって、これはnet4x互換なのですが、いずれにしろわたしがLinux上でビルドするときに「無いとビルドエラーになる」ようなものでは困るわけです。

いろいろ考えた結果、もうXamarin.Macに含まれるCoreMIDI API相当の部分を自前実装しちゃったほうが早いだろう(これ自体はMonoMacで実現していたわけだし)、となって、今のmasterには実装が含まれています。IMidiAccess APIを実装する範囲の最小限のもので雑ですが。

これで自分のプロジェクトがちゃんと単一バイナリでもクロスプラットフォームで動作するといえる状態になったので、気が向いたらバイナリパッケージも作るかもしれません(managed-midi自体はずっとNuGetで配布しています)。

あと最近はWindows方面の開発者がUWP実装やwinmm実装のissueを報告してくれたりパッチを送ったりしてくれているので、managed-midiユーザーいたんだ…?みたいな気持ちでありがたくcontributionを受けています。自分が普段全然使っていない環境をケアしてくれる開発者がいるのはありがたいですね。

残っている課題?で多く来るのが「Unityで使いたい」なのですが、どうしようかな…Unity普段使っていないし、今さら.NET方面に時間使うのもったいないな…という感じで「やる気があったらcontributeしてくれ〜」みたいな感じで雑に返しているのが現状です。(モバイルまわりになると割と大変なので自分で作ってあげたほうがいいのかなーみたいに思ったりはしますが…) MIDIJackとかで対応してくれればそっちに誘導するんだけどなあ。

managed-midiの仮想ポート作成API

あと、仮想MIDIポートを作成するAPIを追加したので、これを関連プロジェクトであるところの仮想MIDIキーボードアプリxmmkでMIDI入力用ポートを追加して、Tracktion Waveformから接続して譜面に入力をペーストできるようにしました*4

実のところ送信内容をキー入力に限定する必要はなく、xmmkにはmugene MMLコンパイルしてMidiPlayerでバックグラウンド演奏できる機能も追加してあり、この内容を仮想入力ポートに送るだけでDAWMMLからペーストできるようになったともいえます*5。ピアノロールに打ち込めるのもまあ面白いのですが、一番「これは便利」と思っているのは、DAWから開いているオーディオプラグインで試しに音を出すとき、自分の好きなように入力UIを設計できることですね。物理MIDIキーボードと違って、ソフトウェアMIDIキーボードならいくらでも工夫の余地があるので…

ちなみにCoreMIDI APIでの実装はまだ何やら機能せず、WinMMには仮想ポートを定義する機能そのものが無いので対応しません。UWPは知らんけど多分無いんでしょう。

f:id:atsushieno:20190430211756p:plain

that's all

とりあえずこんなところでしょうか。本当はもっと音楽制作とかやって遊んでいるつもりだったのですが、ちょっと易きに流れてしまった感じで割と反省しています。来月はもちっと打ち込みに興じたいところです。(フラグ?)

*1:ユーザーとしてLinux上でVSTを動かすアプローチは2つあって、ひとつはLinux用のビルドを使う方式、もうひとつはwineを利用してWindows用のバイナルを動かす方式です。後者は商用のめんどくさいやつ(Kontaktとか)が全く使えないので、VST開発者にとっては前者のサポートが必須といえます。

*2:正確にはjuicysfpluginなどは古いJUCEを依存関係に含んでいるのか未だにビルドできるのですが、ライセンス的に配布できなくなっている状態です。

*3:ここでも何度か書いているかと思いますが、フレームワーク開発者がアプリを開発しない問題…

*4:特別にコードで実装しなくても、OSの機能としてMIDI Throughなどのポートにroutingかければいけたのですが

*5:個人的には楽曲の「土台作り」をDAW上でやりたくないので、ここに大きな可能性はあんまり見ていません