チュートリアル信号の周波数をリアルタイムで可視化する
DSPモジュールのFFTクラスを使用して、入力オーディオデータをスペクトラムアナライザーとして表示する方法を学びます。ウィンドウ関数を使用する利点を理解します。
レベル:中級
プラットフォーム:Windows, macOS, Linux
クラス:dsp::FFT,dsp::WindowingFunction,Decibels
スタート
このチュートリアルはTutorial: The fast Fourier transform.もしまだなら、まずそのチュートリアルを読むべきだ。
このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。
お使いのオペレーティングシステムが、マイクへのアクセス許可を要求する必要がある場合(現在、iOS、Android、macOS Mojave)、Projucerの関連するエクスポーターの下に対応するオプションを設定し、プロジェクトを再保存する必要があります。
このステップでヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.
デモ・プロジェクト
完成すると、このデモプロジェクトは、入力されたオーディオデータを周波数(x軸)と振幅(y軸)の2次元スペクト ルアナライザーとして表示します。画面に表示される値は1秒間に30回更新され、任意の時間枠のウィンドウは次のようになります:
ウィンドウ機能
で見たとおりだ。Tutorial: The fast Fourier transform高速フーリエ変換は、ある信号の個々の周波数成分を処理するために、時間領域の信号を周波数領域に変換することができる。
しかし、フーリエ変換の限界は、オーディオ・アプリケーションのオーディオ・バッファ・ブロックのように、有限の時間間隔の間に適用される場合、変換は、問題の周波数の両側に新しい周波数成分が出現し始める、スペクトル・リークと呼ばれる現象が現れることである。これは、サンプリングされた信号部分が波形の自然な周期に収まらないことがあり、本質的に信号が切り捨てられるためです。
スペクトル漏れは、似たような周波数と似たような振幅を持つ2つの正弦波や、似たような周波数と似たような振幅を持つ2つの正弦波を分析するときに特に問題となる。正弦波 の周波数と振幅が近い場合、漏れによって互いに区別がつかなくなることがある。一方、正弦波の周波数と振幅が遠い場合、最も強い成分からの漏れは、最も弱い成分の存在を覆い隠してしまう。
スペクトル漏洩の影響を減らすために、フーリエ変換を行う前に信号に窓関数を適用することができ、窓関数の種類によって出力への影響が異なります。以下に、JUCE DSPモジュールで利用可能ないくつかの窓とその特徴を示します:
- 長方形:最も低いダイナミックレンジ、最も高い解像度。ウィンドウなしと同等。
- ハミング良好なダイナミック・レンジ、良好な分解能。通常、狭帯域アプリケーションで使用される。
- Hann:良好なダイナミック・レンジ、まずまずの解像度。通常、狭帯域のアプリケーションで使用される。
- ブラックマン:最高のダイナミックレンジ、最低の解像度。通常、広帯域アプリケーションで使用される。
オーディオデータの処理
現在、私たちのアプリケーションは、入力されるオーディオ信号を表示も処理もしないので、FFTを実装することから始めましょう。
FFT初期化
の中でAnalyserComponent
クラスでは、まずenumをパブリック・メンバーとして宣言し、FFT実装に 役立つ定数を定義する:
enum
{
fftOrder = 11, // [1]
fftSize = 1 << fftOrder, // [2]
scopeSize = 512 // [3]
};
- [1]FFTの次数はFFTウィンドウの大きさを表し、FFTの対象となるポイントの数は次数の2のべき乗に相当する。この場合、次数を11とすると、2 ^ 11 = 2048ポイントのFFTが生成される。
- [2]対応するFFTサイズを計算するために、2048を2進数1000000000として生成する左ビットシフト演算子を使用する。
- [3]また、スペクトルの視覚的表現のポイント数をスコープサイズ512とした。
次に、FFT実装に必要なプライベート・メンバー変数を以下のように宣言する:
private:
juce::dsp::FFT forwardFFT; // [4]
juce::dsp::WindowingFunction window; // [5]
float fifo [fftSize]; // [6]
float fftData [2 * fftSize]; // [7]
int fifoIndex = 0; // [8]
bool nextFFTBlockReady = false; // [9]
float scopeData [scopeSize]; // [10]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent)
};
- [4]:を宣言する。dsp::FFTオブジェクトに対して順方向FFTを実 行する。
- [5]を宣言する。dsp::WindowingFunctionオブジェクトにウィンドウ関数を適用する。
- [6]サイズ2048のfifo float配列には、入力されるオーディオ・データがサンプルとして格納されます。
- [7]サイズ4096のfloat配列には、FFT計算の結果が格納される。
- [8]この一時的なインデックスは、fifo内のサンプル数をカウントします。
- [9]この一時的なブール値は、次のFFTブロックがレンダリング可能かどうかを示す。
- [10]scopeData float配列(サイズ512)には、画面に表示する点が格納される。
では、コンストラクタのメンバ初期化リストでこれらの変数を初期化してみよう:
AnalyserComponent()
: forwardFFT (fftOrder),
window (fftSize, juce::dsp::WindowingFunction::hann)
{
この時点でFFTオブジェクトを正しい次数で明示的に初期化し、ウィンドウ関数を選択する必要があります。この場合、Hann関数 を使用することにしますが、他の関数を選択してもかまいません。
オーバーライドgetNextAudioBlock()
関数では、現在のオーディオバッファブロックに含まれるすべてのサンプルを、後で処理するために、単純にfifoにプッシュします:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
if (bufferToFill.buffer->getNumChannels() > 0)
{
auto* channelData = bufferToFill.buffer->getReadPointer (0, bufferToFill.startSample);
for (auto i = 0; i < bufferToFill.numSamples; ++i)
pushNextSampleIntoFifo (channelData[i]);
}
}
サンプルをfifoにプッシュするにはpushNextSampleIntoFifo()
機能については後述する:
void pushNextSampleIntoFifo (float sample) noexcept
{
// if the fifo contains enough data, set a flag to say
// that the next frame should now be rendered..
if (fifoIndex == fftSize) // [11]
{
if (! nextFFTBlockReady) // [12]
{
juce::zeromem (fftData, sizeof (fftData));
memcpy (fftData, fifo, sizeof (fifo));
nextFFTBlockReady = true;
}
fifoIndex = 0;
}
fifo[fifoIndex++] = sample; // [12]
}