市販のEQのようにフィルターのカーブを描くのを、どうやってやろうかと調べてみました。
JUCEにそういった便利な機能はあるのかな?
ハイパスフィルターのフィルターカーブを描きたいと思い、いろいろと調べていました。
JUCEフォーラムなど、いくつかのサイトを見たところ、どうやら、JUCEでは、getMagnitudeForFrequency関数という関数を利用すると、簡単にフィルターカーブを描くことができそうなので、この関数を検証してみました。
※この記事は、私が試行錯誤で作成している、シンプルなハイパスフィルターの実装をベースに行っているものです。シンプルなハイパスフィルターの実装については、こちらの記事もご参照ください。
こんな人の役に立つかも
・JUCEのgetMagnitudeForFrequency関数を利用したい人
・JUCE、DSPモジュールのフィルターカーブを描きたい人
・本ブログのシンプルなハイパスフィルターを作成している人
getMagnitudeForFrequencyの検証
検証の内容
getMagnitudeForFrequencyは、juce::dsp::Coefficientsクラスの関数です。Coefficientsクラスのインスタンスから呼び出すように処理をしなければいけません。
Coefficientsクラスは、makeHighPass関数などを利用することで、得たい(バイクアッドの)フィルターの係数を自動的に算出してくれるような便利な関数などを持っているクラスです。
私が作成しているシンプルなハイパスフィルター、の実装では、「FilterProcessor」クラスという独自(チュートリアルから流用)の中でフィルターの係数をCoefficientsクラスのmakeHighPass関数で計算させています。
FilterProcessorクラス内にCoefficientsのインスタンスが存在しているので、ここで「getMagnitudeForFrequency」関数を呼び出すことができそうです。
ということで、FilterProcessorクラス内でgetMagnitudeForFrequency関数を呼び出して、どのような数値が返ってくるかを見てみました。
検証のための実装
今までの実装では、ProcessorDuplicatorのオブジェクトの「state」にCoefficientsで生成した係数を直接渡していました。Coefficients<float>クラスのオブジェクトへのポインタとして、受けるために、privateなメンバとして、「Coefficients::Ptr filter_coef_ptr」を定義しました。そして、getMaginitudeForFrequency関数を呼び出すために、Coefficientsクラスの「filter_coef」を定義しました。
class FilterProcessor : public ProcessorBase
{
//...略...
private:
std::atomic<float>* cutoffParam_filter = nullptr;
float samplerate;
std::atomic<float>* QParam_filter = nullptr;
//[1]次の2つのメンバを追加しました。
juce::dsp::IIR::Coefficients<float>::Ptr filter_coef_ptr;
juce::dsp::IIR::Coefficients<float> filter_coef;
juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> filter;
};
次に、Coefficients::Ptrを使えるようにしていきます。
コンストラクタで、Ptrをメンバイニシャライザで初期化します。Coefficientsのオブジェクトをnewしてそこへのポインタとして初期化を行います。
class FilterProcessor : public ProcessorBase
{
public:
//[1]追加しました。
FilterProcessor(): filter_coef_ptr(new juce::dsp::IIR::Coefficients<float>()) {}
次に、filterProcessorクラスのprepareToPlay関数です。
今までは、[2-1]のように定義していて、直接ProcessorDuplicatorクラスのstateへ登録されていました。
[2-2]のように、一度先ほど初期化した「filter_coef_ptr」へと取得します。そして、それをstateへ渡すように変更しました。
void prepareToPlay(double sampleRate, int samplesPerBlock) override
{
samplerate = sampleRate;
//[2]変更しました。
//*filter.state = *juce::dsp::IIR::Coefficients<float>::makeHighPass(sampleRate, 1000.0f);//[2-1]
//[2-2]
*filter_coef_ptr = *juce::dsp::IIR::Coefficients<float>::makeHighPass(sampleRate, 1000.0f);
filter.state = *filter_coef_ptr;
juce::dsp::ProcessSpec spec{ sampleRate, static_cast<juce::uint32> (samplesPerBlock), 2 };
filter.prepare(spec);
}
最後に、filterProcessorクラスのprocessBlock関数です。
ptrpareToPlay関数同様、直接stateに入れていたものを、一度「filter_coef_ptr」に受けるようにしました。
そして、[3-1]で、「filter_coef_」Coefficientsクラスの「filter_coef」に再度取得します。これで、getMagnitudeForFrequency関数を実行できる準備ができました。
[3-2]では、DBGでそれぞれの周波数をgetMagnitudeForFrequencyへ入れて出力するようにしました。適当に100Hz、200Hz…としています。
void processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer&) override
{
float cutoff_val = *cutoffParam_filter;
float qParam_val = *QParam_filter;
//[3]変更して、getMagnitude...関数をDBGしました。
//*filter.state = *juce::dsp::IIR::Coefficients<float>::makeHighPass(samplerate, cutoff_val, qParam_val);
*filter_coef_ptr = *juce::dsp::IIR::Coefficients<float>::makeHighPass(samplerate, cutoff_val, qParam_val);
filter.state = *filter_coef_ptr;
filter_coef = *filter_coef_ptr.get();//[3-1]Coefficientsクラスとして受けます。
//[3-2]出力します。
DBG("dbg100:" << filter_coef.getMagnitudeForFrequency(100.0, samplerate));
DBG("dbg200:" << filter_coef.getMagnitudeForFrequency(200.0, samplerate));
DBG("dbg500:" << filter_coef.getMagnitudeForFrequency(500.0, samplerate));
DBG("dbg1000:" << filter_coef.getMagnitudeForFrequency(1000.0, samplerate));
DBG("dbg1500:" << filter_coef.getMagnitudeForFrequency(1500.0, samplerate));
DBG("dbg2000:" << filter_coef.getMagnitudeForFrequency(2000.0, samplerate));
juce::dsp::AudioBlock<float> block(buffer);
juce::dsp::ProcessContextReplacing<float> context(block);
filter.process(context);
}
//...略...
検証の結果
カットオフ周波数:1Khz、Q:0.707のときの出力
dbg100:0.00997119
dbg200:0.0398581
dbg500:0.242029
dbg1000:0.707
dbg1500:0.914248
dbg2000:0.970562
すごく雑な図ですが、フィルターカーブをこれで描くと、以下のような雰囲気になりそうです。
次に、Qを4にあげました。
次のような出力が得られました。1000Hzのとき、4.0となっているので、Qによって曲線にふくらみが出ているようなイメージになりそうです。
dbg100:0.0100689
dbg200:0.0414914
dbg500:0.327873
dbg1000:4
dbg1500:1.71983
dbg2000:1.3117
気づいたことなど
ここで、一つ気づいたことがあります。以前何度か低周波数が見やすいように周波数の間隔をスケーリングしましたが、雰囲気でやっていました。
今回、この周波数をプロットするとき、減衰が一直線になるように周波数の間隔を調整しなければいけないような気が指摘ました。現状だと、減衰がカーブになってしまい、一般的なEQなどのフィルターの曲線のようにならないきがしてきました。
とりあえずは、実際に描画できるように実装していき、それから考えたいと思います。