MIDI 2.0の時代に備えて「FPGAとARMで作る自作 USBオーディオインターフェース」(同人誌)を読み解く

これは【推し祭り】技術書典で出会った良書 Advent Calendar 2019https://adventar.org/calendars/3997のクロスポスティングです。

目次

音楽技術書をもっと増やしたい…!

技術書典は誰でもフルスクラッチで執筆から始められる技術同人誌の即売会で、いわゆる「商業」では販売部数の見込みが立たず全く出版の機会が無いようなマイナー技術の本であっても、自分の信念がある限り何の躊躇もなく執筆して販売するところまでもっていけます*1

個人的には、書店のコンピューター関連書籍のコーナーにCG・DTM関連のコーナーがあるように、技術書典にもグラフィックス方面のテクニックに関連する書籍がいくつか出ているので、音楽の打ち込み指南などの書籍も出てほしいなと思っているところですが、今回は音楽関連の技術書としてハードウェア方面でカテゴライズされていそうな「FPGAとARMで作る自作 USBオーディオインターフェース #1 USB-MIDIバイス編」を紹介します。

mmitti.info

USB-MIDIバイスってなんじゃらほい?と思われるかもしれませんが、ひとつはMIDIキーボードなどの外部MIDI入力デバイス、もうひとつは外部MIDI出力デバイス、いわゆるMIDI音源モジュールです。今どきはあまり見かけないものですが、Roland(GS音源)、YamahaXG音源)、KORGといった会社がよくキーボードの付いていないシンセサイザーを出していました。本書の場合は「DTMにお金をかけない」ためにMIDI音源モジュールを自作することが主眼にあるようです。*2

本書の紹介でも十分な内容になるかと思いますが、ちょうど先月ロンドンで行われたADC (Audio Developers Conference) 2019でMIDI 2.0に関するセッションが行われていたので、MIDI 2.0の時代にこういった自作ハードウェアは使えるのか?といった話も補足的にまとめておこうと思います。

本書の内容

本書はFPGAを使ってUSB-MIDIインターフェースを制作するための情報がまとめられている本です。著者がDigilentのPynq-Z1を使って自らUSB-MIDIインターフェースを制作してきたことをもとに書かれているので、具体的かつ詳細にまとめられています。ほとんどのページに図表が入っていてかなり親切に書かれています。

USB-MIDIバイス編、とあるのは、今回はオーディオI/Oに関連する部分の言及がほぼ無く、オーディオとMIDIを両方処理する部分は「次回」になるようです。もっともあとがきから察するに、次回はオーディオ部分が中心で、MIDIメッセージによってリバーブ/コーラス/ディレイなどのDSP処理をかませたり、ピッチ指定によって周波数を変換したりといった、MIDI音源モジュールらしい部分は、次回よりさらに先ということになるのでしょう(想像)。ここまで到達すると、洋書でも無いレベルなんじゃないかと思いますし、ぜひとも完結したものが見たい…! *3

MIDIの基礎知識

USB-MIDI音源モジュールを制作するためには、まずMIDIの知識が必要になります。MIDI仕様の中には、MIDIメッセージをシリアル通信の規格であるUARTに基づいて接続する方式に関する規定も含まれていて、本書でもしっかり関わってくる部分なので、きちんと言及されています。*4

実装者向けの記述としても、ランニングステータスはきちんと処理する(受ける側は省略されたりされなかったりする送信側の挙動にまんべんなく対応しないといけない)とか、アクティブセンシングとは何であってどのように対応しないといけないとかいった話が、ちょいちょい親切に書かれていて、読み込む価値があります。

MIDI回路(FPGA)

MIDIの基本仕様の話が終わると、いよいよFPGAでこのMIDI回路をどのように実装するかという話になります。この段階では、まだUSB-UARTの接続に関する話題は出てきません。MIDIインターフェース、オーディオインターフェースの部分はFPGAだけで完結します。

FPGAの回路設計は概ねわたしの守備範囲外なので概ね表層的な話しか読み取っていないのですが(この本ではMIDIとかは詳しく解説しているけど、FPGAについてはほぼ知っていることが前提のようなので、他の資料で予習する必要がありそう)、設計レベルではブロック図やステートチャートでメッセージがどのようにやり取りされているかが親切に示されています。

実装されているのは(1) Advanced Extensible Interface (AXI) に基づいてARM CPUとMIDIインターフェースを接続する回路、(2) MIDIシリアル通信用のクロックジェネレーター回路、(3) MIDIバスからUARTに変換してシリアル出力ための回路、(4) 逆にUARTを経由して届いたメッセージをMIDIバスに変換する回路(ここはシステムメッセージとチャンネルメッセージが分岐するのでややこしいらしく、説明も長い)…とあり、これらをCPUプログラムとして配線・論理合成するところまで説明されているので、FPGA使いの人には十分わかりやすいであろう内容なんだと思います。わたしはFPGAの知見が無いので「ステートマシンの様子とか詳しく書かれているし、やればわかりそう」という次元の印象です。

MIDI外部回路の接続(5PINのコネクター)に関する説明の短い章もあります。

USB

本書の後半は、PC側からUSBを経由してARM CPUの搭載されたFPGAベースのMIDIバイスを操作するために必要になるUSB-MIDI接続の部分に関する説明になっていて、まずはUSB通信の仕様について詳しくまとめられています。USBエンドポイントとは何か、USBインターフェースとは何か、といったレベルの解説からあるので、この分野の素人であるわたしでも読める内容でした。物理層は一般的な基板側にあって自前で実装する必要がないので、データリンク以上の話が中心となっています(実践的にもそれで十分そう)。なお本書で説明されるUSBは2.0です(2.0の仕様で十分実現可能であるようです)。

USBパケットの構成要素や、転送方式(USB-MIDIで使用するのはバルク転送とコントロール転送のみです)、USB接続状態の遷移、標準リクエスト、ディスクリプターなど、おそらくUSBハードウェア開発全般に通じる内容なので、ここは他の資料で勉強できそうな部分ですが、親切に書かれているので本書だけで読み進められます。USB-MIDIに関連する部分だけを勉強するには効率的かも? データパケットの説明も含まれているのですが、エラーを明示的に返す場合に実装する必要があることを踏まえてのものです。

USB-MIDIはオーディオクラスのサブクラスとして標準で規定されているため、USB標準に準拠してデバイスを実装すれば、ドライバーを自分で実装する必要が基本的にはなくなるということも説明されています(そのため、USB-MIDIの章ではソフトウェア実装の説明がほとんどありません)。

USB-MIDIの仕様

USBの説明が終わるといよいよUSB-MIDIの仕様の説明に入ってきます(仕様についてはUSB-IFで規格化されているので、この本でなくても学べることではあります)。冒頭から、1本のUSB仮想ケーブルで16本のMIDIケーブルを扱えるという話があって、なるほど確かにこれでMIDI IN / MIDI OUTが8組処理できるな…という発見があります。

前章ではUSB標準の一部としてデバイスディスクリプターを送信する部分を実装する必要があることが説明されており、この章ではUSB-MIDIバイスディスクリプターに求められる内容としてポート数や接続に関するトポロジーがあり、それらをどう記述するか、といったことが説明されます。

それからUSB MIDIストリーミング インターフェース(USBのインターフェースの何たるかについては前章で説明があります)でやり取りされるUSB-MIDIのイベントパケットの形式の説明を経て、USB-MIDIバイスディスクリプターの内容のうち固定値のある部分についての割と長い説明を経てこの章は終わりです。ほぼ仕様のみの話です。

USB-MIDIの実装

ここまでの説明(どれも必要なやつ)を経て、ようやくこのUSB-MIDIを筆者がどう実装されたか説明されています。前半はUSB-MIDIディスクリプターの内容(固定値でない部分)、後半はUSB処理を行うファームウェアで実装されているコードの説明です(使用するマイコンによって実装が変わるので擬似コードのみ)。USBエンドポイントのメッセージ処理は割り込みハンドラー(そういえばこの辺の基礎的な単語はわからないと読めない気がする)として実装され、標準で必須になるEP0とMIDIイベントの送受信に使われるものとで2つあります。EP0のほうも説明がありますが、ファームウェアによってはほぼ実装が丸投げできそうで、Zynqではもろもろ丸投げできたようです。MIDIイベントのほうは、USBメッセージは受信したら即応しないといけないという仕様の都合上、一度リングバッファーにenqueueすることになりそうです(本書の実装ではそうなっています)。

リングバッファーのdequeueはファームウェアのmain関数の中でループ処理で行われ、dequeueされたメッセージはAXI-MIDIの回路にMIDIイベントとして書き込まれます。一方で、MIDI入力メッセージが合った場合には、このループの中でAXI-MIDI回路の対応レジスタの状態をチェックして、メッセージありとなっていたらUSB MIDIメッセージに変換して入力側としてのリングバッファーにenqueueするようです(これはUSBホスト側がfetchするまで保持することになるでしょう)。

…といった全体的な処理の流れが、本書を読んでいくとわかります。MIDIトランスポート、USB、USB-MIDIに関しては素人だったわたしでも読めるレベルでまとめられていて親切な本です。

MIDI 2.0で変わること

さて本書は当然ながらMIDI仕様を前提に書かれていて、MIDI仕様といえば1980年代に策定されたものなのですが、2019年になって、38年ぶりにこの仕様に変更を加えたMIDI 2.0と呼ばれる仕様が議論されつつあります。MIDI 2.0には、MIDI 1.0を前提としたMPE (MIDI Polyphonic Expression)やMIDI-CI (Capability Inquiry) といった既成の仕様に基づく機能拡張も含まれているのですが、他にもMIDIメッセージの拡張やチャンネル数の拡大など、全般的にモダンな内容になる予定です。

www.midi.org

www.dtmstation.com

MIDI 2.0の時代になるとMIDI 1.0にのみ対応したデバイスは使えなくなる、ということはなく、MIDI 2.0はMIDI 1.0と基本的には後方互換性を維持した仕様になるようです。たとえばMIDI-CIでは双方向的なメッセージングが必要になるわけですが(MIDI 1.0にはリクエスト/レスポンスという概念がありません)、MIDI-CIに基づくメッセージをデバイス側が理解しない場合にはレスポンスが返ってこないわけで、その場合はホスト側はMIDI 1.0を前提としたメッセージを送ることになります。その場合でも、MPEのメッセージはMIDI 1.0の形式で送信できるので、たとえば未来のMPEデバイスMIDI 2.0の形式でメッセージを送信しつつ、2019年時点で現存するROLI BLOCKS(MIDI 2.0を解さない)にはMPEに基づくメッセージを送信する、といった処理の分岐が可能です。

この意味で、MIDI 1.0のみを対象とするUSB-MIDIバイスの開発に関する本書の内容は、まだ十分に役に立つと言えるでしょう。本書では入出力の双方が実装されていることがうかがえるので、MIDIメッセージを受け取った側でMIDI-CIに基づくリクエスト/レスポンスを処理出来る可能性は十分にあります。おそらくメッセージングは単発のリクエスト/レスポンスで処理可能で、何らかの状態管理を必要とするものではないとは思いますが、MIDI-CIのProfile ConfigurationとProperty Exchangeがどんな内容になるのかは見ておいても良いかもしれません。AppleがCore MIDIMIDI-CIをサポートしています。

もっとも、MIDI-CIのサポートだけではMIDI 2.0に対応できているとはいえません。MIDI 2.0仕様には、新しくUniversal MIDI Packet(以降UMP)と呼ばれるパケットのフォーマットが含まれています。これは最大128ビットのメッセージを含むパケットであり、MIDI 1.0の範囲を超えるものです。MIDI 2.0ではこれをチャンネルボイスメッセージに適用して高分解能のメッセージ(コントロールチェンジが128段階から65536段階になったものを想像してください)のやり取りが可能になります。MIDI 2.0対応デバイスであるためには、UMPを処理できるようになる必要があるでしょう。もっとも、高分解能のメッセージであっても、MIDI 1.0の機能にマッピングされる範囲では、精度を落として7ビット値(ピッチベンドなら14ビット)で送信する等の措置がとられるようです。

また、MIDI 2.0のUMPにはJitter補正のためのタイムスタンプを含めるようになったようです(フォーマットの詳細は公知情報からは不明です)。これまでソフトウェアのレベルで処理されていた(と思われる)タイムスタンプ補正がMIDIメッセージでやり取りされるハードウェアのレベルで要求され、メッセージのバッファリングの実装が影響を受ける性質の仕様であるかもしれません。ordered queueに実装させられるのはちょっと嫌かも…?

また、チャンネル数が拡大されて、ユーザー的には最大256チャンネルまで利用できるようになるのですが、これは16のグループごとに16チャンネルを操作できるようにUMPが設計されている、というアプローチになっていて、これはもしかしたらUSB-MIDIのインターフェースがそのまま使えるということなのかもしれません(詳細は要確認ですが)。MMAはUSBの仕様を策定できる立場にはないので(USBの仕様を決めるのはUSB-IF)、MIDI 2.0では可能な限りUSB-MIDIの仕様がそのまま使えるような仕様を目指すのではないかと思います(想像)。

…とまあ、いろいろと考慮事項が増えるようですが、MIDI 2.0はまだ仕様策定中であり、実装したデバイスも無ければMacOSMIDI-CIサポート以外は何もない状態なので、まだまだ未来形の話と考えても良いでしょう。

*1:もちろんイベント自体のルールや行動規範はあるわけですが

*2:ハードウェア制作費は…ゲフンゲフン

*3:まあCQ出版あたりでしれっと出ていてもおかしくはない気もしてきました…

*4:ちなみに本書では物理層となっているのですが、わたしにはデータリンク層にも見えるというか、どこまでが物理層といえるのかOSIモデルよくわからん…という感じです。

JUCEモジュールを作って外部ライブラリを参照する

これは音楽技術Advent Calendar 2019の3日目(まあ2本目ですが…)とJUCE Advent Calendar 20193日目のクロスポストエントリーです。

長い前書き

JUCEのビルドシステムは、アプリケーションやオーディオプラグインのビルドに必要なコードを原則として全てソースコードからビルドするという仕組みです。JUCEはさまざまなモジュール群から成り立っていますが、その全てがソースからビルドされることになります。Linuxで言えばGentooです*1。これは複数のプラットフォームをターゲットとするビルドシステムを構築する場合には手っ取り早いハック(やっつけ仕事)であるといえます。

世の中には、アプリケーションのビルドに必要なものは全てソースからビルドされるべきだ、という発想の人もいますが、おそらくその他ほとんどの人はビルド済みのライブラリを使ってビルドするほうが賢いと考えます。地球環境のことを考えれば、モジュールを毎回ソースからビルドして電力を浪費するのは悪であるとすらいえます*2。そして、そもそも自分ではビルドできないようなOSのモジュールを、自分のアプリケーションから参照したくなる場合があります。

バイナリのライブラリを使えるようにするためには、それぞれのプラットフォームやビルドツールチェインでそれらを利用できるようにサポートしなければなりません。JUCEはプラットフォームの違いを吸収するだけでなく、ビルドツールチェインの違いも吸収しなければなりません。Visual Studio (for Windows)の複数バージョン、xcodeMakefile, CMake (CLion)と、多大な組み合わせと対峙することになります。

ライブラリの参照の仕方も、ライブラリファイルを直接指定する(ついでにLD_LIBRARY_PATHのようなライブラリ解決パスも追加指定する)方法と、pkg-configのようにパッケージとしてでないと解決できない方法があります。C++には、node/npmやruby/gemやpython/pipや.NET/nugetやJava/mavenなどきちんと整備された言語開発環境とは異なり、プラットフォームの違いを意識せずに利用できる依存関係解決のためのソリューションがありません。

結局JUCE/Projucerでは、まともなプラットフォーム中立の依存関係解消方法を用意できず、Exporter別にオプションでライブラリ参照を解決することにしました。オプションはProjucerのビルド設定ファイルであるところの.jucerファイルに含まれます。Exporter別に設定するのはちょっと面倒くさいですね。結局出力先ごとのオプションの指定方法を調べないといけないことになりますし。

JUCEモジュールを活用してライブラリをソースからビルドする

こういう煩雑さが面倒だ、それならばライブラリをソースからビルドしてインクルードファイルなどは直接参照したい、という人のために、JUCEではユーザーのモジュールから外部ライブラリを取り込む方法が用意されています。今回はこのカスタムJUCEモジュールを作る方法から説明します。

JUCEモジュールというのは、JUCEを使ってコードを書いている人であれば自明かと思いますが、JUCEの機能ごとにまとまったライブラリの一部分のようなものであり、JUCEではモジュール単位でユーザーのプロジェクトに含めるかどうかを指定します。JUCEモジュールには依存モジュールの概念があり、要するにライブラリのようなものです(ただしソースの集合体であり、バイナリを参照することはありません)。JUCE本体にあるjuce_audio_basicsjuce_gui_basicsなどがモジュールです。

このモジュールはユーザーが独自に作成することもできます。作り方は簡単です。foo_barというモジュールを作るには、foo_barというディレクトリを作って、その中にfoo_bar.hfoo_bar.cppというファイルを2つ作成するだけです。ただし、このfoo_bar.hには「JUCEモジュールフォーマット」に準拠したヘッダコメントが必要になります。ヘッダコメントの書式はこんな感じです。ちょっとPIPっぽいですが別物です。

/*******************************************************************************
The block below describes the properties of this module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.txt file.

BEGIN_JUCE_MODULE_DECLARATION

ID: augene_file_watcher
vendor: atsushieno
version: 0.1.0
name: auegne file watcher
description: Classes for file system watcher/notifier
website: https://github.com/atsushieno/augene
license: MIT

END_JUCE_MODULE_DECLARATION
*******************************************************************************/

このヘッダファイルの残りの部分には、自分のクラス定義などを書いても良いですが、一般的には別のヘッダファイルを含めるほうがクリーンに見えるでしょう。そして、ここにはサードパーティーライブラリのヘッダファイルを含めることもできます。

このヘッダファイルは、Projucerが生成するJuceHeader.hの中で自動的にインクルードされるので、その中でサードパーティーライブラリのヘッダファイルも自動的に参照できるわけです。これは楽ちん。

さて、自作したJUCEモジュールは、まずプロジェクトに追加しないと認識してもらえません。ProjucerでModulesのセクションにある+ボタンをクリックして、モジュールのディレクトリを指定します。

f:id:atsushieno:20191203110405p:plain

モジュールが正しく認識されると、Modulesのリストに自分のモジュールが追加されます。(ヘッダファイルが正しいフォーマットになっていないとここで各種エラーメッセージが出ることになるので、メッセージの内容からエラーを推測して修正します。)

f:id:atsushieno:20191203110429p:plain

さて、さっきはもうひとつ.cppファイルも作成しました。一般的なJUCEモジュールでは、この中に実装を書くことは実際にはあまりなく、このファイルにはこのモジュールに含まれるソースコードを #include で列挙することになります(!) つまりMakefileなどでソースファイルを列挙しているような感じですね。そして、ここでもまた、サードパーティーライブラリをビルドする場合は、ここにそのソースコードを列挙すれば、きれいに外部ライブラリがビルドできるではないか、というのがJUCEプロジェクトのスタンスであるようです。

JUCEモジュールでビルド済みバイナリをリンクする

ところでここまで読まれた皆さんはお気づきでしょうか? JUCEモジュールでサードパーティライブラリのソースのビルドなんて実際にはできるはずがないということに(!)

少なくとも、こんなのは一般的に通用するソリューションではありません。

その理由は、一般的にはライブラリはそれぞれのビルドシステムに沿って作成されているものであり、それぞれには適切なコンパイラーオプションやリンカーオプションがMakefileやCMake、xcodeproj, vcxprojなどで指定されているためです。JUCEモジュールで指定できるのはインクルードするヘッダファイルとビルドするソースコードだけです。これでは全く足りません。だいたいLinuxでautotoolsとか使っていたらconfig.hとか生成するわけで、この仕組みではそういうものに全く対応できないわけです。

そういうわけで、サードパーティーライブラリを取り込むなら、別途ビルドして面倒でもそれをExporter別にリンク指定してやるほうが現実的っぽいですが、実はもうひとつプラットフォーム別のビルド指定を回避できる可能性がひとつあります。それはこのJUCEモジュールで利用できるビルド済みバイナリの参照という選択肢です。

JUCEモジュールでは、ビルド済みのサードパーティーライブラリを以下のようなサブディレクトリに置いておくことで、自動的にビルド時にリンク対象として検索してくれます。たとえば…

  • libs/VisualStudio2019/Win32/MTd - Visual Studio 2019用 MTd - あるいはMT(スレッディングモードに合わせて調整します。VSのバージョンも2017などが使えます)
  • libs/MacOS/x86_64 - MacOS
  • libs/Linux/native - Linux用(要注意)

iOSAndroidなどもあるのですが、先にリンクしたJUCE_Module_Format.txtに詳しく書かれています。ただし、ひとつ要注意なのですが、Linuxに関する記述がデタラメで、このドキュメントにはABIをディレクトリ名として使うように書かれているにもかかわらず、実際にはnativeというディレクトリ名がLinuxMakefile/Makefileに追加されます。これはROLIに報告済みのissueで何が問題なのかも明らかになっているのですが、本エントリー公開時点でも未修正です。

(実のところ筆者が検証したのはLinuxWindowsのみで、MacOSについてはドキュメントに書いてあるのをそのまま紹介しているだけです。見ての通り、このドキュメントは信用できないので、実際に通るかどうか気になる人は検証してみてください。)

そして、このビルド済みライブラリは、自動的にリンクされるわけではなく、プラットフォームごとにJUCEモジュールフォーマットの書式に沿って、それぞれのプラットフォームに合ったプロパティとして指定する必要があります。

    linuxLibs: foo_bar
    OSXLibs: foo_bar
    windowsLibs: libfoo_bar // libfoo_bar.dllの場合. foo_bar.dllなら foo_bar でOK

いずれにしろ、サードパーティーライブラリを使う場合は、それぞれのプラットフォーム上でいったんビルドしておいて、ヘッダファイルを所定のディレクトリ上にコピーしてそれをモジュールのヘッダファイルで #include しつつ、モジュールフォーマットに沿ってプロパティを追加すれば、そのライブラリが使えるようになる可能性が少し上がります。

ここまでお膳立てしてやるくらいなら、Exporter別にリンク指定したほうが早いのでは…?とも思ってしまいますが、少なくともオプションの指定方法などを知らない環境向けにオプションを調べて記述するよりは、こっちのほうが楽かもしれません。わたしも実のところvcxprojやxcodeprojでどんなリンク指定を受け付けるのか知らないのですが(もちろん調べればすぐ分かる話ですが)、この方法であればすぐ指定できますし。

補論: インクルードファイル解決パスの問題

実のところ、リンクするライブラリをビルドするより前に、ヘッダファイルが解決できないという問題が生じるのが一般的です。「サードパーティライブラリのヘッダファイルを別のヘッダファイルで #include すればOK」というProjucerの発想はカジュアルなもので、JUCEモジュールのやり方では、pkg-configが追加で-Iオプションを指定してくれるようなことは、モジュール単位では一切やってくれません(Linux Makefile exporterではpkg-configライブラリを指定できます。もちろんLinux専用)。ビルドするサードパーティライブラリがさらに別のライブラリに依存して、それが追加のインクルードパスを必要とする(そうしないと「システム上にインストールされている」インクルードファイルの解決に失敗する)場合には詰むことになります。

これを回避するには、プラットフォーム別のExporterにコンパイラーオプションで-Iを追加すれば良いということになるのですが(追加インクルードパスはプラットフォーム別のDebug/Releaseの設定の中にHeader Search Pathsがあります。ちなみにexterCompilerFlagsというビルド設定に依らないプロパティもあるのですが、CLion Exporterが取り込んでくれない等の問題があります)、それならJUCEモジュールで解決するのは無駄ですし、システムによって場所が異なる可能性があるからこそpkg-configというソリューションがあるわけで、この意味でもProjucerのやり方はお粗末です。まあもともとビルドシステムとしてはやっつけツールだし…?

いくつかのJUCEモジュールで採用されている対策としては、もうモジュールの中に必要なヘッダファイルを直接放り込んだ上で、参照するインクルードファイルの位置を書き換えて対応する、というものです。さすがにシステム上にあるものは解決できないので、これは部分的な解決方法ということになります。

将来の展望

さて、ここまでいろいろな問題の解決方法を紹介してきましたが、いかがでしたか? わたしの個人的な感想としては「もうProjucerのビルドシステムはあきらめてCMakeを使おう」なのですが(パッケージ参照もちゃんと解決できるしVisual StudioでもAndroid StudioでもCLionでも開けるんですよコレ)、Projucerの負の遺産があるうちはまだまだ無理かもしれませんね。

JUCEの本来的な魅力はなんと言ってもクロスオーディオプラグイン・ホスト開発が可能になることなので、その魅力をフルに引き出せるように、JUCEのビルドシステムはもっとさまざまな開発者にリーチできるように発展的に解消していってほしいと思っています。

*1:っていう説明の仕方はちょっと時代遅れかな? 今はバイナリもインストールできるんですよね

*2:いまCI業界を敵に回した気がしてきたぞ

【音楽技術AC12/1】 ソフトウェアMIDI入力デバイスを作ろう

音楽技術Advent Calendar 2019の1日目エントリーです。今年はちょっと手が回らなそうなので「たまに書く」くらいのノリで進めていきたいと思います。かなり空き枠があるので、もし書けそうな話題がありましたらぜひご参加ください。2018年版を見るとわかりますが、割と手短なエントリーでも全然大丈夫です。

MIDIキーボードを使いこなせない…!

みなさんはDAWで打ち込むときにMIDIキーボードを使っていますか? たぶん少なからぬ人がYESと答えるのではないかと思います。DAWでマウスでポチポチとピアノロールに音を追加していく作業、しんどいんですよね。MIDIキーボードなどで入力して、ちょいちょい音長などを調整していけば効率的です。

…わたしこれがダメなんです。主な作業環境がノートPCなのでMIDIキーボードを繋いでいないことが多いのと、ふだん楽器を弾かないので間違いだらけになってしまうのが主な挫折ポイントです。まあどう考えても慣れるしか無いですね。

とはいえ、DAWでマウス入力していくのはしんどいので、もう少し別の方法で入力できないものか…とは思うわけです。MIDI音源はハードウェアでもソフトウェアでも良いのだから、MIDI入力デバイスもハードウェアではなくソフトウェアで作っても良いのでは…?

というわけで、ソフトウェアMIDIキーボードが誕生しました…というのはちょっと今さら感ありますね。2019年にあるオーディオプラグインには、PCキーボードを押すとMIDIノートメッセージとして処理してくれるものが少なからずあります。いずれにしろ、これは便利なので単体で実装する価値があります。

実際にはキーボードである必要はないのですが、今回はとりあえずどのPCにもありそうな「入力デバイス」であるところのキーボードを使います。

MIDI出力デバイスに接続するソフトウェアMIDIキーボードを作る

まず最初に思いつくのが、プラットフォームの標準MIDI APIを使って、接続したいMIDI出力デバイスを選択して、そのデバイスMIDIメッセージを送る、というものです。これはそんなに難しくありません。 GUIでは、何らかのキーボード入力イベントを受け取れるコントロールをウィンドウに置いて、入力されたキーに応じてMIDIノートオン/ノートオフのメッセージを送信するだけです。

このアプローチでは、MIDI音源で演奏することはできるのですが、DAWのようにVSTAUなどのオーディオプラグインをトラックで指定して打ち込むタイプのツールでは、そのままでは使えないのが問題です。オーディオプラグイン側をMIDIバイスのように設定して認識させることができれば可能かもしれませんが、一般的なオーディオプラグインDAWからしてみれば、MIDIキーボードはMIDI入力デバイスであり、MIDI入力デバイスをサポートしてしまえば終わりです。オーディオプラグインをわざわざ機能の制限されたMIDI音源として利用可能にする意味がほとんど無いのです。

MIDI virtual input device / portを作る

そういうわけで、DAWにも認識してもらえるようなソフトウェアMIDIキーボードを実現するための一番確実な方法は、ソフトウェアMIDI入力デバイスあるいはポートを作るところからです。

一般的には、CoreMIDIもALSAも、作成した仮想ポートにMIDIメッセージを「出力」すると、それが「入力」となって、その仮想ポートに接続されたアプリケーションが最終的にMIDIメッセージを受け取れることになります。というわけで、GUIから受け取ったキー入力イベントは、これらの仮想ポートに出力するようにすると良いでしょう。

実例

わたしが.NET開発者だった頃に作った個人的なプロジェクトですが、仮想ポートを活用して既存のソフトウェアMIDIキーボードアプリに仮想ポートを自動的に作成するようにしたものがあります。このアプリ上で入力したキーボードイベントから生成されたMIDIメッセージは、接続されたMIDI出力デバイスに送られるほか、仮想ポートに接続したDAWが受け取ることもできます。

github.com

自分で作成したアプリなので、入力はまあまあやりやすいので、たまに打ち込み作業で使っています。

いかがでしたか? 自分で好きなように楽器のインターフェースを作れるのは割と楽しいので、もし興味が出てきたらぜひ試してみてください。

DAW・シーケンサーエンジンを支える技術@M3 2019秋

前日の晩になって書いて公開するというのは何とも手遅れ感があるわけですが、明日10/27のM3 2019秋でおよそ1年半ぶりくらいに同人誌をまとめました。サークルはサ-22x (ginga)です。参加する予定のある人・音楽同人イベントに興味がある人は、ぜひ遊びに来てください。

https://atsushieno.github.io/ginga/m3-2019a.html

f:id:atsushieno:20191026200034p:plain
表紙

内容については上記特設サイトを見てもらいたいのですが、今回は「シーケンサーエンジン」についての解説です。概ねtracktion_engineのようなDAWのバックエンド技術…要するにDAWのうちGUIの実装から独立している部分…について、全体的に俯瞰するような内容を目指しています。60ページという、いつになく薄い(!)内容で、トピックも多岐にわたるため、個々の内容についてはほとんど踏み込まず、キーワードを列挙して読者が関心をもった分野は読者に個別に深入りして本書の範囲を超えて調べてもらう…というきっかけをたくさん作るための内容になっています。

筆者としては技術書典4の時に出したMonoDevelop Masters Book(に相当する部分)の「IDEの技術を因数分解して個別に解説することで、誰でもIDE(の一部)を作れるようになる」という目標を、今度はDAWに向けてみよう、と考えながら書いたものです。自分の音楽ソフト開発経験がまだ浅いので、MonoDevelop Masters Bookと比べるとだいぶん表層的な内容になっているのが惜しいところですが、商業本も含めてこのような内容の音楽技術書は今まで出ていなかったと思うので、まずは初めの一歩を踏み出しておきたくて書いたものです。

もっとも、実のところ本書で一番よくやったと思っているのは表紙で、この方面に関する知識がゼロのいつもの絵師に何とかお願いして描いてもらえるように、細かく仕様をまとめて「難しすぎる…!」と言われながらも、何とか説明しきって、結果的にこれ以上無いくらい期待通りの「謎の楽器を操るバンド」を仕上げてもらえたのでした。楽器、わかる人にはわかる感じのものになっています。一番右の水晶玉っぽいテルミンだけはモデルがありません。適当に思いつきました。

当日の展示内容

当日は同書の頒布、旧譜?新婦?の頒布のほか、同アルバム楽曲のMIDI版を、Android端末上でmasterに取り込まれたfluidsynthを使用して再生するデモも行おうと思っています。シンプルなGM互換楽器を多用した楽曲が多いせいか、意外と実用的に聴けます。

敗戦の弁

ここからは反省です。(今書くのか)

今回始めてM3に参加するにあたって、当初は3月の幻想音楽祭と同様、自作ツールに基づいて楽曲を創作してアルバムとして仕上げるつもりでした。ところが申し込んで当選待ちの間に、何の因果か自分の予想を裏切るタイミングでフルタイムの仕事をすることになり、完全に創作のために割ける時間が無いどころか、MMLコンパイラ等のメンテナンスと新ツールの開発すら覚束ない状況になってしまい、結果的に夏の初め頃にあらかたまとめてあった本書のみが出展の目玉ということになりました。

とはいえ、M3参加は初めてなので、3月に頒布した作品もM3的には新作ということになります。

本来であれば、前回MMLからSMFを生成するところまでしか出来なかったテキストツール部分を、今回は先日まとめたオーディオプラグイン用音楽プレイヤーまで延伸して、MMLプラグイン定義だけで出来るところまで「ソースを読めて創作できる」ところまでもっていって、きちんとdogfoodingして当日展示するつもりだったのですが、実際できたのはproof of conceptとしてのツール上で何とか音楽を再生できたところまででした。まあここまででもそれなりに面白いのですが、現実的に自分が作曲作業に着手するに至らないレベルで改良の余地が多大にあるので、これを紹介・宣伝するのは筋が違うだろうと考えました。ブースでの雑談ていどに留めておこうと思います。

この方面はもっとじっくり時間を確保して開発する必要があると考えています。ただ個人的にもうひとつ未公開のプロジェクトもあってそれも進めないといけないと思っているので、今回の反省を踏まえて来月以降はフルタイムワーカーを辞して自分のプロジェクト開発にもっと時間を割いていく予定です。(もっとも来月はADC2019に参加することもあって、まだすぐには進捗しなさそうですが。)

augene: MML compiler for sequencer engine

MMLコンパイラからオーディオプラグイン中心の楽曲を生成できるようなツールを作れないものか…と思っていたのですが、ある程度技術的に実現の目処が立ったので、とりあえず限りなく構想段階に近いですが出してみます。コードは現状ntractiveプロジェクトの下でサンプルとして作られています。

github.com

augeneはMMLコンパイラーmugeneを組み込んでオーディオプラグイン中心のシーケンサーエンジン用の楽曲を生成する複合型コンパイラーです。v1リリースでは、オープンソースの楽曲再生エンジンであるところのtracktion_engineを利用した楽曲プレイヤーと、Tracktion用楽曲データ生成ライブラリNTractiveで実装したSMF取り込み機能、それとJUCEのサンプルにあるAudioPluginHostを組み合わせて機能を実現します。

Rationale

mugeneはSMFを生成するMMLコンパイラーです。これはつまり、mugeneの可能性はSMFの範囲に限られるということを意味します。

筆者の作曲作業フローでは、まずMIDI音源を使用してMMLからコンパイルしたSMFを再生していましたが、GM準拠の命令のみではリズムトラックを2つ指定することはできず、ドラムパートのまともな打ち込みは実現が困難でした。コントロールチェンジもエンベロープやカットオフなどが必ずしも使える状態ではなく、また音源が変わると打ち込んだ内容が再利用できないものでした。

チャネル数が16までというのも、本格的に打ち込みが進んでくると問題になってきます。トラック数とチャネル数は異なり、同じ楽器はたいがいチャネルを共有することになり、特にリズムトラックは同じチャネルを共有することになるので、16チャネルあればそれなりの楽曲が作れるのですが、それでも創作がそこで制限されるのはばかばかしい話です。

打ち込んだMMLから生成されたSMFは、その後DAWにインポートして編曲する作業フローとなっていました。この辺りがウォーターフォールモデルなのはまだ許容範囲なのですが、Roland SC-8820など20世紀のハードウェア音源を利用していたときは、音源に特化した命令で調整していた部分は全て打ち込み直しになりました。部分的にMMLからコンパイルしてインポートしたり、極端な場合はXML形式のtracktion楽曲データを直接加工していました。この辺はMMLから一気通貫で変換できれば楽だったというのは間違いありません。

最初からオーディオプラグインを前提とした楽曲が生成できれば良いのに…。Augeneはそのような動機で作られました。

Augene楽曲データフォーマット

Augeneの楽曲データは、それ自体はどちらかといえばプロジェクトファイルとして捉えられるべき、ファイル参照のリストとなります。

<AugeneProject xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Tracks>
    <AugeneTrack>
      <Id>1</Id>
      <AudioGraph>Unnamed.filtergraph</AudioGraph>
    </AugeneTrack>
  </Tracks>
  <MmlFiles>
    <MmlFile>foobar.mugene</MmlFile>
  </MmlFiles>
  <MmlStrings>
    <MmlString>![CDATA[ 1 @0 V110 v100 o5 l8 cegcegeg  > c1 ]]></MmlString>
  </MmlStrings>
</AugeneProject>

雑なフォーマットですね(!) NTractiveは.NETのプロジェクトで、Augeneはまだ雑にやっつけでXmlSerializerを使ってプロジェクトをシリアライズしているのでこのざまです。これでもDataContractSerializerを使ってやっつけていた時よりはだいぶマシになりました…。JSONではなくXMLをあえて選んだのは、MML中に " やら < やら > やらが出てくるたびにエスケープする羽目になるのは望ましくないからです。XMLならCDATA Sectionが使えるので。実のところ、]]>が出現する可能性はゼロではないのですが、まあ頻繁ではないでしょうし、スペース1つ入れるだけで回避できる問題なので、我慢してもらえればと思っています。

ともあれ、内容は単純で、MMLをファイルあるいは文字列で渡したものと、トラックのリストだけになります。各トラックにはAudioPluginHostで定義して保存したファイルを指定するAudioGraphの情報が必要になります。

Augeneはこのプロジェクトファイルを引数として実行されると、まずMMLコンパイルしてNTractiveのMIDIコンバーターに渡します。

オーディオプラグイン設定はMMLではどうしようもないので、AudioPluginHostで事前にルーティングを設定してもらい、トラックごとにインポートできるように指定するやり方になっています。AudioPluginHostが生成する.filtergraph形式からTracktionの形式へはAugeneが変換します。SMFにはトラックIDが存在しないので、トラックの物理的な位置に対応するIDをユーザーに指定してもらいます(!)。

なおAudioPluginHostでは複雑なルーティングを構築できますが、Augeneは単純な直列接続しかサポートしません。tracktion_engineも複雑なルーティングはサポートしません。

filtergraphもファイル参照ではなく文字列で埋め込めるようにできなくはないのですが、stateのバイナリデータを文字列で書ける強者はそうそういないのではないかと思います…まあstateに頼らないオーディオプラグインもあるかもしれませんし、プログラムで生成できる可能性が無いわけではないので、サポートする可能性もゼロではないです。

実行方法: 楽曲を再生するまで

  • AugeneプロジェクトXMLファイルを作成します。
  • トラックごとに、AudioPluginHostを実行して、フィルターグラフを作成します。各AugeneTrack要素のAudioGraph子要素にそのファイル名を指定します。
  • MMLを作成します。直接プロジェクトに書いても、別のファイルに書いても良いです。MmlStringあるいはMmlFileとして記述します。
  • Augene.exeにプロジェクトXMLを引数として渡すと、それぞれをロードして、tracktioneditファイルとして生成されるべき内容を標準出力に吐き出すので、リダイレクトで保存します。
  • tracktion_engineのPlaybackDemoに、このファイルを渡して実行します。
    • PlaybackDemoの初期状態では、プラグインが何一つロードされていないので、まずプラグインリストを更新する必要があります。
    • また、PlaybackDemoのデフォルトのオーディオ設定では、マイク入力が有効になっていたり、オーディオ出力がspatial audio対応のように多数チャンネルが有効になっていたりして、特にLinuxデスクトップではほぼノイズしか聞こえないような状態になっています。設定を変更して、channel 1 + channel 2だけ有効にすれば、まともにオーディオ再生が機能するようになります。

サポート対象プラットフォームの課題

MMLコンパイラが生成するものは、可能な限り最終的なアウトプットをそのまま生成できることが望ましいです。ただ、これは次に説明するプラットフォーム依存性の問題があるため、妥協的な解決策を導入します。

オーディオプラグインを利用できる環境はクロスプラットフォーム化しつつありますが、オーディオプラグインそのものはまだほとんどがクロスプラットフォーム化できておらず、特に筆者が主に使用しているLinuxデスクトップのサポートが壊滅的です。

また、オーディオプラグイン機構のクロスプラットフォーム化はまだ「実現中」のものが少なからず存在します。JUCEはLinuxのVST3をサポートできていません。Augeneの動作確認は、Waveform上でVST2であるところのCollectiveを使用した楽曲を、VST2SDKを有効にしたtracktion_engine(プロジェクトのjuce_audio_plugin_processorsの設定に変更を加える必要があります)のPlaybackDemoに渡すことで成功しています。

オーサリング環境

実のところオーサリング環境のサポートが無いところで楽曲を打ち込むのはまだだいぶ苦痛だと思うので、何かしらツールを用意したほうが良いかと思っています。さすがにPlaybackDemoではまともなプレイヤーになっているとは言い難いですし。ただNTractiveやmugeneは.NETツール、PlaybackDemoはネイティブ(C++)のツールと、分かれているものをある程度統合する必要が出てくるかもしれません。とりあえずは子プロセスを起動するやり方でしのぐつもりです。

またPlaybackDemoだけがプレイヤーになっている現状はかなり残念なので、Waveform10にもロードできるように、プロジェクトファイル(*.tracktion)も生成できるようにしたいのですが、このファイルはバイナリなので、tracktion_engineがやっていることを自前で(?)再現する必要があります。理想を言えばYeoman generatorあたりを実装して yo tracktion くらいで生成できるようにしたいところです(!?)。

あるいはもっと単純にAugeneにWaveformプロジェクトの一部を書き換えるだけの機能を実装してもよいのですが、その辺はもう少しいろいろ試してみたからにしようと思います。

juicysfpluginをUbuntuデスクトップ環境で使うためのまとめ

小ネタ。

サウンドフォントにはなかなかいい音をもっているものが多く、また統合音源として各種サンプルをバンドルしているものはMIDI楽器として楽に使いやすいので、それなりに活用したい。

juicysfpluginはfluidsynthを利用してサウンドフォントをVSTiにしてしまおうというJUCEベースのオーディオプラグインだ。Linuxでも使えるようには作られているようだ(外部コントリビューターがLinuxサポートを追加している)。

github.com

ただし、juicysfpluginはfluidsynth 2.0系列を前提としている。Ubuntu用にパッケージされているfluidsynthは(というか大半のLinuxデスクトップ環境のfluidsynthが)やや古い1.1系列のもので、fluidsynth 2.0とはAPIがだいぶ異なるようだ。そのため、まずfluidsynth 2.0をビルドするところから始めないといけない。

github.com

juicysfpluginのチェックアウトのトップディレクトリにgithubからチェックアウトしてcmake -DCMAKE_INSTALL_PREFIX=../../fluidsynth-distのようにしてビルドすると、make/make installの後にトップレベルのfluidsynth-distができるはずだ。

JUCEがVST3をサポートしていないLinux環境用にJUCEのプラグインをビルドするには、VST2_SDKを含むVST3SDKが必要になる。githubのリリースからVST2_SDKを含む古いSDKを拾えるようだ(自分でビルドまでしたことはないけど)。

それが出来たら、次はjuicysfplugin.jucerからそれを参照するようにする必要がある。理想を言えばjuicysfpluginがlibfluidsynth.aをstaticリンクすることで余計な外部依存ライブラリをなくしたいところだが、とりあえずは共有ライブラリをリンクすることにする。Projucerで開いて、Linuxビルドの設定でCPPFLAGSに-I../../fluidsynth-dist/include、LDFLAGSに-L ../../fluidsynth-dist/lib64を指定する(fluidsynth 2.0.7時点ではなぜかlibではなくlib64に生成されるようだ)。

いったんfluidsynth 2.0がビルドできたらjuicysfplugin.jucerを保存してBuild/Linux/Makefileを生成し、make -C Build/Linuxで juicysfplugin.soをビルドする。

juicysfplugin.soがビルドできたら、これを~/.vstにコピーすれば良い…というわけではない。このjuicysfplugin.soにはlibfluidsynth.so.2.0.xへの強参照が含まれており、このファイルがLD_LIBRARY_PATHなどでロード可能なパス上に存在していないと、プラグインホストがjuicysfplugin.soのロードに失敗する。~/.vstにlibfluidsynth.so.2.0.xを置いてもロードはしてもらえない。

自分の手元の環境ではfluidsynth 2.0.xを含むlib64にLD_LIBRARY_PATHを追加したターミナル環境からWaveform10などを起動することで呼び出せている。

f:id:atsushieno:20191005213812p:plain
juicysfplugin on Waveform10 on Linux

Xamarin.AndroidXMigrationについて

今回はコレに関連して少し詳しく書く。

devblogs.microsoft.com

目次

Xamarin.Androidは時代遅れ

Xamarin.AndroidとスタンダードなKotlin中心のAndroid開発の乖離は年々ひどくなっている。たとえば

  • data-binding, room: Android Gradle Pluginの中で行われるコード生成を前提としているので難しい。
  • constraint layout designer: constraint layoutはそれ自体はNuGetパッケージで配布されていて、実行時に必要なライブラリとしては使うことができるが、テキストエディタ上で書くには冗長で区別の付けにくい属性が多用されていて、UIデザイナーが無いと使いにくい機能の一つだ。
  • navigation editor: これも実行時ライブラリとしては単にバインドするだけだが、GUIデザイナーで流れを把握できるのと出来ないのとでは大きな違いがある。
  • instant run, apply changes: Xamarin.Androidにはinstant runライクなfast deploymentが実装されているが、デフォルトで有効になっている機能としては、あくまでDLLのみの変更にしか対応していない。Java生成コードの変更を伴うimproved fast deploymentサポートは無効のままだし、その間にAndroid Studioはinstant runを捨ててApply Changesに移行してしまった。

最後に、Kotlin + Android Studioでの開発においては、AndroidサポートライブラリからJetpackへの移行が進んでいる。次の節ではこれについてもう少し詳しく説明しよう。

androidx (a.k.a Jetpack)

Androidサポートライブラリは、Android APIフレームワークレベルで機能追加されたもののうち、特に新しいハードウェア要件などに依存しない部分を、古いバージョンのAndroidでも利用できるようにバックポートしたものだ。android.support.v4.*というパッケージはminSdkVersion=4であるような追加APIが、android.support.v13.*というパッケージならminSdkVersion=13であるような追加APIが、それぞれ含まれていた(他にもv7, v17などいくつか存在した)。

これらのバージョン番号に基づく識別子は、最初は正しかったが、長い年月を経て、Google PlayAndroid API Level 4のサポートが打ち切られたりする中、だんだん意味をなさなくなってきた。またAndroid Architecture Componentはandroid.arch.*というパッケージ名だったが、将来的にはこれらとサポートライブラリの関係なども錯綜する可能性が高く、新機能がどちらに属するのかといった疑問など、トラブルの予感しかなかったといえる。

Googleは2018年にJetpackという新しいブランディングandroidx.*というパッケージ名のもとで、古いサポートライブラリとアーキテクチャコンポーネントを統合した。そしてminSdkVersionは各パッケージごとに決まる事項となった。Androidフレームワークに追加する必要がない新機能はJetpackでサポートすれば良く、しかもフレームワークのリリースサイクルからは独立して基盤的な新機能をリリースできるようになった。

また、Jetpackの大きな側面は、Javaが前提だった頃に作られたライブラリを、Kotlin前提の構成に置き換える、ということにもある。Xamarin.Androidの文脈では「ネイティブ」としてKotlinよりも(C++よりも)Javaのほうが出てくるが、これはmonoランタイムとの相互運用の文脈で出てくるのがほとんどなので、今後も変わらないだろう。これ自体は別にXamarinがKotlin前提のエコシステムに追いついていないということは全く意味しない。

androidxへの移行の課題

さてAndroid開発者はこのJetpackへ移行することが求められているのだけど、これは簡単にbuild.gradleの中にあるパッケージを差し替えるだけでできるようなことではない。パッケージ名が変わっているので、ソースコードも書き換えなければならない。そしてJetpackパッケージは膨大である。 https://developer.android.com/jetpack/androidx/migrate にリストがあるので見てみてほしい。

これはとても全部追っかけていられない

仮に膨大な肉体労働の後に自分が書いたアプリケーションのコードを移行できたとしても、それだけでは十分ではない。自分がアプリケーションで参照しているライブラリがJetpackに移行していない場合は、結局古いサポートライブラリが含まれてしまうことになる。

運の悪いことに、Android Studioのデフォルトテンプレートで作成されたアプリケーションやライブラリは、古いバージョンのAndroidでも動作できるようにandroid.support.v4*の各種パッケージに含まれるクラスが使われていた。

依存ライブラリがバイナリとして参照しているものを、そのユーザー開発者が書き換えるのは無理がある。依存ライブラリがandroidxに移行してくれるのを待つべきだろうか? そもそもそのライブラリの開発者は今でもそのライブラリをメンテしているだろうか? そしてタイムリーに対応してくれるだろうか? 依存ライブラリの開発者が1人でも「まだ…その時ではない」と考えていたら、移行計画が実現することはないだろう。

Jetifier

この問題を解決するためにGoogleが出してきたのがJetifierという自動移行ツールである。Jetifierは単独実行可能なツールとしても存在するが、Android Gradle Pluginでサポートされているので、通常は次のような記述をbuild.gradleに含めて使用することになるだろう(今回はGradleの話ではないので説明は最小限にとどめる)。

  • android.useAndroidX=true
  • android.enableJetifier=true

Jetifierは内部的に次のようなことをやっている。

  • コード中に含まれるandroid.support.*への参照をandroidx.*に変換する
    • ただしパッケージ名だけの変換になるとは限らない
  • binary to binary compiler (jar/aar/zip, ソースではやらない)
  • Java, XML, POM, proguard configs (がaarに含まれている)

ちなみに単独実行ツールでは逆向きにも変換できる… jetifier-standalone -r -i input.aar -o output.aar

ちなみに、移行対象のアプリ中にandroid.support.xyzとandroidx.xyzが混在していても問題なく動作することになっている。移行作業は段階的に行われるはずなので、これが求められている挙動であるといえる。

また、Googleの公式ライブラリ以外でandroid.support.xyzというパッケージが含まれている場合もandroidxに置き換える対象となるようだ。この場合はパッケージ名以外で何を置き換えるのが正しいのかツール側にはわからないので、パッケージ名だけが置き換わる。

いずれにしろ、Jetifierについて改めて強調しておくべきことがひとつある。Jetifierは移行を簡単にするためのツールではなく、移行を実現するためにほぼ必須となるツールだということだ。アプリケーション開発者にはバイナリ参照と手作業で書き換える適切な手段が無いのである。

Xamarin.AndroidJetpack移行戦略

Jetpackへの移行は、もちろんXamarin.Androidについても他人事ではない。Xamarin関係のライブラリの多くはAndroidサポートライブラリに依存している。それはVisual Studio for Mac / for WindowsのテンプレートでもAndroid.Support.v4への参照がデフォルトで含まれていたり、Xamarin.FormsであればそもそもAndroidプラットフォームバインディングXamarin.Forms.Platform.Android)がそうなっていたからだ。

Xamarin.Androidの場合、問題はJetpackよりも悪化している。AndroidバインディングとはDLLであり、NuGetパッケージに含まれるバイナリである。そしてソースコードが無いことも少なくはなく、これを自前で何とかするのは無理がある。

Jetpackへの移行は、本家がアプリケーション開発者の独力では対応できないと判断したタスクなのだから、Xamarin.Androidでも自動対応ツールが必要であると考えるのが妥当だ。そういうわけで、Xamarinからは新しくXamarin.AndroidxMigrationというパッケージが爆誕した。

github.com

JetifierはAndroid Gradle Pluginのタスクとして対応したので、ユーザーはgradle.propertiesに2行追加するだけで済んだ。Xamarinというか.NETの場合は、Gradle Pluginのようなことを実現するためには、MSBuildタスクを含むNuGetパッケージを追加して、ビルドをinjectするかたちで対応する。

ちなみに、Xamarin.Androidのandroidxのバインディングは、2019年7月になってNuGet上に出現するようになった。 https://www.nuget.org/packages?q=xamarin.androidx で検索してみるとよい。ソースコードhttps://github.com/xamarin/AndroidSupportComponents/tree/AndroidX (AndroidXブランチ)にある。ここにはroomなども含まれているが、あくまでライブラリのバインディングに過ぎず、Java/KotlinであればAndroid Gradle Pluginによって利用できていたroomがXamarinで使えるようになったわけではない。

Xamarin.AndroidXMigrationの仕組み

Xamarin.Androidの仕組みについて把握している人なら知っていると思うが、Xamarin.Androidアプリケーションでは、アプリケーション中のDLLで使われているJava派生クラス(Managed Callable Wrapper)からJNIで呼び出されるJavaクラスを含むjarが生成されてパッケージされる。このJarを他の参照ライブラリに含まれるJarと合わせてJetifierの対象として変換を行う。

そしてDLL中の旧ライブラリへの参照をXamarin.AndroidXへの参照に置き換える。Jetifierが行っているようなバイナリ書き換えを、DLLについても行っていることになる。バイナリ書き換えはcecilでやっている。

NuGetパッケージを追加するだけでこの変換機構が自動的に動作する仕組みは、NuGetのMSBuildタスク拡張機能を利用している。これについては日本語ではここで詳しくまとめられているようだ。

www.kekyo.net

ただし、適用されるMSBuildタスク拡張の順番は制御できないので、これはやっつけ仕事に類するものだ(23:30追記: 別のパッケージが、関連するTargetにTargetDependsOnで参照アセンブリのリストやそのアセンブリ群に手を加えた場合のことを考えるとよい)。ユーザーとしてはビルドの「最後」に適用されることを祈るか、そうなるように各プロジェクトファイルをいじることになるかもしれない。(ちなみにGoogleAndroid Gradle Pluginは拡張タスクではなくプロパティで実現しているので、「正しい」雰囲気がある。)

単独ツールとしては androidx-migrator.exe でも実行可能だ。そして、Xamarin.AndroidXMigration.ToolというNuGetパッケージがdotnet toolとして作られている。これは新しいMSBuildの記述スタイルであれば Sdk="Microsoft.NET.Sdk"<IsTool>true</IsTool><PackAsTool>true</PackAsTool> の組み合わせだけで作れる(パッケージがそうやって作られているという意味であって、ユーザーはこう記述する必要はない)。

Xamarin.AndroidXMigrationの注意点

公式ブログにも書かれているが、Xamarin.AndroidXMigrationを適用したアプリケーションには、Xamarin.Android.Support.*Android.Arch.*への参照を(間接的にでも)残しておかなければならない。AndroidXMigrationはアプリケーションをいったん古いコードでビルドすることが前提になっているし、こうしておかないと、AndroidXMigrationが移行元のアセンブリとしてのXamarin.Android.Support.*をロードできなくなってしまうためだ。

その他

androidxへの移行は他のフレームワークでも問題になる。たとえばReact Nativeでも同様のjetifierツールが開発されているようだ。Flutterでも1.7リリースの時にAndroidXへのサポートは作業中である、と説明している(1.9リリースノートにはJetpackやJetifierへの言及が無い)。