UnityのWebビルドでVST3プラグインのGUIを構築する

# 本記事の内容で作れるもの

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">昼休憩で作ったもの。<br>VST3プラグインのGUIをUnityで実装する。<br>Unity UIのスライダーでゲイン調整をしている。<a href="https://twitter.com/hashtag/JUCE?src=hash&amp;ref_src=twsrc%5Etfw">#JUCE</a> <a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://twitter.com/hashtag/VST?src=hash&amp;ref_src=twsrc%5Etfw">#VST</a> <a href="https://t.co/6OYVPYGJ79">pic.twitter.com/6OYVPYGJ79</a></p>&mdash; COx2 ))@COCOTONE, Inc. (@CO_CO_) <a href="https://twitter.com/CO_CO_/status/1864882685270986934?ref_src=twsrc%5Etfw">December 6, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 本記事で扱うサンプルプログラム

https://github.com/COx2/audio-plugin-unity-web-ui

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/dfa92604-10ee-f618-284c-32870fd1bbdf.png)

## はじめに

本記事では、JUCEフレームワークで作成したVST3プラグインのユーザーインターフェースを、Unity Web プラットフォーム向けビルドを使用して実装する方法について解説します。

## 本記事が対象とするバージョン

- Unity 6000.0.29f1
- JUCE 8.0.4
- Visual Studio 2022
- Xcode 15.1

## 目次

1. [用語解説] VST3, JUCE, WebView UI
2. Unity と Unity Web プラットフォームについて
3. Unity ネイティブプラグインについて 
4. 「VST3 × JUCE × Unity Web」連携の仕組みについて
5. サンプルプログラムの実装手順について

## 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

https://juce.com/blog/juce-8-feature-overview-webview-uis/

※「JUCE」についての解説は以下リンクの「JUCEハンズオン」記事をご参照ください。

https://qiita.com/COx2/items/fa1aceb5013f92457469

「WebView UI」の作成手順については本記事以前から詳しく解説をしている記事があります。これから JUCE とWebView UI に入門する方は以下の記事もご参照することをおすすめします。

■ 外部サイト：【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう！！！！【WebView UI】

https://trap.jp/post/2307/

## 2. Unity と Unity Web プラットフォームについて

Unity はクロスプラットフォームのゲームエンジンですが、Webブラウザ上では WebGL 技術や WebGPU 技術を使用してアプリケーションを作成・実行することができます。  
Unity 6 (バージョン番号: 6000.xx)において、それまでの Unity WebGL という名称から Unity Web に移行しました。WebGPUサポートの追加、WebAudioの統合など、高度なWeb機能を提供します。これにより、Webブラウザ上でインタラクティブな3Dグラフィックスやオーディオ処理を実現できます。

https://unity.com/ja/solutions/web

## 3. Unity Web のネイティブプラグインについて 

Unity Webプラットフォームでは、`.jslib`ファイルを使用してネイティブプラグインを実装することができます。このプラグインシステムは、UnityのC#コードとJavaScript間の通信を可能にし、emscriptenを使用してコンパイルされます。特に、後述する emscriptenの `__postset` 機能を活用することで、ビルド時のアセット依存を減らし、より柔軟なJavaScriptコードの管理が可能になります。

※Unity ネイティブプラグインについての解説は、以下の公式ドキュメントをご参照ください。

■ Unity 公式マニュアル

https://docs.unity3d.com/ja/2022.3/Manual/NativePlugins.html

Unity Webプラットフォームのネイティブプラグインを介して、Webページ表示時にロードされるブラウザースクリプトとの相互通信することも可能にしています。 

> Web 用のコンテンツを構築するときは、Web ページ上の他の要素とやり取りする必要があります。また、Web API を使用して Unity が現在デフォルトで公開していない機能を実装したい場合もあるかもしれません。 いずれの場合も、ブラウザーの JavaScript エンジンと直接やり取りする必要があります。Unity WebGL はこれを行うためのさまざまな方法を提供します。

■ Unity 公式マニュアル

https://docs.unity3d.com/ja/2021.3/Manual/webgl-interactingwithbrowserscripting.html

## 4. 「VST3 × JUCE × Unity Web」連携の仕組みについて

本記事で参照する「VST3 × JUCE × Unity Web」連携のシステムは以下のコンポーネントで構成されています。
このうち、Unity プロジェクトおよび Web ブラウザ向け Javascript バインディングの設計については、以下の記事を参考にさせていただきました。ありがとうございます。
特に、`emscripten` に含まれる `__postset` の仕様の知識は、JUCEとの実行時バインディングにおいてとても助かりました。

■ [Unity]WebGLのプラグインをモダンに書く方法

https://qiita.com/yaegaki/items/71cdebc7798784aa3464

```mermaid
graph TB
    subgraph UnityProject["Unityプロジェクト"]
        csharp["C# Scripts<br/>DllImport定義"]
        jslib["plugin.jslib<br/>binding method 宣言<br/>emscripten::__postset 実行"]
    end
        
    csharp --> |"DllImport"| jslib
```
```mermaid
graph TB
    subgraph Native["VST3プラグイン"]
        subgraph Browser["WebView UI"]
            unity_web["Unity Webビルド<br/>UI Components"]
            jslib_p["plugin.jslib<br/>binding method 宣言<br/>emscripten::__postset 実行"]
            libjs["unity-binding-lib.js"]
            mainjs["juce-binding-impl.js"]
            jucejs["juce-framework-frontend.js"]
        end
        vst3["JUCE C++<br/>WebView<br/>VST3 API"]
    end

    subgraph DAW["DAW（VST3ホスト）"]
        daw["VST3 API"]
    end
    
    jslib_p <--> |"DllImport"| unity_web
    libjs <--> |"postset"| jslib_p
    libjs <--> |"関数呼び出し"| mainjs
    mainjs <--> |"パラメータ操作"| jucejs
    jucejs <--> |"バインディング"| vst3
    vst3 <--> |"プラグインロード"| daw
```


## 5. サンプルプログラムの実装手順について

### a. Unity - JavaScript バインディングを組み込む

バインディングレイヤーの実装は以下の記事を参考にさせていただきました。`emscripten` に含まれる `__postset` の仕様を利用することで、Unity WebビルドとJUCEとのバインディングコードを記述するJavascriptを分離することができます。

■ [Unity]WebGLのプラグインをモダンに書く方法

https://qiita.com/yaegaki/items/71cdebc7798784aa3464

```javascript
// 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ファイルの実装

```javascript
// 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# コード

```csharp
// 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 コード

```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オブジェクトをシーン内に配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/c363930c-9e0e-58a5-dde2-a42ad8472f69.png)

本記事で扱うサンプルプログラムでは、スライダーの値を `0.0~1.0` の範囲に収めています。
これらの値の範囲（Range）は、C++側のパラメータの値の範囲の取り扱い方によって具体的な実装が変わる点に注意します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/cc0ed644-79ad-1092-6479-8f3bc3603652.png)

#### スクリプトとコンポーネントのアタッチ

本記事で扱うサンプルプログラムでは、`gain`, `invertPhase` の2個のパラメータを扱います。
サンプルプログラム内のパラメータ識別子に沿って、UIコンポーネントとパラメータIDを紐付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/36394ba6-966a-2f11-99e1-db8e97da5162.png)

#### Player Settingについて

本記事で扱うサンプルプログラムでは、`Player Setting` を以下のように設定しています。

■ `WebGL Template` は **PWA** を選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/a5f5071c-8479-9da9-7a13-e8d08b8a647d.png)

■ `Publishing Settings` の `Compression Format` は **Disabled** を選択する 

サンプルプログラムを作成している際に、`Compression Format`を`Brotli`または`Gzip`を選択した場合に、WebView に圧縮ファイルを正しく渡すことができない事象に遭遇したため、**Disabled** を選択しているという事情があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/33277d62-5e13-52f7-4d44-cb4874c1cba7.png)

#### Web プラットフォーム向けにビルドを実行する

上記で作成したシーンを追加して Web プラットフォーム向けにビルドを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/0bab8345-3d36-4e41-271f-03bbd5b36406.png)

### c. JUCE プロジェクトに Unity Web を組み込む

ここからは、JUCE 8 からサポートされた WebView UI の組み込み手順に従います。  
詳細については以下の記事で詳しく書かれているため、本記事では解説を割愛します。
詳細について知りたい場合は、以下リンク先の記事をご参照ください。

■ JUCE 8 Feature Overview: WebView UIs

https://juce.com/blog/juce-8-feature-overview-webview-uis/

■ 外部サイト：【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう！！！！【WebView UI】

https://trap.jp/post/2307/

### 表示用 Web ページに JavaScript コードの読み込みを追加する

以下のソースコードは、Unity Webビルドで出力時に生成されるHTMLファイル `index.html` に、改変を加えたものになります。

特に、以下のコメントを記載した箇所についてコードを追加します。
```html
    <!-- 以下のJavaScriptコードを挿入する -->
    <script src="unity-binding-lib.js"></script> <!-- Unity - JavaScript バインディング -->
    ...
    <!-- ここまでのJavaScriptコードを挿入する -->
```

■ index.html

```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 に提供する手順については、以下の記事と同様の手順となっています。

https://qiita.com/COx2/items/430086a1a6f87fddbfb5#4-web%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%92webview%E3%81%AB%E6%8F%90%E4%BE%9B%E3%81%99%E3%82%8B

```cmake
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 に提供する
```cpp
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を追加する

```cpp
    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が表示されます。

![スクリーンショット 2024-12-09 012621.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93566/959720e8-da96-7c13-ee47-dbec25b6744f.png)


## サンプルプログラムの課題

本記事執筆時点において、サンプルプログラムは以下の点で改善すべき項目が残っています。残念ながら、時間の都合上以下の内容については割愛しました。

- C++側でプラグインパラメータの値が変更された際には、Unity側にC++からコールバックで通知をする方が良い 


## 参考リンク

- [Unity WebGL Interacting with browser scripting](https://docs.unity3d.com/ja/2020.3/Manual/webgl-interactingwithbrowserscripting.html)
- [Emscripten documentation](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html)
- [[Unity]WebGLのプラグインをモダンに書く方法](https://qiita.com/yaegaki/items/71cdebc7798784aa3464)
- [JUCE 8 Feature Overview: WebView UIs](https://juce.com/blog/juce-8-feature-overview-webview-uis/)