スペクトログラムのアプリと構成が似ています。
一気に本格的なものに近づきます。
引き続き、JUCEチュートリアルの「DSP」の項目、「Visualise the frequencies of a signal in real time」を進めていきます。今回は、デモアプリをProjucerのAudioテンプレートから実装していきます。
窓関数に関する調査と、Projucerプロジェクトの作成についての前回の記事もご参照ください。
公式チュートリアルはこちらになります。
こんな人の役に立つかも
・JUCEチュートリアル「Visualise the frequencies of a signal in real time」をやっている人
・JUCEでスペクトルアナライザを実装したい人
デモアプリの実装
アプリの基本的な構成としては、スペクトログラムのアプリ構成と同じです。音声処理スレッドでFFT用変換用の音声データを準備し、GUIの描画スレッドでFFTやGUI描画を行います。GUIの描画はタイマーで1秒間に30回の更新を行います。
アプリの構成については、スペクトログラムのアプリ実装の記事もご参照ください。
まずは、ヘッダファイルの実装です。
MainComponent.h
class MainComponent : public juce::AudioAppComponent
, private juce::Timer//[1]Timerを継承します。
{
public:
//...略...
//[2]次のメンバ関数を追加しました。
void timerCallback() override;
void pushNextSampleIntoFifo(float sample) noexcept;
void drawNextFrameOfSpectrum();
void drawFrame(juce::Graphics& g);
//[3]enumを追加します。
enum
{
fftOrder = 11,
fftSize = 1 << fftOrder,
scopeSize = 512
};
private:
//[4]privateなメンバ変数を追加します。
juce::dsp::FFT forwardFFT;
juce::dsp::WindowingFunction<float> window;
float fifo[fftSize];
float fftData[2 * fftSize];
int fifoIndex = 0;
bool nextFFTBlockReady = false;
float scopeData[scopeSize];
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
[1]で、Timerクラスを継承します。
[2]では、メンバ関数を追加します。TimerCallback関数はTimerクラスからoverrideした関数で、GUIの描画に利用します。pushNextSampleIntoFifo関数は以前スペクトログラムのアプリで利用したときと同じように、FFTに渡す音声データを準備するための関数です。
drawNextFrameOfSpectrum関数とdrawFrame関数でスペクトルアナライザのGUI描画処理を行います。
[3]でenumとしてfftOrder、fftSize、scopeSizeを定義しています。fftOrderは2の累乗です。fftSizeは、JUCEの振幅スペクトルに変換するためのFFTクラスの関数「performFrequencyOnlyForwardTransform」に与えるデータです。左シフト演算で「2^11」をしています。そのため、2048となります。
scopeSizeは、描画する際の周波数のポイント数です。低域から高域の512のポイントで周波数を描画します。
[4]では、privateなメンバを定義します。ここで利用するメンバ変数などは、スペクトログラムのときから、「window」と「scopeData」が追加されました。それ以外の変数は変化なく、同じ使い方です。scopeDataは周波数の描画の際に利用する配列で、今回の要部となるのがWindowingFunctionクラスの「window」です。これで窓関数のオブジェクトを利用できるようになります。
MainComponent.cpp
次に、処理内容を定義していきます。
コンストラクタ
コンストラクタで各種初期化処理を次のように行います。
MainComponent::MainComponent() : forwardFFT(fftOrder) //[1]メンバイニシャライザを追加しました。
,window(fftSize, juce::dsp::WindowingFunction<float>::hann)
{
//[2]次のように変更しました。
setOpaque(true);
setAudioChannels(2, 0);
startTimerHz(30);
setSize(700, 500);
}
コンストラクタの[1]、メンバイニシャライザでは、FFTクラスのfowardFFTの初期化、窓関数「window」には、ハン窓を設定しました。
[2]のように、コンストラクタの処理で、setOpaqueでアプリの親コンポーネントが不透明であることを明示し、setAudioChannelsで音声入出力を、入力2チャンネル、出力なし、と設定しています。startTimerHzは、30HzでtimerCallbackの処理を行うように設定します。
getNextAudioBlock関数
次に、getNextAudioBlock関数の中身を次の内容に変更します。
void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
//以下の通り内容を変更しました。
if (bufferToFill.buffer->getNumChannels() > 0)
{
auto* channelData = bufferToFill.buffer->getReadPointer(0, bufferToFill.startSample);
for (auto i = 0; i < bufferToFill.numSamples; ++i)
pushNextSampleIntoFifo(channelData[i]);
}
}
ifの条件で、音声バッファのチャンネルが1以上あることとしています。そして、getReadPointer関数で0チャンネル目の音声バッファの最初のサンプルへのポインタを「channelData」として取得します。その後は、音声バッファのサンプル数分繰り返しを行い、pushNextSampleIntoFifoにサンプルをひとつづつ渡していきます。この関数の中身は、スペクトログラムアプリでの処理と全く同じとなります。
pushNextSampleIntoFifo関数
cppファイルに、以下のようにpushNextSampleIntoFifo関数を追記しました。
void MainComponent::pushNextSampleIntoFifo(float sample) noexcept
{
if (fifoIndex == fftSize)
{
if (!nextFFTBlockReady)
{
juce::zeromem(fftData, sizeof(fftData));
memcpy(fftData, fifo, sizeof(fifo));
nextFFTBlockReady = true;
}
fifoIndex = 0;
}
fifo[fifoIndex++] = sample;
}
この処理は、fifoへと音声バッファのデータを格納していく処理です。fifoがいっぱいになったら(今回は2048個)fftDataにデータをコピーしてFFT入力用のデータとします。fftDataはnextFFTBlockReadyフラグで制御され、FFT側(描画処理)で利用されてフラグがfalseとなるまではずっと2048個のデータを保持することになります。この処理内容も、スペクトログラムのアプリと同じ処理内容となります。
FFTへデータを渡すまでの流れは基本的にスペクトログラムのアプリと同じですね。
後は、GUIへ周波数を描画する処理がありますが、描画内容が前回のスペクトログラムのアプリと異なる部分になります。次回の記事にて詳細に内容を見ながら実装を進めたいと思います。