libinstpatchとは

音楽技術etc. Advent Calendar 2019の…うーん…とりあえず21日目くらいにしようかな…のエントリーです。残りは上手く埋まったら埋めていきます。とりあえず今回はlibinstpatchについてです。短めに。

github.com

libinstpatchはswami projectの一環として公開されているインストゥルメンタルパッチの抽象表現ライブラリです。インストゥルメンタルパッチとは何かというと、具体的には次の3つの実装があるので、これを見ると何となくでもすぐに理解できると思います:

要するにサウンドフォントやDLSはどちらも性格が似ているので、両方ともまとめて扱えるようにしてしまおう、というものです(GigaSamplerはどの辺のコミュニティで使われているのか正直よくわかりません)。どちらも懐かしいといえば懐かしい存在ではありますが、現役で使われていなくもないものですね。

どういうところで使われるかというと、サウンドフォントベースのサンプラーや仮想MIDIバイスのソフトウェアで使うことができます…そうです、Fluidsynthの最新版2.1でこのlibinstpatchがサポートされた結果、FluidsynthでもDLSが使えるようになりました。実のところ作者がFluidsynthのメンテナなので、推して知るべしではあります。libinstpatchをどうやって使うかも、ここから使われ方を見てみれば分かるかと思います。

SFもDLSも(たぶんGigaSamplerも)単なるインストゥルメンタルパッチのデータストレージにすぎないので、このライブラリ自体がやっていることは単純です。サウンドフォントを読み書きするライブラリのAPIと機能的には大して変わらないはずです。

データロードの部分だけはIpatchConvert_から始まる名前のフォーマット別実装が含まれています。特に難しいことはないでしょう。

Xamarin/binding-tools-for-swiftについて

Xamarin Advent Calendar 2019 24日目のエントリーです。えっオマエ生きてたの?って感じですが、軽めのネタ・調べ物で参戦です。

目次

what is it?

2019年はXamarin方面では落ち着いた1年になっていたかと思います。ネイティブ・プラットフォーム側はJetpack ComposeやSwiftUIが発表され、これから新しい時代が始まろうとしていますが、Xamarinは独自のクラシックなエコシステムに立脚するだけで続いていくのか、新しい波にきちんと乗っていけるのか、来年は動きを見せていくべき1年なのだろうなと思います。

Xamarin.Android方面ではbinding generatorからKotlin artifactを取り除いたり、AndroidSupportComponentsからAndroidXサポートを切り出したリポジトリが出現するなどの進展は見られましたが、それ以上に大きな動きは無いようです。まあ退屈と言えば退屈ですね。

一方で、今年公開された中でわたしがダントツに面白そうだと思ったのは、xamarin-macios方面で今年の秋頃に公開されたらしいbinding-tools-for-swiftです。

github.com

これまでもtom-swiftyやらSwiftNetifierやら、いろんな名前で開発されてきたSwiftとC#の相互運用のためのツールが、ついに公開されたということですね。tom-swiftyの名前はツール名としても残っているみたいですね(Tom Swiftyという単語はWikipediaにも項目があるくらい一般的であるようです)。

binding-tools-for-swiftに関する公式リポジトリ以外の情報は現状ほぼ皆無ですが、Xamarinで開発を担当してきたメンバー(らしい。わたしがいなくなってから入ってきた人みたい)が導入的なブログポストをまとめています。

plinth.org

現在製品化されているXamarin.iOSとXamarin.MacObjective-CAPIに対応するバインディングバインディング生成機構を、ObjCRuntimeを経由して実現するものですが、Swiftの場合はランタイムも含めさまざまな独自要素が含まれてくるので、新しいバインディング生成ツールが必要とされてきました。

Swift自体はもう何年も前から存在する言語なので、今さらと思われるかもしれませんが、一般的な評価として、Swiftの言語仕様が安定してきたのはSwift3〜4あたりの比較的最近のことであり、言語機能はObjective-Cよりも複雑で、さらにABI安定性もSwift4でようやく目標となったような段階なので*1バインディングツールに関しては、ようやく本腰を入れて取りかかれる状態になったとも言えます。(ABI不安定なときにがっつり実装しても新バージョンのSwiftで台無しになったら残念なことになるので…)

わたしはAppleユーザーとは言いがたいですしこれまで試したことも無かったのですが、今回はこのbinding-tools-for-swiftをどうやって使うのか、何が面白いのかといった話を書きます。

binding-tools-for-swiftという名前は長いので、以降公式ドキュメントの慣行に準じてBTFSと表記します。

今使えるの?

BTFSはまだ著しく開発中で、おそらく大概のライブラリはバインドに失敗するのではないかと思います。仕様としてバインディング作成が難しい(Xamarin.Androidのように、言語間の違いを吸収して完全自動でバインドするのが、チャック・ノリスにしか出来ない)とかいう以前に、そもそもツールチェインが整っていないとか、エラーログがまともに出力されない、といったレベルの状態なので、われわれ外部の開発者はもう少し整備されるのを待ったほうがよさそうです。

BTFSのビルド手順はトップディレクトリでmakeを実行するだけなのですが、現状では、xamarin-maciosをビルドするのと同様に、特定のバージョンのXcode、monoやXamarin.iOS、Xamarin.Macのダウンロードが必要になります(Xamarinプラットフォームはmasterでも可能そうですが、おそらくBTFS開発者向けです)。Xcodeだけでも15GBくらいは必要になるのでストレージに余裕の無い人は試せないと思います。不足コンポーネントをダウンロードするためのjenkinsスクリプトのコマンドなどを示してくれるので、それに従っていればいずれビルドできるでしょう(適当)。

いったんビルドできたら、mono tom-swifty/bin/Debug/tom-swifty.exe --helpを実行して確認してみるとよいでしょう。

実際にライブラリをバインドするときは、quickstart guideにもある以下のコマンドを実行することになります。

mono /path/to/tom-swifty.exe --swift-bin-path SWIFT_BIN_PATH --swift-lib-path SWIFT_LIB_PATH -o /path/to/output_directory -C /path/to/YOURLIBRARY.framework -C /path/to/binding-tools-for-swift/swiftglue/bin/Debug/PLATFORM/XamGlue.framework -module-name YOURLIBRARY

SWIFT_BIN_PATHSWIFT_LIB_PATHには以下のようなパスを指定することになります。BTFS用に手を加えられた独自のswiftコンパイラが必要になります。

/path/to/binding-tools-for-swift/SwiftToolchain-v3/GITHASH/build/Ninja-ReleaseAssert/swift-macosx-x86_64/bin
/path/to/binding-tools-for-swift/SwiftToolchain-v3/GITHASH/build/Ninja-ReleaseAssert/swift-macosx-x86_64/lib

わたしはBTFSをビルドしてコマンドラインで実行できるところまでは進めましたが、バインド出来るライブラリは特に見つけられませんでした。AudioKitあたりはバインディングがあってもいいんじゃないかなーと思ったのですが、framework以下の実行ファイルへのシンボリックリンクを読めないレベルの完成度でした。

一般ユーザーのところに降ってくるまでには、MSBuildタスクなどが完成してMSBuildやVS{Mac, Win}から呼び出せたりNuGetで取ってこられるようになったりしていると思います。

とりあえず、今まだ使えないとしても、BTFSがどんな工程を経てユーザーのライブラリに対するバインディングを生成するのか、という部分は実装のステータスとは別に面白い話になると思うので、このエントリの以降の内容はFunctional Outlineのドキュメントで説明されているビルドプロセスについて、他のドキュメントにも言及しつつ説明していきます。

どんなバインディングが出来るの?

BTFSでは、以下の言語要素がサポートされています。

サポートされていないのは↓のような要素です。

ObjectModelingのドキュメントにおおよその構造が書かれていますが(ちょっと長いのでここでは引用しません)、ISwiftObjectというインターフェースを実装するオブジェクトとなるようです。Xamarin.AndroidにおけるIJavaObjectみたいなものでしょう。(XAの場合は、IJavaObject自体は単なるJavaオブジェクトのマーカーでしかなく、必ずJava.Lang.Objectクラスから派生することになりますが。)

Objective-CのときもプロトコルはストレートにC#マッピング出来ない存在でしたが、Swiftのプロトコルへのバインディングもやや複雑な説明を要する存在になっています。Protocol Handlingのドキュメントで詳しくまとめられていますが、EveryProtocolという何やら神っぽいクラス(!?)を定義して、それを渡して廻すようです。

swiftライブラリのメタデータ構築

Swiftのライブラリをソースからビルドすると、swiftmoduleというライブラリのメタデータを格納したファイルや、SILと呼ばれる中間表現のバイナリファイルになります。BTFSではこれらを対象にバインディングの元になるSwiftTypeという型情報を構築していきます。最終的にSwiftTypeを集めたものはInventoryと呼ばれるメタデータの集合体になり、次のビルドプロセスであるところの「ラッピング」の入力になります。

もっとも、このアプローチでは十分な型情報を得ることができないので、次にXML relectionという解析プロセスを経て型情報を完成させます。

ちなみに、これらの過程ではswiftのライブラリのバイナリから識別子を取得する必要があるのですが、swiftコンパイラはswiftの識別子をバイナリコードとしてプラットフォーム準拠の形式にするためにmanglingという加工処理を行います(C++コンパイラなどでも行われています)。BTFSにはこれを解析するDemanglerというコンポーネントが含まれています。

独自swiftc拡張

BTFSのドキュメントによると、swiftcが生成するswiftmoduleやライブラリファイルには、バインディングの生成に必要となる型情報が十分には含まれていないため、Xamarinで独自にswiftcに手を加えた(?)コンパイラをビルドするようです。XML reflectionの過程で使用されるのはこのスクリプトでビルドされる…かもしれない…コンパイラです。(通常はビルド済バイナリのダウンロードが走るだけになりそう)

https://github.com/xamarin/binding-tools-for-swift/blob/master/jenkins/build-swift.sh

XmlReflectionのドキュメントを眺めてみると、既存のswiftcに手を加えるというよりはそもそも自前でswiftコンパイラを作るみたいな壮大な(?)話が書かれています。まあパーサを作ってXMLメタデータを生成するだけなのでそれほど非現実的でもないかも…? わたしもXamarin.Androidのビルド用でJavaスタブコードのパーサー書いたりしましたし(たぶん今でも使われているはず…?)

ともあれ、このXml reflectionのアウトプットはModule Declarationと呼ばれ、これも次のビルドプロセスであるラッパー生成処理への入力となります。

ラッパーswiftライブラリの生成

InventoryとModule Definitionという2つの類似するメタデータ集合が出来上がったら、次はこれらをもとにいよいよバインディングを生成…というわけにはいきません。 ユーザーのswift APIを、いったんC#のP/Invokeで呼び出せるスタイルのAPIでラップしたswiftコードを生成します。この過程はWrappingBuilderという部品が実現しています。

BTFSの中で、メタデータ情報を解析して型ツリー情報を構築するのはDynamoと呼ばれています。このBTFSから、swiftコードを生成したり、後でC#コードを生成したりすることになります。

ラッパー生成処理は、実際には2つのユーティリティで行われています。WrappingBuilderが基本的なクラス構造を定義して、もうひとつOverrideBuilderという部品が、C#でopenクラスを派生できるように必要になる派生クラスを生成します。Xamarin.Androidにもgen-java-stubs.exeとかGenerateJavaStubsというMSBuildタスクがあるのですが、それに相当するものと考えてよさそうです。

C#バインディング生成

ラッパーを準備してAPIメタデータを整地したところで、いよいよC#バインディングの生成に入ることが出来ます。まず、ラッパーのライブラリから、再びSwiftTypeのInventoryとreflectorによるModule Declarationの2つのメタデータ集合を生成します(処理内容は最初と同じです。対象がラッパーになっただけです)。ここでまたDemanglerが必要になります。

C#のコードを生成するのは、ラッパーを生成する時に登場したDynamoの役割です。これでようやく完成です!

BTFSの制限

BTFSがサポートしているのはSwift 5までの文法で、Swift 5.1はサポートしていません。SwiftUIが多大に依存しているOpaque Result Typesなどはまだ使えないということですね。

まとめ

今回はXamarin/binding-tools-for-swiftで行われているバインディングのビルド過程を追ってみました。途中で独自のswiftコンパイラハックを加えていたりと、非常にいかがわしい萌え技術が使われているのではないでしょうか。

*1:たとえばこのswiftのname manglingに関するドキュメントを見ると4.0と4.2で違っていることがわかります

LV2の概要

音楽技術Advent Calendar 2019の11日目のエントリーです。(まあだいぶ穴が開いているのですが、マイペースに埋めていきます。)

What is LV2?

LV2は主にLinux環境で利用できる、クロスプラットフォームのオーディオプラグインの仕様です。

オーディオプラグインとは、主にDAW (digital audio workstation)環境でDTM(desktop music)の作業を行う場面で、楽器やエフェクターとして利用できるオーディオデータの生成・加工ソフトウェアとして使われるものです。オーディオプラグインは、さまざまな「ホスト」となるDAWなどのソフトウェア*1の上で、さまざまなものを繋ぎ合わせて使用することが多いです。よくある使い方としては、サンプラープラグインでサンプリング音源データをノート(キー)に合わせて波形を調整して、それをロータリーエンジンやリバーブプラグインで加工して、ミキサープラグインでゲイン調整してオーディオデータとして出力する、といった流れになります。

オーディオプラグインは、複数のホスト、複数のプラグインベンダーの間で互換性が求められるもので、デファクトスタンダートとなる仕様がいくつか存在しています。有名どころではSteinberg VSTApple AU (AudioUnit)が挙げられます。オーディオ処理がもともとプラットフォーム固有の実装になりがちだったこともあってか、ここには業界標準となるような仕様が存在しないのが現状です。また、DAW開発社でも、自分たちのDAWでしか使用できない独自のプラグイン形式を規定して公開しているものがあります。Avid Audio eXtensions (AAX)などがその典型です。

本稿で取り上げるLV2とはLADSPA v2の略であり、LADSPAとはLinux Audio Developer's Simple Plugin APIの略です。Linux環境ではVSTAUのようなデファクトスタンダートのように使われている仕様のオーディオプラグイン機構が動作しなかったため、独自にオーディオプラグイン機構を開発して発展させる必要がありました。それでもっともポピュラーだったと言える仕様がLADSPA (v1)です。

オーディオI/Oそのものはプラットフォーム固有の実装とならざるを得ない部分があるのですが、一旦そこを抽象化すると、実際のオーディオ処理の部分は、実のところプラットフォーム固有の実装になることはあまり無かったため、LADSPA v2は最早L (Linux)固有のものではなく、MacでもWindowsでも動作可能なものになっています。LV2と同様、VSTクロスプラットフォームMacでもLinuxでも使用できる存在となっていきました。

LV2は2019年現在、ardourやqtractor、museなど主要なLinuxDAWでサポートされていますが、それ以外の製品、特にWindowsMacでは未採用の製品が多いです。

LV2プラグインの概要

LV2は(VSTなどと同様)現在進行形で拡張されている仕様です。後方互換性を維持するために、LV2仕様はモジュラーアーキテクチャになっており、コア部分と拡張部分を切り分けてあります。そのため、使用可能な最低バージョンがモジュールによって異なります。これはVST3のMA(モジュールアーキテクチャ)と似ている側面があります。

LV2仕様のモジュールリスト どのプラグインフォーマットでも、プラグインにはメタ情報が含まれているものですが、LV2の場合はこれをRDFによって提供することになっています。実際には、XMLとしてのRDFではなく、RDFを「シンプルな」テキスト形式で記述するTurtle (Terse RDF Triple Language)と呼ばれる独自記法に変換した.ttlというファイルに記述することになります。

RDFの記述内容は、プラグインAPIについても当てはまり、VST3やAUの場合は、単にCやObjective-CAPIを実装するだけで済むのですが、LV2の場合は、実際にコードとして実行するために必要になる最小限の部分のみをコードとして実装し、残りの部分はこのメタデータとして作成する作業が必要になってきます。すなわち、(1)プラグイン利用者の視点でいえば、そのプラグインにどんなポートやパラメータがあるか、どんな入力を受け付けるかは、RDFの情報だけでも把握できますし、(2)プラグイン作成者の視点でいえば、そのプラグインにどんなポートやパラメータがあるか、どんな入力を受け付けるかは、コードだけで完結せず、メタデータに記載しなければならないことになります。

プラグインでは独自の型を規定して公開することも可能です。MIDIメッセージやプリミティブ型(Atom型)などLV2標準に含まれる型の多くが、このルールに基づいて規定されています。もっとも、一般的なプラグインにおいて、複雑な独自型を公開型として定義する必要は滅多に無いでしょう。

モダンなプラグインフォーマットでは、プラグインが単一のライブラリファイルとして提供されていることはなく、関連ファイルとまとめて1つのフォルダなどにまとめて配置するものが多いです。LV2も同様で、「LV2パス」のいずれかに.lv2という名前で終わる「プラグインバンドル」のディレクト*2に関連ファイルをまとめます。この中に入るのは典型的にはこのようなファイル群です:

LV2フォルダ構成 ファイル名は任意に決定できますし、1つのプラグインバンドルに複数のプラグインを含めることもできます(manifest.ttlに記載する必要があります)。マニフェストの内容は、このようなテキストにまで圧縮できます(Turtle syntaxとはこのようなものです):

<http://example.com/arbitrary-paths/foobar>
    a lv2:Plugin ;
    lv2:binary <foobar.so>  ;
    rdfs:seeAlso <foobar.ttl> .

.soLinux環境における共有ライブラリなので、実際のプラットフォームに合わせて変わります。

LV2の拡張性

LV2の標準仕様は、バージョン1.0時点で既知であった最小限の機能のみを提供し、それ以降のバージョンで追加された機能については、ホストから渡されない限り、プラグインが利用することはできない仕組みになっています。

拡張機能の中には「URIと一位識別子URID(uint32)のマッピング」「プラグインのステートの保存と復元」「UIの呼び出し」など、かなり本質的なものが含まれているため、LV2をプログラムで扱うのであれば、LV2拡張に対する基本的な理解が不可欠です。

LV2拡張のインスタンスは、LV2_Featureという構造体で表されます。

struct LV2_Feature
{
    const char *URI;
    void *data;
}

LV2拡張はURIで種別が識別され、それぞれの拡張に応じて必要になるデータがdataに渡されます。多くの拡張機能はコードによる実装を必要とし、それらは(直接ないし間接的に)関数ポインタとしてホストからクライアントに渡されることになるので、dataが空であることは通常はありません。たとえばURILV2_URID_MAP_URIhttp://lv2plug.in/ns/ext/urid#map)のとき、dataに渡されるべきものはLV2_URID_Map構造体です。

// typedef void* LV2_URID_Map_Handle
// typedef uint32_t LV2_URID
struct LV2_URID_Map
{
    LV2_URID_Map_Handle handle;
    LV2_URID(* map)(LV2_URID_Map_Handle handle, const char *uri);
}

LV2プラグインは、lv2.hをインクルードしてLV2_descriptor()およびLV2_lib_descriptor()という関数を定義することになります。いずれもLV2ホストがプラグインdlopen()でロードした後、dlsym()で動的に呼び出すことになります。LV2_lib_descriptor()ではLV2_Feature*型のfeatures が定義されており、ここにはLV2ホストから「ホストがサポートするLV2拡張」のデータを含むリストが渡されます。

先のLV2_URID_Mapの例でいえば、mapがこの拡張機能の実体であり、この関数ポインタをプラグイン側はLV2_URID_mapの実装として呼び出せる、という仕組みです。

LV2プラグイン後方互換

LV2プラグイン機構は、LV2 Coreと数多くのプラグインによって成り立っていますが、それぞれのプラグインにはバージョンがあり、これらは必ずしも後方互換性があるとは限らないようです。筆者が試した範囲では、Atom型として定義されている型がXML Schema Datatypesに由来するxsd:*型であるとしてRDF上で定義されているものがいくつかあり、これらのプラグインをlv2_validateツールで検証するとwarningが報告されました。LV2の仕様自体が後方互換性を意識していても、実際に後方互換性が達成できるかどうかは、拡張となるプラグインのバージョン間互換性・設計次第であるようです。

もしかしたらこれはLV2_Atomをサポートした時点で生じた非互換問題で、2012年に出たこの記事でQTractorの開発者に言及されている問題かもしれませんが、2012年の問題が2019年になっても問題になっているとしたら、7年も前の非互換変更が今でも尾を引いているということであり、後方互換性の問題は無視できないかもしれません。

LV2ホスティング

オーディオプラグインを使うためには、オーディオプラグインMIDIなどの制御命令や音声データを渡して処理させるホストが必要です。一般的にはDAWと呼ばれるソフトウェアがこれを担いますが、プログラムとしてはLV2プラグインをロードして処理を実行できるものであれば何でもかまいません。たとえば、プラグインのテストには、テストのみを目的とするローダーが使われるでしょう。

LV2をサポートするDAWとしては、Ardour、Audacity、Carla、QTractorなどが有力です。

LV2ホスティングを実現するためのリファレンス的な実装として、lilvというライブラリがあります。ローカルにインストールされているLV2プラグインプラグインクラスをリストアップして、プラグインインスタンスを生成して、ポートをデータバッファに接続して、オーディオ処理を走らせることができます。

lilv

lilvはLV2ホスティングのためにあるCライブラリです。RDFの詳細の多くを隠蔽しつつ、プラグインクラスを検索し、プラグインインスタンスを生成して、音声データ処理を行えます。

RDFの処理は、同じ作者が開発しているserd(RDFの解析)、sord(Turtle syntaxの解析)、sratom(LV2 atom型とRDFの相互変換)、そしてlv2の4つのソースコード・パッケージにのみ依存しています。実際にはlv2などがさらにlibsoundioやcairoなどをオプションとしてサンプルプラグインのビルド時に参照することになるので、依存関係はもう少し膨れ上がります。

LV2のRDFを書けるようになるためには、RDFについてそれなりに勉強しなければならないことになりますが、率直に言えば、オーディオプラグインを開発するためにRDFを勉強するというのは、全く本質的な作業ではない、と言わざるを得ないでしょう。これは、lilvのAPIを調べていくことである程度緩和されます。RDFの面倒な部分はとりあえず置いておいて、lilvのAPIでworld, plugin class, plugin, port, instanceといった概念を理解したほうが早いです。

*1:有名どころではSteinberg社のCubaseApple社のLogic Proなど

*2:単なる一般則なのか仕様で厳格に決められているのかは不明ですが、筆者はこの例外を見たことがありません

JUCEにおけるz-orderの扱い

また音楽技術Advent Calendar 2019JUCE Advent Calendar 2019のcross postingです。(現状どっちもとても埋まる気がしない)

JUCE GUIのComponentには、その内容としてchild componentsをaddAndMakeVisible()関数を使って載せることができます。これはやや設計として失敗の雰囲気があるものの(全てのComponentが子をもつ設計は200x年代前半の臭いがありますね)、古典的には一般的なGUIコンポーネントの設計です。そして、コンポーネントの描画はpaint()関数で行われます。JUCEの子要素は自分でレイアウトして、自身の描画もpaint()で行うのが基本です。あれ? ちょっと雲行きが怪しくなってきましたね…? 大丈夫、子要素のpaint()関数も呼び出されるので問題ありません。

子要素はデフォルトではabsolute layoutのようにpaint()で渡されたGraphicsで渡される領域の範囲内で描画します。描画する領域は親から渡され、子の描画はその子のpaint()関数が行う…ということは、複数の子がある場合、描画順序によっては、ある子の描画内容を後から別の子が上書きしてしまう可能性があります。

これでは困りますね。少なくとも描画順序を制御できなければ困ります。こういう時にわれわれが使うのは、そう、「前面に移動」「背面に移動」です。MSOfficeですら制御できるアレです。もう少し真面目に言うとz-orderですね。

このz-orderですが、JUCEのComponentには対応するプロパティがありません。z-order無いのかよ!となりそうですが、z-orderを指定できる場所がひとつあります。Componentに子要素を追加する時に使うaddAndMakeVisible()関数です。

void Component::addAndMakeVisible (Component * child, int zOrder = -1)

zOrder : The index in the child-list at which this component should be inserted. A value of -1 will insert it in front of the others, 0 is the back.

これで順番を指定できそうですね。さっそく具体例を見てみましょう。簡単な重ね合わせの生じるデモを作ったので、これを土台に検証します。

f:id:atsushieno:20191210024147p:plain

#pragma once  
#include "../JuceLibraryCode/JuceHeader.h"  
  
class Component1 : public Component {  
public:  
  void paint(Graphics& g) override {  
    g.fillAll(Colours::green);  
  }  
};  
  
class Component2 : public Component {  
public:  
  void paint(Graphics& g) override {  
    g.fillAll(Colours::blue);  
  }  
};  
  
class MainComponent : public Component  
{  
  Component1 c1;  
  Component2 c2;  
  Label l1{"label1", "Label1"};  
  Label l2{"label2", "Label2"};  
public:  
  //==============================================================================  
  MainComponent() {  
  c1.setBounds(0, 0, 200, 200);  
  c2.setBounds(100, 100, 200, 200);  
  l1.setBounds(0, 0, 200, 200);  
  l2.setBounds(100, 100, 200, 200);  
  addAndMakeVisible(c1);  
  addAndMakeVisible(c2);  
  addAndMakeVisible(l1);  
  addAndMakeVisible(l2);  
  
  setSize (300, 300);  
  }  
  
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)  
};

JUCEのソースファイルなのでPIPにしてもよかったのですが、とりあえずデフォルトテンプレートのMainComponent.hを書き換えてMainComponent.cppの中身を空っぽにしただけです。多分ビルドできるでしょう。

緑の矩形と青の矩形が重なり合っていますね。この画面で、緑を青の上に出したいと思ったら、zOrderを指定してaddAndMakeVisible()を呼び出すと良いということですね。

  addAndMakeVisible(c1, 1);  
  addAndMakeVisible(c2, 0);  
  addAndMakeVisible(l1);  
  addAndMakeVisible(l2);  

f:id:atsushieno:20191210024254p:plain

なるほど順番が変わりましたね! 実際にレイアウトを調整するときは間に他の要素が入るかもしれないので、zOrderの差を大きめに設定しておきましょう。

addAndMakeVisible(c1, 100);  
addAndMakeVisible(c2, 1);  
addAndMakeVisible(l1, 110);  
addAndMakeVisible(l2, 10);

f:id:atsushieno:20191210024147p:plain

( ゚д゚) ・・・
 
(つд⊂)ゴシゴシ
 
(;゚д゚) ・・・

あ…れ…? なんか想定外の挙動ですね…?

zOrderってなんか値域とかあるんだっけ…?と思いながら情報を探してみると、こんなforumの議論が…

forum.juce.com

In particular, if a component has N children, then all z-indices >= N will be considered equivalent, because of this code in Component::addChildComponent:

「子要素の数がNだったらzOrderの値がN以上だったら全部Nに揃えられる」…そマ?

まじすか…どうもzOrderの設計はやっつけっぽい…。その後のスレッドでは「Componentのメモリ使用量は可能な限り最小限にしてあって云々…」とあるのですが、ホントか〜? ホントにメモリ使用量を気にするんならComponentがMouseListenerを実装しているのおかしくね〜?とか思ったりしちゃったり何だったりしますね…

ともあれ、zOrderはこういう感じで「期待通りに動作させるには慎重に使わないといけない」ものなので、使うときは十分に注意しましょう。たぶんaddChildComponent()がひと通り済んだ時点で適切な順序でソートしたほうが良いです。

以上juce_gui_basicsの小ネタでした。

オーディオプラグインの理想的なGUIフレームワークを模索する

音楽ツール・ライブラリ・技術 Advent Calendar 20197日目のエントリーです。今日はポエムに近いです。

オーディオプラグインGUIの要求事項

オーディオプラグインフレームワークというソフトウェアはやや特殊な世界で、歴史的な経緯を脇に置いて2019年現在に求められている要件を列挙するなら、

  • WindowsおよびMac、可能なら*1それ以外(Linux, Web, iOS, Androidなど)をサポートすること
  • オーディオ処理とGUI機構を提供すること
  • DAW上から起動できてGUIイベントを部分的に共有すること(キーボードイベントなど)

などが挙げられます。今回はGUIフレームワークについて少し掘り下げて検討します。先日M3で頒布した同人誌では言及しなかった部分ですね。

VST SDKのように独自ブランドのGUIフレームワークを提供するものや、JUCEのように複数オーディオプラグイン機構・複数プラットフォーム向けに独自のGUIフレームワークを提供するものは、いくつか存在します。オーディオプラグインフレームワークにおいては、GUIフレームワークの提供は必須ではありません。しかし、どのようなGUI機構であってもオーディオプラグインフレームワーク側と接続可能になるような、オーディオ/MIDIストリームの出入り口を用意しておく必要はあります。

オーディオプラグインに求められるGUIアプリケーションとしての要件は、一般的なデスクトップGUIアプリケーションのものと比べると、だいぶクロスプラットフォームで開発しやすいものであるといえます。その理由のひとつは、起動および操作がほぼ必ずDAWを経由したものになるためです。メインメニューは無く、キーボードイベントを「DAWから奪い去る」ことがあまり歓迎されず、DAWからダイアログのようにポップアップされているときだけ操作することになります。

さまざまな制約を受けながら作成できるGUIの利便性は当然ながら一般的なGUIアプリケーションよりだいぶ低く、そのようなGUIであれば「ネイティブアプリケーションのポテンシャルを全然引き出せない」クロスプラットフォームGUIフレームワークの欠点が目立たなくなります。

VSTGUIやJUCE (juce_gui_basics) は、そのような特殊な環境で発展してきたといえます。もちろん、Carbon/CocoaWindows APIを直接使うGUIをもつオーディオプラグインもあり、これらも一般的なGUIアプリケーションに求められる要求事項とはだいぶ無縁に作られてきたはずです。

また、オーディオ処理がリアルタイムで厳格なフレームの中での完結を求められることもあり、これに対応するかたちでGUIを構築するのであれば、ウィジェット/コントロールを操作するGUIフレームワークを使うよりは、コールバックベースの低レベルの描画APIに基づいて実装するものが多くなるのも、それなりにわかりみがあります。

もちろんGUIの描画そのものはオーディオスレッドで行われるべきものではないので、ユーザーコードでオーディオコールバックから呼び出される描画呼び出しはpostにとどまり、実際の描画処理はUIスレッドで行われることになります。一方で、そもそもGUIはオーディオ処理に100%追従することが前提となっていないので、ある程度の遅延は容認できるし、何ならオーディオ処理とは異なりGCJITで世界が止まっても致命的な問題ではない、ということがいえます。実際これはAndroidにおけるオーディオAPIの開発方針でもあります(UIはART上でKotlin/Javaでも可能、オーディオはNDK + OpenSLES/AAudio)。

どのようなx-plat GUI Fxが向いているのか

さて、こうなってくると、オーディオプラグインGUIフレームワークは、クロスプラットフォームGUIフレームワークが利用できる分野であるといえそうです。

もし「低レベルの描画APIだけ提供していれば良い」というのであれば、CairoなりSkiaなりを使えば解決ということになりますが、実際にはマウスやキーボード入力のサポートも必要になるので、きちんとアプリケーションループをもったGUIツールキットであることは必要でしょう。

ここで「クロスプラットフォームフレームワークでは実現できない、プラットフォームの入力デバイスを完全にサポートする必要がある」という立場であれば、ネイティブのGUIツールキットで個別に開発するのが妥当でしょう(Surface DialがLinuxでも使えてLeap Motionのようなデバイスがどのプラットフォームでも動作する現在、筆者としてはそのような状況はあまり一般的には想定できないところですが)。

クロスプラットフォームGUIフレームワークを使う路線でいくとしても、このカテゴリにまとめられるGUIフレームワークにはいくつかの種類があります。

(1) JavaScriptとブラウザ環境が前提であるもの。CordovaやIonicなどが挙げられます。JUCEコミュニティの一部ではReactを使ってGUIを構築するアプローチが話題となっているようです。このやり方であればWeb開発の手法でオーディオプラグインGUIを開発できることになるので、開発は楽になりますがブラウザを組み込むことになるのはアプリケーションとしてはだいぶ重量感が増します。(オーディオプラグインにはGB単位でサンプリングを含むものが多く存在するので、それに比べたら誤差のようなものではあります。) またWebViewの組み込みはどんな環境でも問題を引き起こすことが多く、プラットフォームやブラウザコントロールのバージョンなど、環境による挙動の違いをもたらす変数が大きいこともマイナス点でしょう。

(2) プラットフォームごとにネイティブのUIコントロールを呼び出すように実装されたもの。XamarinやReact Native、WxWidgetsはここに分類されます。開発が楽になるかどうかはそれぞれの開発者のバックグラウンド次第、アプリケーションとしての配布が楽かどうかもフレームワーク次第と、十把ひとからげに評価するには変数の多いところです。プラットフォームのネイティブコントロールがUX満足度を高める要因になりますが、オーディオプラグインGUIでは、伝統的に標準コントロールがあまり使われていないので、伝統的なGUIではメリットが小さそうです。一方でプラットフォームごとに挙動がバラバラになるデメリットはそのままです。React NativeはWindows用のバックエンドはあるものの、Gtkなどはサポートされていないでしょう。XamarinのGtkサポートも実は貧弱です。Gtk2なので(!) もともとはGNOMEの開発を主導していた会社なのに(!)

(3) UIコントロールを独自にレンダリングするもの。Gtk, Qt, Flutter, JUCE, Unity UiWidgetsはこのカテゴリに属するといえます。これらは、フレームワークの完成度が重要なファクターになってきます。特にどのプラットフォームでも満足に動作するものは皆無といってもよいでしょう。QtやGtkLinux以外ではあまり歓迎されず、オーディオプラグインGUIとしても実績値は高くありません。またQtもGtkもデスクトップが「どちらであるか」によってエイリアンになる可能性が高く、まだX11が直接使われている可能性が高いといえるでしょう。しかしX11もまたWaylandなどで置き換えられつつある存在です。また、オーディオプラグインの主戦場はデスクトップ環境であり、Androidで快適、iOSでもまあまあ許容されているFlutterは、デスクトップではまだまだ発展途上(そもそも開発中)なので、現状で適切な選択肢であるとは言いがたいところです。不十分な多言語入力もこの種のフレームワークにありがちな問題です。

opinion

JUCEは古典的なオーディオプラグイン用のGUIフレームワークとしては必要最低限の機能を提供していて、しかもC++なので、現在ではよく採用されています。ウィジェット・ツールキットとしては他のフレームワークと比べると描画APIに毛が生えた程度であり、少しでも高度な機能を使おうとすると自前で実装することになります。基本中の基本のようなHBox/VBoxのようなレイアウトが"Advanced GUI layout techniques"と言われるのがJUCEのレベル感です。レイアウトエンジンがウィジェットとして組み込まれておらず、単独で存在するFlexBoxやGridのレイアウトの実装などを自前でコントロールの描画に適用することになります。JUCEはユーザー数の割には開発者層が薄く、特にjuce_gui_basicsは絶望的なので、筆者としては全然将来に期待していません。

(レイアウトエンジンは必ずしもGUIフレームワークと密接に結びついているわけではありません。具体的な例を挙げると、Facebookが開発したCSS Flexbox相当のレイアウトエンジンであるYogaはWeb以外でも使うことができましたし、AppleのAutoLayoutやAndroidのConstraintLayoutはCassowaryと呼ばれる制約付きレイアウトを実装したもので、Cassowary自体はGUIフレームワークから独立して存在しうるものです。なのでたとえばAbletonがQt用にCassowaryを実装することもできたりします。独自のGridレイアウトも同様といえます。)

筆者がこの分野で一番将来に期待しているのはFlutterです。FlutterはMaterial Design Componentをフル実装しており、十分にモダンなGUIを構築できる一方で、描画の低レベルレイヤーはSkiaが使われており、すなわちVulkanやANGLE、Metalのポテンシャルを引き出せる可能性が十分にあります(SkiaのMetalバックエンドはiOS 11以降のみですが)。Flutterでなくても、モバイル環境で育ったGUIフレームワークであれば、たとえばマルチタッチに対応した豊富な機能を持つGUIを構築することが可能でしょう。juce_gui_basicsで満足していたら逆立ちしても実現できません。

ただし、Flutterがオーディオプラグインの世界に適用されるためには、C/C++と相互運用できるだけの十分なFFIサポートが不可欠です。この部分はDartでまだまだ発展途上であり、残念ながら筆者がlibclangで相互運用を試してみた範囲では、とても実用段階にあるとは言いがたいところです。プラットフォーム独立の動的ライブラリのロードや、Stringの変換くらいは、ファーストクラスで実装されている必要があるでしょう。

あと、JUCEで最近さかんに試されているReact NativeというかJavaScriptにもいえることですが、float型が無いのはホントに大丈夫なのか?っていう問題はあるかもしれません。まあWebAudioで実証済みの世界とはいえるかもしれません。

*1:わたしはLinuxをサポートしないフレームワークには価値も未来も無いと考えています

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業界を敵に回した気がしてきたぞ