feature image

2024年8月21日 | ブログ記事

【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう!!!!【WebView UI】

この記事は夏のブログリレー2024 2日目の記事です。


更新情報


こんにちは、23Mのカシワデです。普段はオーケストラ系の曲を作ってます。
(Spotifyが表示されない場合はこちら → Spotify Kashiwade)

TwitterYouTubeもよろしくね!!!!!!

x.com
Kashiwade
オーケストラ系を中心に、サウンドトラック調の曲を作っています!耳コピが得意です! このアカウントでは自分の作った曲やアレンジした曲の動画をアップしていこうと思いますチャンネル登録よろしくお願いします!!!! コメントなかなか返せないのですが1件1件ありがたく読ませてもらっています創作の励みになり、とても助かっています!!ありがとうございます!! FANBOX( https://kashiwade.fanbox.cc )を開いています。更新がかなり少ないのですが、もしよろしければご支援よろしくお願いします! お仕事のご依頼、ご相談などあればkashiwade(at)outlook.comまでよろしくお願いします。 -----------------------------------------------------------------------ヘッダー画像は白葱さん( pixiv.me/winnie_ill )から頂きました!

さてさて、皆さまいかがお過ごしでしょうか。絶賛サマーセールなどが行われる中、DTMerの敵である円安はもうあり得んくらいめちゃめちゃのケタ違いに進み、とてもじゃないがプラグインは買えないよ~~~と発狂してる頃かと思います。

こんなことを思ったことはありませんか。

「あぁ…もしも自分が理想とするVSTプラグインを無料で自作できたら、こんなにも散財することはないのになぁ」
その思いを叶えてしまおう、というのが本記事の目的となっています!

さて

今回メインに扱うのはJUCEと呼ばれるC++のフレームワークです。
オーディオ関連のソフトウェアを開発することに特化したフレームワークで、比較的簡単にVSTプラグインやVSTプラグインホスト、普通のアプリケーションを作ることが出来ます。

非常に使いやすいフレームワークなのですが、初学者がチュートリアルを見ながら進めるには少々チュートリアルが雑だったり、情報が散ってたり、ドキュメントをみようにも説明不足のところがあったり...などと、ちょっと進めにくいなぁと思うところが何点かありました。あと圧倒的に日本語記事が少ない。英語の記事はいっぱいあるんだけども...。

というわけで今回の記事ではJUCEの基本的な使い方を浚いつつ、自力でゼロから、作ってみたいプラグインを完成に持って行けるような状態を目指せるように進めていきます。

最終的にはこんな感じのプラグイン(GainとPanがついたやつ)ができる予定です。
2024-08-21-10-23-30

完成したもののコードは下記にあります。日本語のコミットメッセージが本記事と対応しているので、適宜見比べてください。

GitHub - Kashiwade-music/juce-webview-tutorial: A simple plugin tutorial using WebView with JUCE 8.
A simple plugin tutorial using WebView with JUCE 8. - Kashiwade-music/juce-webview-tutorial

参考

この記事は過去記事のフル・リニューアル版です。

【入門】JUCEを使ってVSTプラグインを作ろう!!
> この記事は新歓ブログリレー2022 / 40日目の記事ですこんにちは、19Bのカシワデです。普段はオーケストラ系の曲を作ってます。 Twitter [https://twitter.com/Kashiwade_music]やSoundcloud[https://soundcloud.com/kashiwade]もよろしくね!!!!!! さてさて、皆さまいかがお過ごしでしょうか。近くスプリングセールやサマーセールなども行われるであろう中、DTMerの敵である円安はめちゃめちゃに進み、とてもじゃないがプラグインは買えないよ~~~と言ってる頃かと思います。 こんなことを思ったことはありませんか。 > 「あぁ…もしも自分が理想とするプラグインを無料で作れたら、こんなにも散財することはないのになぁ」その思いを叶えてしまおう、というのが本記事の目的となっています! さて今回メインに扱うのはJUCEと呼ばれるフレームワークです。オーディオ関連のソフトウェアを開発することに特化したフレームワークで、比較的簡単にVSTプラグインやVSTプラグインホスト、普通のアプリケーションを作

本記事の対象

本記事でやること

注意

環境構築

JUCE

下記のページからJUCEをダウンロードしましょう。
とりあえずはStarterで大丈夫です。JUCEを使って作ったもので$20,000以上稼ぐつもりのある人は商用ライセンス版への移行を考えましょう。

Get JUCE - JUCE
The JUCE Framework

自身のOSにあったものを選んでください。

ダウンロードしたら展開しましょう。
多分展開したら以下のようなディレクトリ構成になるんじゃないかと思います。

juce-x.x.x-windows  <-- xは何等かの数字。本記事執筆時は8.0.1。
└─ JUCE
   ├─ .github
   ├─ docs
   ├─ examples
   ├─ extras
   ├─ modules
   ├─ .clang-tidy
   ├─ .gitignore
   ├─ .gitlab-ci.yml
   └─ ....

このJUCEフォルダ以下をわかりやすい位置に移動しておきましょう。自分はC:\直下に置きました。
どこでもいいと思います。

2024-07-03-13-31-50

Visual Studio

WindowsでJUCEを使って開発をする場合Visual Studio(MacならXcode)は必須です。インストールしましょう。

無料の開発者ソフトウェアとサービス - Visual Studio
無料プラン: Visual Studio Community、Visual Studio Code、VSTS、Dev Essentials。

ダウンロードしたら実行して進めていきましょう。途中下記のようにインストールするワークロードを選択するところが出てくるはずです。ここでは『C++によるデスクトップ開発』を選択して進めましょう。

VSCode

C++のコードを書くのはVisual Studioの方が手っ取り早くていいと思います。が、UI部分のWeb系についてはVSCodeを使った方が手早いように感じているので、こちらも導入します。

無料の開発者ソフトウェアとサービス - Visual Studio
無料プラン: Visual Studio Community、Visual Studio Code、VSTS、Dev Essentials。

ダウンロードしたら実行して進めていきましょう。

WebView2 Runtime

今回作成するプラグインは、GUI部分をWeb技術を使って構築します。そこで必要になるのがWebView2 Runtimeです。ざっくり説明すると、WebView2は簡単なWebブラウザのようなもので、プラグイン上でこのブラウザを動かすことでGUI部分を表示します。

おそらく、ほとんどの人は自動でインストールされているはずです。
Windowsの設定から『アプリ』→『インストールされているアプリ』を選択し、検索窓に『webview』と入力してください。最終的に以下のような画面になっていれば、インストールされていることになります。

2024-07-04-15-15-52

もしインストールされていなかったらこのサイトにアクセスし、『Evergreen Bootstrapper』をダウンロード、インストールしてください。

2024-07-04-15-19-11

Node.js (NVM)

ブラウザで表示する部分を作るのにNode.jsを使います。Node.jsを直接インストールするよりは、バージョン管理ツール経由の方が扱いやすいので、そうします。

GitHub - coreybutler/nvm-windows: A node.js version management utility for Windows. Ironically written in Go.
A node.js version management utility for Windows. Ironically written in Go. - coreybutler/nvm-windows

Releasesというところをクリックしましょう。
2024-07-04-16-33-44

『Latest』のマークがある項目のAssetsからnvm_setup.exeをクリックして、ダウンロード・インストールします。(下記の画像は一部省略しています)
2024-07-04-16-36-00

Windowsターミナルを開いてnvm --versionと入力しEnter。バージョン情報が表示されたら成功です!
2024-07-04-17-32-03

バージョン情報が表示されない場合、Windowsターミナルを閉じて、もう一度インストールをやり直してください。インストールが終わったらPCの再起動もしてみましょう。
完了したのちにnvm --versionしてもバージョン情報が表示されない場合は、Pathが通っていない可能性があります。

スタートメニューの検索窓に『path』と入力し、『システム環境変数の編集』をクリック。
2024-07-04-17-39-46

環境変数ボタンをクリック。
2024-07-04-17-41-05

NVM_HOMENVM_SYMLINKが設定されているのを確認(されていなかったら設定しましょう)し、Pathを選択して、編集ボタンをクリック。
2024-07-04-17-44-28

%NVM_HOME%%NVM_SYMLINK%が設定されているのを確認(されていなかったら設定しましょう)。
2024-07-04-17-50-38

nvm --versionしてもバージョン情報が表示されない場合はPCの再起動。それでもやはりだめなら...ちょっとわからないのでGoogle先生に聞きましょう。

正しく環境構築できたか確認

インストールが成功したかを確認するためにデモプロジェクトをビルドしてみましょう。

今回ビルドするデモプロジェクトはJUCE/extras/AudioPluginHostにある『AudioPluginHost』というソフトウェアで、自分が制作したプラグインを簡単に実行してテストができるソフトウェアになっています。

手順

JUCEフォルダーからProjucer.exeを起動します。
ProjucerはJUCEプロジェクトを管理するアプリケーションです。これから長らくお世話になるので仲良くしておきましょう。
2024-07-03-13-49-52

『Open Existing Project...』をクリックして
2024-07-03-13-50-47

先ほどJUCEをインストールしたフォルダを開いて『extras』フォルダを開き、
2024-07-03-13-51-34

更にその下にある『AudioPluginHost』を開いて、
2024-07-03-13-51-54

『AudioPluginHost.jucer』を選択して『開く』をクリック。
2024-07-03-13-52-07

Projucerがこんな感じになればOKです。
2024-07-03-13-52-25

右上のVisual Studioのアイコンをクリックしてください。
2024-07-03-13-53-05

Visual Studioが起動したら、上部のメニューバーから『ビルド』→『ソリューションのビルド』をクリックしてください。
これでビルドが始まります。
2024-07-03-13-58-45

画面下側に
========== ビルド: 成功 1、失敗 0、最新の状態 0、スキップ 0 ==========
が表示されたらビルド成功です!

生成物の起動

この記事の通り、JUCEをC:\直下においている場合、生成物は下記のパスにあります。
C:\JUCE\extras\AudioPluginHost\Builds\VisualStudio2022\x64\Debug\App

『AudioPluginHost.exe』をダブルクリックしてください。
2024-07-03-14-01-13

このようなウィンドウが表示されたら成功です!
(環境によって緑色のやつの本数が違ったりするかもしれません)
2024-07-03-14-01-59

今後はデバッグにこの『AudioPluginHost.exe』を利用するので、これのショートカットを作成してJUCEフォルダにおいておくと便利です。
2024-07-03-14-03-11

簡単なプラグインを作ってみよう!

プロジェクトの作成

というわけでいよいよプラグインを作っていこうと思います。
今回作るのは、GainとPanだけを持った簡単なものです。簡単なものですがJUCEにおけるプラグイン作成の肝は抑えているので今後さまざまに応用できます。

  1. Projucerの上部メニューバーから『File』→『New Project』をクリックするとこんな画面になると思います。
    2024-07-03-14-20-41

  2. 左側のメニューバーから『Plug-In』→『Basic』を選んで、『Project Name』にGainPanTutorialと入力した後『Create Project』をクリックします。
    2024-07-03-14-23-24

  3. そうするとエクスプローラが表示されるので、わかりやすいところにフォルダを作って『フォルダーの選択』を押しましょう。
    この時、ファイルパスに全角文字が入らないように注意してください。

  4. こんな画面になったら成功です。
    2024-08-19-11-37-50

  5. 今回はUI部分にWebViewを使うのでその設定を行います。左側のメニューバーから『Modules』→『juce_gui_extra』を選択し、JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKINGをEnabledにします。
    2024-07-03-14-30-53

パラメータの追加

まずは必要なパラメータの検討を行います。今回製作するプラグインが持つ機能はGainとPanです。
Gainはデシベル単位で調節したいので、Float型のパラメータgainを用意することにしましょう。
パンの度合いについても同様にFloat型のパラメータpanAngleを用意しましょう。
パンルールも選択できた方が嬉しいかもしれません。ChoiceできるようなパラメータpanRuleを用意します。
後はBypassスイッチとかも欲しいですね。DAW側にもそのボタンはありますが、プラグイン側にもあったほうが嬉しい気がします。bool型のパラメータbypassを用意しましょう。

  1. いよいよコーディングを行っていきます。右上の『Visual Studio』をクリックしましょう。
    2024-08-19-12-30-17

  2. パラメータの追加にはAudioProcessorValueTreeStateクラスが便利なのでこれを使います。
    右側のソリューションエクスプローラからGainPanTutorial_SharedCode → GainPanTutorial → Source → PluginProcessor.hを選択します
    (※ VisualStudioのテーマを変えているので、色が違うかもしれません)
    2024-08-19-12-36-25

  3. PluginProcessor.hにてメンバ変数を宣言します。juce::AudioProcessorValueTreeState parametersがパラメータの追加そのものの記述で、プライベート変数として定義した4つの変数は、パラメータの読み取り専用ポインタです。

    // PluginProcessor.h
    (省略)
    
      //==============================================================================
      void getStateInformation(juce::MemoryBlock& destData) override;
      void setStateInformation(const void* data, int sizeInBytes) override;
    
      //==============================================================================
      /*~~~~ここから追加~~~~*/
      juce::AudioProcessorValueTreeState parameters{
          *this,
          nullptr,
          juce::Identifier("PARAMETERS"),
          {
              std::make_unique<juce::AudioParameterFloat>(
                  "gain", "gain", juce::NormalisableRange<float>(-100.0f, 10.0f),
                  0.0f),
              std::make_unique<juce::AudioParameterFloat>(
                  "panAngle", "panAngle",
                  juce::NormalisableRange<float>(-100.0f, 100.0f), 0.0f),
              std::make_unique<juce::AudioParameterChoice>(
                  "panRule", "panRule", juce::StringArray("linear", "balanced"), 1),
              std::make_unique<juce::AudioParameterBool>("bypass", "bypass", false),
          }};
     /*~~~~~追加終わり~~~~~*/
    
     private:
      /*~~~~ここから追加~~~~*/
      std::atomic<float>* gain = parameters.getRawParameterValue("gain");
      std::atomic<float>* panAngle = parameters.getRawParameterValue("panAngle");
      std::atomic<float>* panRule = parameters.getRawParameterValue("panRule");
      std::atomic<float>* bypass = parameters.getRawParameterValue("bypass");
      /*~~~~~追加終わり~~~~~*/
      //==============================================================================
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GainPanTutorialAudioProcessor)
    };
    
    

    2024-08-19-13-16-34

    コードの解説

    今回使ったjuce::AudioProcessorValueTreeStateはプラグインにおけるパラメータ周りを一元管理するめちゃめちゃ便利なクラスです。GUIとの紐づけやパラメータの種類、パラメータの保存、元に戻す機能の実装等を行う際に便利な様々な機能を提供してくれます。juce::AudioProcessorValueTreeStateは1つのプラグインにつき1つしか定義できない点に注意してください。

    juce::AudioProcessorValueTreeStateで設定できるパラメータの種類として、下記の4種が挙げられます。

    今回はこれらの中からAudioParameterFloat、AudioParameterChoice、AudioParameterBoolを利用しました。これらパラメータの定義方法について説明したいと思います。
    ドキュメントの方が詳しく書かれているので、そちらも是非ご覧ください。

    2024-08-19-13-34-16
    2024-08-19-13-44-34
    2024-08-19-13-47-01

    また、今回はパラメータの値を読みだす為にパラメータの読み取り専用のポインタを作りました。実はパラメータを読み取る別の方法として、parameters.getParameterAsValue("hogehogeParam").getValue()もあるのですが、parameters.getParameterAsValue()で得られるValueクラスがスレッドセーフでなかったり、冗長だったり、ほんの少しだけ遅かったりするので、ここで定義したポインタを読み取った方が良かったりします。読み取り専用なので、例えば*hogehogeParam = 500といった形で新たな値を代入したとしてもパラメータは更新されません。パラメータを更新させたいときはparameters.getParameterAsValue("hogehogeParam").setValue(500)としましょう。

とりあえずこれでパラメータの実装(信号処理部分はまだ)が完了しました!
一旦ビルドして確認してみましょう!

ビルド

メニューバーから『ソリューション』のビルドをクリック!
2024-08-19-13-52-04

画面下の出力に、
========== ビルド: 成功 4、失敗 0、最新の状態 0、スキップ 0 ==========
みたいな感じのものが表示されたらビルド成功です!!

起動

以前作った『AudioPluginHost.exe』で確認します。

  1. まずは起動しましょう
    2024-08-19-13-53-42

  2. ビルドしたVST3を探します。このプロジェクトを作成したフォルダを開いてください。そこに『Builds』というフォルダができているはずです。
    2024-08-19-13-57-03

  3. Builds → VisualStudio2022 → x64 → Debug → VST3 → GainPanTutorial.vst3 → Contents → x86_64-win とフォルダをクリックして開いてください。そこに、『GainPanTutorial.vst3』というファイルがあるはずです!それをAudioPluginHost.exeにドラッグ&ドロップします。
    2024-08-19-14-02-33

  4. するとGainPanTutorial(VST3)という名前の付いた箱が表示されます。それを右クリックし、『Show all parameters』を選択してください。
    2024-08-19-14-06-14

  5. このような画面が出てきます!確かにパラメータが設定できていますね。しかし、Bypassボタンが2つあり少しおかしいようです。DAW側のBypassボタンと、先ほど設定したBypassボタンをリンクさせていないのが原因です。
    2024-08-19-14-08-07

ちょっと修正

DAW側のBypassボタンと、先ほど設定したBypassボタンをリンクさせていなかったため、Bypassが2つ表示されていました。このように、DAW側のBypassボタンは何もしなくても勝手に追加されるため、実は自分で追加する必要はありません。とはいえ、プラグインUIにBypassボタンを表示したり、Bypassをオートメーションで書いても自然な挙動にしたい需要はあるので、リンクさせておこうと思います。

再びPluginProcessor.hを開き、以下のコードを追加します。

// PluginProcessor.h
(省略)

  //==============================================================================
  juce::AudioProcessorValueTreeState parameters{
      *this,
      nullptr,
      juce::Identifier("PARAMETERS"),
      {
          std::make_unique<juce::AudioParameterFloat>(
              "gain", "gain", juce::NormalisableRange<float>(-100.0f, 10.0f),
              0.0f),
          std::make_unique<juce::AudioParameterFloat>(
              "panAngle", "panAngle",
              juce::NormalisableRange<float>(-100.0f, 100.0f), 0.0f),
          std::make_unique<juce::AudioParameterChoice>(
              "panRule", "panRule", juce::StringArray("linear", "balanced"), 1),
          std::make_unique<juce::AudioParameterBool>("bypass", "bypass", false),
      }};

  /*~~~~ここから追加~~~~*/
  juce::AudioProcessorParameter* getBypassParameter() const {
    return parameters.getParameter("bypass");
  }
  /*~~~~~追加終わり~~~~~*/

 private:
  std::atomic<float>* gain = parameters.getRawParameterValue("gain");
  std::atomic<float>* panAngle = parameters.getRawParameterValue("panAngle");
  std::atomic<float>* panRule = parameters.getRawParameterValue("panRule");
  std::atomic<float>* bypass = parameters.getRawParameterValue("bypass");

(省略)

2024-08-19-14-25-44

もう一度ビルドして、AudioPluginHost.exeで確認してみましょう!ビルドをする際は、プラグインがロードされたAudioPluginHost.exeを閉じるのを忘れないでください。

ちゃんとBypassボタンが1つになったと思います。
2024-08-19-14-27-10

パラメーターの保存と読み込み機能の実装

続いてパラメータの保存機能を作っていきます。この機能が無いと、DAWを閉じるたびに全パラメータがリセットされてしまいます。
パラメータをjuce::AudioProcessorValueTreeStateで管理しているので、この部分は非常に簡単に作ることができます。

  1. PluginProcessor.cppを開きます
    2024-08-19-14-46-42

  2. コードの下の方にある関数getStateInformation()setStateInformation()を記述します。

    // PluginProcessor.cpp
    (省略)
    
    juce::AudioProcessorEditor* GainPanTutorialAudioProcessor::createEditor() {
      return new GainPanTutorialAudioProcessorEditor(*this);
    }
    
    //==============================================================================
    void GainPanTutorialAudioProcessor::getStateInformation(
        juce::MemoryBlock& destData) {
      // You should use this method to store your parameters in the memory block.
      // You could do that either as raw data, or use the XML or ValueTree classes
      // as intermediaries to make it easy to save and load complex data.
    
      /*~~~~ここから追加~~~~*/
      auto state = parameters.copyState();
      std::unique_ptr<juce::XmlElement> xml(state.createXml());
      copyXmlToBinary(*xml, destData);
      /*~~~~~追加終わり~~~~~*/
    }
    
    void GainPanTutorialAudioProcessor::setStateInformation(const void* data,
                                                            int sizeInBytes) {
      // You should use this method to restore your parameters from this memory
      // block, whose contents will have been created by the getStateInformation()
      // call.
    
      /*~~~~ここから追加~~~~*/
      std::unique_ptr<juce::XmlElement> xmlState(
          getXmlFromBinary(data, sizeInBytes));
      if (xmlState.get() != nullptr)
        if (xmlState->hasTagName(parameters.state.getType()))
          parameters.replaceState(juce::ValueTree::fromXml(*xmlState));
      /*~~~~~追加終わり~~~~~*/
    }
    
    //==============================================================================
    // This creates new instances of the plugin..
    juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
      return new GainPanTutorialAudioProcessor();
    }
    

    2024-08-19-14-53-02

何が起こっているのかを少し解説します。getStateInformation()関数はDAWがプロジェクトを保存する際にDAWが実行する関数で、プラグインはjuce::MemoryBlock& destDataに自身が保存すべきデータをバイナリにして代入します。ここではjuce::AudioProcessorValueTreeStateの機能を使ってパラメータ情報をコピーし、XML形式で書き直し、それをバイナリにして保存しています。

setStateInformation()関数はDAWがプロジェクトをロードする際にDAWが実行する関数で、プラグインはconst void* dataから自身のデータを読み取り、セットアップを行います。ここでは、バイナリからXMLを復元し、juce::AudioProcessorValueTreeStateの機能を使ってパラメータのロードを行っています。

プラグインはバイナリデータの保存と読み込みを行うことで任意のものを保存できます。そのため、メモ帳やペイントソフト等、なんでもプラグインとして作ることが可能です。

信号処理の実装...の前に

いよいよプラグイン開発の肝となる、信号処理の実装を行っていきます。JUCEでは信号処理の部分をprocessBlock()関数に実装します。DAWがオーディオバッファをプラグインのprocessBlock()関数に渡し、プラグインがオーディオバッファを処理するという流れです。
PluginProcessor.hに書かれているprocessBlock()の定義をで見てみましょう。こんな感じです。

void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;

ふむふむなるほど、float型のオーディオバッファの参照が送られるのでこれをそのまま処理すればよいと。float型、つまり32bit浮動小数点型で処理するのかぁ...。

そうです。Projucerによって自動生成されたコードをそのまま使うと、32bit浮動小数点演算のプラグインが出来上がります。今どきのほとんどのDAWは64bit浮動小数点演算に対応していますし、こちらの方が高音質です。せっかくプラグインを作るなら64bit浮動小数点演算に対応させたいですよね!

また、オーディオデータがサンプル単位で送られるのではなく、ブロック単位で送られる点にも注意が必要です。単純にパラメータの現在値を参照して信号処理を行うような実装をした場合、ブロックを処理している間にDAW上でパラメータが変化した際に、ブロック間にノイズが発生してしまいます。
下図はDAWにおけるオーディオデータとVSTのやり取りを表した模式図です。今、ユーザーが、あるプラグインのGainというパラメータを0.0dBから1.0dBになめらかに遷移するようなオートメーションを書きました。まずDAWはエフェクトを掛けるべきオーディオバッファを固定長のブロックに分割します(→①)。同時にそのブロックが対応するGainパラメータの値を1点とります。次に、DAWはVSTのGainパラメータに、対応する値をセットします(→②)。続いてVSTにオーディオバッファを渡します(→③)。VSTは先ほどDAWからセットされたGainパラメータの値を用いて1ブロック全体を処理します(→④)。DAWは結果を結合します(→⑤)。
結果として、ブロック間で波形が非連続になってしまいます。これはノイズとなります。

これを防ぐためには、VSTで元パラメータの値を保持しておき、パラメータが変化したら元パラメータからなめらかに遷移するような信号処理を実装すれば良いですね!

2024-08-19-16-38-34

プラグインとオートメーション

このように、プラグインのパラメーターにオートメーションを掛ける際は、プラグイン側がオートメーションに適切に対処している必要があります。
実は、一般に販売されてるプラグインの中にはオートメーションへの対処が雑なプラグインも数多くあります。
楽曲制作時は、そのプラグインがオートメーションを適切に処理できるか気を付ける必要がありますね。

パラメータの円滑化

まずはパラメータの円滑化を実装してから、64bit浮動小数点演算に対応した信号処理を書くことにしましょう。
パラメータの円滑化はjuce::SmoothedValueクラスを使うことで簡単に実装できます。

  1. PluginProcessor.hで、各種パラメータの円滑化を行うクラスを定義します。juce::ValueSmoothingTypes::Linearを指定することで、線形に補完するよう指示しています。また、dryWetSmoothedはBypassスイッチ用です。DryWet操作によってBypassを実現します。

    // PluginProcessor.h
    (省略)
    
      juce::AudioProcessorParameter* getBypassParameter() const {
        return parameters.getParameter("bypass");
      }
    
     private:
      std::atomic<float>* gain = parameters.getRawParameterValue("gain");
      std::atomic<float>* panAngle = parameters.getRawParameterValue("panAngle");
      std::atomic<float>* panRule = parameters.getRawParameterValue("panRule");
      std::atomic<float>* bypass = parameters.getRawParameterValue("bypass");
    
      /*~~~~ここから追加~~~~*/
      juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> gainSmoothed;
      juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear>
          panAngleSmoothed;
      juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> dryWetSmoothed;
      /*~~~~~追加終わり~~~~~*/
    
      //==============================================================================
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GainPanTutorialAudioProcessor)
    };
    

    2024-08-19-17-11-13

  2. 続いて、何秒かけて滑らかに遷移するかを設定します。PluginProcessor.cppのprepareToPlay()関数内に記述します。このprepareToPlay()関数はプラグインのprocessBlock()が実行される前に呼ばれ、プラグインの準備を行う関数です。0.01秒かけて遷移することにします。panAngleの値は後で計算しやすいように、0から1の間にスケールしておきます。

    // PluginProcessor.cpp
    (省略)
    
    void GainPanTutorialAudioProcessor::changeProgramName(
        int index, const juce::String& newName) {}
    
    //==============================================================================
    void GainPanTutorialAudioProcessor::prepareToPlay(double sampleRate,
                                                      int samplesPerBlock) {
      // Use this method as the place to do any pre-playback
      // initialisation that you need..
    
      /*~~~~ここから追加~~~~*/
      gainSmoothed.reset(sampleRate, 0.01);
      gainSmoothed.setCurrentAndTargetValue(*gain);
    
      panAngleSmoothed.reset(sampleRate, 0.01);
      panAngleSmoothed.setCurrentAndTargetValue((*panAngle / 100 + 1) * 0.5);
    
      dryWetSmoothed.reset(sampleRate, 0.01);
      dryWetSmoothed.setCurrentAndTargetValue(*bypass ? 0.0f : 1.0f);
      /*~~~~~追加終わり~~~~~*/
    }
    
    void GainPanTutorialAudioProcessor::releaseResources() {
      // When playback stops, you can use this as an opportunity to free up any
      // spare memory, etc.
    }
    
    (省略)
    

    2024-08-19-22-50-37

  3. processBlock()関数が実行されたら、パラメータの更新を行う関数を作ります。PluginProcessor.hに新たな関数を定義します。短いのでヘッダー内に実装も書いちゃいましょう。

    // PluginProcessor.h
    (省略)
    
     private:
      std::atomic<float>* gain = parameters.getRawParameterValue("gain");
      std::atomic<float>* panAngle = parameters.getRawParameterValue("panAngle");
      std::atomic<float>* panRule = parameters.getRawParameterValue("panRule");
      std::atomic<float>* bypass = parameters.getRawParameterValue("bypass");
    
      juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> gainSmoothed;
      juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear>
          panAngleSmoothed;
      juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> dryWetSmoothed;
    
      /*~~~~ここから追加~~~~*/
      void updateParameters() {
        gainSmoothed.setTargetValue(*gain);
        panAngleSmoothed.setTargetValue((*panAngle / 100 + 1) * 0.5);
        dryWetSmoothed.setTargetValue(*bypass ? 0.0f : 1.0f);
      };
      /*~~~~~追加終わり~~~~~*/
    
      //==============================================================================
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GainPanTutorialAudioProcessor)
    };
    

    2024-08-19-22-49-57

信号処理の実装

いよいよ信号処理を実装していきます。信号の流れは下図のようなものにします。
2024-08-19-20-26-40

juce::dspについて
これから各信号処理を実装していきますが、JUCEには既に実装された信号処理ライブラリjuce::dspがあります。多くの場合、これらを利用したほうが楽です。具体的な使い方は前回記事で扱っているので、そちらをご覧ください。

今回は、パラメータの円滑化の説明と実装を行いたかったためjuce::dspを使いませんでした。
juce::dspはモジュールによってはパラメータの円滑化も自動で行ってくれます。

  1. 64bit浮動小数点演算に対応した信号処理を書くために関数を定義します。32bit浮動小数点演算しかできないDAWにも対応させる必要があるため、32bit浮動小数点演算の信号処理も実装しないといけません。同じ処理を2回書くのは手間なので、関数テンプレートを利用します。また、このプラグインが64bit浮動小数点演算に対応していることを伝える関数を実装します。

    // PluginProcessor.h
    (省略)
    
    #ifndef JucePlugin_PreferredChannelConfigurations
      bool isBusesLayoutSupported(const BusesLayout& layouts) const override;
    #endif
    
      /*~~~~ここから追加~~~~*/
      template <typename T>
      void processBlockImpl(juce::AudioBuffer<T>& buffer,
                            juce::MidiBuffer& midiMessages);
    
      void processBlock(juce::AudioBuffer<float>& buffer,
                        juce::MidiBuffer& midiMessages) override {
        processBlockImpl(buffer, midiMessages);
      };
      void processBlock(juce::AudioBuffer<double>& buffer,
                        juce::MidiBuffer& midiMessages) override {
        processBlockImpl(buffer, midiMessages);
      };
    
      bool supportsDoublePrecisionProcessing() const override { return true; }
      /*~~~~~追加終わり~~~~~*/
    
      //==============================================================================
      juce::AudioProcessorEditor* createEditor() override;
      bool hasEditor() const override;
    
    (省略)
    

    2024-08-19-22-52-57

  2. PluginProcessor.cppにて関数を実装します。もともと書いてあったprocessBlock()関数は削除してから、実装を始めましょう。

    // PluginProcessor.cpp
    (省略)
    
    #if !JucePlugin_IsSynth
      if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
        return false;
    #endif
    
      return true;
    #endif
    }
    #endif
    
    /*~~~~ここから追加~~~~*/
    template <typename T>
    inline void GainPanTutorialAudioProcessor::processBlockImpl(
        juce::AudioBuffer<T>& buffer, juce::MidiBuffer& midiMessages) {
      juce::ScopedNoDenormals noDenormals;
      auto totalNumInputChannels = getTotalNumInputChannels();
      auto totalNumOutputChannels = getTotalNumOutputChannels();
    
      // In case we have more outputs than inputs, this code clears any output
      // channels that didn't contain input data, (because these aren't
      // guaranteed to be empty - they may contain garbage).
      // This is here to avoid people getting screaming feedback
      // when they first compile a plugin, but obviously you don't need to keep
      // this code if your algorithm always overwrites all the output channels.
      for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear(i, 0, buffer.getNumSamples());
    
      updateParameters();
    
      auto* leftAudioBuff = buffer.getWritePointer(0);
      auto* rightAudioBuff = buffer.getWritePointer(1);
      auto buffLength = buffer.getNumSamples();
      bool panIsLinear = (*panRule == 0);
    
      for (int samplesIdx = 0; samplesIdx < buffLength; samplesIdx++) {
        auto dryWetValue = dryWetSmoothed.getNextValue();
        auto gainValue =
            juce::Decibels::decibelsToGain(gainSmoothed.getNextValue(), -100.0f);
        auto leftMulValue = gainValue * dryWetValue;
        auto rightMulValue = leftMulValue;
    
        auto panValue = panAngleSmoothed.getNextValue();
        if (panIsLinear) {
          leftMulValue *= (1 - panValue);
          rightMulValue *= panValue;
        } else {
          leftMulValue *= std::min(1.0f, 2 - 2 * panValue);
          rightMulValue *= std::min(1.0f, 2 * panValue);
        }
    
        leftAudioBuff[samplesIdx] = leftAudioBuff[samplesIdx] * leftMulValue +
                                    leftAudioBuff[samplesIdx] * (1 - dryWetValue);
        rightAudioBuff[samplesIdx] = rightAudioBuff[samplesIdx] * rightMulValue +
                                     rightAudioBuff[samplesIdx] * (1 - dryWetValue);
      }
    }
    /*~~~~~追加終わり~~~~~*/
    
    //==============================================================================
    bool GainPanTutorialAudioProcessor::hasEditor() const {
      return true;  // (change this to false if you choose to not supply an editor)
    }
    
    (省略)
    

    2024-08-20-09-37-12

GainもPanも、元信号にどのような値を掛けるかという演算なので、先に掛けるべき値を求め、最後に一気に掛けるという処理をしています。
またGainは-100dB以下を-∞dBとして扱うような設定をしています。

ビルドとテスト

これで信号処理の部分が完成しました!実質プラグインが完成したといっても過言ではありません。ビルドして試してみましょう。

  1. メニューバーから『ソリューション』のビルドをクリック。
    画面下の出力に、
    ========== ビルド: 成功 4、失敗 0、最新の状態 0、スキップ 0 ==========
    みたいな感じのものが表示されたらビルド成功です!!
    2024-08-19-13-52-04-1

  2. Builds → VisualStudio2022 → x64 → Debug → VST3 → GainPanTutorial.vst3 → Contents → x86_64-win とフォルダをクリックして開いてください。そこに、『GainPanTutorial.vst3』というファイルがあるはずです!それをAudioPluginHost.exeにドラッグ&ドロップします。
    2024-08-19-14-02-33-1

  3. するとGainPanTutorial(VST3)という名前の付いた箱が表示されます。それを右クリックし、『Show all parameters』を選択してください。
    2024-08-19-14-06-14-1

  4. 続いて、内臓シンセサイザを接続します。どこか適当なところを右クリックして、『Sine Wave Synth (Internal)』をクリック。
    2024-08-20-10-13-27

  5. 赤色と緑色の端子をドラッグして下図のようにつなぎます。画面下部のキーボードを押すと、スピーカーから音が鳴るはずです!音量には注意してください
    2024-08-20-10-15-32

  6. プラグインのパラメーターをいじりながら、色々音を出してみましょう。意図した挙動をしていますか?

UIの作成(JUCE準備編)

プラグインの信号処理部分は完成しましたが、まだUI部分ができていません。作りましょう。
本記事ではUI部分をWeb技術を使って実装していきます。C++とは勝手が異なり、別領域の知識が必要です。もちろんJUCEの機能だけでUIを作ることもできます。JUCEの機能だけでUIを作りたい!という方は前回の記事をご覧ください。

JUCE機能でのUI作成 vs WebViewによるUI作成

リッチな見た目にしないのであれば、JUCE機能でUIを作ったほうが簡単です。しかし、ちょっと凝ったことをしたくなった場合は、JUCEでUIを作るのは大変しんどいです。本当に大変。
じゃあWebViewで作れば圧倒的超絶簡単かと言えばそうでもありません。特有の変なトラップに引っかかることが多くなります。ただ、開発体験としてはWebViewの方が全然良いので、これから新たにプラグインを作る際はWebViewを使ったほうが良いと思います。

まずは事前準備を行っていきます。これからしばらくPluginEditor.hとPluginEditor.cppをいじっていきます。

  1. まずカスタムブラウザを作ります。ブラウザをUIとして使うため、やろうと思えば任意のページを開けてしまいます(阿部寛さんのHPとか)。念のため防いでおきます。そこまで重要ではないです。

    // PluginEditor.h
    (省略)
    
    #pragma once
    
    #include <JuceHeader.h>
    
    #include "PluginProcessor.h"
    
    /*~~~~ここから追加~~~~*/
    struct SinglePageBrowser : juce::WebBrowserComponent {
      using WebBrowserComponent::WebBrowserComponent;
    
      // Prevent page loads from navigating away from our single page web app
      bool pageAboutToLoad(const juce::String& newURL) override {
        return newURL == juce::String("http://localhost:5173/") ||
               newURL == getResourceProviderRoot();
      }
    };
    /*~~~~~追加終わり~~~~~*/
    
    //==============================================================================
    /**
     */
    class GainPanTutorialAudioProcessorEditor : public juce::AudioProcessorEditor {
     public:
    
    (省略)
    

    2024-08-20-11-09-36

  2. DAWでオートメーション記録モードにした状態でプラグインウィンドウをクリックした際、記録がちゃんと動くように設定します(特にCubase)。

    // PluginEditor.h
    (省略)
    
    class GainPanTutorialAudioProcessorEditor : public juce::AudioProcessorEditor {
     public:
      GainPanTutorialAudioProcessorEditor(GainPanTutorialAudioProcessor&);
      ~GainPanTutorialAudioProcessorEditor() override;
    
      //==============================================================================
      void paint(juce::Graphics&) override;
      void resized() override;
      //==============================================================================
      /*~~~~ここから追加~~~~*/
      int getControlParameterIndex(juce::Component&) override {
        return controlParameterIndexReceiver.getControlParameterIndex();
      }
      /*~~~~~追加終わり~~~~~*/
    
     private:
      // This reference is provided as a quick way for your editor to
      // access the processor object that created it.
      GainPanTutorialAudioProcessor& audioProcessor;
    
      /*~~~~ここから追加~~~~*/
      juce::WebControlParameterIndexReceiver controlParameterIndexReceiver;
      /*~~~~~追加終わり~~~~~*/
    
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(
          GainPanTutorialAudioProcessorEditor)
    };
    
    

    2024-08-20-11-24-31

  3. 続いて、JUCEのWebブラウザコンポーネントと、WebViewにおけるJavaScriptのイベントリスナをリンクする『Relay』というものを定義します。

    // PluginEditor.h
    (省略)
    
     private:
      // This reference is provided as a quick way for your editor to
      // access the processor object that created it.
      GainPanTutorialAudioProcessor& audioProcessor;
    
      juce::WebControlParameterIndexReceiver controlParameterIndexReceiver;
    
      /*~~~~ここから追加~~~~*/
      juce::WebSliderRelay gainRelay{"gain"};
      juce::WebSliderRelay panRelay{"panAngle"};
      juce::WebComboBoxRelay panRuleRelay{"panRule"};
      juce::WebToggleButtonRelay bypassRelay{"bypass"};
      /*~~~~~追加終わり~~~~~*/
    
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(
          GainPanTutorialAudioProcessorEditor)
    };
    
    

    2024-08-20-11-34-01

  4. Webブラウザコンポーネントを定義します。同時に、WebView2を使うよという設定や、アクセス権限に問題のないフォルダをデータフォルダに指定する設定、Relayの設定、リソース送信元の設定を行います。C++からリソースを送信する際に用いるヘルパー関数も定義しています。

    // PluginEditor.h
    (省略)
    
      juce::WebSliderRelay gainRelay{"gain"};
      juce::WebSliderRelay panRelay{"panAngle"};
      juce::WebComboBoxRelay panRuleRelay{"panRule"};
      juce::WebToggleButtonRelay bypassRelay{"bypass"};
    
      /*~~~~ここから追加~~~~*/
      SinglePageBrowser webComponent{
          juce::WebBrowserComponent::Options{}
              .withBackend(juce::WebBrowserComponent::Options::Backend::webview2)
              .withWinWebView2Options(
                  juce::WebBrowserComponent::Options::WinWebView2{}
                      .withUserDataFolder(juce::File::getSpecialLocation(
                          juce::File::SpecialLocationType::tempDirectory)))
              .withOptionsFrom(gainRelay)
              .withOptionsFrom(panRelay)
              .withOptionsFrom(panRuleRelay)
              .withOptionsFrom(bypassRelay)
              .withOptionsFrom(controlParameterIndexReceiver)
              .withResourceProvider(
                  [this](const auto& url) { return getResource(url); },
                  juce::URL{"http://localhost:5173/"}.getOrigin())};
    
      std::optional<juce::WebBrowserComponent::Resource> getResource(
          const juce::String& url);
    
      const char* getMimeForExtension(const juce::String& extension);
      /*~~~~~追加終わり~~~~~*/
    
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(
          GainPanTutorialAudioProcessorEditor)
    };
    

    2024-08-20-11-45-21

  5. juce::AudioProcessorValueTreeStateで管理しているパラメータの値とブラウザ上のパラメータを同期する『Attachment』を定義します。ついでに可読性向上のためにコメントで境界線を入れました。

    // PluginEditor.h
    (省略)
     private:
      // This reference is provided as a quick way for your editor to
      // access the processor object that created it.
      GainPanTutorialAudioProcessor& audioProcessor;
    
      //==============================================================================
    
      juce::WebControlParameterIndexReceiver controlParameterIndexReceiver;
    
      juce::WebSliderRelay gainRelay{"gain"};
      juce::WebSliderRelay panRelay{"panAngle"};
      juce::WebComboBoxRelay panRuleRelay{"panRule"};
      juce::WebToggleButtonRelay bypassRelay{"bypass"};
    
      //==============================================================================
    
      SinglePageBrowser webComponent{
          juce::WebBrowserComponent::Options{}
              .withBackend(juce::WebBrowserComponent::Options::Backend::webview2)
              .withWinWebView2Options(
                  juce::WebBrowserComponent::Options::WinWebView2{}
                      .withUserDataFolder(juce::File::getSpecialLocation(
                          juce::File::SpecialLocationType::tempDirectory)))
              .withOptionsFrom(gainRelay)
              .withOptionsFrom(panRelay)
              .withOptionsFrom(panRuleRelay)
              .withOptionsFrom(bypassRelay)
              .withOptionsFrom(controlParameterIndexReceiver)
              .withResourceProvider(
                  [this](const auto& url) { return getResource(url); },
                  juce::URL{"http://localhost:5173/"}.getOrigin())};
    
      std::optional<juce::WebBrowserComponent::Resource> getResource(
          const juce::String& url);
    
      const char* getMimeForExtension(const juce::String& extension);
    
      //==============================================================================
    
      /*~~~~ここから追加~~~~*/
      juce::WebSliderParameterAttachment gainAttachment{
          *audioProcessor.parameters.getParameter("gain"), gainRelay, nullptr};
      juce::WebSliderParameterAttachment panAttachment{
          *audioProcessor.parameters.getParameter("panAngle"), panRelay, nullptr};
      juce::WebComboBoxParameterAttachment panModeAttachment{
          *audioProcessor.parameters.getParameter("panRule"), panRuleRelay,
          nullptr};
      juce::WebToggleButtonParameterAttachment bypassAttachment{
          *audioProcessor.parameters.getParameter("bypass"), bypassRelay, nullptr};
      /*~~~~~追加終わり~~~~~*/
    
      //==============================================================================
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(
          GainPanTutorialAudioProcessorEditor)
    };
    

    2024-08-20-12-03-45

  6. 定義は終わったので中身の実装に入ります。まずコンストラクタで、Webブラウザコンポーネントを表示させます。同時に、プラグインウィンドウの大きさも暫定で設定します。Webブラウザが表示するページのリンクを"http://localhost:5173/"としていますが、これは開発時の設定です。プラグインが完成したらこの行をコメントアウトし、webComponent.goToURL(juce::WebBrowserComponent::getResourceProviderRoot());をアクティブにします。

    // PluginEditor.cpp
    (省略)
    
    #include "PluginEditor.h"
    
    #include "PluginProcessor.h"
    
    //==============================================================================
    GainPanTutorialAudioProcessorEditor::GainPanTutorialAudioProcessorEditor(
        GainPanTutorialAudioProcessor& p)
        : AudioProcessorEditor(&p), audioProcessor(p) {
      // Make sure that before the constructor has finished, you've set the
      // editor's size to whatever you need it to be.
    
      /*~~~~ここから追加~~~~*/
      addAndMakeVisible(webComponent);
      webComponent.goToURL("http://localhost:5173/");
      // webComponent.goToURL(juce::WebBrowserComponent::getResourceProviderRoot());
      /*~~~~~追加終わり~~~~~*/
    
      setSize(170, 650); // 数値の変更
    }
    

    2024-08-20-12-29-21

  7. g.fillAll()だけを残して他を削除し、ブラウザの大きさをプラグイン画面いっぱいに設定します。

    // PluginEditor.cpp
    (省略)
    
    GainPanTutorialAudioProcessorEditor::~GainPanTutorialAudioProcessorEditor() {}
    
    //==============================================================================
    void GainPanTutorialAudioProcessorEditor::paint(juce::Graphics& g) {
      // (Our component is opaque, so we must completely fill the background with a
      // solid colour)
      g.fillAll(
          getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
     /*~~~~削除~~~~*/
    }
    
    void GainPanTutorialAudioProcessorEditor::resized() {
      // This is generally where you'll want to lay out the positions of any
      // subcomponents in your editor..
    
      /*~~~~ここから追加~~~~*/
      webComponent.setBounds(getLocalBounds());
      /*~~~~~追加終わり~~~~~*/
    }
    

    2024-08-20-12-32-34

  8. C++からリソースを送信する際に用いるヘルパー関数を実装します。getResource()関数がリソースを送信する処理で、getMimeForExtension()関数はリソース送信時に拡張子を元にメディアタイプを判定する関数です。プラグイン完成時にはWebブラウザで表示するページのソースをZipファイルで圧縮し、バイナリでプラグインに埋め込み、それをロードすることでUIを表示させます。そのあたりの処理をgetResource()及びgetMimeForExtension()で実現します。まず、ダミーのZipファイルを生成し、Projucerに登録します。プロジェクトのあるフォルダで右クリックし、Zipフォルダを作成します。名前はassets.zipです。
    2024-08-20-13-56-25
    2024-08-20-13-57-19

  9. Visual Studioを閉じます。閉じた後、ProjucerのFile Explorerにassets.zipをドラッグ&ドロップします。
    2024-08-20-14-00-26
    2024-08-20-14-32-40

  10. もう一度Visual Studioを開き、PluginEditor.cppにコードを記述します。

    // PluginEditor.cpp
    (省略)
    
    void GainPanTutorialAudioProcessorEditor::resized() {
      // This is generally where you'll want to lay out the positions of any
      // subcomponents in your editor..
      webComponent.setBounds(getLocalBounds());
    }
    
    /*~~~~ここから追加~~~~*/
    std::optional<juce::WebBrowserComponent::Resource>
    GainPanTutorialAudioProcessorEditor::getResource(const juce::String& url) {
      const auto urlToRetrive = url == "/"
                                    ? juce::String{"index.html"}
                                    : url.fromFirstOccurrenceOf("/", false, false);
    
      static auto streamZip = juce::MemoryInputStream(
          juce::MemoryBlock(BinaryData::assets_zip, BinaryData::assets_zipSize),
          true);
    
      static juce::ZipFile archive{streamZip};
    
      if (auto* entry = archive.getEntry(urlToRetrive)) {
        auto entryStream = rawToUniquePtr(archive.createStreamForEntry(*entry));
        std::vector<std::byte> result((size_t)entryStream->getTotalLength());
        entryStream->setPosition(0);
        entryStream->read(result.data(), result.size());
    
        auto mime = getMimeForExtension(
            entry->filename.fromLastOccurrenceOf(".", false, false).toLowerCase());
        return juce::WebBrowserComponent::Resource{std::move(result),
                                                   std::move(mime)};
      }
      return std::nullopt;
    }
    
    const char* GainPanTutorialAudioProcessorEditor::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"}};
    
      if (const auto it = mimeMap.find(extension.toLowerCase());
          it != mimeMap.end())
        return it->second;
    
      jassertfalse;
      return "";
    }
    /*~~~~~追加終わり~~~~~*/
    

    2024-08-20-14-36-55

ここまで出来たら一旦ビルドし、出来上がったプラグインをAudioPluginHost.exeで読みこんでみましょう。
読みこんだらプラグイン部分をダブルクリックしてください。次のような画面が表示されれば成功です。いかにもブラウザが起動してる感じですね!
2024-08-20-14-42-09

ここまででやっとUIの事前準備が完了です! 長い~~~あまりにも長い。まだ続きます。

ちょっとこの辺で休憩してください。もしよかったら僕の曲とかBGMで流してください!!!!1再生あたり数円ほど僕に入ると思うので多分...!もしよかったらでいいんで!

デジタルアルバムも売ってます。もしよかったら是非!!

かしわで音楽工房 - BOOTH
KashiwadeがM3等で頒布したもののデジタル版を販売するストアです! Conquest(¥ 1,700), Rainy Resolutions(¥ 1,700), Rainy Resolutions Piano Score(¥ 0), Chronicles of Luna(¥ 1,200), 天光玲瓏・花舞う都の空 (パラデータ・MIDI・プロジェクトファイル)(¥ 1,100), 天光玲瓏(¥ 1,700), 【会場カード購入者向け】天光玲瓏・花舞う都の空 (パラデータ・MIDI・プロジェクトファイル)(¥ 0), Aquilegia(¥ 1,100)
Kashiwade
Composer

あと、この記事を参考にしてプラグインを作ったときは是非この記事の紹介もしてください。エゴサします!

UIの作成(Web編)

プラグインのウィンドウはしばらく開きっぱなしにしておいてください。Viteのホットリロード機能により、UIに変更を加えたら自動で反映されるはずです。

まずNode.jsの環境を作る所から始めます。VSCodeはもうインストールしましたか?

  1. プロジェクトがあるフォルダを開き、何もない所で右クリックして、『Codeで開く』をクリック。この項目が無い場合はVSCodeを開き、『ファイル』 → 『フォルダーを開く』を選択してプロジェクトがあるフォルダを選んでください。
    2024-08-20-15-03-24

  2. VSCodeを開いたら画面上の方にあるボタンをクリックし、でてきた下部タブからターミナルを選択しましょう。僕はPowerShellをカスタマイズしてるのでカラフルですが、気にしないでください。
    2024-08-20-15-09-18

  3. Node環境をアクティベーションします。ターミナルにnvm --versionを入力してバージョン情報が表示されるのを確認したら、nvm install latestを実行して最新のNodeをインストールします。
    2024-08-20-15-14-27

  4. 実行に成功すると、ターミナル出力にnvm use xx.x.xのような文字が出てくるので、それを入力します。Now using node vxx.x.xのようなものが表示されたらOKです。
    2024-08-20-15-16-19

  5. NodeのプロジェクトをViteで作ります。ターミナルでnpm create vite@latestと入力してください。プロジェクトの名前が聞かれます。
    2024-08-20-15-22-50

  6. 次の画像を参考に入力を進めてください。入力はテキスト入力、あるいは矢印キーで行うことができます。
    2024-08-20-15-25-20

  7. ターミナルのカレントディレクトリを移動します。cd webviewと入力してください。
    2024-08-20-15-28-04

  8. Nodeの依存パッケージをインストールし、開発サーバを動かします。npm installを入力し実行。続いてnpm run devを実行してください。実行し、プラグインの『最新の情報に更新』ボタンを押すとこのようになるはずです。
    2024-08-20-15-29-58

あとは幸せWeb開発をしていきます。UIライブラリもバシバシ使っていきます。一度JUCEで凝ったUI作った後だとWebでのUI開発が楽すぎて顎が外れるレベルです。やっていきましょう!!!

Webフロントエンド開発に関する記事はめちゃめちゃいっぱいWeb上に転がっています。
本記事ではWebフロントエンド開発そのものには詳しく触れず、簡易的な実装に留め、JUCEとどうやって連携するかという点に注目して書いていきます。
そのため、本記事で最終的に出来上がったUIは少々UXに改善の余地があります。ここをたたき台として、色々カスタマイズしていくことをお勧めします!

  1. UIライブラリをインストールします。Reactには無数のUIライブラリがあるので、是非お気に入りのものを見つけてください。今回は入門編なので最も使われているMUIを使おう...と思ったのですが、どうしてもこのMaterial UI臭さが気になって好きじゃないので、2番目に多く使われているAnt Designを使うことにします。この記事で初めてAnt Designを使うので、変なこと書いてたらごめんなさい。普段はRadix UIを使っています。おすすめです。

  2. UIライブラリをインストールします。ターミナルを複数立ち上げるため、ターミナルのプラスボタンをクリックします。
    2024-08-20-17-46-10

  3. cd webviewでカレントディレクトリをwebviewに変えて、npm install antd --saveを実行します。
    2024-08-20-17-49-01

  4. JUCE Frontend Libraryをインストールします。これはnpmで配布されておらず、手元のJUCEフォルダ内部のパスをpackage.jsonに直接記入する形でインストールを行います。本記事の通り、C直下にJUCEフォルダを配置している人は、package.jsonのdependenciesに"juce-framework-frontend": "file:C:/JUCE/modules/juce_gui_extra/native/javascript"を追記してください。
    2024-08-20-20-34-25

  5. npm installを実行してください

  6. Viteが作ってくれたHello World的なソースコードを修正していきます。main.tsxがReactのエントリーポイントです。main.tsxは特に直す部分が無いのですが、ここで呼ばれているCSSを修正します。index.cssを開いて全ての内容を削除し、次に書き換えてください。body要素にデフォルトで付与されているmarginを0にし、inputというIDのついた要素の文字揃えを中央にします。どうもAnt DesignのInputフォームは文字の中央揃えができないらしく?とりあえずハック的な対応をしています。

    body {
     margin: 0px;
    }
    #input {
     text-align: center;
    }
    

    2024-08-20-22-14-03

  7. 続いてApp.tsxを書き換えます。ここがUIの実装の中身になります。App.tsxの内容を全て削除し、次に書き換えてください。多分色々エラーがでて怒られると思いますが一旦無視してください。App.tsxでは主に操作子のレイアウトを行っています。操作子はこれから作るcomponentsフォルダに色々書いていきます。

    import { FC } from "react";
    import { Layout, Flex, Typography } from "antd";
    import JuceSlider from "./components/JuceSlider";
    import JuceTextbox from "./components/JuceTextbox";
    import JuceCombobox from "./components/JuceCombobox";
    import JuceToggleSwitch from "./components/JuceToggleSwitch";
    
    const App: FC = () => {
      return (
        <Layout>
          <Layout.Content>
            <Flex
              vertical
              gap="middle"
              justify="space-between"
              align="center"
              style={{ padding: "16px", height: "100vh" }}
            >
              <Typography.Title
                level={3}
                style={{ margin: 0, textAlign: "center" }}
              >
                Pan
              </Typography.Title>
              <JuceSlider identifier="panAngle" />
              <JuceTextbox identifier="panAngle" digits={0} />
              <JuceCombobox identifier="panRule" />
              <Typography.Title
                level={3}
                style={{ margin: 0, textAlign: "center" }}
              >
                Gain
              </Typography.Title>
              <JuceSlider identifier="gain" isVertical={true} />
              <JuceTextbox identifier="gain" suffix="dB" />
              <JuceToggleSwitch identifier="bypass" inverted={true} />
            </Flex>
          </Layout.Content>
        </Layout>
      );
    };
    
    export default App;
    
    

    2024-08-20-22-29-49

  8. componentsフォルダを作成し、中にJuceCombobox.tsx、JuceSlider.tsx、JuceTextbox.tsx、JuceToggleSwitch.tsxを作成。まずはJuceCombobox.tsxから作っていきます。以下のコードをコピペしてJuceCombobox.tsxに貼り付けてください。

    import { FC, useEffect, useState } from "react";
    import { Select } from "antd";
    // @ts-expect-error Juce does not have types
    import * as Juce from "juce-framework-frontend";
    
    interface JuceComboboxProps {
      identifier: string;
    }
    
    const JuceCombobox: FC<JuceComboboxProps> = ({ identifier }) => {
      const comboboxState = Juce.getComboBoxState(identifier);
      const [value, setValue] = useState<number>(comboboxState.getChoiceIndex());
    
      const changeJUCEParamValue = (newValue: number) => {
        comboboxState.setChoiceIndex(newValue);
      };
    
      useEffect(() => {
        const updateWebViewValue = () => {
          setValue(comboboxState.getChoiceIndex());
        };
    
        const valueListenerId =
          comboboxState.valueChangedEvent.addListener(updateWebViewValue);
    
        return () => {
          comboboxState.valueChangedEvent.removeListener(valueListenerId);
        };
      }, [comboboxState]);
    
      return (
        <Select
          value={value}
          style={{ width: "100%" }}
          onChange={(v) => {
            changeJUCEParamValue(v as number);
          }}
          options={comboboxState.properties.choices.map(
            (choice: string, index: number) => ({
              label: choice,
              value: index,
            })
          )}
        />
      );
    };
    
    export default JuceCombobox;
    

    2024-08-20-22-35-31

    コードの説明

    JUCEとの連携の肝の部分なので説明します。JUCEはJUCE Frontend Libraryを提供し、JUCEの各種パラメータを簡単に扱えるようにしています。プラグイン開発者はJUCE Frontend Libraryを利用して各種UIパーツを作っていくことになります。JUCE Frontend Libraryはまだ発展途上の印象が強いため、コード本体を読みながら、足りない機能は適宜自力で実装していくことが求められます。JuceCombobox.tsxではコンボボックスを作成し、Choiceできるようなパラメータを操作できるようにしています。


    コードの流れを追っていきましょう。

      const comboboxState = Juce.getComboBoxState(identifier);
      const [value, setValue] = useState<number>(comboboxState.getChoiceIndex());
    

    このReactコンポーネントはidentifierを受け取り、そのidentifierに対応したパラメータの操作を実現します。identifierとはjuce::AudioProcessorValueTreeStateでパラメータを定義するときに使ったpanRuleとかgainとかのことです。まず、identifierを元にコンボボックスの状態を取得します。続いて、コンボボックスの現在の選択肢のインデックスをReactの状態として保持します。useStateを使って、valueに現在の選択肢のインデックスを保存し、その値を更新するためのsetValue関数も定義します。

      const changeJUCEParamValue = (newValue: number) => {
        comboboxState.setChoiceIndex(newValue);
      };
    

    このchangeJUCEParamValue()関数は、ユーザーが新しい値を選択したときに呼び出され、JUCEのコンボボックスの値を更新する関数です。

      useEffect(() => {
        const updateWebViewValue = () => {
          setValue(comboboxState.getChoiceIndex());
        };
    
        const valueListenerId =
          comboboxState.valueChangedEvent.addListener(updateWebViewValue);
    
        return () => {
          comboboxState.valueChangedEvent.removeListener(valueListenerId);
        };
      }, [comboboxState]);
    

    useEffectを使って、プラグイン側でパラメータが変更されたときにUI側も更新する処理を行っています。

    返り値の部分は、パラメータの現在値を表示し、ユーザーが操作したらchangeJUCEParamValue()を実行した値を更新するという機能を実装しています。

  9. 次にJuceSlider.tsxを実装します。下記のコードをコピペしてください。

    import { FC, useEffect, useState } from "react";
    import { Slider } from "antd";
    // @ts-expect-error Juce does not have types
    import * as Juce from "juce-framework-frontend";
    
    interface JuceSliderProps {
      identifier: string;
      isVertical?: boolean;
    }
    
    const JuceSlider: FC<JuceSliderProps> = ({
      identifier,
      isVertical = false,
    }) => {
      const sliderState = Juce.getSliderState(identifier);
      const [value, setValue] = useState<number>(sliderState.getNormalisedValue());
    
      const changeJUCEParamValue = (newNormalisedValue: number) => {
        sliderState.setNormalisedValue(newNormalisedValue);
      };
    
      useEffect(() => {
        const updateWebViewValue = () => {
          setValue(sliderState.getNormalisedValue());
        };
    
        const valueListenerId =
          sliderState.valueChangedEvent.addListener(updateWebViewValue);
    
        return () => {
          sliderState.valueChangedEvent.removeListener(valueListenerId);
        };
      }, [sliderState]);
    
      const style = isVertical ? {} : { width: "100%" };
    
      return (
        <Slider
          vertical={isVertical}
          min={0}
          max={1}
          step={0.001}
          value={value}
          onChange={(v) => {
            changeJUCEParamValue(v);
          }}
          tooltip={{ open: false }}
          style={style}
        />
      );
    };
    
    export default JuceSlider;
    

    2024-08-20-23-03-06

    コードの解説

    処理の内容はJuceCombobox.tsxと共通している部分が多いです。identifierを受け取り、そのidentifierに対応したパラメータの操作を実現します。追加でisVerticalというオプションの引数をとっていますが、これはスライダーの向きを調整するものです。

      const sliderState = Juce.getSliderState(identifier);
      const [value, setValue] = useState<number>(sliderState.getNormalisedValue());
    

    まず、identifierを元にスライダーの状態を取得します。続いて、スライダーの現在の値(0~1で正規化済み)をReactの状態として保持します。useStateを使って、valueに現在の値を保存し、その値を更新するためのsetValue関数も定義します。

      const changeJUCEParamValue = (newNormalisedValue: number) => {
        sliderState.setNormalisedValue(newNormalisedValue);
      };
    

    changeJUCEParamValue()関数は、ユーザーが新しい値を設定したときに呼び出され、JUCEの値を更新する関数です。

      useEffect(() => {
        const updateWebViewValue = () => {
          setValue(sliderState.getNormalisedValue());
        };
    
        const valueListenerId =
          sliderState.valueChangedEvent.addListener(updateWebViewValue);
    
        return () => {
          sliderState.valueChangedEvent.removeListener(valueListenerId);
        };
      }, [sliderState]);
    

    ここの処理はJuceCombobox.tsxと変わりません。プラグイン側でパラメータが変更されたときにUI側も更新する処理を行っています。

      return (
        <Slider
          vertical={isVertical}
          min={0}
          max={1}
          step={0.001}
          value={value}
          onChange={(v) => {
            changeJUCEParamValue(v);
          }}
          tooltip={{ open: false }}
          style={style}
        />
      );
    

    返り値の部分ですが、ここでminとmaxを設定していることに注目してください。JUCEにおけるパラメータの値としては、例えばgainであれば-100~10で、panAngleであれば-100~100ですが、0~1の範囲で正規化した値を受け取っているので、これをそのまま表示できるようにminとmaxを設定しています。

    今回は実装を省略しちゃってるのですが...スライダーのドラッグ開始時と終了時にはsliderState.sliderDragStarted()sliderState.sliderDragEnded()を呼ぶ必要があります。

  10. 次にJuceTextbox.tsxを実装します。下記のコードをコピペしてください。

    import { FC, useEffect, useState } from "react";
    import { InputNumber } from "antd";
    // @ts-expect-error Juce does not have types
    import * as Juce from "juce-framework-frontend";
    
    interface JuceTextboxProps {
      identifier: string;
      digits?: number;
      suffix?: string;
    }
    
    const JuceTextbox: FC<JuceTextboxProps> = ({
      identifier,
      digits = 2,
      suffix = "",
    }) => {
      const sliderState = Juce.getSliderState(identifier);
      const [value, setValue] = useState<string>(
        sliderState.getScaledValue().toFixed(digits)
      );
      const [tempValue, setTempValue] = useState<string>(
        sliderState.getScaledValue()
      );
      const [isFocused, setIsFocused] = useState(false);
    
      const changeJUCEParamValue = (newScaledValue: number) => {
        const newNormalisedValue = Math.pow(
          (newScaledValue - sliderState.properties.start) /
            (sliderState.properties.end - sliderState.properties.start),
          sliderState.properties.skew
        );
        sliderState.setNormalisedValue(newNormalisedValue);
      };
    
      useEffect(() => {
        const updateWebViewValue = () => {
          setValue(sliderState.getScaledValue().toFixed(digits));
        };
    
        const valueListenerId =
          sliderState.valueChangedEvent.addListener(updateWebViewValue);
    
        return () => {
          sliderState.valueChangedEvent.removeListener(valueListenerId);
        };
      }, [sliderState, digits]);
    
      return (
        <InputNumber<number>
          suffix={suffix}
          style={{ width: "100%" }}
          id={"input"}
          controls={false}
          value={isFocused ? parseFloat(tempValue) : parseFloat(value)}
          onChange={(v) => {
            if (v !== null) {
              if (isFocused) {
                setTempValue(v.toFixed(digits));
              }
            }
          }}
          onFocus={(event) => {
            setTempValue(value);
            setIsFocused(true);
            setTimeout(() => {
              event.target.select();
            }, 100);
          }}
          onBlur={() => {
            const newValue = parseFloat(tempValue);
            if (!isNaN(newValue)) {
              changeJUCEParamValue(newValue);
            }
            setIsFocused(false);
          }}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.currentTarget.blur();
            }
          }}
        />
      );
    };
    
    export default JuceTextbox;
    

    2024-08-20-23-45-12

    コードの解説

    JUCEとの連携部分はJuceSlider.tsxと共通している部分が多いですが、その他の部分はUX向上の為に色々改良が入っています。Ant DesignのInputNumberコンポーネントをそのまま使った状態だと、いくら入門編だとはいえ流石に完成品の使いにくさが目立ってしまうので、最低限何とかしました。
    identifierを受け取り、そのidentifierに対応したパラメータの操作を実現します。追加でdigitssuffixというオプションの引数をとっており、digitsは表示する小数点以下の桁数、suffixは数値の後に表示する文字列です。

      const sliderState = Juce.getSliderState(identifier);
      const [value, setValue] = useState<string>(
        sliderState.getScaledValue().toFixed(digits)
      );
      const [tempValue, setTempValue] = useState<string>(
        sliderState.getScaledValue()
      );
      const [isFocused, setIsFocused] = useState(false);
    

    スライダーの情報をuseStateを使って保存するところはJuceSlider.tsxと同じです。今回はTextBoxであり、表示する値は正規化されてない値が望ましいです。そのため、JuceSlider.tsxとは異なり、getScaledValue()という関数で値を呼び出しています。tempValueisFocusedはテキストボックスがユーザーによって編集中の時に値を保持する変数です。値の編集が終わってからJUCE側のパラメータを更新してほしいので、これらの変数を用意しています。

      const changeJUCEParamValue = (newScaledValue: number) => {
        const newNormalisedValue = Math.pow(
          (newScaledValue - sliderState.properties.start) /
            (sliderState.properties.end - sliderState.properties.start),
          sliderState.properties.skew
        );
        sliderState.setNormalisedValue(newNormalisedValue);
      };
    

    changeJUCEParamValue()関数は、ユーザーが新しい値を設定したときに呼び出され、JUCEの値を更新する関数ですが、今までとは違って少し複雑です。というのも、ユーザーはテキストボックスに正規化されてない値を入力してくるからです。JUCE Frontend LibraryはsetNormalisedValue()は実装してあるのにsetScaledValue()は実装してないので、こちらで正規化する必要があります。

    useEffectフックの内容は他と同じです。

      return (
        <InputNumber<number>
          suffix={suffix}
          style={{ width: "100%" }}
          id={"input"}
          controls={false}
          value={isFocused ? parseFloat(tempValue) : parseFloat(value)}
          onChange={(v) => {
            if (v !== null) {
              if (isFocused) {
                setTempValue(v.toFixed(digits));
              }
            }
          }}
          onFocus={(event) => {
            setTempValue(value);
            setIsFocused(true);
            setTimeout(() => {
              event.target.select();
            }, 100);
          }}
          onBlur={() => {
            const newValue = parseFloat(tempValue);
            if (!isNaN(newValue)) {
              changeJUCEParamValue(newValue);
            }
            setIsFocused(false);
          }}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.currentTarget.blur();
            }
          }}
        />
      );
    
    

    返り値の部分では何やらごちゃごちゃしています。ユーザーの編集が終わったらJUCE側のパラメータを更新するようにするため、このようなことになっています。また、デフォルトのInputNumberだとEnterキーを押しても入力が確定しないため、そのあたりの処理も追加しています。

  11. 最後にJuceTogglebox.tsxを実装します。下記のコードをコピペしてください。

    import { FC, useEffect, useState } from "react";
    import { Switch } from "antd";
    // @ts-expect-error Juce does not have types
    import * as Juce from "juce-framework-frontend";
    
    interface JuceToggleSwitchProps {
      identifier: string;
      inverted?: boolean;
    }
    
    const JuceToggleSwitch: FC<JuceToggleSwitchProps> = ({
      identifier,
      inverted = false,
    }) => {
      const toggleState = Juce.getToggleState(identifier);
      const [value, setValue] = useState(toggleState.getValue());
    
      const changeJUCEParamValue = (newValue: boolean) => {
        toggleState.setValue(newValue);
      };
    
      useEffect(() => {
        const updateWebViewValue = () => {
          setValue(toggleState.getValue());
        };
    
        const valueListenerId =
          toggleState.valueChangedEvent.addListener(updateWebViewValue);
    
        return () => {
          toggleState.valueChangedEvent.removeListener(valueListenerId);
        };
      }, [toggleState, inverted]);
    
      return (
        <Switch
          value={inverted ? !value : value}
          onChange={(v) => changeJUCEParamValue(inverted ? !v : v)}
        />
      );
    };
    
    export default JuceToggleSwitch;
    

    2024-08-21-09-33-18

    コードの解説

    ここまで来れば、このコードを読むだけで何をやってるのかわかると思います!腕試しとして読んでみることをおすすめします!
    処理内容は他のコンポーネントと大きく変わりません。オプションの引数としてinvertedを受け取っていますが、これはbypassスイッチのように、Trueの時は電源Off、Falseの時は電源OnであるようなUI表示を実現したいときにtrueを渡します。

これでUI部分の実装はおしまいです!!!!プラグインの画面を見るとかなりそれっぽくなってるはずです!!!!

微調整

AudioPluginHost.exeでのパラメータ表示(つまりプラグイン内部の値)と、UI側のパラメータ表示を見比べると、微妙に数字が違います。panRuleはUI側で整数値として表示し、gainは小数点以下2桁として表示しているので、このような差が生まれました。

また、スライダーのメモリが等間隔に並んでいるせいで、gainの操作が少しやりにくいですね。このあたりを修正します。

再びVisual Studioを開き、PluginProcessor.hを修正します。下記のコードを参考に、juce::NormalisableRangeに第3・第4引数を追加してください。第3引数は、パラメータが取り得る値の間隔を示し、第4引数は値の分布を調整するSkew Factorです。

// PluginProcessor.h
(省略)

  void getStateInformation(juce::MemoryBlock& destData) override;
  void setStateInformation(const void* data, int sizeInBytes) override;

  //==============================================================================
  juce::AudioProcessorValueTreeState parameters{
      *this,
      nullptr,
      juce::Identifier("PARAMETERS"),
      {
          std::make_unique<juce::AudioParameterFloat>(
              "gain", "gain",
              juce::NormalisableRange<float>(-100.0f, 10.0f, 0.01f, 2), 0.0f), // 修正
          std::make_unique<juce::AudioParameterFloat>(
              "panAngle", "panAngle",
              juce::NormalisableRange<float>(-100.0f, 100.0f, 1.0f), 0.0f), // 修正
          std::make_unique<juce::AudioParameterChoice>(
              "panRule", "panRule", juce::StringArray("linear", "balanced"), 1),
          std::make_unique<juce::AudioParameterBool>("bypass", "bypass", false),
      }};

  juce::AudioProcessorParameter* getBypassParameter() const {
    return parameters.getParameter("bypass");
  }


(省略)

2024-08-21-10-17-02

ビルドして試してみましょう!いい感じです!!!!!!!!!!!!!!やったーー!!!!
20240821_1039_animation

リリース版のビルド

いよいよ総仕上げです。今まで何回かビルドを行ってきましたが、実はそれらはDebugビルドでした。デバッグに必要なあれやこれやがいっぱい詰まっているのでファイルサイズは大きく、最適化もあまりされていません。Releaseビルドを行うことでファイルサイズが小さくなり、パフォーマンスも良くなります。また、UI部分もプラグインに埋め込む必要があります。そのあたりをやっていきましょう。

  1. まずWebViewで表示しているページをビルドし、ソースをZipファイルにします。起動しているViteの開発サーバーを停止してください。VSCodeのターミナルを選択し、Ctrl+Cで停止できます。
    2024-08-21-11-02-27

  2. npm run buildを実行します。黄色い文字で色々warningが出るかもしれませんが気にしないでください。最終的に緑色の文字でbuilt in XX.XXsみたいな感じのものが表示されたらビルド成功です
    2024-08-21-11-07-51

  3. Zipファイルを作っていきます。エクスプローラーでプロジェクトのフォルダを開き、webviewフォルダを開いてください。
    2024-08-21-11-14-51

  4. 開くと、新たにdistというフォルダができているのでこれを選択して開きます。
    2024-08-21-11-16-07

  5. 中身を全選択してZipファイルに圧縮します。ファイル名はassets.zipです。
    2024-08-21-11-19-49
    2024-08-21-11-22-03

  6. できたassets.zipをプロジェクトルートに切り取って貼り付けます。すると、以前作ったダミーのファイルと置き換えるかどうか聞かれるので、ファイルを置き換えます。
    2024-08-21-11-26-47

  7. Visual Studioを閉じて、もう一度Projucerから起動します。これでVisual Studioに読みこまれるassets.zipのバイナリが更新されます。

  8. PluginEditor.cppを開き、JUCEが開くべきWebページのリンクを切り替えます。続いて、ビルドの構成をReleaseにします。
    2024-08-21-12-51-44

  9. あとはいつも通りビルドしましょう!
    2024-08-19-13-52-04-2

  10. ビルドに成功したら、Builds → VisualStudio2022 → x64 → Release → VST3 → GainPanTutorial.vst3 → Contents → x86_64-win とフォルダをクリックして開きましょう。『GainPanTutorial.vst3』というファイルがあるはずです!これがReleaseビルドの生成物となります。それをAudioPluginHost.exeにドラッグ&ドロップし、動作確認を行ってください!
    2024-08-21-13-04-20

DebugビルドとReleaseビルドの比較

Debugビルドはデバッグに必要なあれやこれやが詰まっており、Releaseビルドは色々最適化されているという話をしました。具体的に見てみましょう!まずはサイズから。
下図、DEBUGとついているビルドがDebugビルドで生成したVSTで、何もついてない方がReleaseビルドで生成したVSTです。Releaseビルドの方がDebugビルドに比べてサイズが3分の1程度になっていることが分かります。
2024-08-21-13-09-13

パフォーマンスについても見てみましょう。processBlockを実行するのにどのくらいの時間がかかるかというのをバッファサイズ毎に調べ、平均をとったグラフが下図です。グラフを見ると、Debugビルドに比べてReleaseビルドの方が実行時間が5分の1程度になっていることが分かります。最適化の力は偉大ですね。
2024-08-21-13-53-33

様々な最適化を行っているため、DebugビルドとReleaseビルドはアセンブリコードが違います。また、プログラムのクラッシュを検出できるようDebugビルドは余分なメモリを確保したり、初期化されてない変数を初期化したりといった処理を行っていますが、Releaseビルドはそのようなことはしていません。その結果、Debugビルドでは正しく動くのに、Releaseビルドだと動かないということもおきます。

開発Tips

本編では触れられなかった、プラグイン開発で起きがちな要求への対処法をいくつか書いておきます。参考にしてください!

プログラムがクラッシュする原因を知りたい(デバッグ)

一発で正しいコードを書くのは人間には不可能です。書いたコードには多くの場合バグがあります。このバグを取り除く作業をデバッグと言います。Visual Studioにはデバッグに役立つ機能があるので、その一部を紹介します。
プラグイン開発中によくあるバグが、「プラグインをロードするとクラッシュする」というバグです。めちゃめちゃ致命的なので何とかする必要があります。今回は意図的にそれを起こし、デバッグの練習をしてみましょう。

  1. 完成したプログラムの一部を書き換えます。PluginProcessor.hのgetRawParameterValue()に引数を渡す際に間違えてgaainにしてしまった場合を想定します。gaainに書き換えてください。そしてビルド構成をDebugにし、ビルドしてください。
    2024-08-21-14-22-58

  2. ビルド生成物をAudioPluginHost.exeにドラッグ&ドロップしてみましょう!何も言わずにAudioPluginHost.exeがクラッシュして終了すると思います。

  3. それではデバッグしていきます。まずAudioPluginHost.exeを起動します。まだプラグインは読みこまないでください。続いて、Visual Studioのメニューバーからデバッグ → プロセスにアタッチ をクリックします。
    2024-08-21-14-32-52

  4. アタッチするプロセスを選択するウィンドウが出てくるので、AudioPluginHost.exeを探してアタッチします。
    2024-08-21-14-36-19

  5. なにやらウィンドウの見た目が変わるので、この状態で、クラッシュするVSTプラグインをAudioPluginHost.exeにドラッグ&ドロップしてみましょう。するとVisual Studioの画面が概ねこんな感じになるはずです。下の方にある『呼び出し履歴』を選択します。
    2024-08-21-14-39-47

  6. 『呼び出し履歴』では、プログラムがクラッシュする直前までにどの関数が実行されていたのかを見ることができます。一番上が最新で、下に行くほど呼び出し元をたどることになります。この呼び出し履歴を見る限り、プログラムがクラッシュした直接の原因はstd::_Atomic_Storage::load()関数で値を読み取ろうとしたら、読み取り先がnullptrだったということになります。そして、std::_Atomic_Storage::load()関数を呼んだのは、std::atomic::operator float()関数。更にそのstd::atomic::operator float()関数を呼んだのは、JUCEプラグインのprepareToPlay()のようです。この行をダブルクリックしてみましょう。
    2024-08-21-14-48-35

  7. 自分がいつか書いたコードに飛びます。どうやら、gainSmoothed.setCurrentAndTargetValue(*gain);を実行したらプログラムがクラッシュしたようです。画面下の方に、この行を実行時に各変数がどのような値だったのかを表示する領域があります。これを見ると*gainがNULLになっていることが分かります。
    2024-08-21-14-55-28

  8. 他の同種の変数の値がどのようになっているのかを見てみましょう。コードエディタの方で、変数にマウスポインタをホバーするとその変数の値が表示されます。確認してみると、*gainだけがNULLで他の*panAngle*bypassはNULLではない値が入ってることが分かります。これは明らかにもう*gainが悪いですね
    20240821_1502_debug

  9. というわけで*gainの定義を戻って見てみると、綴りミスをしていることに気が付いて「あ~~~~」ってなるわけです。

  10. デバッグモードを終了するには、上のほうにある停止ボタンを押せばよいです。
    2024-08-21-15-06-23

デバッグをするために任意の情報をログに出力したい

DBG()を使うことで実現できます。DBG()を書いてDebugビルドを行い、プロセスにアタッチすることで出力タブにメッセージが出力されます。
2024-08-21-15-22-04

プログラムが特定の行に到達した時の状態が知りたい

DBG()を使って全部出力させても良いですが、ちょっと大変です。そこで、ブレークポイントを設定します。プログラム実行時、処理する行がブレークポイントを設定した行に到達したら停止するようになります。

  1. プログラムを一時停止させたい行の位置で、下図の赤四角領域をクリックします。すると、クリックしたところに赤丸が表示されます。このままDebugビルドを行います。
    2024-08-21-15-26-09

  2. AudioPluginHost.exeを実行してアタッチ。その後、VSTプラグインをドラッグ&ドロップします。プログラムが指定行に到達すると停止し、Visual Studioでその行実行時の情報が表示されます。
    2024-08-21-15-39-34

WebViewでノブ型の操作子を実現するには?

よくあるReactのUIコンポーネントにスライダーは必ず存在するのですが、ノブはめったにありません。ノブを実装したい場合はp5.jsを使ってCanvasに描画するのが良いと思います。
Reactでp5.jsを書くときはP5-wrapperがおすすめです。『react p5』でググるとよく日本語記事で紹介されるreact-p5というライブラリは既に開発が止まっているので使わないようにしましょう。

ビジュアライザを実装したい

処理の具体的な流れとして、以下のようなものが考えられます。

  1. JUCEのProcessBlockで受け取ったオーディオバッファを元に、UIで描画させたい情報を生成。数フレーム分保持。
  2. 保持したフレームがある程度(5フレームとか)たまったら、WebView側に準備完了を伝える。この部分のコードは下のように書けます。
    // JUCE → webviewへの命令発火
    webComponent.emitEventIfBrowserIsVisible("frameDataReady", juce::var{});
    
  3. WebView側で準備完了を受け取ったら、JUCE側にフレームデータをJSONで要求します。この部分のコードは下のように書けます。
    const id = window.__JUCE__.backend.addEventListener("frameDataReady", () => {
      fetch(Juce.getBackendResourceAddress("frameData.json"))
        .then((response) => response.json())
        .then((data) => {
          frameData = data; // この外で定義してある変数に値を保持
          // この外で、frameDataを元にビジュアライザを描画
        });
    });
    
  4. JUCEがJSONを生成して送信する部分はgetResource()関数内に書けます。こんな感じです。
    (省略)
    
      if (urlToRetrive == "frameData.json") {
        const auto jsonStr = convertFrameDataToJson(); // JUCE側が保持するフレームデータをJSONに書き換える関数。新たに実装が必要。
        juce::MemoryInputStream stream{jsonStr.c_str(), jsonStr.size(), false};
    
        std::vector<std::byte> result((size_t)stream.getTotalLength());
        stream.setPosition(0);
        stream.read(result.data(), (int)result.size());
        return juce::WebBrowserComponent::Resource{
            std::move(result), juce::String{"application/json"}};
      }
    
    (省略)
    

convertFrameDataToJson()を実装する際はjuce::JSONを使うのが手っ取り早いです。しかし、データ数が増えてくるとjuce::JSON::toString()関数の遅さが目立ってくるので、その時は別のライブラリを使うべきです。とにかく早さにこだわって作られたライブラリであるyyjsonを使うのが良いと思います。

またWebViewでのビジュアライザの描画は、描画領域が小さく、内容が簡単な場合はp5.jsP5-wrapperを使うのが良いでしょう。描画領域が大きかったり、リサージュメーターのようなパーティクル状のものを描画する場合はPixiJSを推奨します。

PixiJSにはReactPixiというラッパーがあるのですが、個人的には使いにくさを感じたので、普通に生PixiJSで実装したほうが楽だと思います。

プラグインがアクティブなとき、DAWのショートカットキーが効かない

WebViewがアクティブなとき、WebViewが全てのキー入力イベントを消化してしまうためこのようなことが起こります。これはWebView(ブラウザ)の仕様なので完全な解決は簡単にはできません。
そこで、スペースキー押下のように一部の重要なキーイベントだけWebViewで検知し、直接Win32 APIを叩くことで当座をしのぎます。

まず、WebViewでスペースキーの押下を検知するコードは次の通りです。

  useEffect(() => {
    const pressSpaceKey = Juce.getNativeFunction("pressSpaceKey");
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.code === "Space") {
        pressSpaceKey().then(() => {
          console.log("Space key pressed");
        });
      }
    };
    window.addEventListener("keydown", handleKeyDown);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

JUCE側で受け取ってWin32 APIを叩くコードは次のような感じです。

// ヘッダーでWindows.hを定義
#define NOMINMAX
#include "Windows.h"

(省略)

  // webComponentの定義時、withNativeFunctionを設置
  SinglePageBrowser webComponent{
      juce::WebBrowserComponent::Options{}

          (省略)

          .withNativeFunction("pressSpaceKey",
                              [this](auto &var, auto complete) {
                                DBG("pressSpaceKey");
                                // get window handle
                                auto hwnd = webComponent.getWindowHandle();

                                // setfocus
                                SetFocus(static_cast<HWND>(hwnd));

                                INPUT input;
                                input.type = INPUT_KEYBOARD;
                                input.ki.wVk = VK_SPACE;
                                SendInput(1, &input, sizeof(INPUT));
                                input.ki.dwFlags = KEYEVENTF_KEYUP;
                                SendInput(1, &input, sizeof(INPUT));

                                complete({});
                              })
          .withResourceProvider(
              [this](const auto &url) { return getResource(url); },
              juce::URL{"http://localhost:5173/"}.getOrigin())};

(省略)

フォーカスをwebComponentの外に移して、キーイベントを送信しています。

終わり!!

終わり!!!!!ものすごく長い記事でした!お疲れ様でした!!!!!!
この記事が、JUCEでVSTプラグインを作りたいと思っている人の助けになれば幸いです。
もし助けになったのであれば、是非SNSとかでこの記事をシェアしてくれると助かります!!!

そしてもしよければ、曲を聴いたり買ったりしていってね!よろしくお願いします!!

かしわで音楽工房 - BOOTH
KashiwadeがM3等で頒布したもののデジタル版を販売するストアです! Conquest(¥ 1,700), Rainy Resolutions(¥ 1,700), Rainy Resolutions Piano Score(¥ 0), Chronicles of Luna(¥ 1,200), 天光玲瓏・花舞う都の空 (パラデータ・MIDI・プロジェクトファイル)(¥ 1,100), 天光玲瓏(¥ 1,700), 【会場カード購入者向け】天光玲瓏・花舞う都の空 (パラデータ・MIDI・プロジェクトファイル)(¥ 0), Aquilegia(¥ 1,100)
Kashiwade
Composer

明日のブログリレー担当者は@tobuhitodesu @imoimo @liquid1224 です!お楽しみに!  

kashiwade icon
この記事を書いた人
kashiwade

そこのお前!!DTMをしよう!!!

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2024年9月17日
1か月でゲームを作った #BlueLINE
Komichi icon Komichi
2023年4月25日
【驚愕】作曲4年目だった男が大学3年間ゲームサウンドに関わった末路...【ゲームサウンドのお仕事について】
tenya icon tenya
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2022年9月16日
5日でゲームを作った #tararira
Komichi icon Komichi
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記