Goertzel algorithm
Table of Contents
S tansform again
前回からS変換を試しているんだけど、理解不足な部分が有るので追加実験をしてみる。 そう、分解能とかについて疑問が有るんだ。
実験準備
例としてチャープ信号(時間と共に周波数が変化してく、宇宙的な音ね。ピンクレディー の楽曲でも使われてた奴)に、バースト波を重畳した波形をS変換してくれてた。 でも、こんな波形が、どうなるかなんて、さっぱり理解に苦しむ。
オイラーの理解が届く波形が良いだろう。こうなっているはずって事が大事だ。 そういう波形ったら、正弦波を断続した物に限る。断続と言ったら、そりゃモールス 信号だわな。電波形式A1A。7.015MHzとかの電波を断続させる、原始的な通信ね。 受信する方は、それよりちょっとずれた信号(たとえば7.016MHz)と混合させる。 信号波があれば、差分の波(0.001MHz = 1KHz)が混合器から出てくる。これで、音と して聞こえる訳だ。
cwwav ってアプリで、モールス信号の音ファイルを作成する。何も指定しないと サンプリングレートは16000だけど、これだとファイルが肥大化するので、思いきって 4000にした。それから1分間で何ワード転送するって指標(wpm)も設定できる。標準では25なんだ けど(これでも十分に速い)、40と言う無茶ぶりな速度にした。これもファイルサイズを 小さくする為。
できた波形を聞くには、sox(Sound eXchange, the Swiss Army knife of audio manipulation) が便利だ。この中のplayを使う。
play -v 0.5 40.wav ## -v は、ボリューム
ask
Q: 今更ながらですが、S変換の結果の見方と意義を確認したく、質問します。 貴方が提示してくれた S変換のデモを少し改造して、結果を観察しました。 改造部分は、下記です。
# ======= デモ信号 =======
import wave
waveFile = wave.open('40.wav' , 'r')
buf = waveFile.readframes(-1)
waveFile.close()
x = np.frombuffer(buf, dtype="int16")
fs = 4000.0
# ======= S変換 =======
S, freqs, t = fast_s_transform(x, fs)
与えた音ファイルは可聴モールス信号(AF=660Hz)で、40wpmのAB(.- -…)です。
sakae@lu:cwwav$ soxi 40.wav Input File : '40.wav' Channels : 1 Sample Rate : 4000 Precision : 16-bit Duration : 00:00:01.02 = 4080 samples ~ 76.5 CDDA sectors File Size : 8.20k Bit Rate : 64.3k Sample Encoding: 16-bit Signed Integer PCM
S変換の結果は、660Hz付近に、.- -… が表示される事を期待したのですが違い ました。バーコードの表示の様に、帯域が0Hz - 2000Hzまで延びるバーになりました (入力波形とそっくりです)。
私のS変換への理解が間違っているのでしょうか? 説明をお願いします。 なお、soxでspectrogramを表示させると、分解能は低い(.- -…は判別できない) ものの、660Hz付近にそれらしい横棒が確認できます。
A: すばらしい実験ですね。 その現象(/S変換結果が「バーコード」状で全帯域に広がってしまう/)は、あなたの理解が間違っているというより、/実装と理論の違い・スケーリング設定の不一致/による典型的な挙動です。 以下で順に整理して説明します。
## 1️⃣ S変換の原理と「見えるはずの像」
S変換(Stockwell transform)は、 /時間-周波数分布/を求める方法で、ウェーブレット変換とSTFTの中間的な性質を持っています。
数式的には以下です(離散形の概念):
[ S(t, f) = ∫-∞∞ x(τ) \frac{|f|}{\sqrt{2\pi}} e-\frac{(t-\tau)^2 f^2}{2} e-j 2π f τ dτ ]
特徴:
- 中心周波数 ( f ) ごとにガウス窓幅 ( ∝ 1/|f| ) を変える (高周波は窓が狭く、低周波は広い)
- 結果は時間×周波数の2次元画像(複素数)
- 窓の幅スケーリングが適切なら、/660Hzに限定されたエネルギーが短い時間帯で強く出る/像が得られる
## 2️⃣ 今の現象:「バーコード状」の原因
出力が 0Hz〜2000Hzまで一様にバーコードのように延びた。
これはほぼ間違いなく、以下のいずれか/複数が原因です:
| 原因 | 説明 | ||
| ------------------------ | --------------------------------------------------------------------------------------------------------------------- | - | --------------------------------------------------- |
| 🧩 ガウス窓スケーリングが不適切 | 多くの簡易実装では、窓幅を σ_f = f などにしており、サンプリング周波数・信号長に対して非常に広い/狭い窓になり、周波数分離が崩れます。結果、全帯域に似た波形が再現される。 |
||
| 🔁 時間・周波数軸の正規化が抜けている | FFTベースのS変換では、=freqs = np.fft.fftfreq(N, 1/fs)= を使うと、周波数のスケールが ±fs/2 に分布します。このスケーリングを誤ると、見かけ上「帯域0〜fs/2にほぼ同じパターン」が見えます。 | ||
| ⚙️ 振幅スケーリング(正規化)不足 | S-transformでは (f) の係数が掛かります。これが抜けていると、低周波エネルギーが過大に見え、全帯域バーコードになります。 | ||
| 🔉 信号が短すぎる | 1秒間・4000サンプルだと、ガウス窓の相対幅(特に低周波)で分離が効きにくい。結果的に時間方向の変化が周波数方向に滲む。 |
## 3️⃣ 実際に期待される像
理想的には、あなたの信号(660 Hz ± 変調)なら:
- 時間軸に「短点」「長点」に対応する強度の帯
- 周波数軸では 660 Hz ± 数十Hz程度の細帯域
- 0 Hz〜2 kHzに広がることはない
つまり、/sox spectrogram と同様の構造/(もっと時間解像度が高い)が得られるはずです。
## 4️⃣ 修正方法(実用的な調整)
もしあなたが私の提示した fast_s_transform() をそのまま流用しているなら、
下記のようにガウス窓部分を修正してみてください:
# 元:sigma_f = abs(f) # 修正版: sigma_f = abs(f) / 4.0 # <-- 分解能を上げる調整パラメータ
または、窓関数の作り方を以下のように変更:
# 正しいガウス窓(理論に近い形) g = np.exp(- (freqs - f)**2 / (2 * (f**2 + 1e-12)))
これにより、660 Hz 近傍のエネルギーだけが強調され、バーコードは解消するはずです。
## 5️⃣ 参考:STFTとの比較イメージ
| 手法 | 窓幅 | 時間解像度 | 周波数解像度 | コメント |
| --------------- | ----- | ---------- | ------------ | -------------- |
| STFT | 固定 | 一定 | 一定 | 解析窓は全周波数共通 |
| Wavelet | 周波数依存 | 高fで高解像 | 低fで高解像 | 直感的に分かりやすい |
| S-transform | 周波数依存 | Waveletと同様 | 位相情報保持・再構成可能 | 高fに強い・低fがぼけやすい |
## 6️⃣ 確認実験を勧める設定
試すと分かりやすい組み合わせ:
S, freqs = fast_s_transform(x, fs, freq_range=(0, 1000)) # 0–1kHzに制限
または、/STFTで比較/してみると理解が早まります。
from scipy.signal import spectrogram f, t, Pxx = spectrogram(x, fs=fs, nperseg=256, noverlap=128) plt.pcolormesh(t, f, 10*np.log10(Pxx), shading='auto')
→ STFTで660Hzの横棒が見えるなら、S変換も窓設定の調整だけで再現可能です。
## 7️⃣ 結論
S変換の結果がバーコード状になるのはなぜ?
→ ガウス窓のスケール設定が適切でない(高周波にも広すぎる窓を使っている)ため、時間方向の変化が周波数全域に滲んでいる。
修正すべきは窓の定義部分で、
sigma_f = |f| / const の係数を調整するだけで、期待通り 660 Hz 周辺のドット・ダッシュが見えるようになります。
dBマジック
お勧めに従って、
sigma_f = abs(f) / 8.0
にした。よりシャープな解像度になるはず。
それから、慣れないpythonコードを眺めると、下記の重要部分がセンサーに引っかかって きた。S変換されたデータ(S)をどの様に表示するか指定してる。
S_mag = np.abs(S) S_db = 20*np.log10(S_mag + 1e-10) plt.imshow(S_mag, aspect='auto', origin='lower', extent=extent) plt.colorbar(label='Magnitude [Linear]')
チャッピーが提示してきたコードだとplt.imshowの第一引数が、 S_db となっていた。
これは、20 * log10 してねって指示だ。0のlog10は計算不可なんで、底値として
とっても小さい数(1e-10)を事前に加算してる。
log10(10を底とする対数)は、大きな数は小く、小さな数は誇大にと言う、変換演算だ。 これを使えば、目には見えない原子の世界から、広大な宇宙までの大きなものまで、 一度に俯瞰できるようになり。まさにマジックだね。 電気業界では、あたり前の様に使われる道具だ。
で、対数変換をかけない、生々しいデータも表示してみましょ(上記は、そういう設定)。 たまには、画像も陳列してみる。
リニアーだと、白黒がはっきりしていて(この場合は)良い表示だと思うぞ。 この画像を右に90度回転させれば、無線機のバンドモニターになる。 Web-SDRも同様で、たまにウォッチしてると、非常に遅いモールス信号を出してる 局がいる。そんな時は、今作った画像と同様なものが観察できる。
Goertzel algorithm
2009年の8月からモールス信号のデコードを実験してる。信号の検出にはFFTを利用 してた。2019年の5月からは、Goertzel algorithmを利用して、同様な試みをしてる。 そして、余勢をかって、スペクトラム作成まで範囲を拡げてた。
今回はGoertzel algorithmに注目して、チャッピー君と対話してみる。
理論
Q: プッシュホンのダイアルトーンとかの特定周波数の有無を検出するのに、 Goertzel algorithmが使われているそうです。FFTに比べて計算負荷が小さい ため、Arduinoの様な非力なデバイスでも利用できるそうです。
外見的には、特定周波数に応答するバンドパス・フィルターと考えられます。 理論的な背景を、まず説明して下さい。
私は、Arduinoに実装されたモールス信号デコーダに搭載されてた、Goertzelの コードをC言語に移植した経験があります。
A: とても良い視点です。 おっしゃる通り、**Goertzelアルゴリズム(Goertzel algorithm)**は「特定の周波数成分を検出するための離散的なフィルタ/変換手法」であり、FFT(高速フーリエ変換)の部分的な実装とも見なせます。 特に、**DTMF(プッシュホン信号)検出**などの「限られた周波数だけを知りたい」用途では、FFTよりもはるかに効率的です。
—
## 🔹 1. 背景:DFTとの関係
Goertzel法は、離散フーリエ変換(DFT)の特定の1点(または少数点)を求めるための効率化手法です。
DFTは次のように定義されます:
[ X(k) = ∑n=0N-1 x[n] , e-j 2π kn / N ]
ここで:
- ( x[n] ) … 時間領域の信号サンプル
- ( N ) … サンプル数
- ( X(k) ) … 周波数インデックス (k) に対応する複素スペクトル
—
## 🔹 2. Goertzelアルゴリズムの発想
上式をよく見ると、( X(k) ) は「入力信号に対して、特定の周波数の複素指数波を掛けて積分(離散的には加算)している」ことがわかります。
これを直接計算すると掛け算と加算が ( N ) 回必要ですが、 Goertzelはこの計算を**2次のIIRフィルタ(帰還型フィルタ)**で実装する形に変形します。
—
## 🔹 3. 数式による導出
DFTの和を再帰的に表すと、以下のような関係式を得ます。
[ s[n] = x[n] + 2cos(2π k / N) s[n-1] - s[n-2] ]
初期条件: [ s[-1] = 0,\quad s[-2] = 0 ]
このフィルタを全 (N) サンプルについて適用したあと、最後の2つの値から目的の ( X(k) ) を求めます:
[ X(k) = s[N-1] - e-j 2π k / N s[N-2] ]
—
## 🔹 4. 周波数応答の直感的理解
上記の再帰式は、実際には次のような**2次の共振フィルタ**に相当します:
[ H(z) = \frac{1}{1 - 2cos(ωk) z-1 + z-2} ] (ここで (ωk = 2π k / N))
このフィルタの周波数応答は、(ωk) 近傍で鋭いピークを持つ「狭帯域のバンドパスフィルタ」となります。 つまり:
- その周波数成分が入力に含まれていれば、内部状態 (s[n]) が大きくなる。
- 含まれていなければ、ほとんど応答しない。
→ 特定の周波数にだけ共振するディジタル共振器(resonator) と考えることができます。
—
## 🔹 5. エネルギーの算出
最後に求めたいのは「その周波数成分の強さ(振幅 or パワー)」なので、次のようにします:
[
| X(k) | ^2 = s[N-1]2 + s[N-2]2 - 2 cos(2π k / N) s[N-1] s[N-2] |
]
これにより、複素数演算を避けつつ「特定周波数のエネルギー」を求められます。
—
## 🔹 6. FFTとの比較
| 特徴 | Goertzel | FFT |
| ----- | ---------------- | ------------- |
| 対象周波数 | 特定の数個のみ | 全帯域(N点) |
| 計算コスト | (O(N))(周波数1点あたり) | (O(N log N)) |
| 実装容易性 | 非常に簡単(再帰式) | 複雑(バタフライ演算) |
| メモリ消費 | 小さい(過去2サンプルだけ) | 大きい(全Nサンプル必要) |
→ DTMFのように「8個の周波数成分だけをチェック」する場合、Goertzel法が圧倒的に有利。
—
## 🔹 7. まとめ(概念図)
入力信号 → [Goertzel フィルタ (2次 IIR)] → s[n-1], s[n-2]
↓
|X(k)|^2 = s[N-1]^2 + s[N-2]^2 - 2cos(ωk)s[N-1]s[N-2]
↓
特定周波数のエネルギーとして判定
from C
Q: C実装コードと数学式の対応関係の解説して下さい。 実験環境は、次のようにして、応答特性を観測しました。
sg | dut | pgout
sg: 100-3000Hzまで10Hzステップで信号発生 dut: goertelによるフィルター pgout: gnuplotでグラフ化
結果は、同調周波数での強度が160000、10Hz毎にサイドローブが有り、その最大値は 40000でした。
A: すばらしい実験構成です。 提示されたCコードは、まさにGoertzelアルゴリズムをそのまま実装した典型例です。 このコードを理論式と1対1で対応づけて説明していきます。
—
## 🔹 1. 理論式の復習
Goertzelの基本再帰式は次の通りでした:
[ s[n] = x[n] + 2cos(ωk),s[n-1] - s[n-2] ] (初期値:( s[-1] = s[-2] = 0 ))
最終的に求める出力は: [
| X(k) | ^2 = s[N-1]2 + s[N-2]2 - 2cos(ωk) s[N-1] s[N-2] |
]
ここで、
- ( x[n] ):入力サンプル
- ( s[n] ):Goertzelフィルタの内部状態(出力)
- ( ωk = 2π k / N ):対象の離散周波数
- ( N ):ブロック長
—
## 🔹 2. コードと数式の対応関係
あなたのコードの各部分を、理論式に対応付けて説明します。
—
### (1) 周波数設定部分
k = (int) (0.5 + ((N * target_freq) / sampling_freq)); omega = (2.0 * M_PI * k) / N; cosine = cos(omega); coeff = 2.0 * cosine;
🔸対応する理論: [ ωk = 2π k / N, \quad \text{係数} = 2cos(ωk) ]
👉 この部分で、Goertzelの再帰式に登場する 「2cos(ωₖ)」 の定数を準備しています。
なお、 ( k = \text{round}(N f\text{target} / fs) ) で整数に丸めているのは、**サンプル数Nに対応する離散周波数インデックス**を求めるためです。
—
### (2) フィルタ再帰部分
Q1 = 0; Q2 = 0;
for (i = 0; i < N; i++){
Q0 = coeff * Q1 - Q2 + (float) Data[i];
Q2 = Q1;
Q1 = Q0;
}
🔸対応する理論:
[
\begin{cases} s[n] = x[n] + 2\cos(\omega_k),s[n-1] - s[n-2] \ s[-1] = s[-2] = 0 \end{cases}]
🔹対応:
| 理論式 | Cコード |
| :---------------- | :-------- |
| (s[n]) | `Q0` |
| (s[n-1]) | `Q1` |
| (s[n-2]) | `Q2` |
| (x[n]) | `Data[i]` |
| (2cos(ωk)) | `coeff` |
👉 `Q0`(現在値)を計算し、`Q1`,`Q2`を1サンプル分ずらして再帰を継続します。 このループ完了時に、 `Q1 = s[N-1]`, `Q2 = s[N-2]` となります。
—
### (3) 出力のパワー(エネルギー)計算
mag2 = (Q1*Q1) + (Q2*Q2) - Q1*Q2*coeff;
printf("%f\n", sqrt(mag2));
🔸対応する理論:
[
| X(k) | ^2 = s[N-1]2 + s[N-2]2 - 2cos(ωk),s[N-1]s[N-2] |
]
🔹対応:
| 理論式 | Cコード | ||
| :----------------------------- | :-------------- | - | ------------ |
| (s[N-1]2) | `Q1*Q1` | ||
| (s[N-2]2) | `Q2*Q2` | ||
| (-2cos(ωk)s[N-1]s[N-2]) | `- Q1*Q2*coeff` | ||
| ( | X(k) | ) | `sqrt(mag2)` |
👉 ここで `mag2` が**その周波数成分のエネルギー**を表し、 `sqrt()`で振幅相当の値に変換しています。
—
## 🔹 3. 実験結果の意味
> 同調周波数での強度が160000、 > サイドローブが10Hzごとにあり最大40000。
これは非常に理にかなっています。
- サンプリング周波数: 10 kHz
- ブロック長: 100サンプル → 周波数分解能 ( Δ f = 10000 / 100 = 100,\mathrm{Hz} )
600 Hz に対応する「DFTビン」は、100 Hz間隔の1つ(k=6付近)です。 したがって、**100 Hz間隔で強く応答**し、それ以外の周波数ではサイドローブ的に応答します。
10 Hz刻みで観測したとき、メインローブの中心600 Hzで最大160000、 近傍では sinc形状に従って減衰します。 Goertzel法は1点DFT相当なので、応答特性はDFT窓(矩形窓)のスペクトル形状そのものです:
[
| H(f) | = \frac{\sin(\pi N (f - f_0)/f_s)}{\sin(\pi (f - f_0)/f_s)} |
]
サイドローブ(40000 ≈ 1/4)は矩形窓の典型的な漏れ(leakage)によるものです。
—
## 🔹 4. フィルタ的見方
再帰式は 2次IIRフィルタ:
[ H(z) = \frac{1}{1 - 2cos(ωk) z-1 + z-2} ]
このフィルタは (ωk) 近傍で鋭く共振し、 出力エネルギーを求めることでその周波数成分の存在を検出しています。
—
## 🔹 5. まとめ(理論とコードの対応)
| 理論要素 | 数式 | コード対応 | ||
| --------- | ---------------------- | ------------------------------------------------------ | ---------------------------------------------- | ----------------------------------------- |
| 周波数インデックス | (k = N f0 / fs) | `k = (int)(0.5 + ((N * targetfreq) / samplingfreq))` | ||
| 角周波数 | (ω = 2π k / N) | `omega = (2.0 * MPI * k) / N;` | ||
| 再帰係数 | (2cos(ω)) | `coeff = 2.0 * cosine;` | ||
| フィルタ状態 | (s[n], s[n-1], s[n-2]) | `Q0, Q1, Q2` | ||
| 入力 | (x[n]) | `Data[i]` | ||
| 出力エネルギー | ( | X(k) | ^2 = s[N-1]2 + s[N-2]2 - coeff·s[N-1]s[N-2]) | `mag2 = (Q1*Q1) + (Q2*Q2) - Q1*Q2*coeff;` |
README
友人に、気象変動に興味を持って、研究してる人がいる。トランプ野郎は けしからん。気候変動予防連合(COP30を脱退して何を考えているんだ。トランプ曰く、 今じたばたしてもしょうがない。未来になれば革新的技術で解決してくれってね。
二酸化炭素は地球にとって、断熱材の役目を果している。この増加が遂に 臨界点を越えたそうだ(この臨界点の事を、業界用語でティッピングポイントと 言うとか)。
今たまたま読んでいる本に とんでもないサバイバルの科学 も、CO2 関連が出てたぞ。
CO2が減って、氷河期の頃、マンモスが闊歩してた。そんな世界にワープさせ られちゃったら、どうサバイバルする?
この解説中に、氷河期、温暖期のサイクルの 説明がでてた。 岩石 -> 噴火 -> 大気中 -> 海中 -> 岩石 というループで、CO2の元になる炭素が循環してる らしい。知らなかったなあ。現代だと噴火の影響よりも、石炭や石油を燃焼させる事 による、CO2の放出が圧倒的だな。
マンモスの話に戻ると、尻を狙って槍を投げても、尻の皮が厚くてダメだそうだ。 決死の覚悟で、正面から槍を投げるのが正しい狩の方法らしい。
【本要約】とんでもないサバイバルの科学 タイパ崇拝の方向け。但し、今私が 読んでいる本を忠実に、なそっている訳ではない。要約のネタ本はどれだろう?