本記事はJUCE Advent Calendar 2024の12月6日向けに投稿した記事です。
はじめに
最少のスクリーンショットとソースコードでVSTプラグインの開発イメージをざっくりつかむためのJUCEチュートリアル2回目です。
前回の記事では、UIコントロールを持たないVST Instrumentを作成しました。今回はノブをひとつ持つシンプルなVST Effectを作ります。
チュートリアル
作成するVSTプラグイン
今回は波形を歪ませるFuzzエフェクトを作成します。入力した音声信号を加工して出力するVST Effectの作り方の基本と、音声信号処理とUIとの連携方法について説明します。
![ss](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F22455%2F5d749115-368b-049b-d288-a543cca0fca9.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=ff4e9f693d385980521f8196287aa1a3)
(1) プロジェクトの作成
まず、Projucerを起動して、File → New Project...を実行します。
前回同様、最初に左側でPlug-In → Basicを選んでから、右側のProject Nameを設定します。今回プロジェクト名はFuzzとします。
右下のCreate Project...ボタンを押して各種関連ソースコードを生成します。
(2) プロジェクトの設定
左上の歯車アイコンをクリックして、プロジェクトの設定をします。
DAW上で探しやすくするためにCompany Nameに自分の名前などを入れておきます。
今回はVST Effectなので、Plugin Characteristicsの「Plugin is a Synth」と「Plugin MIDI Input」にチェックが入っていないことを確認します。1
最後に右上のIDEボタンを押して、Visual Studioを起動します。
(3) Visual Studioでプロジェクトを確認
以降はVisual Studioでの作業となります。
ソースファイルの構造もVST Instrumentと同じです。ソリューションエクスプローラーの、Fuzz_SharedCode → Fuzz → Source の下にあるファイルを編集します。
PluginEditor.h/.cppがGUI関係、PluginProcessor.h/.cppが音声信号処理関係です。
(4) 音声信号処理ヘッダへ宣言追加
音声信号処理ヘッダファイルPluginProcessor.hのFuzzAudioProcessorクラスに、パラメータの値を示すpublicメンバ変数の宣言を追加します。
class FuzzAudioProcessor : public juce::AudioProcessor
{
public:
juce::AudioParameterFloat* fuzzParameter; // 追加
(5) 音声信号処理cppへ機能実装
次に音声信号処理の実装PluginProcessor.cppを開きます。ここで見るべきは、コンストラクタFuzzAudioProcessor()と、processBlock()の2か所です。
FuzzAudioProcessor()は、テンプレでは空の関数になっています。ここにパラメータの初期化処理を追加します。juce::AudioParameterFloat()の引数の意味は、ID、name、min, max, defaultです。
FuzzAudioProcessor::FuzzAudioProcessor()
// (省略)
{
fuzzParameter = new juce::AudioParameterFloat("Fuzz", "Fuzz", 0.0f, 1.0f, 0.5f); // 追加
addParameter(fuzzParameter); // 追加
}
音声信号処理本体のprocessBlock()は、テンプレでは無音の出力をするような処理が書かれているので、全部消して書き直します。
最初のfor文はチャンネルの数だけループする処理です。通常ステレオ2chなので2回ループします。きちんと実装する場合、入力チャンネル数と出力チャンネル数とが異なる場合も想定する必要がありますが、簡易的に同数とみなしても多くの場合問題ありません。
二つ目のfor文の中が処理本体で、振幅の値×ノブの値×10倍してからjuce::jlimit関数で±0.1を最大値としてクリップさせています。2
void FuzzAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
// 関数実装を全部消して以下のように書き直す
for (int ch = 0; ch < getTotalNumInputChannels(); ch++)
{
auto* data = buffer.getWritePointer(ch);
for (int i = 0; i < buffer.getNumSamples(); i++)
{
data[i] = juce::jlimit<float>(-0.1f, 0.1f, *fuzzParameter * data[i] * 10.0f);
}
}
}
![fig](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F22455%2F106fa656-4239-e6d0-d5ae-a7aec6e0717f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=68cf9834496d9ec1b65f39e060bbe3b6)
(6) GUI処理ヘッダへ宣言追加
次にGUI処理ヘッダのPluginEditor.hを開きます。
まず、ノブを操作したときにコールバック関数が呼ばれるようjuce::Slider::Listenerクラスを継承します。JUCEではこのように継承で機能を追加していくことがよくあります。
またJUCEのノブはSliderの一種として実装されているため、ソースコード上はSliderとして記述されます。
次にノブ操作時に呼ばれるコールバック関数sliderValueChanged()をpublicで宣言します。
最後にノブを表すメンバ変数fuzzKnobをprivateで宣言します。
class FuzzAudioProcessorEditor : public juce::AudioProcessorEditor
,public juce::Slider::Listener // 追加
{
public:
void sliderValueChanged(juce::Slider* slider) override; // 追加
private:
juce::Slider fuzzKnob; // 追加
(7) GUI処理cppへ機能実装
GUI処理をPluginEditr.cppに実装していきます。
まず、コンストラクタにノブの各種設定を書いていきます。このあたりはほぼ定型的な記述です。
なお、位置やサイズだけでなく、もっとデザインをカスタマイズするためにJUCEではLookAndFeelという仕組みが用意されています。しかしながらLookAndFeelの概念は独特で、WebやゲームのUIフレームワークにも似たようなものがないため、少し難易度が高い印象です。そのため、最初のうちはデフォルトのデザインで作成し、慣れてきたら少しずつ見た目をカスタマイズしていくのがおすすめです。
FuzzAudioProcessorEditor::FuzzAudioProcessorEditor (FuzzAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
{
// ここから追加
fuzzKnob.setSliderStyle(juce::Slider::Rotary); // ノブ型
fuzzKnob.setBounds(125, 90, 150, 150); // ノブの位置とサイズ
fuzzKnob.setTextBoxStyle(juce::Slider::NoTextBox, true, 0, 0); // テキストなし
fuzzKnob.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); // 縦ドラッグで値変更
fuzzKnob.setRange(0.0, 1.0, 0.01); // 値の範囲
fuzzKnob.setValue(*audioProcessor.fuzzParameter); // プロセッサのパラメータをノブの初期値とする
fuzzKnob.addListener(this); // コールバック設定
addAndMakeVisible(fuzzKnob); // 表示
// ここまで追加
setSize (400, 300);
}
次に画面再描画時に呼ばれるpaint()関数ですが、デフォルトで「Hello World!」が表示されるようになっている箇所を、ノブのラベルとして「Fuzz」が表示されるように変更します。
こういった文字の表示や背景の塗りつぶしは、juce::Graphicsコンテキストに対しておこなう必要があるため、juce::Graphicsコンテキストを持たないコンストラクタに書くことはできず、paint()の方に実装します。
void FuzzAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
g.setColour(juce::Colours::white);
g.setFont(20.0f); // 変更
g.drawText("Fuzz", 100, 40, 200, 20, juce::Justification::centred); // 変更
}
最後に、ノブが操作されたら呼ばれるコールバック関数を追加します。複数のノブがある場合を考慮して、イベント発生元のノブをif文で判別しています。
if文の中では、ノブから取得した値をProcessorに渡しています。
ここでVSTプラグインに詳しい人ほど驚くのですが、リアルタイムスレッドで動くProcessorとUIスレッドで動くEditorの間でこのように直接値を渡すことは通常禁止されています。JUCEはそこにひとつトリックがあり、AudioParameterFloatなどパラメータクラスのoperator=をオーバーロードしており、一見そのまま渡しているように見えて実際はスレッド安全に値を渡す処理が動いています。
// ノブ操作時に呼ばれるコールバック関数を追加
void FuzzAudioProcessorEditor::sliderValueChanged(juce::Slider* slider)
{
if (slider == &fuzzKnob)
{ // ノブの値をProcessorに渡す
*audioProcessor.fuzzParameter = slider->getValue();
}
}
実装はこれで完了です。ビルドして、生成された.vst3ファイルを所定のフォルダにコピーするとDAWから利用できるようになります。
この先は?
今回のVST Effectプラグインはかなり簡易的なものなので、実用的なプラグインとするためにはDAW起動時のノブ位置復元処理やオートメーション対応が必要になります。このあたりのパラメータ管理はAudioProcessorValueTreeStateを使うと良いのですが、これはこれで使い方を把握するのがなかなか大変です。
JUCEは多機能なので一度に覚えようとせず、小さなプラグインを作りながら、ひとつずつあらたな機能を覚えていくのが良いと思います。
余談:GUIエディタは滅亡しました
JUCEの古い記事を見ると、GUIエディタを使って画面を設計していることがあります。
しかしながら、GUIエディタはメンテが困難なため、2024年7月29日のJUCE 8.0.1以降廃止されました。
それ以前のバージョンでも、デフォルトでは使えない状態でしたが、後方互換性のためにTools → GUI Editor Enabledを有効にすると使えました。8.0.1ではこのメニューも廃止されました。古い情報を見るときは注意してください。
参考
https://github.com/JamesCameronMathews/BaxterFuzz/tree/main
https://www.amazon.co.jp/dp/B01HSEBPKO
https://docs.juce.com/master/tutorial_dsp_convolution.html
https://trap.jp/post/1558/