オーディオプラグインのインスタンス生成スレッドに関する覚書

AAP (android-audio-plugin-framework) のホスト実装のひとつとして、JUCEのAudioPluginHostを利用しているのだけど、最近全面的に再構成しているホスティングAPIに基づいてJUCE統合(AndroidAudioPluginFormat)を書き直していて、だいぶ行き詰まった問題がある。プラグインを生成するスレッドの問題だ。結論からいうと、AAPやAU v3ではmainスレッド以外からプラグインを生成するか、あるいはmainスレッドをブロックしないことが求められる。mainスレッドをブロックするのは大抵のプラットフォームではアンチパターンあるいは禁止となっているから、ロジックとしては妥当だとわかってもらえるとは思う。

AAPを設計しているのは他ならぬ自分自身なので「求められる」などと他人事みたいに書いているのは不自然に見えるのだけど、自分の趣味で求めているわけではない。

AAPはホストとはAndroidプラットフォームとエコシステムの構造として必然的に別アプリケーションとなり、インスタンスの生成においてはプラグインAndroid ServiceとしてApplication.bindService()を呼び出して生成する必要がある。bindService()を呼び出した後、その結果はそのメソッド呼び出しで引数として渡したServiceConnection(インターフェース)の実装のonServiceConnected()メソッドがコールバックされるという流れで渡される。このonServiceConnected()はメインスレッドで実行されることになるので、メインスレッドが他のコードでブロックされていると、onServiceConnected()にお鉢が回ってこないことになる。

自分の場合、メインスレッドでkotlinx.coroutines.channels.Channelwait()で待機させておいてonServiceConnected()signal()を送るようになっていて、一生先に進まない状況に陥っていた。

juce_audio_processorsを使う場合の注意点

JUCEを使ってオーディオプラグインインスタンスを生成するコードを書いている場合、そのインスタンス生成にはAudioPluginFormat(Manager)::createPluginInstanceAsync()を使うことが期待される。createInstanceFromDescription()は同期APIなので、これを使うと生成結果が返ってくるまでその実行スレッドがブロックされることになる。

std::unique_ptr<AudioPluginInstance> AudioPluginFormat::createInstanceFromDescription (
        const PluginDescription & ,
        double initialSampleRate,
        int initialBufferSize,
        String & errorMessage)

void AudioPluginFormat::createPluginInstanceAsync (
        const PluginDescription & description,
        double initialSampleRate,
        int initialBufferSize,
        PluginCreationCallback)

AudioPluginFormat(Manager)::createPluginInstanceAsync()にはひとつ注意すべき事項として、実際のプラグイン生成処理が内部的にMessageManagerを経由してmainスレッド上で行われてしまうという問題がある。つまり、たとえ自分のホストアプリケーション上ではmainスレッド上でプラグイン生成されないように(juce::Thread等を使用して)別スレッドでこの関数を呼び出していたとしても、実際のインスタンス生成処理はmainスレッドで走ってしまう。

もしプラグインフォーマットのホスティングAPIを実装できる立場であれば(筆者はそう)、mainスレッドでのインスタンス生成を「禁止」しないようにできるが、そうでなければ、juce::AudioPluginFormatを自ら実装して、createPluginInstanceAsync()の実装の中でさらに別のnon-mainスレッドに処理を非同期に実行させる必要があるだろう。

プラグイン・サービス接続処理とインスタンス生成処理の切り離し

オーディオプラグインインスタンス生成はMIDIのポート接続と似ている側面がある。1つのMIDIバイスに接続して複数のMIDIポートを利用することがあり得るように、同一のプラグインの複数のインスタンスを生成するのは一般的なシナリオだ。

Androidアーキテクチャの場合、別アプリケーションのServiceに接続するのは1つのBinderでも、そこから複数のプラグインインスタンスを生成して利用するという構成になる。Android MIDI APIの場合、MIDIバイスへの接続はMIDI_SERVICEへの接続という非同期処理から始めることになるが、いったん接続したMIDIバイスのポート接続は同期的に開くAPIになっている。

仮想MIDIバイスであれUSB/BLEデバイスであれ、対象Serviceとのやり取りはIPC(Androidの場合はBinder)ベースになるので、ポート接続も非同期であるべきではないかとも思えるけど、いったん接続したMIDIバイスとのやり取りにはrealtime IPCが活用され得るため、スレッドの切り替えを伴う非同期APIではむしろ適切ではない可能性がある。

これと同じことがオーディオプラグインインスタンス生成についても当てはまるといえそうだ。AAPの場合は、接続処理は必然的に非同期にならざるを得ないが、すでに接続済みのAudioPluginServiceであれば同期的にインスタンス生成を行える。Androidフレームワークでは現状AAPホストのような任意のアプリケーションにおいてrealtime Binderを生成する窓口が開かれていないようなので、あくまで一般的なpthreadの優先度でしか接続できないが、それでも非同期スレッドによる切り替えが発生しないほうが良いだろう。