Page 1 of 1

Warum sinkt meine Echtzeit-FFT eines logarithmischen Sweep immer wieder-und warum behebt diese seltsame Normalisierung s

Posted: 14 Sep 2025, 11:51
by Anonymous
Ich baue einen Open-Source-Audiospektrumanalysator für die Lautsprecherherstellung. Das „Killer -Feature“ verwendet einen logarithmischen Sweep als Anregungssignal und analysiert die Antwort , wenn der Sweep aufgezeichnet wird, sodass die Benutzeroberfläche nie blockiert. Repo: https://github.com/max-bold/spectrum.

alles war. Dann habe ich einen super nervigen Effekt getroffen: Als der aufgezeichnete Puffer wächst, driftet das aufgetragene Spektrum langsam im Level ab, obwohl sich der Eingangspegel nicht ändert. Klassische Normalisierung wie diese lässt es sinken: < /p>

Code: Select all

yf = np.abs(np.fft.rfft(record))
yf1 = 2 * yf / len(record)  # standard amplitude normalization
< /code>
Das ist seltsam, weil sich die aufgezeichnete Signalpegel im Laufe der Zeit nicht ändert. < /p>
Ich habe also angefangen zu experimentieren. Am Ende hatte ich eine Normalisierung, die unabhängig von den Generatorparametern auf magische Weise stabil bleibt: < /p>
yf = np.abs(np.fft.rfft(record))
yf2 = yf / nk
nk = np.sqrt(length * rate / np.log10(band[1] / band[0]) / 10) * 10 ** (0.35726 / 20)
< /code>
Dies funktioniert erschreckend gut. Aber… warum? Es animiert zwei Spuren (line1
mit "Lehrbuch" Normalisierung + rosa Gewichtung und Zeile2 mit meinem seltsamen NK Normalisierung + Pink Gewichtung). Nur der zweite bleibt stabil, während der Sweep wächst.

Code: Select all

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.lines import Line2D

# Setting sweep params
rate: int = 96000
chunksize: int = 4096
band = np.asarray((50, 2000), np.float64) / rate
t_end = 30
length = int(t_end * rate)
nk = np.sqrt(length * rate / np.log10(band[1] / band[0]) / 10) * 10 ** (0.35726 / 20)

# Generating sweep
k = length * band[0] / np.log(band[1] / band[0])
l = length / np.log(band[1] / band[0])
t = np.arange(0, length, dtype=np.float64)
sweep = np.sin(2 * np.pi * k * (np.exp(t / l) - 1))

# Initialise plt in interactive mode
plt.ion()
_, ax = plt.subplots()
ax: Axes = ax
ax.grid(True, which="both")
(line1,) = ax.semilogx([], [])
(line2,) = ax.semilogx([], [])
line1: Line2D = line1
line2: Line2D = line2
ax.set_xlim(20, 20e3)
ax.set_ylim(-60, 60)

for start in range(0, length, chunksize):
end = min(start + chunksize, int(length))
chunk = sweep[:end]

xf = np.fft.rfftfreq(len(chunk), 1 / rate)
yf = np.abs(np.fft.rfft(chunk))

# "Textbook" amplitude normalization
yf1 = 2 * yf / len(chunk)
pinked_yf1 = yf1 * np.sqrt(xf)          # pink weighting to look at ~constant SPL per octave
log_yf1 = 20 * np.log10(pinked_yf1.clip(1e-12, None))
line1.set_data(xf, log_yf1)

# My weird normalization
yf2 = yf / nk
pinked_yf2 = yf2 * np.sqrt(xf)
log_yf2 = 20 * np.log10(pinked_yf2.clip(1e-12, None))
line2.set_data(xf, log_yf2)

plt.draw()
plt.pause(0.1)

plt.ioff()
plt.show()
< /code>

 Was ich  denke < /em> Ich verstehe (vielleicht?) < /H2>

  Verwenden von np.sqrt (Länge * Rate) < /code> In der Nenner fühlt sich in Richtung der Spezifische Spezifikum von < /psd.10*log10(|rfft(record)/length/rate|^2) == 20*log10(|rfft(record)| / sqrt(length*rate))
< /code>
Ich kann mit dieser Idee leben. Aber in meiner Falllänge == Len (Sweep) 
, nicht Len (Datensatz) . Der Rekord wächst durch Chunk, aber die Sweep -Länge ist festgelegt. Diese Nichtübereinstimmung scheint von Bedeutung zu sein. Intuitiv hängt die Energiedichte des Sweep pro logarithmischer Frequenz vom -Verhältnis von fmax/fmin ab (log Sweep verbringt die gleiche Zeit pro Oktave). Eine Abhängigkeit von log10 (Band [1]/Band [0]) macht Sinn… aber warum unter einem Quadratwurzel ?

Code: Select all

/ 10
. Wo um alles in der Welt hat sich das eingeschlichen? Ist das eine versteckte Einheitumwandlung (LN vs log10? Oktave gegen Jahrzehnt?

Code: Select all

10 ** (0.35726 / 20)
um eine hartnäckige 0,35726 dB Vorspannung abzutöten. Yuck. Das fühlt sich wie eine rauchende Waffe für einen systematischen Fehler an (Fenster? Die "Standard" 2*yf/len (Datensatz) Normalisierung lässt den Spektrum -Drift nach unten als Len (Datensatz) für einen logarithmischen Sweep in einem wachsenden Präfix analysiert? Der Signalpegel ändert sich nicht, aber die Leitungssenkern. rate} {\ log_ {10} (f_ \ text {hi}/f_ \ text {lo})}}
$$
stabilisieren Dinge? FFT Bin Breite usw.) und erklären Warum landet der logarithmische Span in einer quadratischen Wurzel ? sonst? Nicht-stationäres "Fenster") < /li>
Rosa-Gewichtung über SQRT (f) < /code> < /li>
log Sweep-Amplitudengesetz
Das würde genau ~ 0,35726 db? & Kontext

Ich mache Echtzeit Analyse , während der Sweep immer noch läuft. Das bedeutet, dass jedes FFT ein unterschiedliches Präfix eines Protokoll -Sweep sieht, kein stationäres Segment eines stationären Prozesses. Ich weiß, dass dies „typischen“ FFT -Annahmen (Stationarität, Periodizität im Fenster usw.) feindlich ist. Ich bin mir bewusst, dass dies eine Leckage einlädt, wenn die augenblickliche Frequenz nicht genau auf einem Behälter landet - was es hier nie tut.

Code: Select all

* sqrt(f)
) Vor dem Konvertieren in DB. Das ist absichtlich für mein UX. < /Li>
Beispielcode und Parameter sind oben; Sie können es ausführen und Zeile 1 sinken, während Zeile2 flach bleibt. wachsende Prefix logarithmischer Sweep , die Abhängigkeit von der Gesamtdauer, der Stichprobenrate und der logarithmischen Frequenzspanne erklärt. Wenn es sich tatsächlich um tiefere Fehler handelt (z. B. falsche Referenz der rosa Gewichtung, Jahrzehnt gegen Oktavkonstanten, Ln vs log10, RMS vs Peak, ein-gegen-zweiseitiges PSD), würde ich gerne die Grundursache reparieren, die die Ursache nicht reparieren würde, anstatt die Band-Aid zu behalten. Alle DSP-Leute, die mir helfen können, hartkodierende mysteriöse Konstanten zu stoppen, werden meine ewige Dankbarkeit (und einen knusprigen PR-Link im Repo) verdienen. 🙏
Alles, was a sich