VitalをAndroidで動かす

JUCE Advent Calendar 2021、24日目も空いていたので滑り込ませてみました。今年4本目じゃん。

最近Vitaloidという名前でVitalをAndroidに移植して動かしてみたプロジェクトを作って遊んでいるので、どうすればそんなことができたのか、今何が出来るのか、何が出来ないのかについてまとめます。一応GitHub Actionsのbuild artifactにapkも含まれているので、興味がある人はいじってみてください(no supportです)。

github.com

Vitalについて

Vitalは2020年末にGPLv3でリリースされたウェーブテーブルシンセサイザーで、JUCEで作られています。

vital.audio

とにかく機能豊富で、わたしも細かく把握していないのですが、YouTubeのLOBOTICS CHANNELで詳しく紹介されているので、コレを見ながらコードを読んだりしています。

www.youtube.com

今回は使い方の話は主な話題ではないので、その説明に文章を割くつもりはありませんが、かといって細かい機能の話に踏み込む場面が無いわけではないので、説明無く登場する機能については適宜この辺を参考にしてもらえればと思います。

Vitalのソースコードの特色

VitalはProjucerを使ってプロジェクトファイルを生成してビルドするタイプの古典的なJUCEアプリケーションです。ただ、いくつかの点で特殊な構成になっています。また、バイナリディストリビューションをインストールした場合とソースからビルドした場合で、根本的に違う部分もあります。

  • JUCE 6.0.5近辺のmodulesのソースコードに独自に手を加えたものがリポジトリに含まれており、JUCE公式のソースに単純に差し替えてもビルドできません。その代わり(?)LV2サポートなども含まれています。
  • 一般的にはProjucerのプロジェクトでは、1つの.jucerファイルで単独実行アプリケーションとプラグインについて別々にプロジェクトを生成しますが、Vitalの場合はstandaloneとpluginで別々のvital.jucerが使われており、内容もそれなりに異なっています。
  • サーバーと連携する部分でFirebaseサポートなどが使われており、これはそのままではビルドできません(そしてOSSビルドと相性が悪い部分でもあります)。
  • Vitalのソースツリーにはデフォルトで利用可能なプリセットが何も含まれていません。Vitalはこの部分で商品価値を出しているわけです。音楽フリーソフトウェア界隈(というのが適切そうなコミュニティ群があります)ではOSSで利用可能なプリセットバンクを作ろうとしている人がたまにいるようです。
  • GUI部分は基本的にOpenGLで実装されています。分かる人向けに書くと、class OpenGlDeviceSelector : public OpenGlAutoImageComponent<AudioDeviceSelectorComponent> みたいなコードも出てくるので、それなりにガチのOpenGL実装っぽいふいんきを感じます(自分がOpenGL全くやらないマンなので妥当な評価なのかわからない)。
  • SIMDNEONで最適化されていてARM32ではビルドできません。(arm64の命令が使われている)

いずれにせよ、Androidビルドは含まれていないので、独自にセットアップする必要があります。

Androidビルドをjucerファイルに追加する

一般的なJUCEアプリケーション開発者にとっては、Androidをサポートする唯一の方法はProjucerでプロジェクトを生成するやり方です。自分の場合はCMakeで構成されたプロジェクトでも対応できるように、ビルドを把握しています。

atsushieno.hatenablog.com

今回は元ネタがProjucerで作られているので、Projucerでやっています。もっとも、自分の場合、Android移植は自作オーディオプラグイン機構のためにやっているので(Vitaloidもそう)、そのために作った他のプロジェクトの.jucerファイルから<ANDROIDSTUDIO>セクションなどをコピペしてきて、そこにVital固有のオプションなどを追加しています。ちなみに、現在のリポジトリでは、コピペ元の.jucerは、ソースツリー中のpluginからコピーしてきたvital.jucerを加工していますが、オーディオプラグインと無関係にビルドするのであればstandaloneを加工するほうがだいぶ簡単です(理由は後述)。

.jucerの中でFirebaseサポートのために追加されているライブラリやヘッダがありますが、これらが有効になっているとビルドできません。これらは<ANDROIDSTUDIO>要素のextraDefs属性でREQUIRE_AUTH=0&#10;NO_AUTH=1&#10"を追加して対応します。またこの他にOPENGL_ES=1&#10;BUILD_DATE=2021_12_00_00_00&#10;(あるいは他の適当な日付の文字列)もビルドに必要になるので追加しています。

Androidビルドのためにソースを書き換える

Projucerの面倒を見終えたら、今度はソース本体にも変更が必要になります(なりました)。

まず、つい先日書いた話ですが、JUCE on Androidでは同期ダイアログが使えません。

atsushieno.hatenablog.com

Vitalではプリセットバンクのロード/セーブ、WAVファイルの取り込みなどでFileChooserを使い、また各所でAlertWindowを使ってメッセージを表示しているので、けっこうな数の呼び出し部分を書き換えないとビルドできません。手っ取り早いのは「どのダイアログ呼び出しもNot SupportedとしてAlertWindowを(非同期で)出すだけにする」でごまかすやり方ですが、今回は何となくほぼ全部非同期呼び出しに置き換えて対応しています(ちゃんと動いていないところがそれなりにありそう)。

それから、Android NDK環境ではOpenGLサポートまわりでコンパイルに失敗するようなコードがちょこちょこあります。最初に躓くのは実行時に発生するshaderのコンパイルエラー(VITAL_ASSERT(checkShaderCorrect(extensions, shader_id)))でしょう。Android logcatには失敗の具体的な理由が出てきます:

2021-12-24 16:31:48.527 17173-17271/org.androidaudioplugin.vitaloid I/JUCE: ERROR: 0:7: '' : No precision specified for (float)
    ERROR: 0:8: '' : No precision specified for (float)

これに対処するには、translateFragmentShader()translateVertexShader()コンパイルされるシェーダーのスクリプトprecision mediump float;\n を追加してやる必要があります。関連情報はこの辺です:

stackoverflow.com

Vitalのstandalone/vital.jucerをもとにビルドしている場合は、ざっくりこれくらいの対応でビルドして実行できるようになっていると思います(細かく覚えていないので他にもいくつか修正があったかもしれません)。

standaloneとしてビルドした場合

JUCEを更新してpluginとしてビルドする

Androidオーディオプラグインとしてビルドする場合は、プロジェクトの構成が大きく変わってきます。利用する.jucerファイルをpluginのものに変更すると、プラグインアプリケーションのstandaloneビルドの内容も変わってきますし、実際にビルドしたものを起動するとこうなります:

pluginとしてビルドした場合 UIが壊れました。

実はこの問題は最新のJUCE 6.1.4…に至るいずれかのバージョン…で修正されているので、JUCEのソースを更新すると解決します。問題は、vitalのソースに含まれているJUCEには独自の変更が含まれているということです。

そもそも、vitalのソースツリーに含まれるJUCEがどの時点のソースツリーからの差分なのか、わたしには当初は分かりませんでした。JUCEのソースツリーでgit checkout 6.0.xで各バージョンをチェックアウトしながらdiff -ur /path/to/JUCE/modules /path/to/vital/third_party/JUCE/modules で差分の行数を見ていって、6.0.5あたりだと突き止めました。ただその後インポートされたソースファイルの至るところにversion: 6.0.5とヘッダで書かれているので、知っていれば自明だったという話も…。

ともあれ、いったん6.0.5とvitalの独自ソースとの差分を取得してしまえば、これを最新の6.1.4に当てて、パッチを当てられなかった部分を適宜手書きで対応すれば、JUCE 6.1.4でもビルドできるようになります。JUCE 6.1.4向けの差分は、完全ではないかもしれませんがリポジトリに突っ込んであります

JUCE 6.1.0以降にアップデートする場合の注意点として、JUCE 6.1.0ではOpenGLサポートにAPIの破壊的変更が加えられていて、juce::glがデフォルトでインポートされないのでさまざまなOpenGL関数がclangで見つからなくなります。using namespace juce::gl;で対応するのが手っ取り早いでしょう。

これでpluginビルドでも表示がまともになります(standaloneとはセンタリングされているかいないかの違いくらい)。

JUCE 6.1.4に更新後のpluginビルド

現状の課題

Android移植は動くようになりましたが、実用性は今のところあんまりないです。というのは…

  • タッチUIではほとんど何も出来ない: 一見して分かる通り、デスクトップのUIでもかなりでかいものを無理やりモバイルの画面のサイズに合わせているので(自分で対応したわけではないですが)、細かすぎて何も触れないでしょう。移植の動作確認はエミュレーター上でマウスで行っています。
  • 重い: Vitalはもともとかなり多機能で重量級のシンセなので、そもそもデスクトップでも重いと言われています。それをさらにCPUリソースの厳しいAndroidで動かしているので、推して知るべし…です。Advancedメニューでoversamplingを2から1に変更すると少しマシになりますが(Androidの画面だと小さくてどこにあるか分からないやつ)、オシレーターを1つから2つにしたらやっぱり重くなり、エフェクトを設定するとやっぱり重くなって音がブツ切れになるので、まあおもちゃとして使えるか…?というくらいです。
  • テキストやエフェクトのレンダリングがおかしい: OpenGL移植まわりの取りこぼしということでしょうが、テキストが大きくて少しはみ出ているところがちょいちょいあります。これはたぶんscaleの実装でHiDPIに対応してはいても、実画面のほうが小さいLoDPI?な状況には対応できていないということでしょう。
  • ドロップダウンリストのタッチイベントの検出位置もおかしい: たぶんこれもDPI/scaleの小さいやつを想定していない問題で、リスト項目が表示されている位置と実際に発生するイベントに対応する項目の位置が異なる感じです。
  • ファイルダイアログ表示後に再レンダリングがおかしくなる: これは移植の問題なのかもともとの問題なのか正直わかりません。まあ再起動すると直る問題なのでそんなに致命的ではないです(!?)

…とまあ、いろいろ問題があるので、大々的に「作った」とは言い難い感じです。出来る日なんてくるの?という感じですが、ちょいちょいやり方はありそうだなという気もするしオーディオプラグインとしては利用可能性があるので(たとえばプリセットを選ぶだけのプラグインにするとか)、気が向いた時にいじっていこうと思っています。