プラグインの例
このチュートリアルでは、オーディオ/MIDIのプラグインの例をいくつか詳しく説明し、プラグイン開発のオープンな可能性を探ります。
レベル:中級
プラットフォーム:Windows, macOS, Linux, iOS
プラグイン形式:VST, VST3, AU, AAX, Standalone
クラス:MidiBuffer, SortedSet, AudioParameterFloat, Synthesiser, MidiBuffer, MidiMessage, AudioProcessorValueTreeState, GenericAudioProcessorEditor,
はじめる
このチュートリアルにはいくつかのデモプロジェクトがあります。 これらのプロジェクトのダウンロード・リンクは、チュートリアルの関連セクションに記載されています。
各セクションのこのステップでヘルプが必要な場合は、 Projucerパート1:Projucerを始める を参照してください
デモプロジェクト
このチュートリアルで提供されるデモプロジェクトは、オーディオ/MIDIプラグインのいくつかの異なる例を示しています。 要約すると、これらのプラグインは
- シンプルなアルペジエーターで面白い音楽パターンを作る。アルペジエータープラグイン へジャンプ。
- ノイズゲートで不要なノイズをフィルタリング。ノイズゲートプラグイン へジャンプ
- チャンネル数をマルチアウト・シンセサイザーに拡張する。マルチアウトシンセプラグイン へジャンプします。
- チャンネル数をサラウンド対応プラグインに拡張する。サラウンドプラグイン へジャンプします。
私たちは、すべてのプロジェクトに共通する GenericAudioProcessorEditor クラスを使用して、プラグイン例全体のGUIコンポーネントをレイアウトしています。
ここで紹介するコードは、JUCE Examples の PlugInSamples と大まかに似ています。
アルペジエーター・プラグイン
このセクションのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開きます。
Projucerのプロジェクト設定の "Plugin Characteristics" フィールドで "MIDI Effect Plugin" オプションを有効にしてください。
アルペジエーターは、オーディオプロセッシングのないMIDIプラグインで、ソフトウェア・インストゥルメントやDAWのMIDIトラックに挿入して、入力されるMIDI信号を変更することができます。
![アルペジエータープラグインウィンドウ](https://docs.juce.com/master/tutorial_plugin_examples_arpeggiator_screenshot1.png)
アルペジエーターの実装
Arpeggiator
クラスでは、アルペジエーターの動作を実装するために、以下のようにいくつかのプライベート・メンバー変数を定義した:
private:
//==============================================================================
juce::AudioParameterFloat* speed;
int currentNote, lastNoteValue;
int time;
float rate;
juce::SortedSet<int> notes;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Arpeggiator)
};
その中に、あるソートルールに従ってユニークなint変数のセットを保持する SortedSet オブジェクトがある。これによって、MIDIノートを効率的に並べ替えて、希望する音楽パターンを作り出すことができる。
クラスのコンストラクタでは、MIDIプラグインを作成するため、オーディオバスなしでプラグインを初期化します。また、ここに示すように、アルペジエーターのスピードのパラメーターを1つ追加します:
Arpeggiator()
: AudioProcessor (BusesProperties()) // add no audio buses at all
{
addParameter (speed = new juce::AudioParameterFloat ("speed", "Arpeggiator Speed", 0.0, 1.0, 0.5));
}
prepareToPlay()
関数では、以下のようにいくつかの変数を初期化して、その後の処理に備える:
void prepareToPlay (double sampleRate, int) override
{
notes.clear(); // [1]
currentNote = 0; // [2]
lastNoteValue = -1; // [3]
time = 0; // [4]
rate = static_cast<float> (sampleRate); // [5]
}
- まず、MIDIノート番号の SortedSet を空にします。
- currentNote変数は、音符の SortedSet の現在のインデックスを一時的に保持する。
- lastNoteValue変数は、ノートを止めることができるように、前のインデックスを一時的に保持する。
- time変数は、バッファサイズとサンプルレートに対するノートの持続時間を保持する。
- rate は、現在のサンプルレートを float 変数に格納します。
次に、processBlock()
関数の中で以下のように実際の処理を行う:
void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi) override
{
// the audio buffer in a midi effect will have zero channels!
jassert (buffer.getNumChannels() == 0); // [6]
// however we use the buffer to get timing information
auto numSamples = buffer.getNumSamples(); // [7]
// get note duration
auto noteDuration = static_cast<int> (std::ceil (rate * 0.25f * (0.1f + (1.0f - (*speed))))); // [8]
for (const auto metadata : midi) // [9]
{
const auto msg = metadata.getMessage();
if (msg.isNoteOn()) notes.add (msg.getNoteNumber());
else if (msg.isNoteOff()) notes.removeValue (msg.getNoteNumber());
}
midi.clear();
- MIDIプラグインを扱っていることを確認するために、オーディオバッファにオーディオチャンネルがないことを表明する。
- オーディオバッファからブロック内のサンプル数を取得する。
- ユーザーインターフェースのスピードパラメーターとサンプルレートに従って、サンプル数で音符の長さを計算します。
- MidiBuffer 内のすべてのイベントに対して、イベントが "Note On" の場合はノートを SortedSet に追加し、イベントが "Note Off" の場合はノートを削除します。
- 次に、MidiBuffer を空にして、次のステップでノートを1つずつバッファに戻します。
if ((time + numSamples) >= noteDuration) // [11]
{
auto offset = juce::jmax (0, juce::jmin ((int) (noteDuration - time), numSamples - 1)); // [12]
if (lastNoteValue > 0) // [13]
{
midi.addEvent (juce::MidiMessage::noteOff (1, lastNoteValue), offset);
lastNoteValue = -1;
}
if (notes.size() > 0) // [14]
{
currentNote = (currentNote + 1) % notes.size();
lastNoteValue = notes[currentNote];
midi.addEvent (juce::MidiMessage::noteOn (1, lastNoteValue, (juce::uint8) 127), offset);
}
}
time = (time + numSamples) % noteDuration; // [15]
}
- 現在のブロックのサンプル数を加算した現在の時間が、ノートの継続時間より大きいかどうかをチェックする。もしそうであれば、現在のブロックの終わりには音符の遷移に達するので、MidiBuffer を変更します。 そうでなければ、MIDIステートをそのまま維持する。
- 現在のオーディオブロック内で ノートトランジションが発生するサンプルオフセットを計算する。
- 前のノートがまだ再生されている場合、変数lastNoteValueは0より大きいので、正しいサンプルオフセットでノートの再生を停止するために "Note Off "イベントを送信する必要があります。次に、lastNoteValue変数をリセットします。
- SortedSetにシャッフルして再生するノートがある場合、前のノート番号を保存し、次のノート番号を取得した後、セットの最初のノートを再生するために "Note On "イベントを送信します。
- 最後に、ノート遷移に到達したかどうかにかかわらず、ノート持続時間に対する現在の時間を追跡します。
ノイズゲートプラグイン
このセクションのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP。 プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開きます。
ノイズゲートはオーディオプラグインで、DAWのトラックにインサートとして置かれたときに、あるサイドチェインスレッショルド以下の入力音をフィルタリングします。
![ノイズゲート・プラグイン・ウィンドウ](https://docs.juce.com/master/tutorial_plugin_examples_noise_gate_screenshot1.png)
ノイズゲートの実装
NoiseGate
クラスでは、ノイズ・ゲートの動作を実装するために、以下のようにいくつかのプライベート・メンバー変数を定義しました:
private:
//==============================================================================
juce::AudioParameterFloat* threshold;
juce::AudioParameterFloat* alpha;
int sampleCountDown;
float lowPassCoeff;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoiseGate)
};
クラスのコンストラクタで、プラグインを入力、出力、サイドチェイン用の3つのステレオ・バスで初期化します [1]。 また、ここに示すように、スレッショルドとアルファの2つのパラメータを追加します[2]:
NoiseGate()
: AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo()) // [1]
.withOutput ("Output", juce::AudioChannelSet::stereo())
.withInput ("Sidechain", juce::AudioChannelSet::stereo()))
{
addParameter (threshold = new juce::AudioParameterFloat ("threshold", "Threshold", 0.0f, 1.0f, 0.5f)); // [2]
addParameter (alpha = new juce::AudioParameterFloat ("alpha", "Alpha", 0.0f, 1.0f, 0.8f));
}
スレッショルドパラメーターは、ノイズゲートが入力信号に作用するパワーレベルを決定する。alpha パラメーターは、サイドチェイン信号のフィルタリングを制御します。
isBusesLayoutSupported()
関数では、入力チャンネル数が出力チャンネル数と同じで、入力バスが有効になっていることを確認します:
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
{
// the sidechain can take any layout, the main bus needs to be the same on the input and output
return layouts.getMainInputChannelSet() == layouts.getMainOutputChannelSet()
&& ! layouts.getMainInputChannelSet().isDisabled();
}
prepareToPlay()
関数 では、以下のようにいくつかの変数を初期化して、その後の処理に備える:
void prepareToPlay (double, int) override
{
lowPassCoeff = 0.0f; // [3]
sampleCountDown = 0; // [4]
}
- サイドチェイン信号とαパラメータからローパス係数を計算し、ゲーティング動作を決定する。
- サンプル・カウントダウンは、ゲーティングが発生する前に、サンプル・レートに対して残っているサンプル数を追跡することができます。
次に、processBlock()
関数で以下のように実際の処理を行います:
void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override
{
auto mainInputOutput = getBusBuffer (buffer, true, 0); // [5]
auto sideChainInput = getBusBuffer (buffer, true, 1);
auto alphaCopy = alpha->get(); // [6]
auto thresholdCopy = threshold->get();
for (auto j = 0; j < buffer.getNumSamples(); ++j) // [7]
{
auto mixedSamples = 0.0f;
for (auto i = 0; i < sideChainInput.getNumChannels(); ++i) // [8]
mixedSamples += sideChainInput.getReadPointer (i) [j];
mixedSamples /= static_cast<float> (sideChainInput.getNumChannels());
lowPassCoeff = (alphaCopy * lowPassCoeff) + ((1.0f - alphaCopy) * mixedSamples); // [9]
if (lowPassCoeff >= thresholdCopy) // [10]
sampleCountDown = (int) getSampleRate();
// very in-effective way of doing this
for (auto i = 0; i < mainInputOutput.getNumChannels(); ++i) // [11]
*mainInputOutput.getWritePointer (i, j) = sampleCountDown > 0 ? *mainInputOutput.getReadPointer (i, j)
: 0.0f;
if (sampleCountDown > 0) // [12]
--sampleCountDown;
}
}
- まず、サイドチェーンバッファをメインのIOバッファから分離し、後続のステップで別々に処理する。
- 次に、しきい値とアルファパラメータのコピーを取得する。
- 外側のループはオーディオバッファブロック内の個々のサンプルを処理し、内側のループはチャンネルをインターリーブ処理します。これにより、1つのサンプル処理で各チャンネルの状態を同じに保つことができます。
- サイドチェーンの各チャンネルについて、信号を加算し、サイドチェーンのチャンネル数で割って、信号をモノラルに合計します。
- 次に、y[i] = ((1 - alpha) * sidechain) + (alpha * y[i - 1]) という式を使って、alphaパラメータとサイドチェイン・モノ信号からローパス係数を計算します。
- この係数がスレッショルド以上の場合、サンプルカウントダウンをサンプルレートにリセットする。
- 各入力チャンネルについて、カウントダウンがゼロでない場合、入力バッファのサンプルを出力バッファにコピーします。そうでない場合、ゼロサンプルを書き込むことで出力信号をミュートします。
- 各サンプルが処理されるたびに、サンプルカウントダウンの値をデクリメントします。
ここに示した実装は、通常ノイズゲートをプログラムする方法ではない。世の中にはもっと効率的で優れたアルゴリズムがあります。
マルチアウト・シンセ・プラグイン
このセクションのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開きます。
Projucerのプロジェクト設定の "Plugin Characteristics" フィールドで、"Plugin MIDI Input" オプションを有効にしてください。
マルチアウト・シンセは、オーディオファイルのサンプルを基に最大5つのシンセサイザー音色を生成し、最大16のマルチ出力に信号を出力するソフトウェア・インストゥルメント・プラグインです。
![Multi-out synth plugin window](https://docs.juce.com/master/tutorial_plugin_examples_multi_out_synth_screenshot1.png)
マルチアウト・シンセのインプリメンテーション
MultiOutSynth
クラスでは、マルチアウト・シンセの動作を実装するために、以下のようにいくつかのプライベート・メンバー変数を定義した:
//==============================================================================
juce::AudioFormatManager formatManager;
juce::OwnedArray<juce::Synthesiser> synth;
juce::SynthesiserSound::Ptr sound;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiOutSynth)
};
AudioFormatManager は、サンプル・サウンドを読み込むためのオーディオ・ファイル・フォーマットを登録します。 また、Synthesiser オブジェクトの配列があり、チャンネルごとに1つのシンセサイザーと、チュートリアルで使うサンプルサウンドへのスマートポインターを保持しています。
また、MIDIチャンネルの最大数やシンセのボイスの最大数など、便利な定数も宣言しています:
enum
{
maxMidiChannel = 16,
maxNumberOfVoices = 5
};
クラスのコンストラクタでは、プラグインを16のステレオ出力バスで初期化しますが、ソフトウェア・インストゥルメント・プラグインを作成するため、入力バスはありません [1]。 また、".ogg "サンプル・ファイル[2]を読み込むために、AudioFormatManager オブジェクトに基本的なオーディオ・ファイル・フォーマットを登録します:
MultiOutSynth()
: AudioProcessor (BusesProperties()
.withOutput ("Output #1", juce::AudioChannelSet::stereo(), true)
.withOutput ("Output #2", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #3", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #4", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #5", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #6", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #7", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #8", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #9", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #10", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #11", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #12", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #13", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #14", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #15", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output #16", juce::AudioChannelSet::stereo(), false)) // [1]
{
// initialize other stuff (not related to buses)
formatManager.registerBasicFormats(); // [2]
for (auto midiChannel = 0; midiChannel < maxMidiChannel; ++midiChannel) // [3]
{
synth.add (new juce::Synthesiser());
for (auto i = 0; i < maxNumberOfVoices; ++i)
synth[midiChannel]->addVoice (new juce::SamplerVoice()); // [4]
}
loadNewSample (juce::MemoryBlock (singing_ogg, singing_oggSize)); // [5]
}
MIDI/出力チャンネルごとに、新しい Synthesiser
オブジェクトをインスタンス化して配列に追加し[3]、1つのシンセにつき5つの SamplerVoice
オブジェクトを作成します[4]。また、以下に定義する loadNewSample()
プライベート関数を使用して、サンプルファイルをバイナリデータとしてロードします[5]:
void loadNewSample (const juce::MemoryBlock& sampleData)
{
auto soundBuffer = std::make_unique<juce::MemoryInputStream> (sampleData, false); // [6]
std::unique_ptr<juce::AudioFormatReader> formatReader (formatManager.findFormatForFileExtension ("ogg")->createReaderFor (soundBuffer.release(), true));
juce::BigInteger midiNotes;
midiNotes.setRange (0, 126, true);
juce::SynthesiserSound::Ptr newSound = new juce::SamplerSound ("Voice", *formatReader, midiNotes, 0x40, 0.0, 0.0, 10.0); // [7]
for (auto channel = 0; channel < maxMidiChannel; ++channel) // [8]
synth[channel]->removeSound (0);
sound = newSound; // [9]
for (auto channel = 0; channel < maxMidiChannel; ++channel) // [10]
synth[channel]->addSound (sound);
}
- まず、サンプルのバイナリ・データから MemoryInputStream を作成し、そのストリームを "ogg" フォーマットとしてファイルを読み込むように変換する。
- 先に作成したストリーム・リーダーを使って SamplerSound オブジェクトを宣言し、BigInteger を使ってMIDIノートの範囲を制約する。
- シンセ配列内のすべての Synthesiser オブジェクトについて、新しいものをロードする前に、現在ロードされている SynthesiserSound をクリアする。
- 次に、新しく作成した SamplerSound をスマートポインターに代入し、サウンドへの参照を保持します。
- 最後に、全ての Synthesizer オブジェクトに対して、新しいサウンドを SamplerSound オブジェクトとしてロードします。
必要以上にバスが追加されたり削除されたりしないように、AudioProcessor クラスから以下の2つの関数をオーバーライドします:
bool canAddBus (bool isInput) const override { return (! isInput && getBusCount (false) < maxMidiChannel); }
bool canRemoveBus (bool isInput) const override { return (! isInput && getBusCount (false) > 1); }
これにより、インプットバスの追加や削除、アウトプットバスの16チャンネルを超える追加や完全な削除を防ぐことができます。
prepareToPlay()
関数では、setCurrentPlaybackSampleRate()
関数を呼び出すことで、シンセ配列内のすべての Synthesizer オブジェクトのサンプルレートを設定し、後続の処理の準備をする:
void prepareToPlay (double newSampleRate, int samplesPerBlock) override
{
juce::ignoreUnused (samplesPerBlock);
for (auto midiChannel = 0; midiChannel < maxMidiChannel; ++midiChannel)
synth[midiChannel]->setCurrentPlaybackSampleRate (newSampleRate);
}
次に、processBlock()
関数の中で以下のように実際の処理を行う:
void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiBuffer) override
{
auto busCount = getBusCount (false); // [11]
for (auto busNr = 0; busNr < busCount; ++busNr) // [12]
{
auto midiChannelBuffer = filterMidiMessagesForChannel (midiBuffer, busNr + 1);
auto audioBusBuffer = getBusBuffer (buffer, false, busNr);
synth [busNr]->renderNextBlock (audioBusBuffer, midiChannelBuffer, 0, audioBusBuffer.getNumSamples()); // [13]
}
}
- まず、出力バスの数を求める。
- すべての出力バス(したがって、すべての Synthesiser インスタンス)に対して、不要なオーディオバスバッファをフィルタリングし、その後に定義されたプライベートヘルパー関数を呼び出すことによって、シンセシスのmidiチャンネルに対応しないmidiメッセージをフィルタリングする。
- その後、対応する Synthesiser オブジェクト上で直接
renderNextBlock()
関数を呼び出すことで、正しいオーディオバスバッファとMIDIチャンネルバッファを供給してサウンドを生成することができます。
MIDIチャンネルをフィルタリングするヘルパー関数は、後述するように実装されている:
static juce::MidiBuffer filterMidiMessagesForChannel (const juce::MidiBuffer& input, int channel)
{
juce::MidiBuffer output;
for (auto metadata : input) // [14]
{
auto message = metadata.getMessage();
if (message.getChannel() == channel)
output.addEvent (message, metadata.samplePosition);
}
return output; // [15]
}
- すべてのMidiチャンネルバッファについて、Midiメッセージのチャンネルが、探している出力バスのMidiチャンネルと一致するかどうかをチェックし、一致すれば、 MidiMessage を新しく作成した MidiBuffer に追加する。
- すべてのMIDIチャンネルのすべてのMIDIメッセージを繰り返しチェックした後、選択したチャンネルのMIDIメッセージだけを含むバッファを返します。
サラウンド・プラグイン
このセクションのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP。 プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開きます。
サラウンド・ユーティリティは、サラウンド設定を含む各チャンネルの入力信号をモニターし、選択したチャンネルにpingサイン波を送ることができるプラグインです。
![サラウンドプラグインウィンドウ](https://docs.juce.com/master/tutorial_plugin_examples_surround_screenshot1.png)