ラダーフィルタって、名前は聞くけど、実際にはよくわかりません。
モーグシンセサイザーのフィルター部分の仕組みのことみたいだね~
JUCEチュートリアルのIntroduction to DSPを進めています。「Adding a ladder filter」の項目を実装しました。ラダーフィルタについての調査、実装を行いました。
こちらの記事は、以下の記事の続きとなっています。
オシレータの作成、CustomOscillatorクラスの実装
オシレータにいろいろな波形を設定、ルックアップテーブルによる波形の生成
こんな人の役に立つかも
・JUCEチュートリアル、Introduction to DSPをやっている人
・JUCEでシンセサイザーが作成したい人
・JUCEでラダーフィルターを実装したい人
ラダーフィルターについて調査
モーグのラダーフィルターについて
モーグのラダーフィルタについて、あまり詳しくないので、調べてみました。
仕組みとしては、1次のフィルターを4個つなぎ、フィードバックを得るような構成とのことです。
こちらの記事を参考にさせて頂きました。
https://qiita.com/Aogiri-m2d/items/d691eb801071f4402f6a
フィルターが4個直列に並んでいて、フィードバックでレゾナンスを得るようなフィルターであることがわかりました。
JUCEのladderFilterモジュールの機能
こちらのモーグのラダーフィルターモジュールを参考にパラメータがどのようなものかを見ていきます。高級感があって、いい音出しますよ感がすさまじいです。
JUCEのladderFilterには次のような関数が実装されています。
setEnable
ラダーフィルターのオンオフをします。引数にtrueでオン、falseでオフになります。
setMode
フィルタータイプを選択します。Modeには、LPF12、HPF12、BPF12、LPF24、HPF24、BPF24があり、これらを指定します。
LPFがローパスフィルター、HPFがハイパスフィルター、BPFがバンドパスフィルターで、それぞれ12cd/oct、24db/octが選択できるようになっています。比較した500シリーズの2ポール、4ポールスイッチで変更できる部分と対応していることがわかります。500シリーズには、バンドパスの記載がないので、2パターン分、選択できるフィルタタイプが多いです。
setFrequencyHz
カットオフ周波数を設定します。
setResonance
レゾナンス値を設定します。0~1の値を指定します。
setDrive
サチュレーション量を指定します。1より大きい数値を指定することもでき、値が多いほどより深く歪みます。
Frequency、Resonance、Driveは比較した500シリーズのラダーフィルターと同じパラメータですし、音楽的にも常識的なパラメータなので、ladderFilaterは非常に扱いやすいモジュールとしてまとめられている印象です。まとめると、ladderFilterクラスは、モーグシンセサイザーのフィルター部分の仕組みをJUCEで実装して使いやすくしたモジュールととらえることができます。
実装
ラダーフィルタモジュールの実装はすごくシンプルです。
PluginProcessor.hのVoiceクラスに以下のようにプロセッサとしてラダーフィルタークラスのオブジェクトを追加します。
private:
//==============================================================================
juce::HeapBlock<char> heapBlock;
juce::dsp::AudioBlock<float> tempBlock;
enum
{
osc1Index,
osc2Index,
filterIndex,//[1]インデックスに追加しました。
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>,
CustomOscillator<float>,
juce::dsp::LadderFilter<float>,//[2]追加しました。
juce::dsp::Gain<float>> processorChain;
static constexpr size_t lfoUpdateRate = 100;
};
VoiceクラスのProcessorChainに[2]のように、ladderFilterクラスを追加します。そして、ラダーフィルターのインデックス番号を与えるために[1]のように、enumにfilterIndexを追加しました。整数の2がラダーフィルターの番号になります。
次に、Voiceクラスのコンストラクタです。
class Voice : public juce::MPESynthesiserVoice
{
public:
Voice()
{
auto& masterGain = processorChain.get<masterGainIndex>();
masterGain.setGainLinear(0.7f);
//[1]以下のプログラムを追加します。
auto& filter = processorChain.get<filterIndex>();//フィルターの参照を得ます。
filter.setCutoffFrequencyHz(1000.0f); //カットオフ周波数を1KHzにします。
filter.setResonance(0.7f); //レゾナンス値を0.7にします。
}
[1]のように、processorChainから、ラダーフィルターの参照取得して、カットオフ周波数とレゾナンス値を設定します。
各種パラメータをGUIで操作できるようにもしていきたいですね。
ラダーフィルタのモードを指定する
エクササイズにある、フィルタの種類などを指定するには、setModeを利用します。
先ほどのVoiceクラスのコンストラクタに、以下のようにsetMode関数でハイパスフィルタの4ポールのものを適用してみます。「HPF24」というやつです。ちょっとわかりやすいように、カットオフを10Kとかにしてみました。
class Voice : public juce::MPESynthesiserVoice
{
public:
Voice()
{
auto& masterGain = processorChain.get<masterGainIndex>();
masterGain.setGainLinear(0.7f);
auto& filter = processorChain.get<filterIndex>();
filter.setMode(juce::dsp::LadderFilterMode::HPF24);//ここを追加します。
filter.setCutoffFrequencyHz(10000.0f);
filter.setResonance(0.7f);
}
アプリの下部分はスペアナになっていますので、ここでハイパスで削り取られているのが確認できます。(スペアナはスケーリングされていないみたいで、結構カットオフ周波数を高くしないと確認が難しかったです。ここは改善の必要がありそうです。
Voiceクラスの全体
今回の実装を追加することで、Voiceクラスの全体は次のようになりました。
class Voice : public juce::MPESynthesiserVoice
{
public:
Voice()
{
auto& masterGain = processorChain.get<masterGainIndex>();
masterGain.setGainLinear(0.7f);
auto& filter = processorChain.get<filterIndex>();
filter.setCutoffFrequencyHz(1000.0f);
filter.setResonance(0.7f);
}
//==============================================================================
void prepare(const juce::dsp::ProcessSpec& spec)
{
tempBlock = juce::dsp::AudioBlock<float>(heapBlock, spec.numChannels, spec.maximumBlockSize);
processorChain.prepare(spec);
}
//==============================================================================
void noteStarted() override
{
auto velocity = getCurrentlyPlayingNote().noteOnVelocity.asUnsignedFloat();
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz, true);
processorChain.get<osc1Index>().setLevel(velocity);
processorChain.get<osc2Index>().setFrequency(freqHz * 1.01f, true);
processorChain.get<osc2Index>().setLevel(velocity);
}
//==============================================================================
void notePitchbendChanged() override
{
auto freqHz = (float)getCurrentlyPlayingNote().getFrequencyInHertz();
processorChain.get<osc1Index>().setFrequency(freqHz);
processorChain.get<osc2Index>().setFrequency(freqHz * 1.01f);
}
//==============================================================================
void noteStopped(bool) override
{
clearCurrentNote();
}
//==============================================================================
void notePressureChanged() override {}
void noteTimbreChanged() override {}
void noteKeyStateChanged() override {}
//==============================================================================
void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
{
auto block = tempBlock.getSubBlock(0, (size_t)numSamples);
block.clear();
juce::dsp::ProcessContextReplacing<float> context(block);
processorChain.process(context);
juce::dsp::AudioBlock<float>(outputBuffer)
.getSubBlock((size_t)startSample, (size_t)numSamples)
.add(tempBlock);
}
private:
//==============================================================================
juce::HeapBlock<char> heapBlock;
juce::dsp::AudioBlock<float> tempBlock;
enum
{
osc1Index,
osc2Index,
filterIndex,
masterGainIndex
};
juce::dsp::ProcessorChain<CustomOscillator<float>,
CustomOscillator<float>,
juce::dsp::LadderFilter<float>,
juce::dsp::Gain<float>> processorChain;
static constexpr size_t lfoUpdateRate = 100;
};