低い周波数がもっと大きく見えるといいかな、と思いました。
市販のEQだと1kHz以下がもっと幅広いよね~
JUCEで開発中のシンプルなハイパスフィルターのスペクトルアナライザについて、もう少し低い周波数を細かく見ることができるといいなと考えています。そこで、FFTのパラメータとスペアナ表示のアルゴリズム内の係数を見直すことで、より低周波数に特化した表示方法を模索してみました。
※本ブログのシンプルなハイパスフィルターに関する記事になります。記事一覧はこちらです。
こんな人の役に立つかも
・JUCEチュートリアルのスペクトルアナライザのスケーリングを改良したい人
・スペクトルアナライザのスケーリングを検討している人
・本ブログのシンプルなハイパスフィルターを作成している人
FFTのチューニング
FFTで取得できる周波数の分解能を細かくするため、パラメータを操作して検証してみました。
FFTへ与えるデータ数を多くすると、取得できる周波数成分の幅がどんどん細かくなっていきます。その反面、データを多く取得しなければいけないので、データがたまる時間を多くとらなければいけないので、スペクトル表示のレスポンスが悪くなります。
PluginProcessor.hのfftOrderの値
PluginProcessor.hのFFTに関するパラメータを以下のように調整しました。
//10: 1024
//11: 2048
//12: 4096
//13: 8192
static constexpr auto fftOrder = 13;
static constexpr auto fftSize = 1 << fftOrder;
private:
//FFT関連のメンバです。
std::array<float, fftSize> fifo;
std::array<float, fftSize * 2> fftData;
int fifoIndex = 0;
bool nextFFTBlockReady = false;
fftOrderはFFTを行う配列数に影響を与えます。13とすることで「fftData配列」が16384個、「fftSize」配列が8192個となります。
fftOrderを13とすることで、最終的にFFTで得られる最小の周波数の幅が5Hz程度となります。一方で、より細かい周波数の分解能を得るためには、このfftOrderを大きくしていかなければならず、長い時間のサンプル量が必要なので、時間に対する反応が遅くなっていきます。fftOrderを1大きくすると2倍のサンプル量となるので、このくらいの値が現実的なのかな、と思います。
GUI表示領域の考察
PluginEditor.hのdrawNextFrameOfSpectrum関数
次に、周波数の描画に関する部分を改良していきたいと思います。
drawNextFrameOfSpectrum関数の全体は次のようになっています。
void drawNextFrameOfSpectrum()
{
window.multiplyWithWindowingTable(audioProcessor.getFFTDataPtr(), audioProcessor.fftSize);
forwardFFT.performFrequencyOnlyForwardTransform(audioProcessor.getFFTDataPtr());
auto mindB = -100.0f;
auto maxdB = 0.0f;
for (int i = 0; i < scopeSize; ++i)
{
//[1]skewedProportionXの調整
auto skewedProportionX = 1.0f - std::exp(std::log(1.0f - (float)i / (float)scopeSize) * 0.03f);
//[2]fftDataIndexの計算
auto fftDataIndex = juce::jlimit(0, audioProcessor.fftSize / 2, (int)(skewedProportionX * (float)audioProcessor.fftSize * 0.5f));
auto level = juce::jmap(juce::jlimit(mindB, maxdB, juce::Decibels::gainToDecibels(audioProcessor.getFFTDataSample(fftDataIndex))
- juce::Decibels::gainToDecibels((float)audioProcessor.fftSize)),
mindB, maxdB, 0.0f, 1.0f);
scopeData[i] = level;
}
}
この中の[1]のプログラムのskewedProportionXを調整していきます。一番右の係数を0.03にしてみました。
auto skewedProportionX = 1.0f - std::exp(std::log(1.0f - (float)i / (float)scopeSize) * 0.03f);
この式をPythonで計算してグラフ化してみると次のようになります。次のプログラムはPyhtonで、Colaboratoryで実行しました。
#skewedProportionX Plot用
# i / 512
scopeSize = 512;
import matplotlib.pyplot as plt
import math
list = []
for num in range(1,scopeSize):
list.append(num/scopeSize)
#1.0f - ( i / 512 )
list2 = []
for x in list:
list2.append(1 - x)
#log( 1.0f - (i / 512))
list3 = []
for x in list2:
list3.append(math.log(x))
#EXP(log)
list4 = []
for x in list3:
list4.append(math.exp(x * 0.03))#この係数を変化させます。
#1-EXP(skewedPropotionX)
list5 = []
for x in list4:
list5.append(1 - x)
print(list5)
print(list5[256])#指定したインデックスのskewedProportionXを確認します。
plt.plot(list5)
Y軸にskewedProportionXの値、X軸にscopeSizeとの関係性を表現する曲線となります。
scopeSizeは表示領域の横軸のプロット数になります。512個なので、表示領域の横軸方向は、512の点で周波数を表現することになります。それぞれのY軸の点に対するskewedProportionXの値が、表示する周波数を決定します。先ほどの[2]のプログラムで計算するfftDataIndexがそうです。
auto fftDataIndex = juce::jlimit(0, audioProcessor.fftSize / 2, (int)(skewedProportionX * (float)audioProcessor.fftSize * 0.5f));
scopeSizeが512のとき、skewedProportionXが約0.17なので、
skewedProportionX * (float)audioProcessor.fftSize * 0.5f
に当てはめてみると、0.17×8192×0.5=696.…となります。intにキャストして696となり、この数字は、「fftData配列の696番目のデータ」を取得しに行きます。fftDataにはFFT変換後のデータが入っています。(getFFTDataSample関数で取得しています。)
現在のfftDataは4096番目が22050Hzのデータとなるような周波数成分のデータを格納しているので、
22050×696÷4096=3746Hzが512番目、スペクトルアナライザの右端の周波数になります。
ということで、今回のFFT設定でskewedPropotionXの値から該当周波数を求めるには、
Hz = (int)(skewedProportionX × 4096)×22050÷4096
とすればよいことがわかりました。
再度、PythonでskewedProportionXと周波数の対応付けをグラフ化してみました。(次のプログラムはcolaboratoryで実行したPythonです。)
#skewedPropotionXの周波数計算
list6 = []
for x in list5:
val = x*4096
list6.append(int(val) * 22050 / 4096)
plt.xlabel("scopeSize")
plt.ylabel("Hz")
plt.plot(list6)
skewedProportionXを計算するときの係数が0.03fのときは次のように100Hz以下がまだ小さく見えてしまいます。
調整して、0.09fにしたとき、以下のようなグラフになりました。
係数0.09の状態で、リビルドして、左から20Hz、100Hz、500Hzのサイン波をれてみました。
20Hzあたりでは周波数分解能が5Hz程度なので、カクカクしています。低い周波数なので、もう少しFFTのレスポンスが悪くてもいいのかなと思い、分解能重視で、fftOrderの値を14にしてみます。
雰囲気としては、いい感じかなと思います。線の描画をもう少し滑らかにすることで、見た目も改善されるのかなと思います。
このプラグインの周波数領域の範囲はこれで決めていきたいと思います。
バックグラウンドの線の描画
周波数領域のスケーリングができましたので、スペアナの背景の線を調整して描画していきます。
描画位置の当たりを付けて、線を引いていきました^^;
void _01_panda_filterAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
//...略...
//基準線の描画
g.setColour(juce::Colour::fromRGBA(128,128,128,128));
g.drawLine(108.0f,0.0f,108.0f,150.0f,2.0f);
g.drawLine(206.0f, 0.0f, 206.0f, 150.0f, 2.0f);
g.drawLine(392.0f, 0.0f, 392.0f, 150.0f, 2.0f);
//周波数ラベルの描画
g.setColour(juce::Colours::white);
g.drawText("20Hz",83,155.0f,50.0f,10.0f,juce::Justification::centred);
g.drawText("100Hz", 181, 155.0f, 50.0f, 10.0f, juce::Justification::centred);
g.drawText("500Hz", 367, 155.0f, 50.0f, 10.0f, juce::Justification::centred);
大体の座標の位置から、次のイメージで線の座標を模索していきました。
最終的に、次のように周波数の線を描画することができました。
検討事項
やればやるほど、いろいろと盛り込みたい事項が増えてきました^^;
・スペアナの線を滑らかに表示したいです。
・ゲイン値の目安線も引きたいです。
・カットオフの曲線も引きたいです。
描画関連の検討をいろいろと行うと、さらにブラッシュアップできそうな、予感です。