Skip to content

Commit 6ebfdea

Browse files
committed
Add DSP plot generation script
1 parent 58981c9 commit 6ebfdea

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

tools/generate_dsp_plots.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env python3
2+
"""Generate visual DSP validation artifacts for cpp-dsp-showcase.
3+
4+
The script intentionally uses only NumPy and Matplotlib so it can be run locally
5+
or from CI without requiring a heavy DSP stack.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import math
11+
from pathlib import Path
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
16+
ROOT = Path(__file__).resolve().parents[1]
17+
ASSETS = ROOT / "docs" / "assets"
18+
ASSETS.mkdir(parents=True, exist_ok=True)
19+
20+
21+
def design_lowpass(num_taps: int = 127, cutoff: float = 0.12) -> np.ndarray:
22+
n = np.arange(num_taps) - (num_taps - 1) / 2
23+
h = 2 * cutoff * np.sinc(2 * cutoff * n)
24+
h *= np.blackman(num_taps)
25+
h /= np.sum(h)
26+
return h
27+
28+
29+
def save_fir_response() -> None:
30+
h = design_lowpass()
31+
nfft = 8192
32+
h_fft = np.fft.rfft(h, nfft)
33+
freq = np.fft.rfftfreq(nfft, d=1.0)
34+
mag_db = 20 * np.log10(np.maximum(np.abs(h_fft), 1e-12))
35+
36+
plt.figure(figsize=(10, 4.8))
37+
plt.plot(freq, mag_db, linewidth=2)
38+
plt.axvline(0.12, linestyle="--", linewidth=1)
39+
plt.title("Windowed-sinc FIR low-pass response")
40+
plt.xlabel("Normalized frequency [cycles/sample]")
41+
plt.ylabel("Magnitude [dB]")
42+
plt.grid(True, alpha=0.35)
43+
plt.tight_layout()
44+
plt.savefig(ASSETS / "fir_response.png", dpi=180)
45+
plt.close()
46+
47+
48+
def goertzel_power(x: np.ndarray, fs: float, freq: float) -> float:
49+
n = len(x)
50+
k = int(round(n * freq / fs))
51+
omega = 2.0 * math.pi * k / n
52+
coeff = 2.0 * math.cos(omega)
53+
s_prev = 0.0
54+
s_prev2 = 0.0
55+
for sample in x:
56+
s = sample + coeff * s_prev - s_prev2
57+
s_prev2 = s_prev
58+
s_prev = s
59+
return s_prev2**2 + s_prev**2 - coeff * s_prev * s_prev2
60+
61+
62+
def save_goertzel_detection() -> None:
63+
fs = 8000.0
64+
n = 1024
65+
t = np.arange(n) / fs
66+
target = 1000.0
67+
x = 0.9 * np.sin(2 * np.pi * target * t) + 0.15 * np.sin(2 * np.pi * 2200 * t)
68+
x += 0.05 * np.random.default_rng(7).normal(size=n)
69+
70+
freqs = np.arange(200, 3001, 50)
71+
powers = np.array([goertzel_power(x, fs, f) for f in freqs])
72+
powers_db = 10 * np.log10(np.maximum(powers / np.max(powers), 1e-12))
73+
74+
plt.figure(figsize=(10, 4.8))
75+
plt.plot(freqs, powers_db, marker="o", markersize=3, linewidth=1.8)
76+
plt.axvline(target, linestyle="--", linewidth=1)
77+
plt.title("Goertzel tone detection")
78+
plt.xlabel("Test frequency [Hz]")
79+
plt.ylabel("Normalized power [dB]")
80+
plt.grid(True, alpha=0.35)
81+
plt.tight_layout()
82+
plt.savefig(ASSETS / "goertzel_detection.png", dpi=180)
83+
plt.close()
84+
85+
86+
def save_gcc_phat_delay() -> None:
87+
rng = np.random.default_rng(11)
88+
n = 512
89+
true_delay = 37
90+
source = rng.normal(size=n)
91+
source *= np.hanning(n)
92+
delayed = np.concatenate([np.zeros(true_delay), source[:-true_delay]])
93+
delayed += 0.03 * rng.normal(size=n)
94+
95+
nfft = 2 * n
96+
x = np.fft.fft(source, nfft)
97+
y = np.fft.fft(delayed, nfft)
98+
cross = x * np.conj(y)
99+
phat = cross / np.maximum(np.abs(cross), 1e-12)
100+
corr = np.fft.ifft(phat).real
101+
corr = np.fft.fftshift(corr)
102+
lags = np.arange(-nfft // 2, nfft // 2)
103+
estimated_delay = lags[np.argmax(corr)]
104+
105+
plt.figure(figsize=(10, 4.8))
106+
plt.plot(lags, corr, linewidth=1.8)
107+
plt.axvline(estimated_delay, linestyle="--", linewidth=1)
108+
plt.title(f"GCC-PHAT delay estimate: {estimated_delay} samples")
109+
plt.xlabel("Lag [samples]")
110+
plt.ylabel("PHAT correlation")
111+
plt.xlim(-120, 120)
112+
plt.grid(True, alpha=0.35)
113+
plt.tight_layout()
114+
plt.savefig(ASSETS / "gcc_phat_delay.png", dpi=180)
115+
plt.close()
116+
117+
118+
def main() -> None:
119+
save_fir_response()
120+
save_goertzel_detection()
121+
save_gcc_phat_delay()
122+
print(f"Generated DSP plots in {ASSETS}")
123+
124+
125+
if __name__ == "__main__":
126+
main()

0 commit comments

Comments
 (0)