メインコンテンツまでスキップ

チュートリアル信号の周波数をリアルタイムで可視化する

📚 Source Page

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]
}
  • [11]fifoに十分なデータ(この場合は2048サンプル)が格納されていれば、データをftData配列にコピーしてFFT処理する準備が整いました。また、次のフレームをレンダリングするフラグを設定し、常にインデックスを0にリセットして、fifoへのデータ入力を再開します。
  • [12]この関数が呼び出されるたびに、サンプルがfifoに格納され、インデックスがインクリメントされます。

これでFIFOデータがFFT入力配列の前半を占め、処理と表示の準備が整った。

分析装置の表示

の中でdrawNextFrameOfSpectrum()関数で、以下に説明するフレーム描画の実装を挿入する:

    void drawNextFrameOfSpectrum()
{
// first apply a windowing function to our data
window.multiplyWithWindowingTable (fftData, fftSize); // [1]

// then render our FFT data..
forwardFFT.performFrequencyOnlyForwardTransform (fftData); // [2]

auto mindB = -100.0f;
auto maxdB = 0.0f;

for (int i = 0; i < scopeSize; ++i) // [3]
{
auto skewedProportionX = 1.0f - std::exp (std::log (1.0f - (float) i / (float) scopeSize) * 0.2f);
auto fftDataIndex = juce::jlimit (0, fftSize / 2, (int) (skewedProportionX * (float) fftSize * 0.5f));
auto level = juce::jmap (juce::jlimit (mindB, maxdB, juce::Decibels::gainToDecibels (fftData[fftDataIndex])
- juce::Decibels::gainToDecibels ((float) fftSize)),
mindB, maxdB, 0.0f, 1.0f);

scopeData[i] = level; // [4]
}
}
  • [1]まず、受信データにウィンドウ関数を適用する。multiplyWithWindowingTable()関数を実行し、データを引数として渡す。
  • [2]を使用してFFTデータをレンダリングします。performFrequencyOnlyForwardTransform()関数は、ftData 配列を引数として FFT オブジェクトに渡します。
  • [3]次に、スコープ幅の各ポイントのforループで、希望する最小デシベルと最大デシベルに比例したレベルを計算します。これを行うには、まずX軸を歪ませて対数スケールを使用し、周波数をより適切に表現する必要があります。次に、このスケーリング係数を正しい配列インデックスを取得するために与え、振幅値を使ってそれを0.0 .. 1.0.
  • [4]最後に適切なポイントを適切な振幅で設定し、描画の準備をする。

タイマー・コールバック関数を使用して分析装置を更新する。drawNextFrameOfSpectrum()次のFFTブロックの準備ができたときにのみ、フラグをリセットし、GUIを更新します。repaint()関数である:

    void timerCallback() override
{
if (nextFFTBlockReady)
{
drawNextFrameOfSpectrum();
nextFFTBlockReady = false;
repaint();
}
}

最終段階としてpaint()コールバックはヘルパー関数を呼び出します。drawFrame()の場合はいつでもrepaint()リクエストが開始され、フレームは次のように描かれる:

    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) });
}
}

ここでは、配列から最初の点を除いたすべての点に対して、スコープのサイズをスクリーン境界のサイズにマッピングすることによって、前の点と現在の点の間に線を引く。

エクササイズ

FFTで使用するウィンドウ関数を変更してみて、スペクトラムアナライザーの反応がどのように異なるかに注目してください。

注記

この修正版のソースコードはSpectrumAnalyserTutorial_02.hファイルにある。

概要

このチュートリアルでは、スペクトルアナライザでオーディオデータを表示するために、ウィンドウ関数とFFTを使用する方法を学びました。特に

  • ウィンドウ関数の基礎を学ぶ
  • fifoを使ってオーディオをサンプルごとに処理
  • サンプルポイント間に線を引いてデータを表示

参照