本記事の内容で作れるもの
昼休憩で作ったもの。
— COx2 ))@COCOTONE, Inc. (@CO_CO_) December 6, 2024
VST3プラグインのGUIをUnityで実装する。
Unity UIのスライダーでゲイン調整をしている。#JUCE #Unity #VST pic.twitter.com/6OYVPYGJ79
本記事で扱うサンプルプログラム
はじめに
本記事では、JUCEフレームワークで作成したVST3プラグインのユーザーインターフェースを、Unity Web プラットフォーム向けビルドを使用して実装する方法について解説します。
本記事が対象とするバージョン
- Unity 6000.0.29f1
- JUCE 8.0.4
- Visual Studio 2022
- Xcode 15.1
目次
- [用語解説] VST3, JUCE, WebView UI
- Unity と Unity Web プラットフォームについて
- Unity ネイティブプラグインについて
- 「VST3 × JUCE × Unity Web」連携の仕組みについて
- サンプルプログラムの実装手順について
1. [用語解説] VST3, JUCE, WebView UI
VST3(Virtual Studio Technology 3)とは、DAW(Digital Audio Workstation)にオーディオエフェクトやインストゥルメント(音源)の機能を追加するためのプラグイン規格です。DAWソフトウェア上で、エフェクター(リバーブ、ディレイなど)や楽器音源として動作し、音楽制作のワークフローを拡張します。
JUCEは、このようなオーディオプラグインの開発をサポートするC++フレームワークで、JUCE 8からはプラグインのUIをWebView上に構築する機能が追加され、HTML/CSS/JavaScriptを使用したモダンなUIの実装が可能になりました。
■ JUCE 8 Feature Overview: WebView UIs
※「JUCE」についての解説は以下リンクの「JUCEハンズオン」記事をご参照ください。
「WebView UI」の作成手順については本記事以前から詳しく解説をしている記事があります。これから JUCE とWebView UI に入門する方は以下の記事もご参照することをおすすめします。
■ 外部サイト:【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう!!!!【WebView UI】
2. Unity と Unity Web プラットフォームについて
Unity はクロスプラットフォームのゲームエンジンですが、Webブラウザ上では WebGL 技術や WebGPU 技術を使用してアプリケーションを作成・実行することができます。
Unity 6 (バージョン番号: 6000.xx)において、それまでの Unity WebGL という名称から Unity Web に移行しました。WebGPUサポートの追加、WebAudioの統合など、高度なWeb機能を提供します。これにより、Webブラウザ上でインタラクティブな3Dグラフィックスやオーディオ処理を実現できます。
3. Unity Web のネイティブプラグインについて
Unity Webプラットフォームでは、.jslib
ファイルを使用してネイティブプラグインを実装することができます。このプラグインシステムは、UnityのC#コードとJavaScript間の通信を可能にし、emscriptenを使用してコンパイルされます。特に、後述する emscriptenの __postset
機能を活用することで、ビルド時のアセット依存を減らし、より柔軟なJavaScriptコードの管理が可能になります。
※Unity ネイティブプラグインについての解説は、以下の公式ドキュメントをご参照ください。
■ Unity 公式マニュアル
Unity Webプラットフォームのネイティブプラグインを介して、Webページ表示時にロードされるブラウザースクリプトとの相互通信することも可能にしています。
Web 用のコンテンツを構築するときは、Web ページ上の他の要素とやり取りする必要があります。また、Web API を使用して Unity が現在デフォルトで公開していない機能を実装したい場合もあるかもしれません。 いずれの場合も、ブラウザーの JavaScript エンジンと直接やり取りする必要があります。Unity WebGL はこれを行うためのさまざまな方法を提供します。
■ Unity 公式マニュアル
4. 「VST3 × JUCE × Unity Web」連携の仕組みについて
本記事で参照する「VST3 × JUCE × Unity Web」連携のシステムは以下のコンポーネントで構成されています。
このうち、Unity プロジェクトおよび Web ブラウザ向け Javascript バインディングの設計については、以下の記事を参考にさせていただきました。ありがとうございます。
特に、emscripten
に含まれる __postset
の仕様の知識は、JUCEとの実行時バインディングにおいてとても助かりました。
■ [Unity]WebGLのプラグインをモダンに書く方法
5. サンプルプログラムの実装手順について
a. Unity - JavaScript バインディングを組み込む
バインディングレイヤーの実装は以下の記事を参考にさせていただきました。emscripten
に含まれる __postset
の仕様を利用することで、Unity WebビルドとJUCEとのバインディングコードを記述するJavascriptを分離することができます。
■ [Unity]WebGLのプラグインをモダンに書く方法
// unity-binding-lib.js
// グローバル変数の定義
let Module = null // Emscriptenモジュールを保持する変数
let helperFunctions = null // UTF8文字列変換などのヘルパー関数を保持する変数
const functionMap = new Map() // メソッド名と実装の対応を管理するマップ
// Unity側からJavaScriptの関数を取得するためのバインディング関数
// module: Emscriptenモジュール
// _helperFunctions: 文字列変換用のヘルパー関数群
// methodName: 呼び出すメソッド名
function __unity_getBinding(module, _helperFunctions, methodName) {
Module = module
helperFunctions = _helperFunctions
return functionMap.get(methodName) // 指定された名前の関数を返す
}
// メソッド名と実装を紐付けるための関数
// methodName: メソッド名
// func: 実装する関数
function bindFunction(methodName, func) {
functionMap.set(methodName, func)
}
.jslibファイルの実装
// plugin.jslib
// プラグインオブジェクトの定義
const plugin = {}
// バインドするJavaScriptメソッドの一覧
const methodNames = [
'js_SetParameter', // パラメータ設定用メソッド
'js_GetParameter', // パラメータ取得用メソッド
]
// UTF8文字列変換に必要なヘルパー関数の一覧
const helperFunctionNames = [
'lengthBytesUTF8', // UTF8文字列の長さを取得
'stringToUTF8', // 文字列をUTF8に変換
'UTF8ToString', // UTF8から文字列に変換
]
// ヘルパー関数をオブジェクト形式に変換
const helperFunctions = '{' + helperFunctionNames.map(x => `${x}:${x}`).join(',') + '}'
// 各メソッドについて、空の関数とpostset処理を設定
for (let i = 0; i < methodNames.length; i++) {
const methodName = methodNames[i]
plugin[methodName] = function () { } // 初期化時は空の関数を設定
// postsetでバインディング関数を設定
plugin[methodName+'__postset'] = `_${methodName} = __unity_getBinding(Module, ${helperFunctions}, '${methodName}')`
}
// プラグインをEmscriptenのライブラリに追加
mergeInto(LibraryManager.library, plugin)
Unity 側の C# コード
// AudioPluginController.cs
using System;
using System.Runtime.InteropServices;
using AOT;
using UnityEngine;
using UnityEngine.UI;
public class AudioPluginController : MonoBehaviour
{
// JavaScriptのパラメータ設定関数をインポート
[DllImport("__Internal")]
private static extern void js_SetParameter(String paramId, float value);
// JavaScriptのパラメータ取得関数をインポート
[DllImport("__Internal")]
private static extern float js_GetParameter(String paramId);
// ゲインパラメータのUI要素
public Slider gainParameterSlider;
public String gainParameterId;
// 位相反転パラメータのUI要素
public Toggle invertPhaseParameterToggle;
public String invertPhaseParameterId;
void Start()
{
// UIイベントリスナーの設定
gainParameterSlider.onValueChanged.AddListener(OnGainSliderValueChanged);
invertPhaseParameterToggle.onValueChanged.AddListener(OnInvertPhaseToggleValueChanged);
}
void Update()
{
#if !UNITY_EDITOR
// ゲインパラメータの同期
{
float value = js_GetParameter(gainParameterId);
gainParameterSlider.SetValueWithoutNotify(value);
}
// 位相反転パラメータの同期
{
float fValue = js_GetParameter(invertPhaseParameterId);
bool bValue = fValue > 0.5f ? true : false;
invertPhaseParameterToggle.SetIsOnWithoutNotify(bValue);
}
#endif
}
// ゲインスライダーの値変更時の処理
void OnGainSliderValueChanged(float value)
{
#if !UNITY_EDITOR
js_SetParameter(gainParameterId, value);
#endif
}
// 位相反転トグルの値変更時の処理
void OnInvertPhaseToggleValueChanged(bool bValue)
{
#if !UNITY_EDITOR
float value = bValue ? 1.0f : 0.0f;
js_SetParameter(invertPhaseParameterId, value);
#endif
}
}
JUCE 側の JavaScript コード
// juce-binding-impl.js
// JUCEのスライダーオブジェクトの状態とトグルオブジェクトの状態を取得
const gainSliderState = getSliderState("gain");
const invertPhaseToggleState = getToggleState("invertPhase");
// パラメータ設定関数のバインド処理
bindFunction('js_SetParameter',
(id_utf8, value) => {
// パラメータIDをUTF8文字列に変換
const id_str = helperFunctions.UTF8ToString(id_utf8);
if(id_str == "gain")
{
// ゲインパラメータの設定
gainSliderState.setNormalisedValue(value);
}
else if(id_str == "invertPhase")
{
// 位相反転パラメータの設定
let bValue = false;
if(value == 1)
{
bValue = true;
}
invertPhaseToggleState.setValue(bValue);
}
}
)
// パラメータ取得関数のバインド
bindFunction('js_GetParameter',
(id_utf8) => {
// パラメータIDをUTF8文字列に変換
const id_str = helperFunctions.UTF8ToString(id_utf8);
let value = 0.0;
if(id_str == "gain")
{
// ゲインパラメータの取得
value = gainSliderState.getNormalisedValue();
}
else if(id_str == "invertPhase")
{
// 位相反転パラメータの取得
value = invertPhaseToggleState.getValue();
}
return value;
}
)
b. Unity で GUI を作成してビルドする
GUI を Unity プロジェクトで実装していきます。なお、本記事では時間の都合上、Unity 本体の操作やプロジェクト構築に関する背景情報については触れません。詳細については、Unity 公式ドキュメントおよび各解説記事等をご参考ください。
シーンにUIオブジェクトを配置する
パラメータを操作するためのUIオブジェクトをシーン内に配置します。
本記事で扱うサンプルプログラムでは、スライダーの値を 0.0~1.0
の範囲に収めています。
これらの値の範囲(Range)は、C++側のパラメータの値の範囲の取り扱い方によって具体的な実装が変わる点に注意します。
スクリプトとコンポーネントのアタッチ
本記事で扱うサンプルプログラムでは、gain
, invertPhase
の2個のパラメータを扱います。
サンプルプログラム内のパラメータ識別子に沿って、UIコンポーネントとパラメータIDを紐付けます。
Player Settingについて
本記事で扱うサンプルプログラムでは、Player Setting
を以下のように設定しています。
■ WebGL Template
は PWA を選択する
■ Publishing Settings
の Compression Format
は Disabled を選択する
サンプルプログラムを作成している際に、Compression Format
をBrotli
またはGzip
を選択した場合に、WebView に圧縮ファイルを正しく渡すことができない事象に遭遇したため、Disabled を選択しているという事情があります。
Web プラットフォーム向けにビルドを実行する
上記で作成したシーンを追加して Web プラットフォーム向けにビルドを実行します。
c. JUCE プロジェクトに Unity Web を組み込む
ここからは、JUCE 8 からサポートされた WebView UI の組み込み手順に従います。
詳細については以下の記事で詳しく書かれているため、本記事では解説を割愛します。
詳細について知りたい場合は、以下リンク先の記事をご参照ください。
■ JUCE 8 Feature Overview: WebView UIs
■ 外部サイト:【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう!!!!【WebView UI】
表示用 Web ページに JavaScript コードの読み込みを追加する
以下のソースコードは、Unity Webビルドで出力時に生成されるHTMLファイル index.html
に、改変を加えたものになります。
特に、以下のコメントを記載した箇所についてコードを追加します。
<!-- 以下のJavaScriptコードを挿入する -->
<script src="unity-binding-lib.js"></script> <!-- Unity - JavaScript バインディング -->
...
<!-- ここまでのJavaScriptコードを挿入する -->
■ index.html
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Unity Web Player | audio-plugin-ui</title>
<link rel="shortcut icon" href="TemplateData/favicon.ico">
<link rel="stylesheet" href="TemplateData/style.css">
<link rel="manifest" href="manifest.webmanifest">
</head>
<body>
<div id="unity-container">
<canvas id="unity-canvas" width=960 height=600 tabindex="-1"></canvas>
<div id="unity-loading-bar">
<div id="unity-logo"></div>
<div id="unity-progress-bar-empty">
<div id="unity-progress-bar-full"></div>
</div>
</div>
<div id="unity-warning"> </div>
</div>
<!-- 以下のJavaScriptコードを挿入する -->
<script src="unity-binding-lib.js"></script> <!-- Unity - JavaScript バインディング -->
<script src="check_native_interop.js"></script> <!-- JavaScript - JUCE バインディング -->
<script src="juce-framework-frontend.js"></script> <!-- JavaScript - JUCE バインディング -->
<script src="juce-binding-impl.js"></script> <!-- Unity - JavaScript - JUCE バインディング -->
<!-- ここまでのJavaScriptコードを挿入する -->
<script>
window.addEventListener("load", function () {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("ServiceWorker.js");
}
});
// 中略
</script>
</body>
</html>
Webページをバイナリリソース化する
本記事のサンプルプロジェクトでは、WebView 用リソース(HTML/CSS/JavaScript)を1つの ZIP ファイルにアーカイブした後、JUCE のバイナリリソース機能を用いて C++ プログラムにリンクします。
※あくまでも、本記事で扱うプロジェクトにおいての事例であり、WebリソースをC++プログラムに提供する手段には種々の選択肢があります。
参考情報:Web リソースをバイナリ化して WebView に提供する手順については、以下の記事と同様の手順となっています。
juce_add_binary_data("${BaseTargetName}_WebViewBundle"
HEADER_NAME "WebViewBundleData.h"
NAMESPACE "WebView"
SOURCES
resources/WebViewBundle.zip
)
# 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)
Web リソースを WebView に提供する
std::optional<juce::WebBrowserComponent::Resource> WebViewBackendComponent::getResource(const juce::String& url)
{
const auto urlToRetrive = url == "/" ? juce::String{ "index.html" }
: url.fromFirstOccurrenceOf("/", false, false);
if (auto* archive = zipWebViewBundle.get())
{
if (auto* entry = archive->getEntry(urlToRetrive))
{
auto stream = juce::rawToUniquePtr(archive->createStreamForEntry(*entry));
auto v = streamToVector(*stream);
auto mime = getMimeForExtension(getExtension(entry->filename).toLowerCase());
return juce::WebBrowserComponent::Resource{
std::move(v),
std::move(mime)
};
}
}
if (urlToRetrive == "index.html")
{
auto fallbackIndexHtml = SinglePageBrowser::fallbackPageHtml;
return juce::WebBrowserComponent::Resource{
convertFromStringToByteVector(fallbackIndexHtml.toStdString()),
juce::String { "text/html" }
};
}
return std::nullopt;
}
MIME Typeを追加する
static const char* getMimeForExtension(const juce::String& extension)
{
static const std::unordered_map<juce::String, const char*> mimeMap =
{
{ { "htm" }, "text/html" },
{ { "html" }, "text/html" },
{ { "txt" }, "text/plain" },
{ { "jpg" }, "image/jpeg" },
{ { "jpeg" }, "image/jpeg" },
{ { "svg" }, "image/svg+xml" },
{ { "ico" }, "image/vnd.microsoft.icon" },
{ { "json" }, "application/json" },
{ { "png" }, "image/png" },
{ { "css" }, "text/css" },
{ { "map" }, "application/json" },
{ { "js" }, "text/javascript" },
{ { "woff2" }, "font/woff2" },
{ { "data" }, "application/octet-stream" },
{ { "wasm" }, "application/wasm" },
{ { "webmanifest" }, "text/webmanifest" },
};
if (const auto it = mimeMap.find(extension.toLowerCase()); it != mimeMap.end())
return it->second;
jassertfalse;
return "";
}
動作確認
ビルドしたVST3プラグインをDAWでロードして、GUIを表示するとUnityで構築したUIが表示されます。
サンプルプログラムの課題
本記事執筆時点において、サンプルプログラムは以下の点で改善すべき項目が残っています。残念ながら、時間の都合上以下の内容については割愛しました。
- C++側でプラグインパラメータの値が変更された際には、Unity側にC++からコールバックで通知をする方が良い