feature image

2022年4月19日 | ブログ記事

【入門】JUCEを使ってVSTプラグインを作ろう!!

この記事は新歓ブログリレー2022 / 40日目の記事です

こんにちは、19Bのカシワデです。普段はオーケストラ系の曲を作ってます。

TwitterSoundcloudもよろしくね!!!!!!

さてさて、皆さまいかがお過ごしでしょうか。近くスプリングセールやサマーセールなども行われるであろう中、DTMerの敵である円安はめちゃめちゃに進み、とてもじゃないがプラグインは買えないよ~~~と言ってる頃かと思います。

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

「あぁ…もしも自分が理想とするプラグインを無料で作れたら、こんなにも散財することはないのになぁ」

その思いを叶えてしまおう、というのが本記事の目的となっています!

さて

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

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

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

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

対象

JUCEのダウンロード

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

https://juce.com/get-juce
upload_e0b6967941dfab02e00f58b2e9541bac

自身のOSにあったものを選んでください。
upload_153a14245e9cb5cef4d25d7de65b236c
ダウンロードしたら展開しましょう。
多分展開したら以下のディレクトリ構成になるんじゃないかと思います。

- JUCE
---- .github
---- docs
---- examples
---- extras
---- modules
---- .gitignore
---- .gitlab-ci.yml
---- ....

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

Visual Studioのダウンロード

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

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

正しくインストールできたか確認

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

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

手順

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

『Open Existing Project...』をクリックして
upload_1dcec20fa40854a5fa80c9ba9f54f6e5

先ほどJUCEをインストールしたフォルダを開いて『extras』フォルダを開き、
upload_8a2e882ea18c109b76500c47cecdd2c5

更にその下にある『AudioPluginHost』を開いて、
upload_c5102c02f0a7024f3774581e38fe63db

『AudioPluginHost.jucer』を選択して『開く』をクリック。
upload_83ca47ec40d3c4c868bb336ed2ba2c24

Projucerがこんな感じになれば大丈夫です。
upload_fd1d26826a899e7dbbb3305ffcf1dab5

右上のVisual Studioのアイコンをクリックしてください。
upload_618ef096ad478406c1fc28e21f6b3301

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

画面下側に
========== ビルド: 1 正常終了、0 失敗、0 更新不要、0 スキップ ==========
が表示されたらビルド成功です!

生成物の起動

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

そこにある『AudioPluginHost.exe』をダブルクリックしてこうなったら完璧です!
(環境によって緑色のやつの本数が違ったりするかもしれません)
upload_c00591e67e641169b8aed1b047a75f5f

右クリックすることでプラグインを追加できます。

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

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

プロジェクトの作成

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

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

  2. 左側のメニューバーから『Plug-In』→『Basic』を選んで、『Project Name』に適当に入力した後『Create Project』をクリックします。
    upload_521bca52c587621c4eb84375b6ba318c

  3. そうするとエクスプローラが表示されるので、わかりやすいところにフォルダを作って『フォルダーの選択』を押しましょう。
    Document以下にJUCEフォルダーを作って、それを選択するのがわかりやすくて良いと思います。

  4. こんな画面になったら成功です
    upload_9b60f4d71f6f2d897419c9d21485253c

パラメータの追加

まずは必要なパラメータの検討を行います。
今回製作するプラグインが持つ機能はGainとPanで、信号処理部分の実装を楽するためにdsp::Gaindsp::Pannerモジュールを使いたいと思います。
ドキュメントを見るとdsp::Gainはデシベル単位で音量を調整できるようなので、Float型のパラメーターgainを用意することにしましょう。
dsp::Pannerはパンルールも選択できるようです。DAWによってデフォルトのパンルールも違う可能性があるので、プラグインのパンルールも選択できた方が嬉しいかもしれません。ChoiceできるようなパラメーターpanRuleを用意します。
そしてパンの度合いについては、-1~1の範囲で指定できるようです。Float型のパラメーターpanAngleを用意しましょう。ただ実際に操作するときにパンの値が小数で表示されるのはちょっと慣れないですね。どうにかする必要がありそうです。

  1. いよいよコーディングを行っていきます。右上の『Visual Studio』をクリックしましょう
    upload_f9e5f96db506830e231fa70c95c331f3

  2. パラメータの追加にはAudioProcessorValueTreeStateクラスを用いると便利なのでそれを使います。
    右側のソリューションエクスプローラからGain-Pan-tutorial_SharedCode->Gain-Pan-tutorial->Source->PluginProcessor.hを選択します
    upload_1353bc51d6221a6af32e3ee3cbdcd063

  3. PluginProcessor.hのプライベート変数を宣言します

     (省略)
        void getStateInformation (juce::MemoryBlock& destData) override;
        void setStateInformation (const void* data, int sizeInBytes) override;
    
    private:
        juce::AudioProcessorValueTreeState parameters; // 追加
    
        std::atomic<float>* gain = nullptr; // 追加
        std::atomic<float>* panRule = nullptr; // 追加
        std::atomic<float>* panAngle = nullptr; // 追加
        //==============================================================================
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GainPantutorialAudioProcessor)
    };
    

    upload_b6623f550655af76de3a2dbbe7ea73f0

  4. PluginProcessor.cppにてパラメータの中身を書いていきます。parametersクラスのコンストラクタに初期引数を渡してるというイメージです。右側のソリューションエクスプローラからPluginProcessor.cppを選択してパラメータを追加しましょう。

    (省略)
    #endif
        , parameters(*this, nullptr, juce::Identifier("tutorial"), // 追加
            {// 追加
                std::make_unique<juce::AudioParameterFloat>(
                    "gain",  // 追加
                    "Gain",  // 追加
                    juce::NormalisableRange<float>(-100.0f, 10.0f), // 追加
                    0.0f), // 追加
                std::make_unique<juce::AudioParameterChoice>(
                    "panrule", "Pan Rule",
                    juce::StringArray("linear", "balanced", "sin3dB", "sin4p5dB",
                                        "sin6dB", "squareRoot3dB",
                                        "squareRoot4p5dB"),
                    1),
                std::make_unique<juce::AudioParameterFloat>(
                    "panangle",  // 追加
                    "Pan Angle",  // 追加
                    juce::NormalisableRange<float>(-100.0f, 100.0f),// 追加
                    0.0f),// 追加
            })// 追加
    {
    }
    
    GainPantutorialAudioProcessor::~GainPantutorialAudioProcessor()
    (省略)
    

    upload_323e175a736cc9034764dd59f3f9576f

    今回使ったAudioParameterFloatとAudioParameterChoiceの指定方法について説明したいと思います。
    基本的にはドキュメント見た方が早いです。

    upload_7ac81992a58b1e7fc24598cd020aed05


    upload_1615e6182c8408058b0c3e6f07165d8c

  5. パラメーターの紐づけを行います。パラメータの値をただ読み取りたいだけなら、parameters.getParameterAsValue("gain").getValue()とやるよりもここで紐づけを行う*gainの値を読んだ方が簡単で速いです。

    (省略)
    
                    std::make_unique<juce::AudioParameterFloat>(
                    "panangle", 
                    "Pan Angle", 
                    juce::NormalisableRange<float>(-100.0f, 100.0f),
                    0.0f),
            })
    {
        gain = parameters.getRawParameterValue("gain"); // 追加
        panAngle = parameters.getRawParameterValue("panangle"); // 追加
        panRule = parameters.getRawParameterValue("panrule"); // 追加
    }
    
    (省略)
    

    upload_66ab6a103e2e1ee6413a079e63de1872

  6. UI側にもparametersを渡しておきたいので渡します。この時点ではエラーが出ても大丈夫です。

    (省略)
    
    juce::AudioProcessorEditor* GainPantutorialAudioProcessor::createEditor()
    {
        return new GainPantutorialAudioProcessorEditor (*this, parameters); // parametersを追加
    }
    
    (省略)
    

    upload_479e2d23aff7dfda89460f0fd345808c

  7. UI側のコードをいじります。まずは右側のソリューションエクスプローラからPluginEditor.hを開いてコンストラクタに引数を追加します。

    (省略)
    
    class GainPantutorialAudioProcessorEditor  : public juce::AudioProcessorEditor
    {
    public:
        GainPantutorialAudioProcessorEditor (GainPantutorialAudioProcessor&, 
            juce::AudioProcessorValueTreeState& vts); // juce::Audio~vtsを追加
        ~GainPantutorialAudioProcessorEditor() override;
    
        //==============================================================================
        void paint (juce::Graphics&) override;
        void resized() override;
    
    (省略)
    

    upload_21173c6eb15bd2bf0327ff9bb3fb3234

  8. UI要素(スライダーとか)を追加します。typedefで長いクラス名を省略してます。

    (省略)
    
    private:
        // This reference is provided as a quick way for your editor to
        // access the processor object that created it.
        GainPantutorialAudioProcessor& audioProcessor;
    
        typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment; // 追加
        typedef juce::AudioProcessorValueTreeState::ComboBoxAttachment ComboBoxAttachment; // 追加
    
        juce::AudioProcessorValueTreeState& valueTreeState; // 追加
        juce::Slider gainSlider; // 追加
        std::unique_ptr<SliderAttachment> gainSliderAttachment; // 追加
        juce::Slider panAngleSlider; // 追加
        std::unique_ptr<SliderAttachment> panAngleSliderAttachment; // 追加
        juce::ComboBox panRuleBox; // 追加
        std::unique_ptr<ComboBoxAttachment> panRuleBoxAttachment; // 追加
    
    
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GainPantutorialAudioProcessorEditor)
    };
    

    upload_29ca4ae7e4a92938b553e670af0bb3c6

  9. PluginEditor.cppを開いて、コンストラクタの引数を追加したり、内部クラスのコンストラクタに引数わたしたり、UIとパラメーター要素をくっつけたりします

    (省略)
    
    GainPantutorialAudioProcessorEditor::GainPantutorialAudioProcessorEditor (
        GainPantutorialAudioProcessor& p, juce::AudioProcessorValueTreeState& vts) // valueTreeStateを追加
        : AudioProcessorEditor (&p), audioProcessor (p), valueTreeState(vts) // valueTreeStateを追加
    {
        gainSliderAttachment.reset(new SliderAttachment(valueTreeState, "gain", gainSlider)); // 追加
        addAndMakeVisible(gainSlider); // 追加
        panAngleSliderAttachment.reset(new SliderAttachment(valueTreeState, "panangle", panAngleSlider)); // 追加
        addAndMakeVisible(panAngleSlider); // 追加
        panRuleBox.addItemList(
            juce::StringArray("linear", "balanced", "sin3dB", "sin4.5dB", "sin6dB",
                "sqrt3dB", "sqrt4.5dB"),
            1); // 追加
        panRuleBoxAttachment.reset(new ComboBoxAttachment(valueTreeState, "panrule", panRuleBox)); // 追加
        addAndMakeVisible(panRuleBox); // 追加
            // Make sure that before the constructor has finished, you've set the
            // editor's size to whatever you need it to be.
        setSize(400, 300);
    }
    
    (省略)
    

    (図の方はちょっとコードをミスってます)
    upload_cd18de0502a87b382b0f57945cf6b0be

    addAndMakeVisible(gainSlider);をしないとUIに表示されません。
    今後何か自分でプラグインを作ってて「あれ~表示されないな??」ってなったら真っ先にこれを疑ってください。
    僕は以前、別の所を疑ってしまい時間を溶かした経験があります。

  10. UIを配置します。とりあえず適当においてみます

    (省略)
    
    void GainPantutorialAudioProcessorEditor::resized()
    {
        // This is generally where you'll want to lay out the positions of any
        // subcomponents in your editor..
        gainSlider.setBounds(10, 10, 200, 30); // 追加
        panAngleSlider.setBounds(10, 40, 200, 30); // 追加
        panRuleBox.setBounds(10, 70, 200, 30); // 追加
    
    }
    

    upload_43d4f59cf48fcb91c63dce8ba90f82ce

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

ビルド

メニューバーから『ソリューションのビルド』をクリック!
upload_81bdecc979cb6ec91a4d53224475b208

画面下側に
========== ビルド: 3 正常終了、0 失敗、0 更新不要、0 スキップ ==========
みたいなものが表示されたらビルド成功です!

起動

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

  1. まずは起動しましょう
    upload_f8607b37b4979c8589f0ca39aa69aacf
  2. 上部メニューバーから『Options』→『Edit the List of Available Plug-ins』をクリック
    upload_228e68d68998a1d4c92b142cb512f491
  3. 出てきた画面の一番下にある『Options...』から『Scan for new or updated VST3 plug-ins』をクリック
    upload_ebcf0e75db9f08e1ac5ad215504f43e3
  4. 出てきた画面から、『+』ボタンをクリックしてプロジェクトフォルダを追加しましょう
    upload_7cb34c13d6bb09687e7656f1c4ee3235
  5. 追加したら『Scan』をクリック
  6. Scanが終わったら元の画面にもどって任意のポイントで右クリック。メニューから『yourcompony』→『Gain-pan-tutorial』をクリックしましょう
    upload_589eb71f46d1616ff8b9e10631c08525
  7. こんな感じで読み込まれるはずです
    upload_a6748ce3f43d4f4eb93b71d77f85d12f
  8. ダブルクリックでプラグインが表示されます!!
    upload_bc501207b52afc403fe1caf574001f02
  9. プラグインを右クリックして『Show all parameters』をクリックしましょう。パラメータが見えます
    upload_a18e129dd905609f8ed9fd26a7fceb4a
  10. 連動して動くはずです
    upload_7621eda48b0bb1549513abe207c0ea6f

今...何が起こった???

気が付いたらパラメーターが追加されてなんかそれっぽい見た目になってしまいました。まだ信号処理の部分は実装してないので見た目だけの状態ですが、「なんか色々したらパラメーターができた」って感じてる人も多いのではないでしょうか。この段落では、何が起こったのかを説明したいと思います。

とりあえず先に進めたい人は、次の「パラメーターの保存」まで読み飛ばしちゃってください。


JUCEでのパラメータの追加はざっくり下記のような流れで行います。

  1. juce::AudioProcessorValueTreeState parameters;PluginProcessor.hで定義。
    • AudioProcessorValueTreeStateクラスをオブジェクト名parametersで生成します。AudioProcessorValueTreeStateクラスはプラグインが持つ状態を一括で管理し、GUIとの紐づけやパラメータの種類、パラメータの保存等に便利なすごいクラスになっています。このクラスはプラグイン当たり1つしか生成することが出来ません。また、このクラスはコンストラクタとして初期化情報(パラメータの種類や初期値等)が必要になります。この初期化情報は後でPluginProcessor.cppで書かれることになります。
  2. std::atomic<float>* hogehogeParam = nullptr;PluginProcessor.hで定義。
    • パラメータの読み取り専用のポインタを作ります。信号処理の際にパラメータの値を読み取る時に使います。実はパラメータを読み取る別の方法として、parameters.getParameterAsValue("hogehogeParam").getValue()もあるのですが、parameters.getParameterAsValue()で得られるValueクラスがスレッドセーフでなかったり、冗長だったり、ほんの少しだけ遅かったりするので、ここで定義したポインタを読み取った方が良かったりします。読み取り専用なので、例えば*hogehogeParam = 500といった形で新たな値を代入したとしてもパラメータは更新されません。パラメータを更新させたいときはparameters.getParameterAsValue("hogehogeParam").setValue(500)としましょう。
  3. PluginProcessor.cppparametersの初期化情報を与える。
    • ValueTreeStateの肝になる部分です。詳しくは是非クラスリファレンスを見て欲しいのですが、「元に戻すボタン」を作る際に有用なUndoManagerとのリンクはココで行います。また、第4引数のParameterLayoutで使うであろうAudioParameterBoolAudioParameterChoiceAudioParameterFloatAudioParameterIntについては絶対にリファレンスを1度は見ておいた方が良いです。また、AudioParameterFloatで使うNormalisableRangeは非常に有用なのでこれも絶対に見てお来ましょう。NormalisableRangeはパラメータの上限・下限を設定するだけでなく、値の間隔(0.5刻みとか)やskew(EQの周波数ノブとか。値が小さい時と大きい時で値の変動幅を変化させることができる)も設定できるなどかなり必須級です。リファレンスをみよう。
  4. hogehogeParam = parameters.getRawParameterValue("hogehoge");のようにPluginProcessor.cppで値を渡す。
    • 前述の読み取り専用の値が入るポインタに、値を入れます。
  5. createEditor()にてAudioProcessorEditorにparametersを渡す。
    • return new hogehogeAudioProcessorEditor(*this, parameters);のように、GUI側でもValueTreeStateの値を読み取れるように渡しておきます。この段階ではまだhogehogeAudioProcessorEditor()が定義されているPluginEditor.hに第二引数が宣言されていないのでエラーが出るはずです。
  6. PluginEditor.hにてAudioProcessorEditorのコンストラクタの第二引数にjuce::AudioProcessorValueTreeState& vtsを追加。
    • 前述のエラーを解消します。ただこの段階ではまだjuce::AudioProcessorValueTreeState& vtsに渡されたものがクラスのどこに入るのかを書いてないのでまた別のエラーが出ます。
  7. PluginEditor.hにてAudioProcessorEditorのPrivate変数としてjuce::AudioProcessorValueTreeState& valueTreeState;を追加。
    • juce::AudioProcessorValueTreeState& vtsに渡されたもの将来的に代入される箱を用意します。「この箱に代入しろ!」という命令はまだ書いてないのでエラーが出ます。
  8. PluginEditor.cppにてjuce::AudioProcessorValueTreeState& vtsに渡されたものを前述の箱に代入するよう指示します。
    • 下記に今回作るプラグインでの該当部分を示します。このように書くことでGUI側でValueTreeStateを扱うことが出来ます。
      GainPantutorialAudioProcessorEditor::GainPantutorialAudioProcessorEditor (
          GainPantutorialAudioProcessor& p, juce::AudioProcessorValueTreeState& vts) // valueTreeStateを追加
          : AudioProcessorEditor (&p), audioProcessor (p), valueTreeState(vts) // valueTreeStateを追加
      
  9. PluginEditor.hにてGUIのアタッチメントのクラス名を省略する。
    • この操作はやってもやらなくてもいい操作なんですが、スライダーを追加する度にjuce::AudioProcessorValueTreeState::SliderAttachment hogehogeSliderと書くのは補完があってもつらいので、typedefを使って省略を定義するのがお勧めです。
      typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
      
  10. PluginEditor.hにてスライダー等のUI要素を定義します。
    • こんな感じです。クラス名を省略したので可読性が向上しています。
      juce::Slider hogehogeSlider
      std::unique_ptr<SliderAttachment> hogehogeAttachment;
      
  11. PluginEditor.cppにてGUIの設定と紐づけを行う。
    • 下記のコードで、見えるようにして(addAndMakeVisible())パラメータとリンク(hogehogeAttachment.reset())をしてます。addAndMakeVisible()をしないと画面に表示されないので注意しましょう。その他操作や見た目の設定もここで行います。詳しくはUIクラスのリファレンスを見ましょう(例 : Slider)。GUIの見た目については後述するLookAndFeelを使って大胆にカスタマイズできます。
      addAndMakeVisible(hogehogeSlider);
      hogehogeAttachment.reset(new SliderAttachment(valueTreeState, "hogehoge", hogehogeSlider));
      
  12. PluginEditor.cppにてGUI要素を表示する座標を設定する。
    • hogehogeSlider.setBounds(10, 10, 100, 100);のようにすることで座標を指定できます。後述しますが、座標を数値で設定するのは避けた方がよいです。

パラメーターの保存

今のままだと、DAWを閉じたときにパラメータの設定画全てリセットされてしまいます。そこで、DAW側が正しくプラグインのパラメータを保存できるようにしましょう。
ValueTreeStateを用いることでこの部分は簡単に実装できてしまいます。

PluginProcessor.cppを開いて下記を追加してください。

(省略)

//==============================================================================
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)); // 追加
}

(省略)

upload_abac79a3e27c2a76f22b61410b1c4f37

終わりです!これでパラメータの保存までできてしまいます。

信号処理の実装

このままだと、パラメータを変えても音量やパンニングがかわりません。なぜなら信号処理の部分を実装してないからです。というわけで信号処理の部分を実装しましょう。

本当だったら、Gain部分はサンプルごとにn倍したり、Pan部分はLRチャンネルに重みづけをしたりするのですが、このレベルの簡単な信号処理だったらJUCEのdsp::Gaindsp::Pannerモジュールを使うことが出来ます。JUCEはかなりの数の信号処理機をデフォルトで持っているので、余程のこだわりが無ければこれらのモジュールを使ってしまうのが便利です。
またこれらのモジュールは自作もできますし、既存のものを少し編集したりなども簡単です。是非やってみてください。

  1. まずはプロジェクトにJUCEモジュールを追加します。Projucerから下記のようにしてjuce_dspをクリックして、プロジェクトに追加してください。そのあとVisualStudioを一旦閉じてもう一度Projucerから起動してください。
    upload_b7da40e8e83ffa2055e03572b2a2be53
  2. PluginProcessor.hにて、使用するモジュールの定義を行います。今回はとりあえず処理精度がfloat(単精度浮動小数点演算)のものを作るので<float>としています。juce::dsp::ProcessSpec spec;はjuce::dspモジュールが動作前の準備にて必要となる情報が格納される変数です。
    (省略)
    
        std::atomic<float>* panAngle = nullptr;
    
        juce::dsp::Gain<float> gainDSP; // 追加
        juce::dsp::Panner<float> pannerDSP; // 追加
    
        juce::dsp::ProcessSpec spec; // 追加
        //==============================================================================
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GainPantutorialAudioProcessor)
    };
    
    
    upload_58ae368685573ae2e7772a3160f8ff0a
  3. PluginProcessor.cppにてjuce::dspモジュールの動作前準備を記述します。サンプルレートと1ブロック当たりのサンプル数、そしてブロックが持つオーディオチャンネル数を渡します。
    void GainPantutorialAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
    {
        // Use this method as the place to do any pre-playback
        // initialisation that you need..
        spec.maximumBlockSize = samplesPerBlock; // 追加
        spec.numChannels = 2; // 追加
        spec.sampleRate = sampleRate; // 追加
    
        gainDSP.prepare(spec); // 追加
        pannerDSP.prepare(spec); // 追加
    }
    
    upload_7a4a386b2544962b08fe0dfc8957ea5c
  4. PluginProcessor.cppに信号処理部分を記述します。処理はprocessBlock()関数内に書きます。この関数の引数としてDAWから入力のサンプルブロックが渡されて、そのサンプルブロックをそのまま編集することでエフェクトをかけるということになります。初めに読み取り専用のパラメータポインタから値を読み取ってjuce::dspにセットし、次にjuce::AudioBufferをラップしてjuce::dspに渡して処理してもらいます。
    (省略)
    
    void GainPantutorialAudioProcessor::processBlock (juce::AudioBuffer<float>& 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());
    
        gainDSP.setGainDecibels(*gain); // 追記
        pannerDSP.setRule(static_cast<juce::dsp::PannerRule>((int)*panRule)); // 追記
        pannerDSP.setPan(*panAngle / 100); // 追記
    
        juce::dsp::AudioBlock<float> audioBlock(buffer); // 追記
        juce::dsp::ProcessContextReplacing<float> context(audioBlock); // 追記
    
        gainDSP.process(context); // 追記
        pannerDSP.process(context); // 追記
    }
    
    (省略)
    
    upload_861bf9f13bc1e97e034785b66a0db80d
  5. これで信号処理部分は完成です。ビルドして『AudioPluginHost.exe』で読み込んでみましょう。
    upload_0e1cdcf8c71f8d95c037e5d1cc6c82bb
  6. このままだと音が意図通りに加工されているのかわからないので、付属のシンセサイザーを起動します。画面上を右クリックして「Sine Wave Synth」を起動し下記のように結線しましょう。
    upload_c6819f8aad6e750176428225baf83c7f
  7. 作ったプラグインの画面を表示し、パラメータを適当にいじったあと鍵盤を押して音を鳴らしてみましょう!意図したとおりに音量やパンが変化していたら成功です!

UIを何とかする

プラグインとして最低限の機能ができました。ここで完成!としちゃってもいいのですがせっかくなのでUIも何とかしてみましょう。はじめに断っておくのですが、JUCEにおいてUIを頑張るのはとてもとても大変です。何か別のライブラリを作るとかした方が時間の節約になると思います。

ここではあえてJUCEの機能だけを使って頑張ってUIをいい感じにしたいと思います。

ここからしばらくPluginEditor.cppPluginEditor.hしかいじりません。
どのファイルのコードをいじっているのかは、スクリーンショットの情報から確認してください。

  1. とりあえず画面の縦横を何とかしたいと思います。PluginEditor.cppのコンストラクタ内にあるsetSize(400, 300)をいじれば簡単に変えられるのですが、ここを数値で直打ちすると後々困るので、プライベート変数として画面の横幅と縦幅を決めたいと思います。

    // PluginEditor.h    
    int width = 150;
    int height = 550;
    
    // PluginEditor.cpp   
    setSize(width, height);
    

    upload_3bfe492a67a4251eb9c13699c5ec3361

  2. UI要素の配置は数字を直打ちして決めることもできるのですが、とても大変なので、UIエリアの四角形を用意してその中の相対座標で決めていくと良いです。とりあえず下記のような感じでエリアを2つ作ります。
    upload_4c691178069cd32181317d73bb3ff417

    // PluginEditor.h   
    juce::Rectangle<int> GainAria{ width, height * 4 / 5 };
    juce::Rectangle<int> PannerAria{ 0, height * 4 / 5, width, height / 5 };
    
    // PluginEditor.cpp   
    g.setColour (juce::Colour::Colour(46, 52, 64));
    g.fillRect(GainAria.toFloat()); // エリアをわかりやすくするために一部を塗った
    

    (↓コードが一部間違ってます)
    upload_8923318df9b35f27ee9cf62ddc65ada7

  3. Pannerは横長のスライダーじゃなくてノブっぽい感じにしたいのでそうします。

    // PluginEditor.cpp
    (省略)
    
    gainSliderAttachment.reset(new SliderAttachment(valueTreeState, "gain", gainSlider));
    addAndMakeVisible(gainSlider);
    
    panAngleSliderAttachment.reset(new SliderAttachment(valueTreeState, "panangle", panAngleSlider));
    // ↓スライダーのスタイルを回転ノブ型にして、操作方向は垂直にする
    panAngleSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryVerticalDrag);
    // ↓テキストボックスはノブの↓に表示し高さと幅を設定する
    panAngleSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, width * 2 / 3, PannerAria.getHeight() / 6);
    addAndMakeVisible(panAngleSlider); 
    
    panRuleBox.addItemList(
    
    (省略)
    
    gainSlider.setBounds(10, 10, 200, 30); 
    // ↓PannerAriaの四角形から5px内側の四角形をノブの領域として与える
    panAngleSlider.setBounds(PannerAria.reduced(5));
    panRuleBox.setBounds(10, 70, 200, 30); 
    
    (省略)
    

    upload_a9a967b888606cd24fd711dbc15011f8

  4. ここまでの操作でこんな感じになります。
    upload_8e4dd3af46c3831341d1485a3d351994

  5. ところで、Pan RuleのComboBoxの存在を完全に忘れていたのでPannerAriaを上に上げて下に余白を作ることにします。

    // PluginEditor.h
    
    // ↓修正
    juce::Rectangle<int> GainAria{ width, height * 4 / 5 - 30 };
    // ↓修正
    juce::Rectangle<int> PannerAria{ 0, GainAria.getBottom(), width, height / 5};
    // ↓追記
    juce::Rectangle<int> PannerRuleAria{ 0, PannerAria.getBottom(), width, height - GainAria.getHeight() - PannerAria.getHeight()};
    
    // PluginEditor.cpp
    
    g.setColour (juce::Colour::Colour(46, 52, 64));
    g.fillRect(GainAria.toFloat());
    // ↓追記
    g.fillRect(PannerRuleAria.toFloat());
    

    upload_62a8af7ed6f5fc057e32bdcc2de88bfa

  6. ComboBoxを移動します

    // PluginEditor.cpp
    
    void GainPantutorialAudioProcessorEditor::resized()
    {
        // This is generally where you'll want to lay out the positions of any
        // subcomponents in your editor..
        gainSlider.setBounds(10, 10, 200, 30); 
        panAngleSlider.setBounds(PannerAria.reduced(5));
        panRuleBox.setBounds(PannerRuleAria.reduced(width / 6, 4)); // 追記
    }
    
  7. Gainフェーダーを何とかします。

    // PluginEditor.cpp
    (省略)
    
    gainSliderAttachment.reset(new SliderAttachment(valueTreeState, "gain", gainSlider));
    // ↓スライダーの種類を縦方向のスライダーにする
    gainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearVertical);
    // ↓テキストボックスの位置をスライダーの下にして、高さと幅を調整する
    gainSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, width * 2 / 3, PannerAria.getHeight() / 6);
    addAndMakeVisible(gainSlider);
    
    (省略)
    // ↓修正
    gainSlider.setBounds(GainAria.reduced(5)); 
    panAngleSlider.setBounds(PannerAria.reduced(5));
    panRuleBox.setBounds(PannerRuleAria.reduced(width / 6, 4)); 
    
    

    upload_9ee3d4a079795f4b8253805df316b384

  8. 色を塗る指示のところを消して完成です!お疲れさまでした!
    upload_098b39a061f9eaa8b1bd117695bdf6f4

もっとUIを何とかする

大分まともになりましたし、個人で作る分には問題ないレベルになっています。しかしながらプラグインの見た目ってDTMのモチベに影響するのでカッコいいほうが良いに決まっています!!!
というわけで何とかしていきましょう。

JUCEのUI要素はLookAndFeelクラスによって定義されています。つまりこのクラスにおいてスライダーやコンボボックスの描画をしている部分を上書きしてしまえば、お望みの見た目に簡単に変えることができるということです!

しかしLookAndFeelクラスに関してのドキュメントは皆無に近く、本家のコードを読みながら実装していくことになります。頑張るぞ~~~

前準備

コーディング

  1. これで必要な情報は手に入れたことになるので、実際にコードを書いていきます。juce::LookAndFeel_V4クラスを継承してCustomLookAndFeelクラスを用意し、その中のdrawRotarySlider()を上書きします。

    // PluginEditor.cpp
    
    #include "PluginProcessor.h"
    // ↓ここから追記↓
    #include "PluginEditor.h"
    #define _USE_MATH_DEFINES
    #include "math.h"
    void CustomLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height,
        float sliderPos, float rotaryStartAngle,
        float rotaryEndAngle, juce::Slider& slider)  {
    
        // 色情報の取得
        auto outline = slider.findColour(juce::Slider::rotarySliderOutlineColourId);
        auto fill = slider.findColour(juce::Slider::rotarySliderFillColourId);
        auto background = slider.findColour(juce::Slider::backgroundColourId);
        auto thumb = slider.findColour(juce::Slider::thumbColourId);
        
        // 回転ノブを描画する領域を用意。与えられた領域から10px小さい長方形を描画領域とします。
        auto bounds = juce::Rectangle<int>(x, y, width, height).toFloat().reduced(10);
        // 回転ノブの半径は先ほど用意した描画領域の縦辺と横辺の内短い方の半分の長さとします。
        auto radius = juce::jmin(bounds.getWidth(), bounds.getHeight()) / 2.0f;
    
        // ノブの現在値を示す角度を計算します。  
        auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
        // ノブの周りの線の幅です。
        auto lineW = juce::jmin(4.0f, radius * 0.5f);
        // 線を描くときに使う円の半径です。
        auto arcRadius = radius - lineW * 0.5f;
    
        juce::Path backgroundArc;
        backgroundArc.addCentredArc(bounds.getCentreX(), bounds.getCentreY(), arcRadius, arcRadius, 0.0f, rotaryStartAngle, rotaryEndAngle, true);
        g.setColour(outline);
        g.strokePath(backgroundArc, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
    
        juce::Path valueArc;
        valueArc.addCentredArc(bounds.getCentreX(), bounds.getCentreY(), arcRadius, arcRadius, 0.0f, 2 * M_PI, toAngle, true);
        g.setColour(fill);
        g.strokePath(valueArc, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
    
        radius = radius - lineW * 2;
        g.setColour(background);
        g.fillEllipse(bounds.getCentreX() - radius, bounds.getCentreY() - radius, radius * 2, radius * 2);
    
        juce::Path p;
        auto pointerLength = radius * 0.5f;
        auto pointerThickness = 2.4f;
        p.addRectangle(-pointerThickness * 0.5f, -radius, pointerThickness, pointerLength);
        p.applyTransform(juce::AffineTransform::rotation(toAngle).translated(bounds.getCentreX(), bounds.getCentreY()));
        g.setColour(thumb);
        g.fillPath(p);
    }
    

    upload_b70c7aa69080842cf36cd869cac2f5a2

    プラグイン開発においてはやや非本質なのでこのコードの解説は省きますが、下記の図とコードを見比べてみると何をやっているのかわかりやすいと思います。
    upload_cdfc024a83613e4f6bc0de69dec82ae1

  2. 新たに用意したCustomLookAndFeelクラスをスライダーに適応させます。

    // PluginEditor.h
    (省略)
    
    private:
        CustomLookAndFeel customLookAndFeel; // 追記
    
    (省略)
    
    
    // PluginEditor.cpp
    (省略)
    
    
    panAngleSliderAttachment.reset(new SliderAttachment(valueTreeState, "panangle", panAngleSlider));
    panAngleSlider.setSliderStyle(juce::Slider::SliderStyle::RotaryVerticalDrag);
    panAngleSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, width * 2 / 3, PannerAria.getHeight() / 6);
    panAngleSlider.setColour(juce::Slider::ColourIds::backgroundColourId, juce::Colour::Colour(236, 239, 244)); // 追記
    panAngleSlider.setColour(juce::Slider::ColourIds::rotarySliderOutlineColourId, juce::Colour::Colour(46, 52, 64)); // 追記
    panAngleSlider.setColour(juce::Slider::ColourIds::rotarySliderFillColourId, juce::Colour::Colour(136, 192, 208)); // 追記
    panAngleSlider.setColour(juce::Slider::ColourIds::thumbColourId, juce::Colour::Colour(46, 52, 64)); // 追記
    panAngleSlider.setLookAndFeel(&customLookAndFeel); // 追記
    addAndMakeVisible(panAngleSlider); 
    
    (省略)
    
    

    upload_58a415e7db0df31a5f25999ceb99134e

  3. ビルドしてどのようになったか見てみましょう!かなりそれっぽくなっています。
    upload_abd6b5f26ef1e170a1a84cbb648b6027

  4. 背景色とテキストボックスの色を調整しました。全体的に統一感が出てきています。

    // PluginEditor.cpp
    (省略)
    
    panAngleSlider.setColour(juce::Slider::ColourIds::thumbColourId, juce::Colour::Colour(46, 52, 64));
    panAngleSlider.setColour(juce::Slider::ColourIds::textBoxOutlineColourId, juce::Colour::Colour(114, 122, 140)); // 追記
    panAngleSlider.setColour(juce::Slider::ColourIds::textBoxTextColourId, juce::Colour::Colour(236, 239, 244)); // 追記
    panAngleSlider.setColour(juce::Slider::ColourIds::textBoxHighlightColourId, juce::Colour::Colour(129, 161, 193)); // 追記
    panAngleSlider.setLookAndFeel(&customLookAndFeel);
    
    (省略)
    
    // (Our component is opaque, so we must completely fill the background with a solid colour)
    g.fillAll(juce::Colour::Colour(59, 66, 82)); // 修正
    
    (省略)
    

    upload_7fde80b699df876816b52f5720cc9b15

    upload_8b5a90a66fe853dbd70f543a31061a2f

  5. 同じようにしてComboBoxの見た目にも少し手を入れてみましょう。デフォルトだと矢印がおおきすぎますもんね

    // PluginEditor.h
    (省略)
    
    class CustomLookAndFeel : public juce::LookAndFeel_V4 {
        void drawRotarySlider(juce::Graphics&, int x, int y, int width, int height,
            float sliderPosProportional, float rotaryStartAngle,
            float rotaryEndAngle, juce::Slider&) override;
        void drawComboBox(juce::Graphics& g, int width, int height, bool, int, int, // 追記
            int, int, juce::ComboBox& box) override; // 追記
    };
    
    (省略)
    
    // PluginEditor.cpp
    (省略)
    
        g.setColour(thumb);
        g.fillPath(p);
    }
    
    // ↓以下追記
    void CustomLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool, int, int,
        int, int, juce::ComboBox& box) {
        juce::Rectangle<int> boxBounds(0, 0, width, height);
    
        g.setColour(box.findColour(juce::ComboBox::backgroundColourId));
        g.fillRect(boxBounds.toFloat());
    
        g.setColour(box.findColour(juce::ComboBox::outlineColourId));
        g.drawRect(boxBounds.toFloat(), 1.0f);
    
        juce::Rectangle<int> arrowZone(width - 15, 0, 10, height);
        juce::Path path;
        path.startNewSubPath((float)arrowZone.getX() + 2.0f, (float)arrowZone.getCentreY() - 1.0f);
        path.lineTo((float)arrowZone.getCentreX(), (float)arrowZone.getCentreY() + 2.0f);
        path.lineTo((float)arrowZone.getRight() - 2.0f, (float)arrowZone.getCentreY() - 1.0f);
    
        g.setColour(box.findColour(juce::ComboBox::arrowColourId).withAlpha((box.isEnabled() ? 0.9f : 0.2f)));
        g.strokePath(path, juce::PathStrokeType(2.0f));
    }
    
    (省略)
    
    panRuleBox.addItemList(
            juce::StringArray("linear", "balanced", "sin3dB", "sin4.5dB", "sin6dB",
                "sqrt3dB", "sqrt4.5dB"),
            1);
    panRuleBoxAttachment.reset(new ComboBoxAttachment(valueTreeState, "panrule", panRuleBox));
    panRuleBox.setLookAndFeel(&customLookAndFeel); // 追記
    panRuleBox.setColour(juce::ComboBox::ColourIds::backgroundColourId, juce::Colour::Colour(59, 66, 82)); // 追記
    panRuleBox.setColour(juce::ComboBox::ColourIds::textColourId, juce::Colour::Colour(236, 239, 244)); // 追記
    panRuleBox.setColour(juce::ComboBox::ColourIds::arrowColourId, juce::Colour::Colour(236, 239, 244)); // 追記
    panRuleBox.setColour(juce::ComboBox::ColourIds::outlineColourId, juce::Colour::Colour(114, 122, 140)); // 追記
    addAndMakeVisible(panRuleBox); 
    
    (省略)
    

    upload_dc0dd8dd405dcd72f80cacbf488ef5f5

  6. ビルドするとこんな感じになるはずです。
    upload_58411a7ae541c68bf2216d7267072044

  7. 最後にフェーダーを何とかしましょう。フェーダーのつまみもJUCEのグラフィック機能を用いてゴリゴリ書くこもできるのですが、めんどくさいのでそのまま画像を表示してもらうことにしましょう。適当にそれっぽい画像を作ったのでダウンロードしてください。
    upload_38a49a3b5ae98f011dceb44a05ac0924

  8. ダウンロードした画像をProjucerの「File Explorer」にドラッグ&ドロップします。
    upload_cdd733d2c1c53283eb6043d1bb64db61

  9. VisualStudioを閉じて、Projucerからもう一度起動してください。

  10. スライダーの描画はLookAndFell_V4クラスのdrawLinearSlider()が扱っています。今回は簡単のために縦型のスライダーに絞って実装します。関数の引数はいくつかありますが今回はつまみが1つしかないタイプなのでminSliderPosmaxSliderPosは使いません。その他の引数の意味については下記の画像を見てください。
    upload_a6f08056dde606e648bd3bbd1b470c54

  11. 実装します。画像の読み込みはjuce::ImageCache::getFromMemory(BinaryData::fader_png, BinaryData::fader_pngSize);で行うことができます。

    // PluginEditor.h
    (省略)
    
    class CustomLookAndFeel : public juce::LookAndFeel_V4 {
        void drawRotarySlider(juce::Graphics&, int x, int y, int width, int height,
            float sliderPosProportional, float rotaryStartAngle,
            float rotaryEndAngle, juce::Slider&) override;
        void drawComboBox(juce::Graphics& g, int width, int height, bool, int, int,
            int, int, juce::ComboBox& box) override;
        void drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height, // 追記
            float sliderPos, float minSliderPos, float maxSliderPos, // 追記
            const juce::Slider::SliderStyle style, // 追記
            juce::Slider& slider) override; // 追記
    };
    
    (省略)
    
    // PluginEditor.cpp
    (省略)
    
    // ↓以下追記
    void CustomLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height,
        float sliderPos, float minSliderPos, float maxSliderPos, const juce::Slider::SliderStyle style,
        juce::Slider& slider) {
    
        auto track = slider.findColour(juce::Slider::trackColourId);
        auto background = slider.findColour(juce::Slider::backgroundColourId);
    
        auto bounds = juce::Rectangle<int>(x, y, width, height).toFloat();
    
        float lineW = 10.0f;
    
        g.setColour(background);
        g.fillRect(juce::Rectangle<float>(bounds.getCentreX() - lineW * 0.5, bounds.getY(), lineW, bounds.getHeight()));
    
        g.setColour(track);
        g.fillRect(juce::Rectangle<float>(bounds.getCentreX() - lineW * 0.5, sliderPos, lineW, bounds.getBottom() - sliderPos));
    
        juce::Image knob = juce::ImageCache::getFromMemory(BinaryData::fader_png, BinaryData::fader_pngSize);
        float knobWidth = knob.getWidth() * 0.15;
        float knobHeight = knob.getHeight() * 0.15;
        g.drawImage(knob, juce::Rectangle<float>(bounds.getCentreX() - knobWidth * 0.5, sliderPos - knobHeight * 0.5, knobWidth, knobHeight));
    }
    
    (省略)
    
    gainSliderAttachment.reset(new SliderAttachment(valueTreeState, "gain", gainSlider));
    gainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearVertical);
    gainSlider.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, width * 2 / 3, PannerAria.getHeight() / 6);
    gainSlider.setColour(juce::Slider::ColourIds::backgroundColourId, juce::Colour::Colour(46, 52, 64)); // 追記
    gainSlider.setColour(juce::Slider::ColourIds::trackColourId, juce::Colour::Colour(136, 192, 208)); // 追記
    gainSlider.setColour(juce::Slider::ColourIds::textBoxOutlineColourId, juce::Colour::Colour(114, 122, 140)); // 追記
    gainSlider.setColour(juce::Slider::ColourIds::textBoxTextColourId, juce::Colour::Colour(236, 239, 244)); // 追記
    gainSlider.setColour(juce::Slider::ColourIds::textBoxHighlightColourId, juce::Colour::Colour(129, 161, 193)); // 追記
    gainSlider.setLookAndFeel(&customLookAndFeel); // 追記
    addAndMakeVisible(gainSlider);
    
    (省略)
    

    upload_6ed7a92f81bd6d3f02bec447ea58a45a

  12. ほぼ完成です!だいぶそれっぽくなりましたね。
    upload_fc20fd2f2566323c083502e9bb5fd403

  13. 最後に操作感について微調整を行います。Gainのスライダーは、大きい値なら細かく調整でき小さな値ならざっくり調整できるべきであるのでSkew Factorで調整しました。ついでに値の変動間隔も調整することで小数点表示を改善しました。

    // PluginProcessor.cpp ←←注意
    (省略)
    
    , parameters(*this, nullptr, juce::Identifier("tutorial"),
            {
                std::make_unique<juce::AudioParameterFloat>(
                    "gain", 
                    "Gain",
                    juce::NormalisableRange<float>(-100.0f, 10.0f, 0.01, 4.5), // 修正
                    0.0f),
                std::make_unique<juce::AudioParameterChoice>(
                    "panrule", "Pan Rule",
                    juce::StringArray("linear", "balanced", "sin3dB", "sin4p5dB",
                                        "sin6dB", "squareRoot3dB",
                                        "squareRoot4p5dB"),
                    1),
                std::make_unique<juce::AudioParameterFloat>(
                    "panangle",
                    "Pan Angle",
                    juce::NormalisableRange<float>(-100.0f, 100.0f, 1), //修正
                    0.0f),
            })
    
    (省略)
    
    
  14. Gainスライダーには単位を表示して欲しいので"dB"を表示させるようにしました。

    // PluginEditor.cpp
    (省略)
    
    gainSlider.setLookAndFeel(&customLookAndFeel);
    gainSlider.setTextValueSuffix(" dB"); // 追加
    addAndMakeVisible(gainSlider);
    
    (省略)
    
  15. あたり判定が大きすぎるので少し小さくしました。これ以上小さくするとテキストボックスも小さくなってしまうので何とかする必要があります。

    // PluginEditor.cpp
    (省略)
    
    void GainPantutorialAudioProcessorEditor::resized()
    {
        // This is generally where you'll want to lay out the positions of any
        // subcomponents in your editor..
        gainSlider.setBounds(GainAria.reduced(width / 6, 5)); // 修正
        panAngleSlider.setBounds(PannerAria.reduced(width / 6, 5)); // 修正
        panRuleBox.setBounds(PannerRuleAria.reduced(width / 6, 4)); 
    
    }
    
  16. ComboBoxはスクロールホイールで選択できると便利なのでそのようにしました。

    // PluginEditor.cpp
    (省略)
    
    panRuleBox.setColour(juce::ComboBox::ColourIds::outlineColourId, juce::Colour::Colour(114, 122, 140));
    panRuleBox.setScrollWheelEnabled(true); // 追加
    addAndMakeVisible(panRuleBox); 
    
    (省略)
    
  17. 0.0dBの所には目印があった方が良さそうだったので線を引きました。

    // PluginEditor.cpp
    (省略)
    
    auto track = slider.findColour(juce::Slider::trackColourId);
    auto background = slider.findColour(juce::Slider::backgroundColourId);
    
    auto bounds = juce::Rectangle<int>(x, y, width, height).toFloat();
    
    float lineW = 10.0f;
    
    // ↓ここを追記
    juce::Path line;
    auto lineMain = slider.findColour(juce::Slider::textBoxTextColourId);
    line.startNewSubPath(bounds.getCentreX() - 30, 136.86);
    line.lineTo(bounds.getCentreX() + 30, 136.86);
    g.setColour(lineMain);
    g.strokePath(line, juce::PathStrokeType(1.0f));
    // ↑ここまで
    
    g.setColour(background);
    g.fillRect(juce::Rectangle<float>(bounds.getCentreX() - lineW * 0.5, bounds.getY(), lineW, bounds.getHeight()));
    
    g.setColour(track);
    g.fillRect(juce::Rectangle<float>(bounds.getCentreX() - lineW * 0.5, sliderPos, lineW, bounds.getBottom() - sliderPos));
    
    
    (省略)
    
  18. 今度こそ完成です!お疲れさまでした。
    upload_d4536381b1600b024c40575b3fbfdfe4

  19. プラグインを配布するときはここを「Release」にしたうえでビルドしましょう
    ----------2022-04-15-182100

Tips / Q&A

本編では紹介できなかったTipsを紹介します。

デバッグのやり方

「プラグインを読み込んだら落ちた」とか、「思った挙動をしない」というときにはデバッグを行います。

  1. 赤枠の部分が「Debug」となっていることを確認したうえでビルドします。
    upload_d80b3931ee9083f0b60afd2cda7c3d81
  2. Juce Plu-in Hostを起動しますが、まだプラグインは読み込まないでください。
  3. 「デバッグ」→「プロセスにアタッチ」をクリック
    upload_5c5e7c7f6e39a92c9a2ee05cc8c52347
  4. 「AudioPluginHost.exe」を探して選択し、「アタッチ」をクリック。
    upload_2a5ddb8d09cd445b59ab693aad0b1b23
  5. デバッグが始まるので、Juce Plu-in Hostにてプラグインを読みこむなどしてください。
  6. 例えば今回、プラグインを起動するとソフトが落ちるというバグがある状態でデバッグを行いました。呼びだし履歴を見ていると、どうやらprocessBlock()の動作が原因で落ちたようです。このようにしてバグの原因を見つけていきます。
    upload_583f3bd73a29b5630c405f5936460693
  7. 「出力」というタブに文字列を出力したいときは、プログラム中にDBG("hogehoge");のように書くと出力することができます。

ComboBoxに初期値が表示されない

ドキュメントを見ると「setSelectedId()とかsetText()で初期値を設定する必要があるよ」って書いてありますが、ValueTreeStateを使えばその必要はありません。但し注意すべき点として、PluginEditor.cppのコンストラクタにて、ComboBoxにアイテムを追加した後にValueTreeStateとリンクする必要があるという点があります。
具体的には、下記のような書き方だとプラグインの初回起動時に規定値がComboBoxに表示されません。

panRuleBoxAttachment.reset(new ComboBoxAttachment(valueTreeState, "panrule", panRuleBox));
panRuleBox.addItemList(
        juce::StringArray("linear", "balanced", "sin3dB", "sin4.5dB", "sin6dB",
            "sqrt3dB", "sqrt4.5dB"),
        1);

下記のような書き方なら、初回起動時に規定値が表示されます

panRuleBox.addItemList(
        juce::StringArray("linear", "balanced", "sin3dB", "sin4.5dB", "sin6dB",
            "sqrt3dB", "sqrt4.5dB"),
        1);
panRuleBoxAttachment.reset(new ComboBoxAttachment(valueTreeState, "panrule", panRuleBox));

倍精度(double)で演算させたいんだけど

初期の状態だとPluginProcessor.cppprocessBlockの所が

void GainPantutorialAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)

ってなってると思います。倍精度で演算するためにはここの<float><double>に変えればよいです。

void GainPantutorialAudioProcessor::processBlock (juce::AudioBuffer<double>& buffer, juce::MidiBuffer& midiMessages)

今どき大部分のDAWは64bit処理に対応しているはずなのですが、まれに32bit処理にしか対応してないDAWやホストが存在するので、proseccBlockは単精度と倍精度の両方を用意しておく方が良いです。但し、同じものを2回書くのは面倒なので、テンプレートクラスを用いたプログラムを書くことをおすすめします。

プロジェクトファイル配布

今回作ったプログラムをGithubに上げておきます。最終的なコードを見たい人は御覧ください。

GitHub - Kashiwade-music/JUCE-traPblog-Tutorial
Contribute to Kashiwade-music/JUCE-traPblog-Tutorial development by creating an account on GitHub.
kashiwade icon
この記事を書いた人
kashiwade

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

この記事をシェア

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

関連する記事

2022年4月7日
traPグラフィック班の活動紹介
annin icon annin
2022年4月5日
アーキテクチャとディレクトリ構造
mazrean icon mazrean
2022年3月29日
課題・レポートの作成、何使う?【新歓ブログリレー2022 21日目】
aya_se icon aya_se
2021年12月8日
C++ with JUCEでステレオパンを作ってみた【AdC2021 26日目】
liquid1224 icon liquid1224
2020年3月16日
【 #LogicProX 】#DrumKitDesigner から #UltraBeat に乗り換えよう 第2話
SolunaEureka icon SolunaEureka
2020年3月15日
【 #LogicProX 】#DrumKitDesigner から #UltraBeat に乗り換えよう 第1話
SolunaEureka icon SolunaEureka
記事一覧 タグ一覧 Google アナリティクスについて