3
0

JUCEとCHOCを組み合わせてオーディオプラグインのGUIをWeb技術で作ろう -補足情報-

Last updated at Posted at 2023-12-24

本記事はJUCE Advent Calendar 2023 の12月25日向けに投稿した記事です。

本記事は、12月16日に投稿した記事 "JUCEとCHOCを組み合わせてオーディオプラグインのGUIをWeb技術で作ろう" で伝えきれなかった情報を補足する記事になります。


前回の振り返り

要約

JUCEとCHOCを組み合わせることで、オーディオプラグインのGUIをWeb技術で制作することができます。このアプローチは、複数の技術要素を組み合わせながらも、柔軟で効果的なGUIプロトタイピングを可能にします。

本記事では、筆者が作成した WebGain と DenoGain での実践を通じて、C++によるネイティブ向けのオーディオプラグイン、オーディオアプリケーションの開発にWeb技術を導入する手順について解説します。

なお、用いている技術スタックが多層であることと、開発フローがまだ標準化できていないため、本記事では、「WebViewを用いてGUIを構築する基礎技術」に焦点を当てており、Web技術スタックそのものについては触れません。また、ソースコードの解説は要点のみに留めさせていただくことをご承知おきください。

JUCEとは

JUCE(Jules' Utility Class Extensions)は、C++言語によるマルチメディア系アプリケーションの開発を支援するフレームワークです。クロスプラットフォーム設計のライブラリから Windows, macOS, Linux, iOS, Android で動作するアプリケーションを作成することができます。
Julian Storer氏がDAWソフトウェアを開発する過程でライブラリ化された経緯があることから、オーディオプラグインを開発するためのテンプレートが充実していることが挙げられます。VST3/AudioUnit/AAX/LV2/ARAプラグインといった、DTMユーザーにはお馴染みとなっているプラグインフォーマットをワンソースから開発することができます。
公式サイト

CHOCとは

CHOC(Classy Header Only Classes)は、上記のJUCEを開発した Julian Storer氏が新たに作成したC++のヘッダーオンリーなライブラリです。C++プロジェクトで標準ライブラリの不足を補完し、ビルド手続き不要で利用することができます。各ファイルは自己完結しており、必要な部分だけをプロジェクトに取り込むことができます。ISCライセンスで配布されています。

対象とするバージョン

  • JUCE 7.0.9
  • CHOC SHA:8ec4005bc8768312a4200f700180c8c696677400
  • Visual Studio 2022
  • Xcode 14.2
  • Clang 14
  • Ninja-build 1.11
  • CMake 3.25以上
  • Git 2.3以上

サンプルコードについて

本記事で取り上げるサンプルコードはGitHubリポジトリで確認することができます。
このリポジトリでは、WebGain と DenoGain という2つのオーディオプラグイン(VST3, AudioUnit, CLAP)をビルドすることができます。

WebGain

webgain.gif

WebGainでは、単一のHTMLファイルをC++のバイナリデータとして組み込み、プロジェクトにシームレスに統合します。コントローラーはHTMLのinput要素を使用し、背景の描画にはp5.jsが、HTML要素のアニメーションにはCSSアニメーションが利用され、GUIが構築されます。

DenoGain

denogain.gif

DenoGainでは、Vite + Deno + Svelte + TypeScriptといったWebフロントエンド開発で用いられる技術スタックを使用します。ビルドシステムによって生成された配布用のWebリソースを描画することと、開発用のサーバーからWebリソースを取得することもできることを確認しています。
このプログラムでは、Webリソースの提供はCHOCのAPIを用いた簡易なWebサーバー相当の関数を実装し、WebViewからのFetch呼び出しに応じてリソースをストリームする手法でWebViewに提供します。


WebViewの開発に関する補足情報

開発サーバーからWebリソースを取得する

WebView(HTML, CSS, JavaScript)を開発途中における動作確認は、Webフロントエンド開発環境で設置したWebサーバーを用いて行うことができます。具体的な事例として、本記事のサンプルコードの DenoGain では以下のコマンドから開発サーバーを設置することができます。
※Node.jsで開発している場合は、コマンドをnpmに置き換えることで同様の開発環境での確認を行うことができます。

cd Plugins/DenoGain/WebView/vite-project
$ deno task dev

  VITE v5.0.8  ready in 13815 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
  

開発サーバーを設置した状態で、C++側のソースコードで、WebViewのnavigate先を開発サーバーに向けます。

PluginEditor.cpp

AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor& p)
    : AudioProcessorEditor (&p)
    , processorRef (p)
    , valueTreeState (p.getAPVTS())
{
    ~省略~

    chocWebView = std::make_unique<choc::ui::WebView>(options);
#if JUCE_WINDOWS
    juceHwndView = std::make_unique<juce::HWNDComponent>();
    juceHwndView->setHWND(chocWebView->getViewHandle());
    addAndMakeVisible(juceHwndView.get());
#elif JUCE_MAC
    juceNsView = std::make_unique<juce::NSViewComponent>();
    juceNsView->setView(chocWebView->getViewHandle());
    addAndMakeVisible(juceNsView.get());
#elif JUCE_LINUX
    juceXEmbedView = std::make_unique<juce::XEmbedComponent>(chocWebView->getViewHandle());
    addAndMakeVisible(juceXEmbedView.get());
#endif

    // Make sure that before the constructor has finished, you've set the
    // editor's size to whatever you need it to be.
    setSize (400, 800);
    setResizable(true, true);
    
    ~省略~
    
    chocWebView->bind("onToggleChanged", web_view_callback_on_toggle_changed);
    chocWebView->bind("onSliderChanged", web_view_callback_on_sliider_changed);
    chocWebView->bind("onInitialUpdate", web_view_callback_on_initial_update);

    // 設置した開発サーバーからWebリソースを取得する
    chocWebView->navigate("http://localhost:5173/");

    valueTreeState.addParameterListener("gain", this);
    valueTreeState.addParameterListener("invertPhase", this);
}

JUCEにWebViewを組み合わせる際のイシュー

前回の記事では、JUCEのGUIをWebViewで実装することについてのポジティブな側面について触れてきましたが、WebViewがJUCE開発における銀の弾丸のようなものかというと、そうとは言い切れない側面もあります。ここでは、イシュー項目について触れています。

JUCEのUIコンポーネントとWebViewを混ぜることが難しい

本記事のサンプルコードでは、CHOC APIから生成したWebViewをJUCEのGUIウインドウ内に取り込む手段として、juce::HWNDComponent, juce::NSViewComponent を用いています。

元記事:1. CHOCのWebViewをJUCEコンポーネントに統合する

この手法のネガティブなポイントとして、JUCEのUIコンポーネントと混ぜることが難しいということが挙げられます。juce::HWNDComponent, juce::NSViewComponent のAPIリファレンスには以下のように記述されています。

Of course, since the window is a native object, it'll obliterate any JUCE components that may overlap this component, but that's life.

意訳

もちろん、ウィンドウはネイティブオブジェクトなので、このコンポーネントと重なる可能性のあるJUCEコンポーネントはすべて消去されますが、仕方ないよね。

この課題については、@Talokayさんの記事が参考になります。

ここで起こる問題が、Heavy-Weight(ヘビー君)とLight-Weight(ライトちゃん) Componentの問題です。
VideoComponentに限らず、OpenGL、Web系のComponentは全てヘビー君であり、ライトちゃんであるJuceのComponentと同じレベルで使用すると、ヘビー君が常に、あらゆるライトちゃんよりも前面に表示される、という問題が起こります。

CHOC APIのWebViewだけでなく、JUCE APIの juce::WebBrowserComponent を使用する場合でも、同様の問題に遭遇することがあります。この問題に対処するためには、WebViewのレンダリング結果をキャプチャしてJUCEのUIコンポーネント上で再レンダリングするなどの手法が考えられます。ただし、これらの手法は新しい実装が必要であり、簡単なアプローチではありません。

この種の課題に対処するための解決策の実例が既に存在し、それを参考にすることができます。具体的な例として、Ultralight と呼ばれるサードパーティのヘッドレスHTMLレンダラをC++プログラムに組み込む方法があります。なお、この解決方法はCHOC APIを用いたOSネイティブのWebViewを組み込む方法とは異なるため、本記事で解説しているアプローチとは大きく異なる点にご注意ください。

上記のリポジトリでは、Ultralight を使用してWebリソースのレンダリングを行い、その結果をBitmapとして取得し、最終的にJUCEのGUIレンダリングシステム内で再レンダリングするプロセスが実装されています。これにより、JUCEのUIコンポーネントとWeb技術スタックで実装されたUI要素とをミックスすることができます。

WebView::evaluateJavascript は同期的にreturnを返さない

タイトルの通り、WebView::evaluateJavascript 関数は同期的にreturn値を返さない仕様になっています。これは、WebView上で実行するJavaScriptコードは、C++から見て非同期処理で行われることを意味します。WebView上で実行するJavaScriptコードで何かを処理させたい場合は、その点を意識して実装する必要があります。
標記の件については、JUCEフォーラムでも議論が行われていました。

This is a little more complicated. In your JavaScript, instead of returning a value, you need to call back to a C++ function that you’ve bound using WebView’s bind. Pass the value you want to return to C++ as a parameter to that function. Then in C++, your C++ lambda gets called, and the choc::value::ValueView args is the “returned” value from JavaScript.

Note that this will be asynchronous :neutral_face: Meaning: When your call to evaluateJavascript() returns, your C++ lambda (from bind()) hasn’t yet been called, so the “return” value isn’t there yet. Adjust your code to be async and continue inside your lambda, once that gets called.

なお、同期的に処理したいJavaScriptコードを実行する目的であれば、juce::JavascriptEngine, choc::javascript を使用するという解決方法があります。

namespace juce
{
...
class JUCE_API  JavascriptEngine  final
{
public:
    /** Creates an instance of the engine.
        This creates a root namespace and defines some basic Object, String, Array
        and Math library methods.
    */
    JavascriptEngine();

    /** Destructor. */
    ~JavascriptEngine();

    /** Attempts to parse and run a block of javascript code.
        If there's a parse or execution error, the error description is returned in
        the result.
        You can specify a maximum time for which the program is allowed to run, and
        it'll return with an error message if this time is exceeded.
    */
    Result execute (const String& javascriptCode);

    /** Attempts to parse and run a javascript expression, and returns the result.
        If there's a syntax error, or the expression can't be evaluated, the return value
        will be var::undefined(). The errorMessage parameter gives you a way to find out
        any parsing errors.
        You can specify a maximum time for which the program is allowed to run, and
        it'll return with an error message if this time is exceeded.
    */
    var evaluate (const String& javascriptCode,
                  Result* errorMessage = nullptr);
...
namespace choc::javascript
{
...
class Context
    {
    public:
        /// To create a Context, use a function such as choc::javascript::createQuickJSContext();
        Context() = default;
        Context (Context&&);
        Context& operator= (Context&&);
        ~Context();

        /// When parsing modules, this function is expected to take a path to a module, and
        /// to return the content of that module, or an empty optional if not found.
        using ReadModuleContentFn = std::function<std::optional<std::string>(std::string_view)>;

        //==============================================================================
        /// Evaluates the given chunk of javascript.
        /// If there are any parse errors, this will throw a choc::javascript::Error exception.
        /// If the engine supports modules, then providing a value for the resolveModuleContent
        /// function will treat the code as a module and will call your function to read the
        /// content of any dependencies.
        /// None of the methods in this class are either thread-safe or realtime-safe, so you'll
        /// need to organise your own locking if you're calling into a single Context from
        /// multiple threads.
        choc::value::Value evaluate (const std::string& javascriptCode,
                                     ReadModuleContentFn* resolveModuleContent = nullptr);
...

上記のJavaScriptエンジンを用いてJavaScriptコードを実行する場合と、WebView上でJavaScriptコードを実行する場合との違いは、WebViewで実行できるAPIがJavaScriptエンジンの方には実装されておらず、実行できないAPI呼び出しが存在します。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0