本記事はJUCE Advent Calendar 2023 の12月16日向けに投稿した記事です。
要約
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では、単一のHTMLファイルをC++のバイナリデータとして組み込み、プロジェクトにシームレスに統合します。コントローラーはHTMLのinput要素を使用し、背景の描画にはp5.jsが、HTML要素のアニメーションにはCSSアニメーションが利用され、GUIが構築されます。
DenoGain
DenoGainでは、Vite + Deno + Svelte + TypeScriptといったWebフロントエンド開発で用いられる技術スタックを使用します。ビルドシステムによって生成された配布用のWebリソースを描画することと、開発用のサーバーからWebリソースを取得することもできることを確認しています。
このプログラムでは、Webリソースの提供はCHOCのAPIを用いた簡易なWebサーバー相当の関数を実装し、WebViewからのFetch呼び出しに応じてリソースをストリームする手法でWebViewに提供します。
準備:プロジェクトの作成とライブラリの導入
JUCEを導入する
まずはJUCEを導入しましょう。
本記事では、JUCEのコンセプトや具体的な手順については割愛しますが、以下の記事を参考に開発環境に導入をしましょう。
JUCEはGitHubからクローンすることができます
git clone https://github.com/juce-framework/JUCE.git
JUCEプロジェクトを作成する
JUCEプロジェクトを作成する具体的な手順については本記事では割愛します。
CHOCを導入する
CHOCはGitHubからクローンすることができます。
git clone https://github.com/Tracktion/choc.git
CHOCはヘッダーオンリーなC++ライブラリです。プロジェクトからインクルードするだけで使用することが可能です。
プロジェクトへのインクルード方法は各ビルドシステム、IDEの説明に従って導入してください。本記事で参照しているaudio-plugin-web-ui
リポジトリでは、CMakeを用いてプロジェクトにインクルードしています。
本編:WebViewでオーディオプラグインのGUIを作る手順
ここからは、WebGainのサンプルコードを用いながら、JUCEとCHOCを組み合わせてオーディオプラグインのGUIをWeb技術で作るまでの手順について解説します。
1. CHOCのWebViewをJUCEコンポーネントに統合する
WebViewとC++プログラムとを統合するために、CHOCのWebViewから取得したネイティブハンドルを、JUCEコンポーネントに統合します。この統合は、WebベースのGUIとC++ロジックとの双方向の通信の基盤となります。
■ ソースコード
ネイティブハンドルをJUCEコンポーネントに統合する仕組みや背景となる技術については、筆者が過去に作成した記事を参照ください。こちらの記事では、ゲームエンジンUnityを始めとした他のGUI付プログラムを対象に、JUCEで作成したC++プログラムのウインドウ内に取り込む仕組みについて解説しています。
a. WebViewの生成と可視化
OSネイテイブのウインドウをJUCEコンポーネントに統合するクラスは、JUCEのAPIにOS毎のクラスが用意されています。
- Windows向け
juce::HWNDComponent
- macOS向け
juce::NSViewComponent
- Linux(X11)向け
juce::XEmbedComponent
JUCEコンポーネントを可視化するにはaddAndMakeVisible
を呼ぶことでコンポーネントの追加と可視化を同時に行うことができます。
AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor& p)
: AudioProcessorEditor (&p)
, processorRef (p)
, valueTreeState (p.getAPVTS())
{
~省略~
// WebViewの実行時オプションを生成する
choc::ui::WebView::Options options;
options.enableDebugMode = false;
// CHOCのAPIからWebViewオブジェクトを生成する.引数には実行時オプションを渡す
chocWebView = std::make_unique<choc::ui::WebView>(options);
// choc::ui::WebView::getViewHandle() 関数からWebViewのネイティブハンドルを取得する
// WebViewのネイティブハンドルをjuce::Component配下でハンドリングできるようにセットする
#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
~省略~
}
b. WebViewを配置する
加えて、resized
関数の中で、WebViewの配置やサイズの設定を行います。
~省略~
// ウィンドウのサイズが変更されたときに呼び出される
void AudioPluginAudioProcessorEditor::resized()
{
// エディタ内の要素の配置やサイズの調整を行います。
auto rect_ui = getLocalBounds();
~省略~
// WebViewの配置(座標とサイズの適用)を実行する。
#if JUCE_WINDOWS
juceHwndView->setBounds(getLocalBounds());
#elif JUCE_MAC
juceNsView->setBounds(getLocalBounds());
#elif JUCE_LINUX
juceXEmbedView->setBounds(getLocalBounds());
#endif
}
2. C++とJavaScriptの関数をバインディングする(C++側)
ここでは、C++とWebView内のJavaScriptを双方向で連携するために関数をバインディングする手順について解説します。
以下に、コードの主な部分について解説をします。
a. WebViewの生成:
chocWebView = std::make_unique<choc::ui::WebView>(options);
CHOCのAPIを使用して、WebViewオブジェクトを生成しています。このWebViewは、C++とJavaScriptの関数を相互に呼び出すことを可能にする仕組みを備えています。
b. JavaScriptからのコールバック処理:
auto web_view_callback_on_toggle_changed = [...] { /* ... */ };
auto web_view_callback_on_sliider_changed = [...] { /* ... */ };
auto web_view_callback_on_initial_update = [...] { /* ... */ };
JavaScriptからの特定のイベントに対するコールバック関数を定義しています。これらの関数は、WebViewオブジェクトのbind
関数を使用して、JavaScriptの関数とバインドされます。各関数は、引数としてCHOCのValueView型を受け取り、それを解析・変換することでC++側で持つオーディオパラメータやロジック制御変数の状態を更新します。
c. JSONの変換:
const auto choc_json_string = choc::json::toString(args);
const auto juce_json = juce::JSON::parse(choc_json_string);
JavaScriptからのデータはJSON形式で渡されます。これをCHOCのAPIを使用して文字列に変換し、それをJUCEのjuce::JSON::parse
関数を使ってJUCEのjuce::var
型に変換しています。
なお、JSON形式のデータをパースする手法は様々なライブラリが提供しているので、JUCE以外のJSONパーサーを使用することも可能です。
d. パラメータの更新:
safe_this->valueTreeState.getParameter("invertPhase")->setValueNotifyingHost((bool)juce_json[0]["toggleValue"]);
safe_this->valueTreeState.getParameter("gain")->setValueNotifyingHost(normalised_value);
パラメータの更新は、JUCE APIのjuce::AudioProcessorValueTreeState
を使用しています。これにより、オーディオプラグインのパラメータがホストアプリケーションに通知されます。
e. WebViewとの関数バインディング:
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);
各コールバック関数を、対応するJavaScript関数名とバインドしています。これにより、WebView上のJavaScriptからこれらの関数が呼び出されると、C++の関数が実行されます。これにより、C++とJavaScriptの間でデータの送受信やイベントトリガーを相互に連携することができます。
f. C++内のパラメータ変更をWebViewのJavaScriptに通知する関数:
AudioPluginAudioProcessorEditor
は、juce::AudioProcessorValueTreeState::Listener
を継承することで、C++内で行われるパラメータの変更に対するイベントハンドルを実装することができます。(本記事ではその継承についての手続きについては割愛するので、ソースコードを参照ください)
C++内でパラメータが変更されたときに呼び出されるparameterChanged
関数の中に、そのパラメータ変更情報をJSON形式に変換してJavaScript側に通知します。
以下はコードの解説です:
f-1. パラメータの変更検知:
if (parameterID == "gain")
{
// "gain"パラメータの変更を検知
}
else if (parameterID == "invertPhase")
{
// "invertPhase"パラメータの変更を検知
}
パラメータIDが"gain"または"invertPhase"であるかどうかを確認し、それぞれの条件ブロックに進みます。これにより、どのパラメータが変更されたのかが判断されます。
f-2. JSONオブジェクトの構築:
juce::DynamicObject::Ptr json = new juce::DynamicObject();
json->setProperty("parameterName", "gain");
json->setProperty("parameterValue", newValue);
JUCE APIを利用して、JSONオブジェクトを作成することができます。
ここでは、パラメータの変更情報を持つJSONオブジェクトが構築されています。"parameterName"にはパラメータの名前("gain"または"invertPhase")、"parameterValue"には新しい値が設定されます。
f-3. JSON文字列への変換:
const auto js_args_json = juce::JSON::toString(json.get());
juce::JSON::toString
メソッドを使用して、構築したJSONオブジェクトをJavaScript実行コードに埋め込むために、JSON形式の文字列に変換しています。
f-4. JavaScriptの関数をC++から呼び出す:
juce::String javascript = juce::String("onParameterChanged(") + js_args_json + juce::String(")");
this->chocWebView->evaluateJavascript(javascript.toStdString());
JavaScript側のonParameterChanged
関数を呼び出しています。この時、変更されたパラメータの情報が含まれたJSON文字列を関数の引数としてonParameterChanged
関数に渡しています。これは、後述するJavaScriptコード内の関数であり、C++からの呼び出しに対応しています。また、evaluateJavascript
メソッドを使用して、WebView上でJavaScriptコードを実行しています。
バインディング処理に関するC++側のソースコード(抜粋)
AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor& p)
: AudioProcessorEditor (&p)
, processorRef (p)
, valueTreeState (p.getAPVTS())
{
~省略~
// CHOCのAPIからWebViewオブジェクトを生成する
chocWebView = std::make_unique<choc::ui::WebView>(options);
~省略~
// JavaScriptから受け取ったJSONの値を取り出して、invertPhaseパラメータの値を更新する関数
auto web_view_callback_on_toggle_changed =
[safe_this = juce::Component::SafePointer(this)](const choc::value::ValueView& args)
-> choc::value::Value {
~省略~
// CHOC API: JavaScriptから受け取ったJSONを文字列に変換する
const auto choc_json_string = choc::json::toString(args);
// JUCE API: JSON文字列からJUCEで読み取り可能なjuce::var型に変換する
const auto juce_json = juce::JSON::parse(choc_json_string);
safe_this->valueTreeState.getParameter("invertPhase")->setValueNotifyingHost((bool)juce_json[0]["toggleValue"]);
return choc::value::Value(0);
};
// JavaScriptから受け取ったJSONの値を取り出して、gainパラメータの値を更新する関数
auto web_view_callback_on_sliider_changed =
[safe_this = juce::Component::SafePointer(this)](const choc::value::ValueView& args)
-> choc::value::Value {
~省略~
// CHOC API: JavaScriptから受け取ったJSONを文字列に変換する
const auto choc_json_string = choc::json::toString(args);
// JUCE API: JSON文字列からJUCEで読み取り可能なjuce::var型に変換する
const auto normalised_value = juce::jmap<float>(
(float)juce_json[0]["sliderValue"],
(float)juce_json[0]["sliderRangeMin"], (float)juce_json[0]["sliderRangeMax"],
0.0f, 1.0f);
// 0.0~1.0に正規化した値をプラグインパラメータに渡す
// パラメータ値のレンジマッピング処理の詳細は、C++,GUIの実装に依存します
safe_this->valueTreeState.getParameter("gain")->setValueNotifyingHost(normalised_value);
return choc::value::Value(0);
};
// WebViewにページが表示されたことをトリガーとしてパラメータの初期状態の同期をトリガーする関数
auto web_view_callback_on_initial_update =
[safe_this = juce::Component::SafePointer(this)](const choc::value::ValueView& args)
-> choc::value::Value {
~省略~
return choc::value::Value(0);
};
// WebView内のJavaScriptから呼ぶことができる関数を宣言して、C++側の関数とバインドする
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);
~省略~
}
~省略~
// C++内でパラメータが変更されたときに呼び出される`parameterChanged`関数
void AudioPluginAudioProcessorEditor::parameterChanged(const juce::String& parameterID, float newValue)
{
if (parameterID == "gain")
{
// gainパラメータが変更されたことを通知するJSONデータを生成する
juce::DynamicObject::Ptr json = new juce::DynamicObject();
json->setProperty("parameterName", "gain");
json->setProperty("parameterValue", newValue);
const auto js_args_json = juce::JSON::toString(json.get());
// WebView側のJavaScriptの関数を呼び出すJavaScriptコードを作成する
juce::String javascript = juce::String("onParameterChanged(") + js_args_json + juce::String(")");
// 生成したJavaScriptコードをWebView側に渡して関数の実行をリクエストする
this->chocWebView->evaluateJavascript(javascript.toStdString());
}
else if (parameterID == "invertPhase")
{
juce::DynamicObject::Ptr json = new juce::DynamicObject();
json->setProperty("parameterName", "invertPhase");
json->setProperty("parameterValue", newValue);
const auto js_args_json = juce::JSON::toString(json.get());
juce::String javascript = juce::String("onParameterChanged(") + js_args_json + juce::String(")");
this->chocWebView->evaluateJavascript(javascript.toStdString());
}
}
3. C++とJavaScriptの関数をバインディングする(JavaScript側)
以下は、JavaScriptおよびHTMLコードについて、WebGainの実装を例としてサンプルコードを解説します。このサンプルコードでは、JavaScriptおよびHTMLで書かれたWebViewを示しています。
以下に、コードの主な部分について解説をします。
a. HTML構造:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGain GUI</title>
<style>
/* スタイルシートを記述します */
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
- HTML文書の基本構造が定義されています。
- メタ情報やスタイルシートのリンク、p5.jsライブラリの読み込みが行われています。
b. HTMLコンテンツ:
<div id="content">
<h1>WebGain</h1>
<p>Gain [<span id="gainValueLabel">50</span>]</p>
<input type="range" min="0" max="100" value="50" id="gainSlider">
<p>Invert Phase [<span id="invertPhaseValueLabel"></span>]</p>
<input type="checkbox" id="toggleInvertPhase">
</div>
- WebGain GUIの要素がHTML内に配置されています。
- ゲイン調整用のスライダーとインバートフェーズのトグルボタンなどがあります。
c. JavaScriptコード:
<script>
// JavaScriptコードが含まれています
</script>
- JavaScriptコードが記述されています。この部分では、ユーティリティ関数やイベントリスナー、C++から呼び出されるコールバック関数が定義されています。
e. Utility関数:
// Utility functions.
function mapRange(value, fromMin, fromMax, toMin, toMax) {
const clampedValue = Math.min(Math.max(value, fromMin), fromMax);
const normalizedValue = (clampedValue - fromMin) / (fromMax - fromMin);
const mappedValue = normalizedValue * (toMax - toMin) + toMin;
return mappedValue;
}
-
mapRange
関数は、値の範囲を別の範囲にマッピングするためのユーティリティ関数です。
f. イベントリスナー:
// HTML要素側で`input`イベントが発生した際に呼ばれるJavaScript内のイベント・コールバック
gainSlider.addEventListener('input', function() {
// ゲインスライダーの値が変更された際の処理
});
// HTML要素側で`change`イベントが発生した際に呼ばれるJavaScript内のイベント・コールバック
toggleInvertPhase.addEventListener('change', function() {
// インバートフェーズのトグルが変更された際の処理
});
- GainスライダーとInvertPhaseトグルに対するイベントリスナーが設定されています。
- スライダーの変更やトグルの変更に対する処理が記述されています。
g. C++からのコールバック関数:
// C++側でパラメータ変更が発生した際に呼ばれるC++からのイベント・コールバック
function onParameterChanged(jsonData){
// C++から呼び出されるコールバック関数
}
- C++から呼び出されるコールバック関数が定義されています。
- C++側で発生したイベントに対する処理がここで行われます。
h. C++からの初期化コール:
// 初期更新を要求する C++ コールバックを呼び出します。
onInitialUpdate();
- ページロード時にC++から初期化処理をリクエストするための関数が呼び出されています。
WebView(HTML,JavaScript)側のソースコード(抜粋)
このHTMLおよびJavaScriptコードは、C++と連携してWebベースのGUIを構築しています。各要素やイベントに対する処理は、JavaScriptコード内で定義され、C++とのデータの受け渡しやコールバックを行っています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGain GUI</title>
<style>
~スタイルシートを記述する~
</style>
<!-- Include p5.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
<!-- p5.js canvas container -->
<script>
~p5.jsの実装コードのため省略~
</script>
<!-- Your HTML content here -->
<div id="content">
// HTML要素を記述する
<h1>WebGain</h1>
// Gainパラメータ操作用スライダー
<p>Gain [<span id="gainValueLabel">50</span>]</p>
<input type="range" min="0" max="100" value="50" id="gainSlider">
// InvertPhaseパラメータ操作用トグル
<p>Invert Phase [<span id="invertPhaseValueLabel"></span>]</p>
<input type="checkbox" id="toggleInvertPhase">
</div>
<!-- Your Script content here -->
<script>
// Utility functions.
function mapRange(value, fromMin, fromMax, toMin, toMax) {
const clampedValue = Math.min(Math.max(value, fromMin), fromMax);
const normalizedValue = (clampedValue - fromMin) / (fromMax - fromMin);
const mappedValue = normalizedValue * (toMax - toMin) + toMin;
return mappedValue;
}
// ボタンとスライダーのイベントを処理するJavaScriptコード
const gainSlider = document.getElementById('gainSlider');
const gainValueLabel = document.getElementById('gainValueLabel');
const toggleInvertPhase = document.getElementById('toggleInvertPhase');
const invertPhaseValueLabel = document.getElementById('invertPhaseValueLabel');
// HTML要素側で`input`イベントが発生した際に呼ばれるJavaScript内のイベント・コールバック
gainSlider.addEventListener('input', function() {
const jsonData = {
sliderName: gainSlider.id,
sliderValue: gainSlider.value,
sliderRangeMin: gainSlider.min,
sliderRangeMax: gainSlider.max
};
gainValueLabel.innerText = gainSlider.value;
// C++からバインドした関数を呼び、C++内でパラメータ更新を実行する
onSliderChanged(jsonData)
});
// HTML要素側で`change`イベントが発生した際に呼ばれるJavaScript内のイベント・コールバック
toggleInvertPhase.addEventListener('change', function() {
const jsonData = {
toggleName: toggleInvertPhase.id,
toggleValue: toggleInvertPhase.checked,
};
if(toggleInvertPhase.checked){
invertPhaseValueLabel.innerText = "ON";
}
else{
invertPhaseValueLabel.innerText = "OFF";
}
// C++からバインドした関数を呼び、C++内でパラメータ更新を実行する
onToggleChanged(jsonData)
});
// C++側でパラメータ変更が発生した際に呼ばれるC++からのイベント・コールバック
function onParameterChanged(jsonData){
// JSONデータの'parameterName'プロパティにアクセスする。
const parameterName = jsonData.parameterName;
// HTML要素を更新する
if(parameterName === "gain")
{
const mapped_value = mapRange(jsonData.parameterValue, 0.0, 1.0, gainSlider.min, gainSlider.max)
gainSlider.value = mapped_value;
gainValueLabel.innerText = gainSlider.value;
}
else if(parameterName === "invertPhase")
{
toggleInvertPhase.checked = jsonData.parameterValue;
if(toggleInvertPhase.checked){
invertPhaseValueLabel.innerText = "ON";
}
else{
invertPhaseValueLabel.innerText = "OFF";
}
}
}
// 初期更新を要求する C++ コールバックを呼び出します。
onInitialUpdate();
</script>
</body>
</html>
4. WebリソースをWebViewに提供する
WebViewはWebリソースを描画するコンポーネントなので、Webリソースを提供する手段を何らかの手段で用意する必要があります。本記事で扱っているWebGain、DenoGainでは、それぞれ異なる手法でWebViewにWebリソースを提供しています。
- WebGainの場合:HTMLファイルをバイナリ化してプログラムに同梱する手法
- DenoGainの場合:CHOCのAPIを用いた簡易なWebサーバー相当の関数を実装し、WebViewからのFetch呼び出しに応じてWebリソースをストリームする手法
ここでは、それぞれの手法について解説します。
a. HTMLファイルをバイナリ化してプログラムに同梱する手法
JUCEには開発ツールの一部として、任意のデータをバイナリデータ化してプログラムにリンクする手段を持っています。
WebGainでは、このバイナリデータ生成機能を利用して、HTML, CSS, JavaScriptが記述された1つのファイルをバイナリ化してプログラムにリンクします。1つのファイルにHTML, CSS, JavaScriptのコードをまとめることの難しさはありますが、これが最も簡単な方法と言えるでしょう。
JUCEの開発フローやビルドシステムの選択によってバイナリデータを生成してリンクするまでの手順が異なります。
Projucerでバイナリデータを作成してリンクする場合
Projucerを用いてプロジェクトを構成する場合は、Projucerにバイナリ化したいファイルをドラッグアンドドロップして、バイナリ化のオプションを有効にすることでバイナリデータに変換してプログラムにリンクすることができます。
CMakeでバイナリデータを作成してリンクする場合
CMakeを用いてプロジェクトを構成する場合は、CMakeLists.txtにバイナリ化したいファイルの指定と、生成したバイナリデータをプログラムにリンクする命令を記述します。
juce_add_binary_data("${BaseTargetName}_WebViewBundle"
HEADER_NAME "WebViewBundleData.h"
NAMESPACE "WebView"
SOURCES
WebView/view.html
)
# Need to build BinaryData with -fPIC flag on Linux
set_target_properties("${BaseTargetName}_WebViewBundle"
PROPERTIES
POSITION_INDEPENDENT_CODE TRUE)
~省略~
target_link_libraries(${BaseTargetName}
PRIVATE
"${BaseTargetName}_WebViewBundle"
juce::juce_gui_extra
juce::juce_audio_utils
juce::juce_audio_devices
PUBLIC
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags)
C++側でバイナリデータをロードする
JUCEのBinaryBuilderで作成したバイナリデータは、C++ヘッダーファイルを介してアクセスすることができます。実態としてはバイナリ列の塊なので、バイナリデータをHTMLファイルを文字列として取得し、HTMLファイルの文字列をWebViewに読み込ませることで、WebViewにWebリソースを渡すことができます。
~省略~
#include "WebViewBundleData.h"
AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor& p)
: AudioProcessorEditor (&p)
, processorRef (p)
, valueTreeState (p.getAPVTS())
{
~省略~
// リンクしたバイナリデータをHTMLファイルを文字列として取得する
const auto html = juce::String::createStringFromData(WebView::view_html, WebView::view_htmlSize);
// HTMLファイルの文字列をWebViewに読み込ませる
chocWebView->setHTML(html.toStdString());
}
b. C++側に簡易的なWebサーバーに相当する関数を実装する手法
CHOCには、 C++のコールバック関数からブラウザにリソースを提供するためのAPIが予め用意されており、このAPIをアプリケーション開発者が実装することで、簡易的なWebサーバーの機能に相当するリソース提供APIをC++プログラム内に組み込むことができます。
DenoGainでは、このリソースサーバー機能を利用して、HTML, CSS, JavaScriptを始め、SVG画像等の様々なWebリソースをWebViewに渡すことができます。
この関数の実装についての要点は次の通りです。
- この関数が初めて呼び出されると、ルートパス("/")で初期コンテンツを提供するために使用される。この時、クライアントは「text/html」MIMEタイプのHTMLを返す必要があります
- リソースに応じたMIMEタイプの情報を実装するリソースサーバー側で記述する必要がある
- ルートパス("/")として用いるファイルパスをシステムのパスに設定することができるので、ファイルシステムのディレクトリツリーをそのまま使用することもできる
以下に、具体的なソースコードを示します。
~省略~
namespace
{
// ファイル拡張子に応じてWebView側に返すMIMEタイプの文字列を定義する
std::string getMimeType(std::string const& ext)
{
static std::unordered_map<std::string, std::string> mimeTypes{
{ ".html", "text/html" },
{ ".js", "application/javascript" },
{ ".css", "text/css" },
{ ".json", "application/json"},
{ ".svg", "image/svg+xml"},
{ ".svgz", "image/svg+xml"},
};
if (mimeTypes.count(ext) > 0)
{
return mimeTypes.at(ext);
}
return "application/octet-stream";
}
~省略~
}
AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor& p)
: AudioProcessorEditor (&p)
, processorRef (p)
, valueTreeState (p.getAPVTS())
{
~省略~
// WebViewの実行時オプションを生成する
choc::ui::WebView::Options options;
options.enableDebugMode = false;
// ルートパス("/")として用いるファイルパスを設定する。ここでは、プログラム・プラグインのバイナリ本体のパスと同一ディレクトリにある("WebView")フォルダをルートパス("/")にする
auto asset_directory = juce::File::getSpecialLocation(juce::File::SpecialLocationType::currentExecutableFile).getSiblingFile("WebView");
// WebViewの実行時オプションの1つとして、WebView側の`fetch`要求に対応するコールバック関数を実装する
options.fetchResource = [this, assetDirectory = asset_directory](const choc::ui::WebView::Options::Path& path)
-> std::optional<choc::ui::WebView::Options::Resource> {
// WebViewからルートパス("/")のリクエストを受けた場合、"/index.html"をWebViewに提供する
auto relative_path = "." + (path == "/" ? "/index.html" : path);
auto file_to_read = assetDirectory.getChildFile(relative_path);
// WebViewに提供するリソースはバイナリデータとして読み込む
juce::MemoryBlock memory_block;
if (!file_to_read.existsAsFile() || !file_to_read.loadFileAsData(memory_block))
return {};
// WebViewにWebリソース(バイナリデータ+MIMEタイプ)を返す
return choc::ui::WebView::Options::Resource {
std::vector<uint8_t>(memory_block.begin(), memory_block.end()),
getMimeType(file_to_read.getFileExtension().toStdString())
};
};
// CHOCのAPIからWebViewオブジェクトを生成する.引数には実行時オプションを渡す
chocWebView = std::make_unique<choc::ui::WebView>(options);
}
まとめ
本記事では、JUCEとCHOCを組み合わせることで、オーディオプラグインのGUIをWeb技術で制作する手法について紹介しました。WebViewの導入により、C++のプロジェクトでのGUI開発がより柔軟で効率的になる可能性があります。WebView特有の開発の難しさはあるとは思いますが、JUCEでGUIを開発することも難しい課題と遭遇することが経験上多いため、開発体験が改善される可能性があるのであれば、筆者としては積極的に触れていこうと考えました。