# JUCEでエフェクターを作って遊ぼう:04 こちらの記事は デジクリ Advent Calender 2022 22日目の記事です。デジクリとは何ぞや、という方は[こちらのページ](https://digicre.net)をご覧ください。 ## はじめに こんにちは、またお前です。[これ ](https://hackmd.io/@bayashi/BkbvY21Dj)[までの ](https://hackmd.io/@bayashi/r1zehiWDo)[記事 ](https://hackmd.io/@bayashi/HyGN9tZdo)では主に私の思いつきで珍妙なプラグインを作ってきました。そのうちのいくつかは私にとって(私だけ?)実際に制作の中で使ってみたいと思うものでした。 ところで、これまでのプラグインはちゃんと動作はするものの、実は実用上の課題をいくつか抱えていました。特に「オートメーションが書けない」「パラメータが保存されない」というのが困り物で、これはDAW上で動作させるプラグインとして致命的です。 そこで今回は、主にこの2点を解決するための実装を行っていきます。 ### 注意 : 記載内容について 私自身のJUCEやC++への理解は初歩の段階です。記事の内容も今回扱っている内容の範疇では問題なく使えていますが、もしかするとこのやり方では上手くいかない局面も出てくるかもしれません。 今回の内容は基本的に[公式チュートリアル](https://docs.juce.com/master/tutorial_audio_processor_value_tree_state.html)を参考にしていますが、もし理解や記載に不十分・不適切な点がありましたら是非ご指摘をお寄せください。 なお、ここに記載の情報は2022年12月21日現在のものです。 ## パラメータを保持するクラス JUCEには、プラグインなどのパラメータ(ここではGUIから入力されるスライダーの値など)を扱うクラスが4種類用意されています。 * `AudioParameterInt` : 整数 * `AudioParameterFloat` : (32bit浮動)小数 * `AudioParameterBool` : trueかfalse * `AudioParameterChoice` : 選択肢 例えば、`AudioParameterFloat`クラスのパラメータは、次のように宣言でき(というかインスタンス、つまりクラスの実体を作れ)ます。[^1] ``` // 説明用のコードです juce::AudioParameterFloat (juce::ParameterID("hoge", 1), // パラメータID "Hoge", // パラメータ名 0.0f, // 最小値(f は"float"の意) 1.0f, // 最大値 0.25f) // 初期値 ``` 今までは`PluginProcessor`にパラメータを保持する用の変数を用意してきましたが、 **これからは`AudioParameter`系列のクラスにそれを任せます。** ## パラメータ群を管理するクラス `AudioParameter`系列のクラスはパラメータを保持する入れ物を用意してくれる訳ですが、その入れ物をまとめて管理してくれる`juce::AudioProcessorValueTreeState`というクラスが用意されています。どんな風に使うのかというと、とりあえず[公式ドキュメント](https://docs.juce.com/master/classAudioProcessorValueTreeState.html)を見てみると ``` // 説明用のコードです (公式ドキュメントをもとに作成) AudioProcessorValueTreeState::AudioProcessorValueTreeState (AudioProcessor& processorToConnectTo, // 扱いたいPluginProcessorを指定する UndoManager* undoManagerToUse, // UndoManagerという「取り消し・やり直し」操作をプラグイン上でサポートするクラスを指定する const Identifier& valueTreeType, // パラメータ情報を保存するときに使うプラグイン名の識別子を指定する ParameterLayout parameterLayout) // 使いたいパラメータ群を指定する ``` だそうです。最後の`parameterLayout`については「こんな風に中括弧`{}`で括って、C++の`std::make_unique()`を使って`AudioParameter`系列のパラメータを並べてね」と書いてあります。 ``` // 説明用のコードです {std::make_unique<juce::AudioParameterFloat> (juce::ParameterID("hoge", 1), "Hoge", 0.0f, 1.0f, 0.25f) std::make_unique<juce::AudioParameterFloat> (/* 略 */), /* 略 */ } ``` とりあえず言われた通りに[^2][^3]、例えば前回の記事後半で作ったプラグイン`Space Saturation`のパラメータ(`strength`と`range`)は次のように実装できます。 まず、`PluginProcessor.h`に、パラメータ群を格納する`juce::AudioProcessorValueTreeState`を保持するための変数`parameters`を宣言します。 ``` // 前略 private: //============================================================================== juce::AudioProcessorValueTreeState parameters; // 後略 ``` そして`PluginProcessor.cpp`にて、このように書きます。 ``` // 前略 SpaceSaturationAudioProcessor::SpaceSaturationAudioProcessor() : // 元々 #ifndef 内にあったコロンを外に移動 #ifndef JucePlugin_PreferredChannelConfigurations AudioProcessor (BusesProperties() #if ! JucePlugin_IsMidiEffect #if ! JucePlugin_IsSynth .withInput ("Input", juce::AudioChannelSet::stereo(), true) #endif .withOutput ("Output", juce::AudioChannelSet::stereo(), true) #endif ), // 末尾にカンマを追加 #endif parameters (*this, nullptr, juce::Identifier("SpaceSaturation"), {std::make_unique <juce::AudioParameterFloat> (juce::ParameterID("strength", 1), "Strength", 0.0f, 1.0f, 0.1f), std::make_unique<juce::AudioParameterFloat> (juce::ParameterID("range", 1), "Range", 1.0f, 1000000.0f, 2.0f)}) // この行を追加 // 後略 ``` ごちゃごちゃしていますが、関係ないところを消して、改行しながら整理するとこのようになります。やっていることは変数`parameters`の初期化です。[^4] ``` SpaceSaturationAudioProcessor::SpaceSaturationAudioProcessor() : parameters (*this, // 扱いたいPluginProcessor (これ) nullptr, // UndoManagerは使わないのでnullptrを渡す juce::Identifier("SpaceSaturation"), // プラグイン名の識別子 // これ以降はAudioParameter系列のパラメータを作って渡す {std::make_unique <juce::AudioParameterFloat> (juce::ParameterID("strength", 1), "Strength", 0.0f, 1.0f, 0.1f), std::make_unique<juce::AudioParameterFloat> (juce::ParameterID("range", 1), "Range", 1.0f, 1000000.0f, 2.0f) } ) ``` まあ、とりあえず動かすだけなら **「どこに どんな書き方で 何を書くのか」** を意識すれば大丈夫です。その書き方にどんな意味があるのかは必要に応じて調べるといいでしょう。(上手く動かないときとか) とりあえず、これで **`AudioParameter`系列が持っているパラメータ群をまとめて管理してくれる`AudioProcessorValueTreeState`が、`parameters`という変数を通じて扱えるようになりました。** ## パラメータ群へのアクセス さて、`parameters`という便利そうな変数を手にした訳ですが、この中に入っているパラメータ本体はどうやって使うのかを説明します。 ### PluginProcessor側 `PluginProcessor`で使うには、とりあえず個々のパラメータを変数として扱えるようにしたいです。そこで、`strength`と`range`の値(を渡してくれるメモリのアドレス)はそれぞれ`strengthParameter`と`rangeParameter`という変数が受け取ることにします。 `PluginProcessor.h`に、さっきの`parameters`と一緒にこの2つの変数を宣言します。[^5][^6] ``` // 前略 private: //============================================================================== juce::AudioProcessorValueTreeState parameters; std::atomic<float>* strengthParameter; std::atomic<float>* rangeParameter; // 後略 ``` (なお、初回の記事で「今後の記事で改良する」と言っていた、`PluginProcessor`の内部パラメータを`public`で宣言していた問題もこれで解決します。) 一方`PluginProcessor.cpp`の方は、さっき書いた`parameters`の初期化の後にこう書きます。 ``` SpaceSaturationAudioProcessor::SpaceSaturationAudioProcessor() : /* 中略 */ { strengthParameter = parameters.getRawParameterValue("strength"); rangeParameter = parameters.getRawParameterValue("range"); } ``` これで、`PluginProcessor`内でパラメータの値を貰うことが出来るようになりました。 これを使って、前回も紹介した`processBlock()`は、次のように書き換えられます。 ``` if (totalNumInputChannels == 2) { auto* channelData_L = buffer.getWritePointer (0); auto* channelData_R = buffer.getWritePointer (1); for (int sample = 0; sample < buffer.getNumSamples(); sample++) { double processed_L = (1.0 + (double)*strengthParameter * pow((double)*rangeParameter, -(channelData_R[sample] * channelData_R[sample]))) * channelData_L[sample]; double processed_R = (1.0 + (double)*strengthParameter * pow((double)*rangeParameter, -(channelData_L[sample] * channelData_L[sample]))) * channelData_R[sample]; channelData_L[sample] = processed_L; channelData_R[sample] = processed_R; } } ``` 波形は`double`型で処理しているので、念のため`AudioParameterFloat`のパラメータの値は`double`型へ明示的にキャストしています。 ### PluginEditor側 `PluginEditor`で`parameters`を扱うには、まず`parameters`を`PluginProcessor`から受け取る必要があります。 実は`PluginProcessor`には`createEditor()`という関数があり、この関数が`PluginProcessor`と`PluginEditor`の橋渡しをしているらしい…**というか`PluginEditor`のインスタンス(実体)をここで生成しているらしい**ので、ここから`parameters`を渡せるようにします。 渡せるようにするには、受け取る側の`PluginEditor`も一部書き換える必要があります。まず、`PluginEditor.h`をこのように変更します。 ``` // 前略 class SpaceSaturationAudioProcessorEditor : public juce::AudioProcessorEditor { public: // SpaceSaturationAudioProcessorEditor (SpaceSaturationAudioProcessor&); // 変更前 SpaceSaturationAudioProcessorEditor (SpaceSaturationAudioProcessor&, juce::AudioProcessorValueTreeState&); // 変更後 ~SpaceSaturationAudioProcessorEditor() override; // 後略 ``` 次に`PluginEditor.cpp`もこのように書き換えます。 ``` // 前略 // SpaceSaturationAudioProcessorEditor::SpaceSaturationAudioProcessorEditor (SpaceSaturationAudioProcessor& p) : AudioProcessorEditor (&p), audioProcessor (p) // 変更前 SpaceSaturationAudioProcessorEditor::SpaceSaturationAudioProcessorEditor (SpaceSaturationAudioProcessor& p, juce::AudioProcessorValueTreeState& vts) : AudioProcessorEditor (&p), audioProcessor (p), valueTreeState (vts) // 変更後 { // 後略 ``` 長いですが、やったことは単に`parameters`を受け取るために引数を増やしただけです。 そして、`PluginProcessor`の`createEditor`のところで`parameters`を渡します。 ``` // 前略 juce::AudioProcessorEditor* SpaceSaturationAudioProcessor::createEditor() { // return new SpaceSaturationAudioProcessorEditor (*this) // 変更前 return new SpaceSaturationAudioProcessorEditor (*this, parameters); // 変更後 } // 後略 ``` これで、`PluginEditor`には`valueTreeState`という変数として`parameters`が渡されました。 さらにここからパラメータをスライダーと紐付けなければなりませんが、これは簡単です。これまでは 1. スライダーの値の範囲を指定する → `setRange()` 2. パラメータの初期値をスライダーにセットする → `setValue()` 3. スライダーが変化したらパラメータに反映させる → `addListener()`と`sliderValueChanged()` という操作が必要でしたが、 **`valueTreeState`を使うとこれをたった2行で完了できます。** 試しに`strength`パラメータを操作する`strengthSlider`に対して実装してみます。 まず`PluginEditor.h`に`strengthAttachment`という変数を用意します。(別に変数名は分かりやすければ何でもいいです) ``` // 前略 private: // 中略 juce::Slider strengthSlider; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> strengthAttachment; // 後略 ``` 次に`PluginEditor.cpp`で`strengthAttachment`を使って次のようなコードを書きます。 ``` // 前略 SpaceSaturationAudioProcessorEditor::SpaceSaturationAudioProcessorEditor (SpaceSaturationAudioProcessor& p, juce::AudioProcessorValueTreeState& vts) : AudioProcessorEditor (&p), audioProcessor (p), valueTreeState (vts) { // 中略 strengthAttachment.reset(new juce::AudioProcessorValueTreeState::SliderAttachment(valueTreeState, "strength", strengthSlider)); // 後略 ``` これでスライダーとの紐付けは完了です。 ## パラメータの保存 ここまで、`AudioProcessorValueTreeState`という、パラメータ群を管理してくれる便利なクラスを利用して、`AudioProcessorFloat`型のパラメータをいくつか用意し、それをGUIと紐付けたり処理側に値を渡したりすることが出来ました。 あとは、このパラメータを保存したり、保存したパラメータを読めるようにすれば、楽曲制作でも安心して使うことが出来ます。 それらはどこでやるかというと、`PluginProcessor.cpp`の`getStateInformation()`と`setStateInformation()`という関数で処理します。この2つは初めから空白の関数として実装されていて、書き足す内容も基本的には公式ドキュメントの記述をコピペするだけです。 ``` // 前略 //============================================================================== void SpaceSaturationAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { auto state = parameters.copyState(); // parametersの内容をコピーする std::unique_ptr<juce::XmlElement> xml (state.createXml()); // パラメータ内容を記録するデータ(xml形式らしい)を生成する copyXmlToBinary (*xml, destData); // それを記録する } void SpaceSaturationAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { 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)); // その情報をもとにパラメータを更新する } // 後略 ``` というわけで、これで晴れてパラメータが保存されるようになり、オートメーションも書くことが出来るようになりました。 ## おわりに さて、2022年のアドカレ記事としてこのシリーズを書くのも今回が最後となりますが、いかがだったでしょうか。 もう9つ寝るとお正月です。お正月にはJUCE入れて、音歪ませて遊びましょう。 なお、JUCEには`juce::dsp`という信号処理を扱うクラスもあります。つまり、フィルタやリバーブなどを実装するための機能も用意されています。さらに、今回はエフェクターのプラグインに絞っていましたが、JUCEはソフトシンセも作れますし、スタンドアローンのアプリケーションも作れます。 本シリーズで今までに触れたものはJUCEの機能の、音に関わる処理のごく一部にすぎません。 *私たちのサウンドプログラミングは、まだまだ始まったばかりだ・・・!* お読みいただきありがとうございました。`次回作 : url (nullptr)` にご期待ください。[^7] [^1]:パラメータIDについて、macOSでAudioUnit形式のプラグインを作るとき以外は文字列で`"hoge"`などと入力しても大丈夫ですが、JUCEでAudioUnit形式のパラメータを作る際には version hint という概念があり、これを基にAudioUnit(のv2)特有の仕様と互換性を持たせています。そのため、`juce::ParameterID`というクラスを使ってIDと同時に version hint も指定しています。(ただし、後続フォーマットのAudioUnit v3ではJUCEの用意したやり方は通用しないらしく「v3で作りたいならパラメータの数を後から足したり減らしたりしないように」と言及されています。[ソース](https://docs.juce.com/master/structHostedAudioProcessorParameter.html)) [^2]:`std::make_unique()`とは、`unique_ptr`型のポインタを作るための関数です。`unique_ptr`型というのは、メモリ解放を自動でやってくれるスマートポインタの一種で、あるメモリ上のデータ(今回の場合は`AudioParameter`系列のパラメータ)を弄ることのできるポインタがただ一つであることを保証してくれるものです。1つのパラメータの値を弄れるポインタがいくつもあったらバグの因になるので、それが1つだけだと確定してくれるのは嬉しいですね。[参考](https://qiita.com/hmito/items/9b35a2438a8b8ee4b5af) [^3]:ちなみに「ポインタ」については[苦しんで覚えるC言語 15章 ポインタ変数の仕組み](https://9cguide.appspot.com/15-01.html)を参照してみるといいかと思います。そこそこ高度な内容ではありますが、解説が非常に丁寧です。 [^4]:クラス名(正確にはクラスのコンストラクタ)の後ろにコロンを付けて、その次に変数名を書いて括弧を始める書き方( `ClassName : valueHoge(...), valuePiyo(...)` みたいなやつ)をすると、その変数を括弧の中の値で初期化することができます。("c++ 初期化子リスト コンストラクタ" などでググってください。) 変数`parameters`は、パラメータ群をまとめて管理してくれる`juce::AudioProcessorValueTreeState`を作るのに必要なあれこれをここで渡して初期化されます。 [^5]:`std::atomic<>`型は、その変数がスレッドの割り込みによって変なことにならないようにするための型です。ここではそれへのポインタなので、つまり「今私がパラメータの値書いてる最中だから勝手に読まないでよね」と言ってくる変数のポインタを安全に受け取ります(多分)。 ポインタの方は`atomic`の効果に入りません(多分)。 [^6]:ちなみに`AudioParameterBool,Int,Choice`でも受け取り型は`std::atomic<float>*`です。どうやら`getRawParameterValue`ではどの`AudioParameter`でも`float`型で返ってくるようです。`Choice`の場合は選択肢のインデックス番号が返ってくるらしい(?)ので必要に応じて`int`型にキャストしてあげましょう。 [^7]:きっといずれ勝手に書きます。