GUIとしてスペクトルの表示部分を実装していきます。
仕組み的にもだんだん見慣れてきた形だね~
前回の記事に引き続き、スペクトルアナライザのアプリをプラグイン化していきます。今回は、GUIを表示する部分を作っていきます。
今回もmacの開発環境、Xcodeで挑戦しています。前回の音声処理スレッド作成はこちらの記事をご参考ください。
mojaveなど旧バージョンのOSにXcodeをインストールしたい方はこちらの記事もご参考ください。
※この記事は、JUCEチュートリアルのスペクトルアナライザのプラグイン化を主としますので、JUCEチュートリアルで実装するスペクトルアナライザのプログラムの内容を理解していることが前提となっています。JUCEチュートリアルのスペクトルアナライザについては以下の記事をご覧ください。
こんな人の役にたつかも
・JUCEアプリをプラグインに移植したい人
・JUCEでスペクトルアナライザのプラグインを実装したい人
アクセサの追加
PluginProcessor.hのProcessorクラス側にアクセサを定義します。
Processor側で利用している、privateなメンバ、「nxtFFTBlockReady」フラグとfftDataのポインタ、fftData配列のデータに対して、PluginEditorから参照したり、操作できるように、以下のアクセサを追加しました。
class FFT_speanaAudioProcessor : public juce::AudioProcessor
{
public:
//...略...
//アクセサを追加します。
bool getFFTReady(){
return nextFFTBlockReady;
}
void setFFTReady(bool b) {
nextFFTBlockReady = b;
}
float* getFFTDataPtr() {
return fftData.data();
}
float getFFTDataSample(int sample) {
return fftData[sample];
}
//...略...
プラグインにするにあたり、描画処理スレッドはProcessorEditorクラス側に実装するので、アクセサを利用して、参照が必要なものだけを見ることができるようにしています。
アクセサでAudioProcessor側のデータにアクセスしながら、ProcessorEditorクラス側でFFT処理を行ってGUI描画を行います。
PluginEditor.h
PluginEditor.hのクラス宣言を以下のように変更しました。
[1]では、描画する処理をタイマーで開始するように、Timerクラスを継承するようにしています。
[2]では、publicなメンバ関数として、「TimerCallback」関数をoverrideして処理を実装します。今回は、ヘッダーファイルに処理内容も直接実装を行いました。TimerCallback関数で、AudioProcessor側のnextFFTBlockReadyフラグがtrueのときに、周波数のrepaintを行います。
class FFT_speanaAudioProcessorEditor : public juce::AudioProcessorEditor,
//[1]追加しました
private juce::Timer
{
public:
//...略...
//[2]TimerCallbackを追加します。
void timerCallback() override
{
auto flg = audioProcessor.getFFTReady();
if (flg)
{
drawNextFrameOfSpectrum();
audioProcessor.setFFTReady(false);
repaint();
}
}
//...[3]へ続く...
次に、[3]は、スペクトルの描画関連の関数「drawNextFrameOfSpectrum」と「drawFrame」です。これらの関数の詳細な挙動は、以前スタンドアロンで実装した際と同じになっていますので、そちらの記事もご参考ください。
スタンドアロンと変更した点としては、fftDataとfftSize、fftDataのデータの中身をAudioProcessor側から読み込むため、それぞれアクセサなどを使って参照するようにした点です。
//[3]描画関連の関数を追加します。
void drawNextFrameOfSpectrum()
{
//[3-1]窓関数の指定、引数をアクセサとProcessorのfftSizeで与えます。
window.multiplyWithWindowingTable (audioProcessor.getFFTDataPtr(), audioProcessor.fftSize);
//[3-2]周波数データへの変換処理、Processor側のデータfftDataはアクセサで与えます。
forwardFFT.performFrequencyOnlyForwardTransform (audioProcessor.getFFTDataPtr());
auto mindB = -100.0f;
auto maxdB = 0.0f;
//[3-3]スケーリング処理です。AudioProcessor.fftSizeに、fftDataの中身はアクセサに変更しました。
for (int i = 0; i < scopeSize; ++i)
{
auto skewedProportionX = 1.0f - std::exp (std::log (1.0f - (float) i / (float) scopeSize) * 0.2f);
auto fftDataIndex = juce::jlimit (0, audioProcessor.fftSize / 2, (int) (skewedProportionX * (float) audioProcessor.fftSize * 0.5f));
//fftData[fftDataIndex]はaudioProcessor.getFFTDataSample(fftDataIndex)へ変更。
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;
}
}
void drawFrame (juce::Graphics& g)
{
for (int i = 1; i < scopeSize; ++i)
{
auto width = getLocalBounds().getWidth();
auto height = getLocalBounds().getHeight();
g.drawLine ({ (float) juce::jmap (i - 1, 0, scopeSize - 1, 0, width),
juce::jmap (scopeData[i - 1], 0.0f, 1.0f, (float) height, 0.0f),
(float) juce::jmap (i, 0, scopeSize - 1, 0, width),
juce::jmap (scopeData[i], 0.0f, 1.0f, (float) height, 0.0f) });
}
}
//...[4]に続く...
最後に、[4]では、scopeSizeを指定します。この変数は描画でしか利用しないので、ProcessorEditorクラス側に実装しておきます。
//[4]publicなメンバ、scopeSizeを追加します。
static constexpr auto scopeSize = 512;
private:
FFT_speanaAudioProcessor& audioProcessor;
//[5]FFT関連のprivateなメンバを追加します。
juce::dsp::FFT forwardFFT;
juce::dsp::WindowingFunction<float> window;
float scopeData [scopeSize];
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FFT_speanaAudioProcessorEditor)
};
最後に、[5]で、FFTクラスのオブジェクトなどを定義しておきます。
PluginEditor.cpp
PluginEditor.cppのProcessorEditorクラスの関数をいくつか変更します。
コンストラクタ
コンストラクタでは、FFTのオブジェクトの初期化をメンバイニシャライザで行っておきます。この時、窓関数も初期化しておきます。コンストラクタの処理内では、タイマースタートが重要ですね。
FFT_speanaAudioProcessorEditor::FFT_speanaAudioProcessorEditor (FFT_speanaAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p),
//[1]FFT関連の初期化です。
forwardFFT (p.fftOrder),
window (p.fftSize, juce::dsp::WindowingFunction<float>::hann)
{
//[2]タイマーをスタートさせるなど初期化を行います。
setOpaque (true);
startTimerHz (30);
setSize (700, 500);
}
paint関数
描画処理を、drawFrame関数が動作するように以下のように変更します。これは、timerCallbackで毎秒30回repaintされてよびだされます。単純にdrawFrameの処理を行うだけです。
void FFT_speanaAudioProcessorEditor::paint (juce::Graphics& g)
{
//変更しました。
g.fillAll (juce::Colours::black);
g.setOpacity (1.0f);
g.setColour (juce::Colours::white);
drawFrame (g);
}
動作検証
今回のプラグインは、入力0チャンネル目の音声をスペクトル表示するので、オーディオインターフェースの入力2を表示したい場合、次のように接続します。今回は、インターフェースの入力2にエレキギターを接続してみました。
エレキでA音を鳴らしたとき440Hzを中心とする周波数が表示されるはずなので、LogicのEQの表示と比較してみました。
自作のスペアナでは、全体的にまだ低周波の方を広く表示しても良さそうです。スペアナとしては、だいたい同じように動作しているように見えます。
波形的に比較してみても、音量の小さい部分の表示が隠れているだけで、ほぼ同じように周波数を取得できていることがわかります。
改良点
次の点を改良していきたいと思いました。
・周波数のメモリを付けたい。
・スケーリングを既存のプラグインのように見やすくしたい。
・ステレオチャンネルを合成して周波数表示を行いたい。
現状ですと、モノラルなので、ステレオにも対応できるともっと実用的になりそうです。