Log Power FFT
FFT, Betragsquadrat, Mittelung und Log-Skalierung in einem Schritt — der fertige Baustein für ein dB-Spektrum, wie er hinter jedem Waterfall- und Frequency-Sink steckt.
1. Beschreibung des Blocks
Der Log Power FFT-Block (logpwrfft.logpwrfft_f / logpwrfft.logpwrfft_c) ist kein eigenständiger Signalverarbeitungsblock, sondern ein fertig verdrahteter hierarchischer Block, der mehrere Einzelschritte bündelt, die man sonst manuell mit dem rohen FFT-Block nachbauen müsste:
Stream → Stream-to-Vector (mit Decimation) → FFT → |·|² → IIR-Mittelung → 10·log10(·) → Ausgang (dB)
Stream → Stream-to-Vector (mit Decimation) → FFT → |·|² → IIR-Mittelung → 10·log10(·) → Ausgang (dB)
Konkret (verifiziert anhand der tatsächlichen logpwrfft.py-Implementierung in GNU Radio 3.10.12.0): Ein stream_to_vector_decimator bündelt den Eingangsstrom zu Vektoren der Länge fft_size und reduziert dabei die Ausgaberate auf ungefähr frame_rate Vektoren pro Sekunde. Es folgt eine gefensterte FFT (Blackman-Harris per Default), complex_to_mag_squared, ein single_pole_iir_filter zur optionalen zeitlichen Mittelung, und zuletzt nlog10_ff, das $10 \cdot \log_{10}(\cdot)$ bildet und dabei automatisch um FFT-Größe, Fensterleistung und ref_scale korrigiert.
2. Technischer Einsatz
Log Power FFT wird eingesetzt, wenn ein fertiges dB-Spektrum gebraucht wird, ohne die einzelnen Zwischenschritte (Fensterung, Betragsquadrat, Skalierung) selbst konfigurieren zu wollen — etwa als Datenquelle für eine eigene Spektrumsanzeige, für eine automatisierte Störerkennung (Schwellwert auf bestimmten Bins) oder als Messgrundlage in einem headless-Test, der eine erwartete Sperrdämpfung in dB nachweisen soll.
Für die reinen komplexen FFT-Koeffizienten (z. B. zur Frequenzbereichsfilterung) bleibt der rohe FFT-Block die richtige Wahl — Log Power FFT liefert bewusst nur noch den (gemittelten, logarithmierten) Betrag, keine Phaseninformation mehr.
3. Parameter und Dimensionierung
| Parameter | Typ/Einheit | Bedeutung | Dimensionierungshinweis |
|---|---|---|---|
type (Input Type) |
Enum | Complex oder Float | Complex für IQ-Basisbandsignale, Float für reelle Signale |
sample_rate |
Hz | Abtastrate des Eingangs | muss der tatsächlichen Flowgraph-Rate entsprechen — wird zur Berechnung des internen Decimation-Faktors benötigt |
fft_size (FFT Size) |
Integer | Anzahl Frequenz-Bins | wie beim rohen FFT-Block ein Kompromiss aus Frequenzauflösung und Rechenlast, siehe FFT |
ref_scale (Reference Scale) |
linear | Referenzamplitude für die dB-Skalierung | Standard 2; bestimmt den Nullpunkt der dB-Skala — Details und Grenzen siehe Abschnitt 5 |
frame_rate (Frame Rate) |
Vektoren/Sekunde | Zielrate, mit der neue Spektren ausgegeben werden | ein Sollwert, keine exakte Rate — siehe verifizierte Messung unten |
average (Average) |
Bool | ob aufeinanderfolgende Spektren zeitlich geglättet werden | „On" reduziert Rauschen im Spektrum auf Kosten der Reaktionszeit auf echte Änderungen |
avg_alpha (Average Alpha) |
0,0–1,0 | Glättungskonstante des internen Single-Pole-IIR-Filters | kleiner = stärkere Glättung, aber trägeres Ansprechen; 1,0 entspricht keiner Mittelung |
shift (FFT Shift) |
Bool | ob die DC-Komponente in die Mitte des Ausgabevektors verschoben wird | „On" für die übliche, symmetrische Spektrumdarstellung bei komplexem Eingang |
frame_rate wird intern in einen ganzzahligen Decimation-Faktor umgerechnet — die tatsächliche Ausgaberate weicht deshalb vom eingestellten Wert ab, statt ihn exakt zu treffen:
from gnuradio.fft import logpwrfft
lp = logpwrfft.logpwrfft_f(sample_rate=48000, fft_size=1024, ref_scale=2.0,
frame_rate=30, avg_alpha=1.0, average=False)
print("decimation():", lp.decimation())
print("volle Vektorrate (samp_rate/fft_size):", 48000/1024)
print("tatsächliche Ausgaberate (volle Rate/decimation):", (48000/1024)/lp.decimation())
from gnuradio.fft import logpwrfft
lp = logpwrfft.logpwrfft_f(sample_rate=48000, fft_size=1024, ref_scale=2.0,
frame_rate=30, avg_alpha=1.0, average=False)
print("decimation():", lp.decimation())
print("volle Vektorrate (samp_rate/fft_size):", 48000/1024)
print("tatsächliche Ausgaberate (volle Rate/decimation):", (48000/1024)/lp.decimation())
Ausgabe:
decimation(): 2
volle Vektorrate (samp_rate/fft_size): 46.875
tatsächliche Ausgaberate (volle Rate/decimation): 23.4375
decimation(): 2
volle Vektorrate (samp_rate/fft_size): 46.875
tatsächliche Ausgaberate (volle Rate/decimation): 23.4375
Warum diese Werte: Bei 48 kHz und fft_size=1024 entstehen ohne Decimation 46,875 vollständige FFT-Vektoren pro Sekunde. Um die gewünschten 30 fps zu erreichen, müsste um Faktor $46{,}875/30 \approx 1{,}56$ dezimiert werden — da nur ganzzahlige Decimation-Faktoren möglich sind, rundet der Block auf 2 und liefert real 23,4 fps statt der eingestellten 30. Wer eine bestimmte Ausgaberate braucht, sollte sie nach dem Bauen des Flowgraphs mit lp.frame_rate() gegenprüfen statt sich auf den eingestellten Wert zu verlassen.
4. Ein- und Ausgänge
| Ein-/Ausgang | Typ | Vektorlänge |
|---|---|---|
| Eingang | Complex oder Float (je nach type) |
Skalarstrom |
| Ausgang | Float (dB-Werte) | fft_size |
5. Weitere wichtige Aspekte
dB-Skala ist relativ linear, der exakte Nullpunkt aber nicht trivial: Eine Verdopplung der Eingangsamplitude verschiebt den Pegel eines Bins nachweislich um exakt $20 \cdot \log_{10}(2) \approx 6{,}02,\text{dB}$ nach oben — das übliche dB-Verhalten stimmt also uneingeschränkt:
from gnuradio import gr, blocks, analog
from gnuradio.fft import logpwrfft
import math
samp_rate, fft_size, ref_scale = 48000, 1024, 2.0
freq = 100 * samp_rate / fft_size # exakt bin-zentriert
def peak_db(amp):
tb = gr.top_block()
src = analog.sig_source_f(samp_rate, analog.GR_SIN_WAVE, freq, amp)
head = blocks.head(gr.sizeof_float, samp_rate * 2)
lp = logpwrfft.logpwrfft_f(sample_rate=samp_rate, fft_size=fft_size, ref_scale=ref_scale,
frame_rate=30, avg_alpha=1.0, average=False)
snk = blocks.vector_sink_f(fft_size)
tb.connect(src, head, lp, snk)
tb.run()
return max(snk.data()[-fft_size:])
p1, p2 = peak_db(1.0), peak_db(2.0)
print("amp=1.0:", round(p1, 3), "dB")
print("amp=2.0:", round(p2, 3), "dB")
print("Differenz:", round(p2 - p1, 3), "dB (erwartet:", round(20*math.log10(2), 3), "dB)")
from gnuradio import gr, blocks, analog
from gnuradio.fft import logpwrfft
import math
samp_rate, fft_size, ref_scale = 48000, 1024, 2.0
freq = 100 * samp_rate / fft_size # exakt bin-zentriert
def peak_db(amp):
tb = gr.top_block()
src = analog.sig_source_f(samp_rate, analog.GR_SIN_WAVE, freq, amp)
head = blocks.head(gr.sizeof_float, samp_rate * 2)
lp = logpwrfft.logpwrfft_f(sample_rate=samp_rate, fft_size=fft_size, ref_scale=ref_scale,
frame_rate=30, avg_alpha=1.0, average=False)
snk = blocks.vector_sink_f(fft_size)
tb.connect(src, head, lp, snk)
tb.run()
return max(snk.data()[-fft_size:])
p1, p2 = peak_db(1.0), peak_db(2.0)
print("amp=1.0:", round(p1, 3), "dB")
print("amp=2.0:", round(p2, 3), "dB")
print("Differenz:", round(p2 - p1, 3), "dB (erwartet:", round(20*math.log10(2), 3), "dB)")
Ausgabe:
amp=1.0: -9.045 dB
amp=2.0: -3.024 dB
Differenz: 6.021 dB (erwartet: 6.021 dB)
amp=1.0: -9.045 dB
amp=2.0: -3.024 dB
Differenz: 6.021 dB (erwartet: 6.021 dB)
Warum diese Werte: Die gemessene Differenz von 6,021 dB trifft den theoretischen Wert $20\log_{10}(2)$ exakt — die dB-Skalierung ist also korrekt kalibriert. Bemerkenswert dagegen: Bei Amplitude 1,0 (= ref_scale/2, was man naiv als „0 dB-Punkt" erwarten könnte) zeigt der Block tatsächlich −9,045 dB, nicht 0 dB. Der Grund liegt in der Blackman-Harris-Fensterfunktion: Der Korrekturterm im Block (window_power-Kompensation) normiert auf die Rauschleistung des Fensters, nicht auf dessen kohärente Verstärkung für einen einzelnen Ton — für breitbandige Signale (Rauschen) stimmt die ref_scale-Angabe daher direkt, für einen einzelnen Sinuston liegt der tatsächliche 0-dB-Punkt einige dB darüber. Wer eine exakte Pegelkalibrierung für Einzeltöne braucht, sollte den realen Offset wie hier gezeigt einmal selbst messen, statt sich auf „ref_scale/2 = 0 dB" zu verlassen.
Mittelung reduziert messbar das Rauschen im Spektrum:
Ausgabe:
ohne Averaging: std der Bin-Werte über Frames = 6.227
mit Averaging (alpha=0.1): std der Bin-Werte über Frames = 0.938
ohne Averaging: std der Bin-Werte über Frames = 6.227
mit Averaging (alpha=0.1): std der Bin-Werte über Frames = 0.938
Warum diese Werte: Die Standardabweichung sinkt durch die Mittelung von 6,2 dB auf unter 1 dB — ein klar messbarer Glättungseffekt. Das bestätigt die Funktion des internen Single-Pole-IIR-Filters: Er dämpft frame-zu-frame-Schwankungen, reagiert dafür aber auch langsamer auf echte, plötzliche Pegeländerungen (Trade-off, den ein kleineres avg_alpha verstärkt).
FFT (Fast Fourier Transformation)
Der rohe Baustein darunter — komplexe Frequenzbin-Werte statt fertiges dB-Spektrum.
Fensterfunktionen
Warum Blackman-Harris als Default-Fenster kohärente Verstärkung und Rauschnormierung unterschiedlich behandelt.
Testen mit GRC
Wie sich ein solches Spektrum headless per assert statt per Sichtkontrolle auf einem Frequency Sink prüfen lässt.