クラスをまたいでのデータのやり取りが意外と混乱します。
少しづつオブジェクト指向にも慣れてきたのかな~
JUCEプラグイン開発、シンプルなハイパスフィルターにフィルターの特性カーブを描画しようと奮闘中です。とりあえずgetMagnitudeForFrequency関数で雰囲気できた気がしますので、ここまでの実装をまとめようと思います。
こんな人の役に立つかも
・getMagnitudeForFrequency関数の利用方法を知りたい人
・JUCEでフィルターカーブを描きたい人
・本ブログのシンプルなハイパスフィルターを作成している人
エディタ側からgetMagnitudeForFrequencyを呼び出す
エディタの実装クラスである「●●PluginEditor」クラスからgetMagnitudeForFrequencyを呼び出すために、次のような順番で呼び出していく必要があります。
①:●●PluginEditor内のaudioProcessorから、●●PluginProcessorクラスのインスタンスへアクセスできます。
②●●PluginProcessor内に定義したMyFilterから、filterProcessorクラスに定義したfilter_coefへアクセスできます。
③filter_coefはCoefficientsクラスのインスタンスなので、getMagnitudeForFrequencyを呼び出すことができます。
ただ、MyFilterとfilter_coefは、privateなメンバとして、定義しているので、これらのアクセサを作成して、外部から参照できるようにしておきます。
この流れを実装していきたいと思います。
意外と遠く見えますね~
PluginProcessor.hに定義しているfilterProcessorクラスに、以下のアクセサを追加しました。publicなメンバです。これで、●●PluginProcessorクラスからMyFilter内のfilter_coefを参照することができます。
//※filter_processorクラス内のpublicなメンバです。
juce::dsp::IIR::Coefficients<float> getFilterCoef() {
return *filter_coef_ptr;
}
次に、PluginProcessor.hに定義してある●●PluginProcessorクラスにfilterProcessorクラスのインスタンスにアクセスするためのアクセサを追加しました。今回の私のプラグインでは、3つのハイパスフィルターを作成しているので、3つ作成しました。
//01_panda_filterPluginProcessor内のpublicなメンバです。
FilterProcessor& getMyFilter() {
return MyFilter;
}
FilterProcessor& getMyFilter2() {
return MyFilter2;
}
FilterProcessor& getMyFilter3() {
return MyFilter3;
}
そして、PluginEditor.cppのPaint関数内で値を取得して描画したいので、次のようにDBGでgetMagnitudeForFrequency関数を呼び出してみました。
void _01_panda_filterAudioProcessorEditor::paint (juce::Graphics& g)
{
//...略...
//[1]getMagnitudeForFrequencyの呼び出しです。
DBG(audioProcessor.getMyFilter().getFilterCoef().getMagnitudeForFrequency(1000.0, (float)audioProcessor.getSampleRate()));
g.setOpacity(1.0f);
g.setColour(juce::Colours::white);
drawFrame(g);
}
[1]のように、audioProcessorからgetMyFilterアクセサを呼び出して、返ってきたMyFilterからさらにgetFilterCoefアクセサ、その先にgetMaginitudeForFrequency関数を呼び出すことができました。
引数としては、固定値の1000Hz、第二引数にサンプルレートを与えています。サンプルレートは、audioProcessorクラスのgetSampleRate関数で取得することができます。
これで、エディタ側のpaint関数でフィルタの曲線を描く準備ができました。
フィルターカーブを描く
PluginEditor.cppのエディタクラスのprivateなメンバに、フィルターカーブの描画に利用する各種数値を定義しました。
//カーブの描画に必要な数値です。
//[1]アルゴリズムに必要な変数、数値です。
double preMagnitude = 0;
double currentMagnitude = 0;
const double maxGain = 2.0;
//[2]スペアナのサイズに関する数値です。
const double displayHeight = 150.0;
const int diaplayWidth = 350;
const int displayMargin = 75;
//[3]サンプルレートを計算して入れておきます。
const float currentSampleRate = (float)audioProcessor.getSampleRate();
const float sampleRateDiv2 = (float)audioProcessor.getSampleRate() / 2;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (_01_panda_filterAudioProcessorEditor)
};
[1]のpreMagnitudeとmaxGainは、描画時のアルゴリズムに必要な変数です。
[2]は、スペアナのサイズなど定義しています。
[3]は、サンプルレートに関する数値を取得します。
PluginEditor.cppのpaint関数に以下のようにフィルターカーブの描画処理を追加しました。
一番上に描かれるように、paint関数の一番最後に処理を追加しました。
void _01_panda_filterAudioProcessorEditor::paint (juce::Graphics& g)
{
//...略...
//ここからフィルターカーブの描画です。
//[1]色の設定と描画前の変数の初期化です。
g.setColour(juce::Colours::yellow);
preMagnitude = 0;
currentMagnitude = 0;
//[2]描画ループです。
for (int i = 0; i < diaplayWidth; i++) {
//[2-1]周波数に対するスケーリングです。
auto skewedProportionX = 1.0f - std::exp(std::log(1.0f - (float)i / diaplayWidth) * 0.009f);
//[2-2]周波数に対するゲイン値(Y軸)を得ます。
currentMagnitude = audioProcessor.getMyFilter().getFilterCoef().getMagnitudeForFrequency(skewedProportionX * sampleRateDiv2 , currentSampleRate);
//[2-3]ここで線を描画しています。
g.drawLine( i + displayMargin //start X
, (preMagnitude * displayHeight)/ maxGain * -1 + displayHeight //start Y
, i + displayMargin + 1 //end X
, (currentMagnitude * displayHeight)/ maxGain * -1 + displayHeight //end Y
, 1.0f); // Line thickness
//[2-4]次の描画の準備です。
preMagnitude = currentMagnitude;
}
}
[1]では、線の描画色を黄色として、描画時に利用する変数「preMagnitude」と「currentMagnitude」を初期化しました。
[2]で、フィルターカーブの描画のためのループを行います。
[2-1]では、まず横軸の周波数スケーリングを行います。この式は、以前のスペアナの周波数スケーリングを行った時のものを持ってきました。iが0に近いほど増加量が少なく、350に近づくほど増加量が多いような数値がskewdPropotionXとなります。ここの係数は、0.009fとして、表示周波数とフィルターカーブの周波数が一致させるために、以前のスペアナの周波数軸を割り当てた数値と同一にしてあります。
あとは、iの比例的増加がskewedPropotionXによって対数的増加に変換されたので、ループ毎にskewedPropotionXの値を周波数へ変換して、この周波数をgetMagnitudeForFrequencyの引数にあたえればよいです。
[2-2]ではgetMagnitudeForFrequencyを実行しています。第一引数には周波数をいれますが、これは、周波数の最大値を掛けることで、skewedPropotionXを周波数へ変換しています。
[2-3]で、直線を紐づけるように描画しています。drawLine関数は線分の開始(X,Y)、終了(X,Y)、線の太さを引数にとります。第二、第四引数の計算がポイントです。それぞれ、getMagnitudeForFrequencyから得たゲイン値を利用して描画座標を指定します。
得られる「preMagnitude」と「currentMagnitude」は0~4のゲインの値です。これをY軸座標値に変換しています。Y軸の座標計算で利用しているmaxGainは、表示上の最大値を決めるものなので、現状、Qでゲイン値4まで行くので、実際には、描画ははみ出してしまうような仕様です。-1を掛けているのは、Y軸が上から下方向に値増加していくので、反転させています。
[2-4]で、次のピクセルの描画に行くために、現在のcurrentMagnitudeをpreMagnitudeとしています。
課題
フィルターカーブの周波数スケーリングと、スペアナの周波数スケーリングがずれてしまっているので、もう少し見直す必要がありそうです。
フィルター2個目、3個目で-12db/octや-18db/octとなるようなも曲線を描くことができるようにしたいです。
地味な改善点として、skewedPropotionXはforの内部で計算しなくても良いので、配列などに格納して定数化することで、リソースに余裕を持たせることができそうです。