JUCEチュートリアル、Introduction to DSP、ラダーフィルタについて調査、ラダーフィルタを実装する

ラダーフィルタって、名前は聞くけど、実際にはよくわかりません。

モーグシンセサイザーのフィルター部分の仕組みのことみたいだね~

JUCEチュートリアルのIntroduction to DSPを進めています。「Adding a ladder filter」の項目を実装しました。ラダーフィルタについての調査、実装を行いました。

こちらの記事は、以下の記事の続きとなっています。

デモアプリ作成のための準備

デジタル信号処理の概要とDSPモジュールについて調査

オシレータの作成、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;
};
よかったらシェアしてね!
目次
閉じる