Goertzel アルゴリズム:1つの周波数だけを見たいとき
FFTは万能だけど、特定の1周波数だけ欲しいときは非効率になる。Goertzelはそこを突いた2次IIRフィルターの話。
FFT を知ってから、周波数解析といえばとりあえず FFT、という感じになっていた。でも少し前に「特定の1周波数成分だけを N 回サンプルごとに更新したい」という状況になって、FFT を使うのが明らかに過剰だと気づいた。そこで見つけたのが Goertzel アルゴリズムだった。
問題設定
電話のプッシュボタン音(DTMF)を思い浮かべてほしい。「1」を押すと 697 Hz と 1209 Hz の正弦波が同時に鳴る。検出側がやるべきことは、8 つの周波数(697, 770, 852, 941, 1209, 1336, 1477, 1633 Hz)それぞれに「今この周波数のエネルギーはあるか?」を判定すること。
N = 205 サンプル(8 kHz サンプリング)を集めて判定するとして、FFT を使うと 205 点すべての複素スペクトルを計算することになる。8 周波数しか必要ないのに。
DFT の k 番目だけを取り出す
DFT の定義は:
と書けば 。
ここで (k は整数)という事実に注目する。これを乗じても値は変わらない:
右辺は「 と の畳み込みを で評価したもの」に見える。だから次のフィルターを走らせて、最後の出力だけ読み出せばいい:
になる。ただしこれは複素係数なので、実装が少し重い。
実数演算に落とす
の極は単位円上にある。この1次フィルターは安定しているが、複素乗算を含む。Goertzel のトリックは、これを共役極と組み合わせて実係数の2次フィルターにすること。
初期値 。N サンプル処理した後:
実部・虚部に展開すると:
ループ内は実数加減乗算だけ。複素演算はループが終わった後に1回だけかかる。エネルギー(パワー)だけ欲しいなら:
位相が要らなければ複素演算ゼロで完結する。
計算量の比較
| 手法 | 演算量(乗算数オーダー) |
|---|---|
| 直接 DFT(全 N 点) | |
| FFT(基数2) | |
| Goertzel(K 周波数) |
K 周波数だけ必要なとき、Goertzel が FFT より速くなる条件は:
N = 1024 なら K < 10、N = 512 なら K < 9。DTMF の 8 周波数はちょうどこの境界あたりにいる。
実装してみると
Python で書くとこんな感じだ:
import math
def goertzel(samples, k, N):
coeff = 2 * math.cos(2 * math.pi * k / N)
s1, s2 = 0.0, 0.0
for x in samples:
s0 = x + coeff * s1 - s2
s2, s1 = s1, s0
# パワーだけ欲しい場合
power = s2**2 + s1**2 - coeff * s1 * s2
return power
ループの中身は加算2回と乗算2回。マイコンの DSP 命令に乗せやすい形をしている。
何が面白いか
FFT はすべての周波数を一括して求める「グローバルな」変換だ。それに対して Goertzel は「特定の周波数を狙い撃ちにする局所的なフィルター」と捉えられる。
面白いのは、Goertzel フィルターを走らせながらサンプルを受け取るとき、中間状態 が更新され続けるという点だ。N サンプル集まったら出力を取り出して初期化する、というブロック処理をするのが基本だけど、スライディング窓でリアルタイムに更新する拡張もある(Sliding DFT と呼ばれる)。
もう一つ: を整数に限る必要はない。 を実数にすれば、DFT グリッド外の任意の周波数を計算できる。これは「ゾーム FFT(Zoom FFT)」の一形態にも繋がる話で、スペクトルの特定帯域を高分解能で見たいときに使える。
小さなアルゴリズムなのに、意外と奥がある。
— ランキン
コメント
まだコメントはないよ。最初のひとことをどうぞ。