Delphiで学ぶ高速フーリエ変換(FFT)|解析コードと窓関数の実装例
DelphiでFFT(高速フーリエ変換)を実装する方法を、実部・虚部の計算から振幅・周波数解析まで具体的なコード付きで解説します。
ハン窓・ハミング窓・ブラックマンハリス窓・フラットトップ窓など、各種窓関数の処理も網羅。
FFT解析をDelphiで手軽に始めたい方に向けた技術資料です。
参考:FFTを使ってピッチ変換するボイスチェンジャー アプリケーションの サンプルコード
高速フーリエ変換のユニット
//高速フーリエ変換
unit UFFT;
interface
uses System.SysUtils,System.Math;
type
TFFTData=array of Extended;
//高速フーリエ変換(入力信号,出力実部,出力虚部)
procedure fft(InRe:TFFTData;var OutRe,OutIm:TFFTData);
procedure _fft(InRe:TFFTData;var OutRe,OutIm:TFFTData);
//*******************窓関数*****************************
//ハン窓関数
procedure WinHanning(var data:TFFTData);
//ハミング窓関数
procedure WinHamming(var data:TFFTData);
//ガウス窓関数
procedure WinGauss(var data:TFFTData;m:integer=1);
//ブラックマンハリス窓関数
procedure WinBlackmanHarris(var data:TFFTData);
//ブラックマンナットール窓関数
procedure WinBlackmanNuttall(var data:TFFTData);
//フラットトップ窓関数
procedure WinFlatTop(var data:TFFTData);
//半正弦波窓関数
procedure WinHalfSin(var data:TFFTData);
implementation
procedure fft(InRe:TFFTData;var OutRe,OutIm:TFFTData);
var i :Integer;
InN:Integer;//入力データ数
n :Integer;//補正後データ数
begin
InN:=Length(InRe);
//データ数が2の乗数に満たない場合は0のデータを追加する
i:=1;
while InN > Power(2,i) do inc(i);
n:=trunc(IntPower(2,i));
if InN < n then
begin
setlength(InRe,n);
for i := InN to n-1 do InRe[i]:=0;
end;
//高速フーリエ変換
_fft(InRe,OutRe,OutIm);
end;
procedure _fft(InRe:TFFTData;var OutRe,OutIm:TFFTData);
var n:Integer;
i:integer;
InIm:TFFTData; //複素数の虚部
ct1,ct2,ct3:integer;
TmpRe,TmpIm:extended;
nfft:array[0..3] of integer;
fcos,fsin:TFFTData;
tmp:extended;
noblk:integer;
cntb:array[0..1] of integer;
begin
n:=Length(InRe);
setlength(InIm,n);
for i := 0 to n-1 do InIm[i]:=0;
ct2:=1;
for ct1 := 1 to length(InIm)-2 do
begin
TmpRe:=0;
TmpIm:=0;
if ct1<ct2 then
begin
TmpRe:=InRe[ct1-1];
InRe[ct1-1]:=InRe[ct2-1];
InRe[ct2-1]:=TmpRe;
TmpIm:=InIm[ct1-1];
InIm[ct1-1]:=InIm[ct2-1];
InIm[ct2-1]:=TmpIm;
end;
ct3:=length(InIm) div 2;
while ct3<ct2 do
begin
ct2:=ct2-ct3;
ct3:=ct3 div 2;
end;
ct2:=ct2+ct3;
end;
//誤差調整
nfft[0]:=floor(Log2(length(InIm))/Log2(2)+0.0000001);
SetLength(fcos,n);
SetLength(fsin,n);
fcos[0]:=1;
fsin[0]:=0;
for ct1 := 1 to nfft[0] do
begin
nfft[2]:=floor(System.math.Power(2,ct1));
nfft[1]:=n div nfft[2];
nfft[3]:=nfft[2] div 2;
for ct2 := 1 to nfft[3] do
begin
tmp:=-Pi/nfft[3]*ct2;
fcos[ct2]:=cos(tmp);
fsin[ct2]:=sin(tmp);
end;
for ct2 := 1 to nfft[1] do
begin
noblk:=nfft[2]*(ct2-1);
for ct3 := 0 to nfft[3]-1 do
begin
cntb[0]:=noblk+ct3;
cntb[1]:=cntb[0]+nfft[3];
TmpRe:=InRe[cntb[1]]*fcos[ct3]-InIm[cntb[1]]*fsin[ct3];
TmpIm:=InIm[cntb[1]]*fcos[ct3]+InRe[cntb[1]]*fsin[ct3];
InRe[cntb[1]]:=InRe[cntb[0]]-TmpRe;
InIm[cntb[1]]:=InIm[cntb[0]]-TmpIm;
InRe[cntb[0]]:=InRe[cntb[0]]+TmpRe;
InIm[cntb[0]]:=InIm[cntb[0]]+TmpIm;
end;
end;
end;
setlength(OutRe,length(InRe) div 2);
setlength(OutIm,length(InIm) div 2);
for i := 0 to (n div 2)-1 do
begin
OutRe[i]:=InRe[i]; //実部
OutIm[i]:=InIm[i]; //虚部
end;
end;
//ハン窓関数
procedure WinHanning(var data:TFFTData);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=(
0.5 - 0.5*Cos(2*Pi*i/(n-1))
)*data[i];
end;
end;
//ハミング窓関数
procedure WinHamming(var data:TFFTData);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=(
0.54 - 0.46 * Cos(2*Pi*i/(n-1))
)*data[i];
end;
end;
//ガウス窓関数
procedure WinGauss(var data:TFFTData;m:integer=1);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=Exp(
-2 * power(m,2) / power(n-1,2) * power(i-(n-1)/2,2)
)*data[i];
end;
end;
//ブラックマンハリス窓関数
procedure WinBlackmanHarris(var data:TFFTData);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=(0.35875-0.48829*cos(2*Pi*i/(n-1))
+0.14128*cos(4*Pi*i/(n-1))
-0.01168*cos(6*Pi*i/(n-1))
)*data[i];
end;
end;
//ブラックマンナットール窓関数
procedure WinBlackmanNuttall(var data:TFFTData);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=(0.3635819-0.4891775*cos(2*Pi*i/(n-1))
+0.1365995*cos(4*Pi*i/(n-1))
-0.0106411*cos(6*Pi*i/(n-1))
)*data[i];
end;
end;
//フラットトップ窓関数
procedure WinFlatTop(var data:TFFTData);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=(1-1.930*Cos(2*Pi*i/(n-1))
+1.290*Cos(4*Pi*i/(n-1))
-0.388*Cos(6*Pi*i/(n-1))
+0.032*Cos(8*Pi*i/(n-1))
)*data[i];
end;
end;
//半正弦波窓関数
procedure WinHalfSin(var data:TFFTData);
var i,n:integer;
begin
n:=length(data);
for i := 0 to n-1 do
begin
data[i]:=Sin(pi*i/(n-1))*data[i];
end;
end;
end.
参考URL https://gijyutsu-keisan.com/excel/numcal/fourier/fourier_2_2.php
高速フーリエ変換は、入力データ数が2のn乗個であることが前提の為、関数fft内で入力データ数が2のn乗個になるように調整してから関数_fftを呼び出しています。
また、高速フーリエ変換は、入力データの開始と終了が連続していることが前提となっており、そうでない場合は窓関数を使用することになります。
高速フーリエ変換を使ってみる
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button2Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses UFFT,system.math;
procedure TForm1.Button1Click(Sender: TObject);
var InRe:TFFTData;
//複素数の実部と虚部 i = √-1
// z = Re + Im * i
OutRe,OutIm:TFFTData;
n,i:integer;
bs:extended;// サンプリング間隔(秒)
hz:extended;// サンプリング周波数(Hz)= 1/bs
bhz:extended;
OutHz:extended; //周波数
OutAmp:extended; //振幅
begin
n:=200; //サンプル数
bs:=0.01;//サンプリング間隔(10ミリ秒毎のデータ)
hz:=1/bs;//サンプリング周波数(100Hz)
//テストデータの作成
setlength(InRe,n);
for i := 0 to n-1 do
begin
InRe[i]:=4*sin(i*bs*2*pi*10)+ //10Hz
5*sin(i*bs*2*pi*20)+ //20Hz
6*sin(i*bs*2*pi*40); //40Hz
end;
//窓関数処理が必要な(データの開始と終了が連続でない)場合は
//いずれかの窓関数処理を行う
WinHanning(InRe); //ハン窓関数
//WinHamming(InRe); //ハミング窓関数
//WinGauss(InRe,1); //ガウス窓関数 1~3σ
//WinBlackmanHarris(InRe); //ブラックマン・ハリス窓関数
//WinBlackmanNuttall(InRe);//ブラックマン・ナットール窓関数
//WinFlatTop(InRe); //フラットトップ窓関数
//WinHalfSin(InRe); //半正弦波窓関数
//高速フーリエ変換を行う
fft(InRe,OutRe,OutIm);
bhz:=hz/(length(OutRe)*2);
for i := 0 to length(OutRe)-1 do
begin
//周波数
OutHz :=bhz*i;
//振幅 = √(実部^2 + 虚部^2)
OutAmp:=Sqrt(power(OutRe[i],2)+power(OutIm[i],2));
end;
end;
end.
サンプリング周波数Fsの場合、
ナイキスト周波数 = Fs/2
FFTはナイキスト周波数を境にして左右対称の鏡像データが得られるので、FFTで得られる最大周波数はナイキスト周波数となる。
振幅はFFTで得られた実数部の2乗と虚数部の2乗の和を平方根した値。
ちなみに位相はFFTで得られた虚数部÷実数部 をアークタンジェントした値。
上記例ではサンプリング間隔10ms(サンプリング周波数100Hz、ナイキスト周波数50Hz)で
振幅4の10Hz、振幅5の20Hz、振幅6の40Hzの合成データを与えています。
また、FFTでは入力データの最初と最後が連続していることが前提なので、
この例ではハン窓関数(ハニング窓関数)を用いてデータを補正してからFFT関数を呼び出しています。
データが連続(一定間隔でデータを取得)していれば高速フーリエ変換で周波数解析ができるのですが、
一様にサンプリングされず欠損データがある場合は
Lomb-Scargle ピリオドグラム(Lomb-Scargle Periodogram)法
を使うのがいいそうです。
