チュートリアル高速フーリエ変換
DSPモジュールのFFTクラスを使用して、入力されたオーディオデータをスペクトログラムとして表示する方法を学びます。高速フーリエ変換を使用する利点を理解します。
レベル:中級
プラットフォーム:Windows, macOS, Linux
クラス: dsp::FFT,Image,Colour,FloatVectorOperations
スタート
このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。
お使いのオペレーティングシステムが、マイクへのアクセス許可を要求する必要がある場合(現在、iOS、Android、macOS Mojave)、Projucerの関連するエクスポーターの下に対応するオプションを設定し、プロジェクトを再保存する必要があります。
このステップでヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.
デモ・プロジェクト
完成すると、このデモプロジェクトは、入力されたオーディオデータを、時間(x軸)、周波数(y軸)、振幅(色)の3次元スペクトログラムとして表示します。画面に表示される値は1秒間に60回更新され、どの時間枠でもウィンドウは次のようになります:
ここで紹介するコードは、大まかに以下のものと似ている。シンプルなFFTEの例JUCE Examplesより。
高速フーリエ変換
時間領域または空間領域の信号は、フーリエ変換と呼ばれる変換式を用いて周波数領域に変換することができます。この変換関数の一般的な効率的実装は高速フーリエ変換(FFT)で、JUCE DSPモジュールに含まれており、このチュートリアルで使用します。
FFTは、オーディオ信号を周波数に分解し、それぞれの周波数の大きさと位相情報を表現することができます。逆関数を使用すると、信号を元のドメインに戻すことができるため、フィルタリングなど個々の周波数成分を処理するのに非常に便利です。
このチュートリアルでは、実際の出力処理を行わずにオーディオデータを表示することだけを扱いますので、逆FFTではなく順FFTに焦点を当てます。
オーディオデータの処理
現在、私たちのアプリケーションは、入力されるオーディオ信号を表示も処理もしないので、FFTを実装することから始めましょう。
FFT初期化
の中でSpectrogramComponent
クラスでは、FFTの実装に役立つ定数を定義することから始めます:
static constexpr auto fftOrder = 10; // [1]
static constexpr auto fftSize = 1 << fftOrder; // [2]
private:
- [1]FFTの次数はFFTウィンドウの大きさを表し、その次数の2のべき乗がFFTを行うポイント数となる。この場合、次数を10とすると、2 ^ 10 = 1024点のFFTが得られる。
- [2]対応するFFTサイズを計算するために、1024を2進数10000000000として生成する左ビットシフト演算子を使用します。
次に、FFT実装に必要なプライベート・メンバー変数を以下のように宣言する:
juce::dsp::FFT forwardFFT; // [3]
juce::Image spectrogramImage;
std::array fifo; // [4]
std::array fftData; // [5]
int fifoIndex = 0; // [6]
bool nextFFTBlockReady = false; // [7]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SpectrogramComponent)
};
- [3]:を宣言する。dsp::FFTオブジェクトに対して順方向FFTを実行する。
- [4]サイズ1024のfifo float配列には、入力されるオーディオデータがサンプル数で格納されます。
- [5]サイズ2048のfloat配列には、FFT計算の結果が格納される。
- [6]この一時的なインデックスは、fifo内のサンプル数をカウントし ます。
- [7]この一時的なブール値は、次のFFTブロックがレンダリング可能かどうかを示す。
では、コンストラクタのメンバ初期化リストでこれらの変数を初期化してみよう:
SpectrogramComponent()
: forwardFFT (fftOrder),
spectrogramImage (juce::Image::RGB, 512, 512, true)
{
FFTオブジェクトは、この時点で正しい次数で明示的に初期化されなければならない。
オーバーライド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 line should now be rendered..
if (fifoIndex == fftSize) // [8]
{
if (! nextFFTBlockReady) // [9]
{
std::fill (fftData.begin(), fftData.end(), 0.0f);
std::copy (fifo.begin(), fifo.end(), fftData.begin());
nextFFTBlockReady = true;
}
fifoIndex = 0;
}
fifo[(size_t) fifoIndex++] = sample; // [9]
}
- [8]fifoに十分なデータ(この場合は1024サンプル)が格納されていれば、データをftData配列にコピーして、FFTで処理する準備が整いました。また、次の行をレンダリングするフラグを設定し、常にインデックスを0にリセットして、fifoへのデータ入力を再開します。
- [9]この関数が呼び出されるたびに、サンプルがfifoに格納され、インデックスがインクリメントされます。
これでFIFOデータがFFT入力配列の前半を占め、処理と表示の準備が整った。
スペクトログラムの表示
の中でdrawNextLineOfSpectrogram()
関数に、以下に説明するピクセル描画の実装を挿入する:
void drawNextLineOfSpectrogram()
{
auto rightHandEdge = spectrogramImage.getWidth() - 1;
auto imageHeight = spectrogramImage.getHeight();
// first, shuffle our image leftwards by 1 pixel..
spectrogramImage.moveImageSection (0, 0, 1, 0, rightHandEdge, imageHeight); // [1]
// then render our FFT data..
forwardFFT.performFrequencyOnlyForwardTransform (fftData.data()); // [2]
// find the range of values produced, so we can scale our rendering to
// show up the detail clearly
auto maxLevel = juce::FloatVectorOperations::findMinAndMax (fftData.data(), fftSize / 2); // [3]
juce::Image::BitmapData bitmap { spectrogramImage, rightHandEdge, 0, 1, imageHeight, juce::Image::BitmapData::writeOnly }; // [4]
for (auto y = 1; y < imageHeight; ++y) // [5]
{
auto skewedProportionY = 1.0f - std::exp (std::log ((float) y / (float) imageHeight) * 0.2f);
auto fftDataIndex = (size_t) juce::jlimit (0, fftSize / 2, (int) (skewedProportionY * fftSize / 2));
auto level = juce::jmap (fftData[fftDataIndex], 0.0f, juce::jmax (maxLevel.getEnd(), 1e-5f), 0.0f, 1.0f);
bitmap.setPixelColour (0, y, juce::Colour::fromHSV (level, 1.0f, level, 1.0f)); // [6]
}
}
- [1]を使い、画像を左方向に1ピクセルずつシャッフルする。
moveImageSection()
関数を使用する。Imageオブジェクトを指定します。画像セクションは、全幅から1ピクセルを引いた値と全高で指定します。 - [2]を使用してFFTデータをレンダリングします。
performFrequencyOnlyForwardTransform()
関数は、ftData 配列を引数として FFT オブジェクトに渡します。 - [3]レンダリングをスケーリングしてディテールを明確に表示できるように、生成される値の範囲を求めます。そのためには、FloatVectorOperations::findMinAndMax()関数を使います。
- [4]スペクトログラム画像の右端の列のピクセルを参照するBitmapDataインスタンスを作成する。画像内の複数のピクセルに対して読み書きする場合、BitmapDataインスタンスを使用してピクセル値を内部的にバッファリングし、一度に読み書きすることができます。この方法は、通常Image個々のピクセルにアクセスする。
- [5]次に、スペクトログラムの高さの各ピクセルについて、forループでサンプルセットに比例したレベルを計算する。これを行うには、まずY軸を歪めて対数スケールを使用し、周波数をより適切に表現する必要があります。次に、このスケーリング係数を与えて正しい配列インデックスを取得し、振幅値を使ってそれを0.0 .. 1.0.
- [6]最後に、FFTデータを表示するために、適切なピクセルに適切な色を設定します。
最後のステップとして、タイマーコールバック関数を使用してスペクトログラムを更新する。drawNextLineOfSpectrogram()
次のFFTブロックの準備ができたときにのみ、フラグをリセットし、GUIを更新します。repaint()
関数である:
void timerCallback() override
{
if (nextFFTBlockReady)
{
drawNextLineOfSpectrogram();
nextFFTBlockReady = false;
repaint();
}
}
FFTの分解能を上げ、スペクトログラムの更新レートを変えてみる。
この修正版のソースコードはSimpleFFTTutorial_02.h
ファイルにある。
概要
このチュートリアルでは、FFT 関数を使ってオーディオデータをスペクトログラムで表示する方法を学びました。特に
- 高速フーリエ変換関数の基礎を学ぶ
- FIFOを使ってオーディオをサンプルごとに処理
- データをImageオブジェクトのピクセル単位