本記事はJUCE Advent Calendar 2024の12月5日向けに投稿した記事です。
はじめに
JUCEによるVSTプラグイン作成の初心者向けチュートリアルです。数年前にJUCEに出会ったばかりの自分が、当時こんなチュートリアルがあればなあという内容を書いてみました。
JUCEのチュートリアルは公式のものも非公式のものもすでにたくさんあります。とはいえ、公式チュートリアルを見てみると、プラグインのチュートリアルと、信号処理のチュートリアルと、GUIのチュートリアルが別になっており、現実的なVSTプラグインを作るときの、実装ポイントや全体のワークフローを知りたいという需要には応えられていないように感じていました。
また、他の言語にくらべてC++言語はソースコードが長くなりがちで、相当工夫しないと着目すべき点と、そうでない点が分かりづらくなっていしまいます。JUCEの場合、設定画面の項目も多く、各項目を網羅的に説明したり、スクリーンショットを多く載せるとステップ・バイ・ステップで進めやすくなる一方、俯瞰した概念がつかみにくくなります。
そんなわけで、従来のチュートリアルによる貢献は最大限にリスペクトした上で、最少のスクリーンショットとソースコードでVSTプラグインの開発イメージをざっくりつかみやすいように、ということを意識して記事を書いてみました。
JUCEのワークフローについて
最初にJUCEのことを知ったとき、VSTとAUのプラグインをワンソースで生成できる、というところに魅力を感じました。そのとき想像したワークフローは次のようなものです。
しかしこの図は間違っており、実際にはこちらの図の方がイメージとして近いです。Projucerの簡易テキストエディタはほぼ使うことがなく、補完機能などが充実して使い慣れたIDEのエディタで実装作業とデバッグをおこないます。1
このあたりを最初なかなかイメージできなくて戸惑いました。
また、たとえばソースコードを生成してある程度機能を実装したあとにVST InstrumentとVST Effectを間違えたことに気づいて、Projucerに戻って設定を変えてから再度IDEを開いても、実装部分は上書きされずに正しく設定だけ更新されます。こういった後戻り可能であることを知ると、大分気が楽になります。
チュートリアル
作成するVSTプラグイン
今回は基本中の基本、MIDI NoteOnメッセージに応じて適切な音程のサイン波を出力するモノシンセ「SineSynth」を作成します。ソースコードがなるべく小さくなるように最低限の機能で構成されたVST Instrumentプラグインです。
(1) プロジェクトの作成
まず、Projucerを起動して、File → New Project...を実行します。2
この画面では、最初に左側でPlug-In → Basicを選んでから、右側の項目を設定します。ここで最低限設定すべきはProject Nameです。
Project Nameは、ソースコードのクラス名にも、プラグインファイル名にも、DAWからの表示名にもなります。ここは後戻りできないので慎重に決めてください。今回はSineSynthとしておきます。
右下のCreate Projectボタンを押すと、Projucer用のプロジェクトファイル、Visual Studio、Xcodeのプロジェクトファイル、ソースコードのテンプレ、各種関連ソースコードが生成されます。
(2) プロジェクトの設定
左上の歯車アイコンをクリックすると、プロジェクトの設定ができます。ここで最低限設定すべきは、Company NameとPlugin Characteristicsです。3
Company Nameは必須というわけではありませんが、設定しておくとDAWから探しやすくなるのでデバッグ作業がはかどります。
VST Instrumentsの場合はPlugin Characteristicsの「Plugin is a Synth」と「Plugin MIDI Input」にチェックを入れ、VST Effectの場合はそれらのチェックを外します。
プロジェクトファイルや各種ソースコードを生成してからプロジェクトの設定をする、というのは順番的にわかりづらいですが、それがJUCEの流儀として理解してください。
最後に右上のIDEボタンを押すと、プロジェクト設定が保存され、Visual Studioが起動します。
(3) Visual Studioでプロジェクトを確認
以降はVisual Studioでの作業となります。
ソリューションエクスプローラーの、SineSynth_SharedCode → SineSynth → Source の下にあるファイルが機能を実装するソースコードで、それ以外のファイルは触りません。
PluginEditor.h/.cppがGUI関係、PluginProcessor.h/.cppが音声信号処理関係です。今回GUIはデフォルトのままで、PluginProcessor.h/.cppのみ実装します。
(4) ヘッダファイルへ宣言追加
ヘッダファイルのSineSynthAudioProcessorクラスに、サイン波の生成状態を示すprivateメンバ変数の宣言を4個追加します。
private:
//==============================================================================
double currentSampleRate = 0.0; // 追加
double currentAngle = 0.0; // 追加
double angleDelta = 0.0; // 追加
double currentGain = 0.0; // 追加
(5) cppファイルへ機能実装
cppファイルで見るべきは、prepareToPlay()とprocessBlock()の2か所です。
prepareToPlay()は、発音の直前に呼ばれる関数です。テンプレでは空の関数なので引数として与えられるサンプルレートをメンバ変数に記憶する処理を1行追加しておきます。動的にサンプルレートがころころ変わるDAWはあまりないとは思いますが、JUCEの設計としてこの関数で取得するようになっています。
void SineSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
currentSampleRate = sampleRate; // 追加
}
processBlockが、MIDIメッセージ処理と音声信号処理の本体です。テンプレでは無音の出力をするような処理が書かれているので、全部消して書き直します。4
関数の前半はMIDIメッセージ処理です。キューに複数メッセージがたまっている可能性があるのでforループで回しています。
NoteOnメッセージがあれば、ノートナンバーから出力周波数を求め、周波数から1サンプルごとの更新角度(angleDelta)を求めます。
NoteOffメッセージがあるときは、サイン波の計算をやめるのではなく、音量をゼロにしています。5
関数の後半は音声信号処理です。sin()関数を使ってサイン波を生成して左chと右chの出力バッファに格納しています。
void SineSynthAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
// 関数実装を全部消して以下のように書き直す
for (const auto metadata : midiMessages)
{
const auto msg = metadata.getMessage();
if (msg.isNoteOn()) {
double frequency = msg.getMidiNoteInHertz(msg.getNoteNumber());
angleDelta = 2.0 * juce::MathConstants<double>::pi * frequency / currentSampleRate;
currentAngle = 0.0;
currentGain = 0.1;
}
else if (msg.isNoteOff()) {
currentGain = 0.0;
}
}
float* l_ch = buffer.getWritePointer(0);
float* r_ch = buffer.getWritePointer(1);
for (int n = 0; n < buffer.getNumSamples(); n++) {
l_ch[n] = r_ch[n] = sin(currentAngle) * currentGain;
currentAngle += angleDelta;
}
}
実装はこれで完了です。ビルドして、生成された.vst3ファイルを所定のフォルダにコピーするとDAWから利用できるようになります。
GUIはデフォルトの「Hello World!」が表示されているので、次回はそのあたりの簡単なカスタマイズ方法について説明します。
参考
https://docs.juce.com/master/tutorial_sine_synth.html
https://m1m0zzz.github.io/juce-tutorial-ja/synth/tutorial_sine_synth/
https://forum.juce.com/t/sine-wave-synth-as-plugin/34250
https://qiita.com/COx2/items/e50e8f29bea633c6e5b0
https://www.amazon.co.jp/dp/B07HQHFKX9
-
検索するとCMakeを利用したワークフロー情報も多数出てきますが、CMakeガチ勢じゃない人は気にしなくて良いです。 ↩
-
本記事ではJUCE自体のインストール方法や設定方法などには触れません。もしプラグインのソースコードがうまく生成できないときやビルドできないときは、この画面のPath to Modulesや、FileメニューのGlobal Paths...から各種パスの設定が正しいことを確認してください。 ↩
-
公開用プラグインを作る場合は、Company CopyrightやPlugin Descriptionなど他の項目も正しく埋めてください。ここでは自分の学習用プラグインを想定して最低限の設定としています。 ↩
-
本当は、先頭の「ScopedNoDenormals noDenormals;」は消さない方が負荷軽減になって良いのですが、これの意味を理解するにはそれなりに知識が必要なので今回は削除します。 ↩
-
モノシンセの鍵盤を複数同時に押したときや、そのうちのいくつかを離したときの挙動はそれなりに複雑です。今回の例では簡略化して、最後に押した鍵盤の音程を発音し、いずれかの鍵盤が離されたら他の鍵盤が押されていても音が止まるようになっています。 ↩