# JUCEでエフェクターを作って遊ぼう:01 こちらの記事は デジクリ Advent Calender 2022 6日目の記事です。デジクリとは何ぞや、という方は[こちらのページ](https://digicre.net)をご覧ください。 ## はじめに こんにちは、14期のbayashiです。今年はJUCEというフレームワークを使ってプラグイン自作を試してみた話を数回にわたって[^1]書いてみようと思います。 この記事を書き始めた当時、世の中はブラックフライデーセールの真っ最中で、記事が公開される頃にはクリスマスセールが徐々に始まる頃かと思います。この時期は定価の高いソフトウェア製品やプラグインが大幅に値引きされることも珍しくなく、多くの音楽製作者が目を光らせる時期でもあります。 ところで、色々なプラグインが手元に集まってくると「こんな音を作れるプラグインが欲しいんだけどな〜なんなら作りたいな〜〜」って思うことありませんか?あるとします。 そんな願望を比較的手軽に叶えてくれるのが”JUCE”というC++のフレームワークです。 JUCEはプラグイン規格の仕様やOSの差異、さらには開発環境でのメタデータ設定などといった面倒な部分を肩代わりしてくれるだけでなく、使いやすい内部ライブラリのお陰で割と気軽にプラグイン作りをすることが出来ます。 今回はそのJUCEを使ってステレオイメージング系の処理を実装してみようとしたら、いくつか珍妙なプラグインが出来たので紹介してみたいと思います。 …ですが、今回の記事ではJUCEの説明と簡単な音量操作を作るところまで書いて結構な分量になったため、プラグイン自体の話は次回をお待ちください。 では始めましょう。 ## この記事でやること Step.1 : 単純なやり方で出力音量を変化させるプラグインを作ります Step.2 : そいつを少し弄って遊んでみます ## 用意するもの * [JUCEフレームワーク一式](https://juce.com/get-juce) * Personalプランはアカウント登録等も必要なく、無料でダウンロードできます * [JUCEの定義するライセンス条項](https://juce.com/juce-7-licence)か、GPLv3に従って利用できます * 統合開発環境(IDE) または コンパイル環境 * Windowsなら Visual Studio * Macなら Xcode * Linuxなら Makefile ## プラグインを作り始める準備 JUCEフレームワークには"Projucer"というアプリケーションが内包されていて、このアプリケーションを通じてテンプレートの生成や各IDEに向けたファイル出力を行う仕組みになっています。 1. Projucerを起動します。 3. 起動すると、テンプレートから新規プロジェクトを作成する画面が表示されるので、今回はPlug-in > Basicを選択します。![](https://i.imgur.com/0IpCHmF.png) 3. Project Nameを入力し、適宜Exporterで出力したいIDEやコンパイル環境を指定して、Create Project... をクリックすればProjucerのプロジェクトファイルが生成されます。今回は"TestGain"という名前にしました。![](https://i.imgur.com/PB9Qj2C.png) 4. 必要に応じて、歯車マーク(Project Settings)をクリックし、プラグイン名やバージョン番号などを編集しましょう。![](https://i.imgur.com/rFPpTU6.png) 5. 最後にVisual StudioやXcodeのプロジェクトファイルを生成します。Projucerのウインドウ上部の Select exporter 右側にある丸いアイコンをクリックすると、それまでのプロジェクト設定を反映したVisual StudioやXcode用のプロジェクトファイルを生成してくれます。![](https://i.imgur.com/f7SC5G6.png) 6. 生成されたプロジェクトファイルをVisual StudioやXcodeで開きます。これで準備完了です。(筆者はXcodeで作業します) というわけで、ここからはGUIの入力にしたがって出力音量を変化させるエフェクトプラグインを作ってみます。 ## Step.1 出力音量を変化させるプラグインを作る ### Step.1-1 GUIをつくる IDEでプロジェクトファイルを開くとなにやら色々出てきますが、Sourceというフォルダに入っている `PluginEditor.cpp` と `PluginEditor.h` というコードを見つけてください。これらがGUIの描画を司るものです。 完成イメージとしては、ウインドウ上に "Volume" と書かれたスライダーを描画させて、左に動かせば音が小さくなり、右に動かせば音が大きくなるような感じにしたいと思います。 JUCEではGUIのパーツがクラスとして用意されているので、ますは `PluginEditor.h`に以下の変数を宣言します。 ``` // 前略 class TestGainAudioProcessorEditor : public juce::AudioProcessorEditor { public: // 中略 private: // This reference is provided as a quick way for your editor to // access the processor object that created it. TestGainAudioProcessor& audioProcessor; juce::Slider volumeSlider; // 追加 juce::Label volumeLabel; // 追加 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestGainAudioProcessorEditor) }; // 後略 ``` `volumeSlider`はスライダー本体、`volumeLabel`はスライダーに付ける表記を指します。 この`::`というのはスコープ解決演算子などと呼ばれるものですが、よく分からない方はフォルダ階層を表すスラッシュ(/)や円マーク(¥)みたいなものだと思ってください。[^2]とても大雑把に言うなら、JUCEでは各々の機能が階層別に分かれている、というようなイメージです。(本当に大雑把なのでちゃんと知りたい方はググってください) 続いて、`PluginEditor.cpp`の方でこれらの形や大きさなどを指定していきます。 ``` // 前略 //============================================================================== TestGainAudioProcessorEditor::TestGainAudioProcessorEditor (TestGainAudioProcessor& 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. setSize (400, 300); volumeSlider.setSliderStyle(juce::Slider::LinearHorizontal); // スライダーの形を指定します volumeSlider.setRange(0.0, 1.0); // スライダーの値の範囲を指定します volumeSlider.setBounds(100, 100, 250, 20); // スライダーの x座標 y座標 横幅 縦幅 を指定します addAndMakeVisible(&volumeSlider); // スライダーをGUI上に描画できるようにします volumeLabel.setText("volume", juce::dontSendNotification); // "volume"というテキストの表記を用意します volumeLabel.attachToComponent(&volumeSlider, true); // 表記をvolumeSliderにくっつけます } // 中略 //============================================================================== void TestGainAudioProcessorEditor::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)); g.setColour (juce::Colours::white); g.setFont (15.0f); // g.drawFittedText ("Hello World!", getLocalBounds(), juce::Justification::centred, 1); // これは消します } // 後略 ``` この状態でビルドすると次のような見た目になります。 ![](https://i.imgur.com/wLsULYH.png) ### Step.1-2 処理を実装する 当然ですが、このままでは特に何も起きません。なので実際に音の処理を実装していきます。 `PluginProcessor.cpp`を覗いてみると、`processBlock()`という関数の中に次のような記述があります。 ``` // 前略 void TestGainAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { juce::ScopedNoDenormals noDenormals; auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels(); // 中略 for (int channel = 0; channel < totalNumInputChannels; ++channel) { auto* channelData = buffer.getWritePointer (channel); // ..do something to the data... (意訳 : ここに処理を書いてね) } } // 後略 ``` この `channelData` という変数が、処理したい波形の値(のポインタ)を格納する変数になっています。最も単純な形で音量変化を実装するなら、例えば変数 `volume` という値を用意して、次のように振幅を`volume`倍すれば良いでしょう。 ``` for (int channel = 0; channel < totalNumInputChannels; ++channel) { auto* channelData = buffer.getWritePointer (channel); for (int channel = 0; channel < totalNumInputChannels; ++channel) { auto* channelData = buffer.getWritePointer (channel); for (int i = 0; i < buffer.getNumSamples(); ++i) { channelData[i] = volume * channelData[i]; } } } ``` 変数を用意するにあたっては、`PluginProcessor.h`で次のように宣言し、[^4] ``` // 前略 class TestGainAudioProcessor : public juce::AudioProcessor #if JucePlugin_Enable_ARA , public juce::AudioProcessorARAExtension #endif { public: // 中略 double volume; // 変数を宣言します private: //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestGainAudioProcessor) }; ``` `PluginProcessor.cpp`で次のように初期化します。 ``` TestGainAudioProcessor::TestGainAudioProcessor() #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 { volume = 1.0; // 変数を初期化します } ``` なお、本当はこういったクラス内部のパラメータが他のクラスからも直接弄れるpublicな変数になっているのは設計上よろしくないですが、これは今後の記事で改良するので今はどうか目を瞑ってください。[^3] では、この`volume`という変数を、GUI上のスライダーの値によって変化させられるようにイベント管理を実装します。 再び`PluginEditor.cpp`を開き、さきほど編集したGUI関連のコードの続きに、スライダーの値の初期値を変数`volume`から取ってくるように、またスライダーを動かしたときのイベント情報を拾えるようにするため、2行のコードを追記します。 ``` // 前略 TestGainAudioProcessorEditor::TestGainAudioProcessorEditor (TestGainAudioProcessor& p) : AudioProcessorEditor (&p), audioProcessor (p) { // 中略 volumeSlider.setValue(audioProcessor.volume); // PluginProcessor内の変数volumeの初期値をセットします volumeSlider.addListener(this); // volumeSliderが動いたらイベント情報を受け取れるようにします } // 後略 ``` このイベント情報は`juce::Slider::Listener`クラスに含まれている`sliderValueChanged()`という関数が実際に拾います。それを使えるようにするため、まず`PluginEditor.h`の`class TestGainAudioProcessorEditor`で始まる行の最後に`, private juce::Slider::Listener`と追記します。 ``` class JUCEGainAudioProcessorEditor : public juce::AudioProcessorEditor, private juce::Slider::Listener ``` 次に、序盤でスライダーとラベルの変数を宣言した次の行あたりに、以下の宣言を加えます。 ``` // 前略 class TestGainAudioProcessorEditor : public juce::AudioProcessorEditor { public: // 中略 juce::Slider volumeSlider; juce::Label volumeLabel; void sliderValueChanged (juce::Slider) override; // この宣言です JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestGainAudioProcessorEditor) }; // 後略 ``` この`sliderValueChanged()`という関数は元々「イベント情報を発するスライダーが値を変化させたときに、スライダーの情報を受け取って実行される」ということだけ決まっている関数で、その具体的な処理内容は定義されていません。そのため、それを上書き(override)してやりたい処理を書くことで、GUIからオーディオ処理に値を渡すなどの操作を実現します。 `PluginEditor.cpp`の末尾に以下を追記します。 ``` void TestGainAudioProcessorEditor::sliderValueChanged(juce::Slider *slider) { if (slider == &volumeSlider) // もし動いたスライダーが volumeSlider だったら { audioProcessor.volume = slider->getValue(); // PluginProcessorの変数volumeに値を渡す } } ``` これでビルドすれば、このように音量を変化させるプラグインが動きます。 <iframe width="560" height="315" src="https://www.youtube.com/embed/9wvnI_BRp38" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> ### Step.1-3 ひとことで言うと? 長々と書きましたが、要点だけまとめるとこうなります。 1. `PluginEditor`に見た目と、操作に応じてパラメータを渡す処理を記述します 2. `PluginProcessor`(の特に`processBlock()`)にエフェクトの動作や処理を記述します 基本的にJUCEでエフェクターを作るための作業はその2つが中心だと考えて良いでしょう。 ## Step.2 遊んでみる せっかく動くものが出来たので遊んでみましょう。弄る部分は概ね`PluginProcessor`の`processBlock()`です。 ### Step.2-1 Clip Distortion というわけでまずは~~みんな大好き~~ディストーションから。最も単純なディストーションはクリップディストーションといって「閾値以上の波形を一定値に置き換える」というものです。 `PluginProcessor`の`processBlock()`内にある`channelData`に対して、次のような処理を行ってみましょう。 ``` for (int channel = 0; channel < totalNumInputChannels; ++channel) { auto* channelData = buffer.getWritePointer (channel); for (int i = 0; i < buffer.getNumSamples(); ++i) { if (channelData[i] > volume) { channelData[i] = volume; } if (channelData[i] < -volume) { channelData[i] = -volume; } } } ``` では、処理結果をオシロスコープ表示を添えてお聞きください。 <iframe width="560" height="315" src="https://www.youtube.com/embed/PtNWNE3gQZQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> 振幅がプラグインで指定した値を超えると強烈に歪みます。歪むほど音量自体は下がっていくのにやたらうるさく感じるのが不思議ですね。 ### Step.2-2 Stereo Balancer これまではfor文で入力チャンネル数に対してそれぞれに同じ処理をするような実装でしたが、次は用途をステレオ(2チャンネル)に限定して、左と右に別々の処理を加えてみましょう。 例えばStep.1-1と1-2で触れたコードを一部書き換え、スライダーの値の範囲を-1.0~+1.0に、volumeの初期値を0としたとき、 ``` volumeSlider.setRange(-1.0, 1.0); ``` ``` volume = 0.0; ``` こんな風に書くと、いわゆる「パン振り」をするプラグインになります。 ``` if (totalNumInputChannels == 2) { auto* channelData_L = buffer.getWritePointer (0); auto* channelData_R = buffer.getWritePointer (1); for (int i = 0; i < buffer.getNumSamples(); ++i) { if (volume > 0) { channelData_L[i] = (1.0 - volume) * channelData_L[i]; } else { channelData_R[i] = (1.0 + volume) * channelData_R[i]; } } } ``` 動かしてみるとこんな感じです。 <iframe width="560" height="315" src="https://www.youtube.com/embed/JAD4cFtWf74" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> ### Step.2-3 Stereo Balance Clip Distortion そしたら同じ要領でクリップディストーションにしてみましょう。(?) ``` if (totalNumInputChannels == 2) { auto* channelData_L = buffer.getWritePointer (0); auto* channelData_R = buffer.getWritePointer (1); for (int i = 0; i < buffer.getNumSamples(); ++i) { if (volume > 0) { if (channelData_L[i] > (1.0 - volume)) { channelData_L[i] = (1.0 - volume); } if (channelData_L[i] < -(1.0 - volume)) { channelData_L[i] = -(1.0 - volume); } } else { if (channelData_R[i] > (1.0 + volume)) { channelData_R[i] = (1.0 + volume); } if (channelData_R[i] < -(1.0 + volume)) { channelData_R[i] = -(1.0 + volume); } } } } ``` <iframe width="560" height="315" src="https://www.youtube.com/embed/Bpit5A2W7Xs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> はい。 「なんだ、ただのパン振りか…」と思ったら片耳だけ歪んでいく謎プラグインが爆誕しました。 案外シンセ系と相性が良く、ちょっとした隠し味にはなりうる気がします。(実際には左右のクリップ閾値を個別に設定できた方が使い勝手よさそうだけど) ## 今後の課題 今回までの実装で、GUIの入力に従って音を変化させる処理まで作ることが出来ます。作って出音を確認して遊ぶ分にはこれで良いのですが、実際にプラグインとして使うためには以下のような課題があります。 * パラメータが保存されない (セーブして再度開くと初期値に戻ってしまう) * スライダーを一気に動かすとプチプチとしたノイズが乗る これらは今後の記事で改良に触れたいと思います。 ## まとめ というわけで、JUCEで出力音量を変化させるプラグインを作るまでと、それを弄って遊んでみる様子を書いてみました。 コードをざっと見るといかにも複雑そうに見えますが、とりあえず動かしてみるまでに書かなければならないコードはそれほど多くなく、おそらく一度書いてみればなんとなく分かる感じがするのではないかと思います。 そして、`processBlock()`に簡単な処理をいろいろと書いてみるだけでも音が大きく変化し、波形を直接加工する面白さを感じることができます。 次回からは、このように比較的単純な処理を組み合わせて作ってみたプラグインについて紹介していきます。実用性があるかというと正直何とも言えないところですが、なんか面白そうだな、と思ってもらえるといいなと思いつつ鋭意編集中です。 なお、記事の内容に誤りがある場合、説明が不適切だと感じる場合は是非教えてくださると非常に嬉しいです。 それでは今回はこの辺で。ありがとうございました。 ## 次回予告 ![](https://i.imgur.com/w4NUj0t.png) ※ [次回](https://hackmd.io/@bayashi/r1zehiWDo)はステレオ~~ディストーション~~プラグインです [^1]:回数未定です、失踪はしないはずです [^2]:環境によっては両方とも円マークに見えたり、円マークがバックスラッシュに見える場合もあるかもしれません [^3]:おそらく3回目の記事以降となるものと思われます(もしかすると3回目が長文になるかも?) [^4]:追記 : 変数の型を`float`から`double`に変更しました (JUCE自体は64ビット処理に対応しており、DAWによっては入力信号も64ビットである場合があるため)