◀ Back to Theory of operation

FFT analyser

What it is for

The FFT analyser is the frequency-domain workhorse: it shows the spectrum and measures harmonic distortion (THD, THD+N), signal-to-noise ratio, SINAD and ENOB, the individual harmonics, and - in two-tone mode - the intermodulation products. Its defining capability is coherent averaging, which digs a signal far below the single-shot noise floor so distortion products tens of dB under the noise become measurable. The design target is extreme: THD measured with confidence down to 10⁻⁸ % - eight digits after the decimal point - and most of the machinery in this chapter exists to make a complex average stable enough, for long enough, to reach that. For the general foundations - windowing, leakage, resolution, averaging - we strongly recommend reading S. L. Gjelstrup's FFT guide[6] (summarised locally in FFT analysis essentials): it explains many aspects of precise FFT measurement excellently, though not all of the techniques this analyser uses appear there - and, for the record, Phonalyser.web's analyser was developed before that guide's publication in May 2026. This chapter covers what Phonalyser.web does specifically.

Resolution, windows, overlap

An FFT of length N at sample rate fs produces bins spaced fs/N apart; longer transforms (up to 4M points here) buy finer resolution at the cost of capture time. A tone seldom lands exactly on a bin, so its energy would leak across neighbours; a window[1] tapers each block to suppress that. Phonalyser.web offers a range from Rectangular and Hann through Blackman-Harris, flat-top, the very low-leakage HFT (Heinzel) family and Kaiser-Bessel, to Dolph-Chebyshev[2] windows with side-lobe floors down to −300 dB - the dynamic range a distortion measurement needs.

A tapered window has a price: it multiplies the samples near the block edges by almost zero, so the signal power those samples carry is simply thrown away - for a Hann-class taper, roughly half the captured energy never reaches the transform. Overlapping successive blocks (up to 93.75 % here) recovers it: a sample sitting in one block's discarded edge sits near the peak of a neighbouring block's window, so across the overlapped set virtually every captured sample contributes at full weight. The noise floor of the average then falls at the rate the data arrives - the same floor is reached in less wall-clock time than un-overlapped analysis would need.

window weight ≈ zero weight ≈ zero weight one tapered block: the edge samples' power is captured - then thrown away edge of one block = peak of the next overlapped blocks: every sample gets full weight in some block - no power wasted, the same noise floor in less time

Coherent averaging

There are two ways to average spectra, and the difference is the whole point of this analyser:

10·log₁₀ M  dB(1)

- so 100 averages buy about 20 dB, 10000 about 40 dB. This is what exposes harmonics buried far beneath the single-shot floor.

The crux: every captured block arrives with a different fundamental phase, because the tone is not synchronous with the block rate. Summed naively, the blocks would point in random directions on the complex plane and cancel the signal along with the noise. Coherent averaging therefore stands on two legs:

Sub-bin frequency and de-rotation

Write the tone's exact position as a fractional bin number κ. Between two blocks spaced s samples apart, bin k's phase advances by a known amount; comparing the measured phases of the tone in two clean blocks recovers κ from that phase difference (a far more precise estimate than a magnitude peak-pick). Each block m is then de-rotated before summing:

Φm = −2π · κ · m·s / N(2)

Harmonic h of the tone sits near bin h·κ and advances h× as fast, so its lobe is de-rotated by h·Φ. Applying a constant phase per harmonic lobe - rather than a per-bin frequency ramp - is what keeps the leakage skirt of each harmonic aligned too; a ramp would over-rotate the skirt and grow a spurious comb on it.

m=0 m=1 m=2 m=3 each block's tone arrives rotated by m·Φ - summed naively, they cancel each other rotate block m back by −m·Φ signal: adds up noise: random directions, cancels de-rotated: every block points the same way - the average grows while the noise floor falls
Why κ is fitted over the whole segment. A κ taken from just one pair of blocks carries a small residual error, which acts as a slow de-rotation drift across a long average - the harmonics gradually de-cohere and read low (the "harmonics decline on long captures" effect). Phonalyser.web refits κ by regressing the tone's phase across every block in the clean segment, a baseline hundreds of times longer than one hop, so even very long averages stay coherent. How precise that fit is, and why its residual error does not accumulate, is worked out quantitatively in the de-rotation-accuracy chapter.

Keeping the tone on the grid: snap and the FLL

De-rotation is most accurate when there is least to de-rotate. Two mechanisms keep the tone essentially on a bin:

That alignment almost eliminates spectral leakage from the fundamental and harmonic bins. The small residual that remains - even at 1/5000 of a bin or less - is still measured as κ and accounted for exactly by the de-rotation. Alignment reduces what de-rotation must correct; de-rotation guarantees correctness either way.

The neighbour bin: where the leaked power goes

However good the alignment, the tone's true position κ is never perfectly on the bin - and any offset splits the tone's power between the two straddling bins. The split is brutally asymmetric: on a well-aligned tone the main bin holds, say, 99.9999 % of the power and the neighbour the remaining 0.0001 %. Tempting to ignore - but the design target is 10⁻⁸ %, and dropping a 10⁻⁶ share of the fundamental or of a harmonic is an error orders of magnitude above it. So the level of every tone and harmonic is always computed from both bins, weighted by the sub-bin offset w = |κ − k|:

A² = (1 − w) · Pk + w · Pk±1(3)
bin k−1 bin k bin k+1 99.9999 % 0.0001 % true position κ (slightly off the bin) both enter A² - equation (3) (symbolic - not to scale)

Choosing the phase reference

The phase difference that drives the de-rotation has to be measured on something - and what it is measured on decides how much phase noise enters every block's correction:

fundamental - sets Φ harmonics follow with h·Φ strong fundamental: one clean phase reference common rotation (clock drift) tones = tines of one rigid fork weak fundamental / two tones: "rotate the fork" - the common angle is measured from all tines pooled

The picture to keep: the strong tones form one rigid fork - the signal fixes their phases relative to each other, and only the fork's common rotation (the converters' clock drift) changes from block to block. Measuring that one rotation angle from all tines at once, weighted by their strength, is far quieter than reading it off any single tine - and the whole fork is then rotated back as one.

This pooling is not a corner case; it is what a serious THD measurement looks like. To measure THD at 1 kHz near the 10⁻⁸ % level, the fundamental is removed by a 1 kHz twin-T notch in front of the ADC - making it up to 80 dB weaker (of order 0.1 mV RMS) so the converter no longer generates its own distortion from a full-scale tone and its dynamic range is spent on the harmonics. The de-rotation then locks onto the pooled tone set and stays locked, which is what makes hours-long complex averaging of the weakest harmonics possible.

Keeping bad blocks out of the average

One corrupted block - a stretch of swallowed samples, a paused generator, a click - poured into a coherent average can undo thousands of good blocks: it splatters power across the spectrum and steps the phase, de-cohering everything collected so far. Two failure classes are detected and their blocks excluded:

Buffer overrun

If the analyser falls behind the capture far enough that unread samples are overwritten (see ring buffer - overrun), the stream has a hole of unknown length. The read interface reports this explicitly - torn data is never silently delivered - and the analyser re-synchronises: the incomplete window is discarded and the cursor re-anchors at the newest sample.

Signal discontinuity - detected in the spectrum

Subtler breaks - the generator pausing (the signal goes stale), a capture hiccup swallowing a span, a phase jump - leave no reported hole in the stream: they have to be found in the signal itself. Detection runs on two complementary fronts. The first is the frequency domain, where each new block is compared with the running statistics of the blocks already accepted. Per block:

  1. The spectrum is reduced to a set of log-spaced band levels (average power per band, in dB), and bands holding genuine spectral lines are identified and set aside - a real line references itself and must not count as "lift".
  2. Gate 1 - near-carrier pedestal. The noise level in the skirt just outside each tone's actual main lobe (the lobe extent is found from the data, so ordinary window leakage is excluded), measured as its excess over the local floor. A discontinuity splatters energy here first, long before the broadband floor moves.
  3. Gate 2 - broadband floor lift. The mean rise of the non-line bands over their per-band running median - catches a uniform floor lift the pedestal gate can't see.
  4. Gate 3 - total power. A jump or collapse of the block's total power against its history - a stalled generator or a long dropout, where lines vanish instead of the floor rising.

Every threshold is self-calibrated: a running median of the collected history plus a multiple of its robust spread (MAD[3]), with the pedestal gate given a wider margin because window leakage in a tone's skirt naturally breathes block to block. The detector thus learns the clean baseline of this signal at this level instead of using fixed numbers; the first block seeds the reference, and from the second block on every gate is live. The band reference is updated only from accepted blocks, while the gate baselines track all blocks - so one early glitch can neither poison the reference nor cause false rejections later.

Signal discontinuity - caught in the raw waveform

The spectral gates judge a block by its statistics, and statistics can be diluted: a splice of a hundred-odd microseconds - the observed USB capture dropouts run 120-160 µs - sits inside a block that may span seconds, and its splattered energy, spread across the whole spectrum, can stay under thresholds that must be generous enough not to fire on ordinary leakage breathing. So each block's raw samples are screened as well, by the same discontinuity detector that drives the oscilloscope's glitch trigger: a sinusoid-recurrence predictor whose error baseline is the noise floor itself - not the signal's own curvature - so an amplitude step or a slope break stands decades proud of it, anywhere on the waveform and at any frequency. (The predictor's derivation - and why the naive second difference goes deaf at high frequencies - is in the oscilloscope chapter's trigger section.) The predictor is pinned to the measured fundamental when one is known and estimates its own coefficient otherwise, so the screen works on external signals too. When it fires, the block is excluded and the stream re-synchronises exactly as for a spectral rejection.

The two screens cover each other's blind spots. The time screen catches the sharp, local break whose spectral footprint is faint - dropped samples, a converter cutoff, a phase splice; the spectral gates catch damage that leaves every individual sample plausible - a uniform floor lift, or a stalled generator whose output is perfectly smooth but stale. And because the analyser and the oscilloscope share one detector, the two instruments agree on what counts as a damaged capture: a glitch the scope will trigger on is a block the average will refuse.

One timing subtlety applies to the re-sync itself. The detector fires on the glitch's entry edge - but the dropout that raised the alarm may still be in flight when the read cursor re-anchors at the newest sample, so rebuilding immediately could start the new window inside the glitch and trip the detector again on the same event. After a discontinuity re-sync the analyser therefore reads and discards the first 5 ms of fresh samples - some 30-40 times the observed dropout length - before rebuilding, so the first block after a re-sync provably starts past the glitch's end and enters the average clean.

What a re-sync does - and does not - cost. In every case the offending window is discarded and the stream re-anchors, but the accumulated average survives: the de-rotation absorbs the coverage gap, and the running phase lock applies a one-shot full realignment on the next clean block (a re-synchronised stream generally resumes at a new phase) before returning to normal tracking. The blinking capture re-sync indicator reports the event. This exclusion - rather than letting one stale or torn block in - is what almost completely eliminates power-leakage bursts and the slow decoherence of the collected statistics.

The distortion and noise metrics

From the averaged spectrum, with the fundamental level A1 and harmonic levels Ah:

THD = √( Σh≥2 Ah² ) / A1(4)

SNR compares the fundamental against the integrated noise power in the measurement band, with every signal bin (fundamental, harmonics, and their leakage zones) excluded from the noise sum:

SNR = 10·log₁₀ ( A1² / Σnoise Ak² )(5)

One factor in that noise sum is invisible until it bites: a windowed spectrum bin does not hold one bin-width of broadband power but NENBW of them - the window's normalized equivalent noise bandwidth (Hann: 1.5, flat-top: ≈3.8). A sine is immune, its amplitude being normalized by the window's coherent gain; but a noise sum taken over raw bins overstates the band power by exactly that factor, and SNR, SINAD, ENOB and THD+N would all read pessimistic - 1.76 dB with Hann, almost 6 dB with flat-top. The analyser therefore divides the integrated noise by NENBW - and it does not look the value up in a table of window names, it computes it from the very window table in use:

NENBW = N · Σ wn² / ( Σ wn(6)

so every window - including any added in the future - carries its exact factor. The value is stamped into each result, so a later band change recomputes with the window that actually produced the spectrum. The factors of the analyser's windows (computed from their tables at N = 64k; the dependence on N is negligible):

WindowNENBW (bins)Noise-sum error if uncorrected
Rectangular1.00000.00 dB
Hann1.50001.76 dB
Blackman-Harris 42.00443.02 dB
Blackman-Harris 72.63034.20 dB
Flat-top3.77025.76 dB
HFT144D4.53866.57 dB
HFT248D5.65127.52 dB
Kaiser-Bessel 242.80134.47 dB
Kaiser-Bessel 383.50725.45 dB
Dolph-Chebyshev 150 dB2.36603.74 dB
Dolph-Chebyshev 200 dB2.72594.36 dB
Dolph-Chebyshev 250 dB3.04354.83 dB
Dolph-Chebyshev 300 dB3.33105.23 dB

SINAD puts both noise and distortion in the denominator, and ENOB - the effective number of bits - follows from it by the converter's ideal-quantization law[4]:

SINAD = 10·log₁₀ ( A1² / (Σnoise + Σdist) )(7)
ENOB = (SINAD − 1.76) / 6.02(8)

THD+N is simply the reciprocal ratio to SINAD (the same band, the same numerator), and N+D is that same denominator read against the fundamental - the total non-fundamental residual, shown in dB with an A suffix (THD+N is the identical figure as a percentage). Both terms of the denominator - the integrated noise and the harmonic distortion - are multiplied, bin by bin, by the IEC 61672 A-weighting curve[5] before they are summed: the curve is near unity around 1 kHz and rolls off the deep bass and the extreme treble where the ear is far less sensitive, so N+D, THD+N, SINAD and ENOB reflect audibility rather than raw wideband energy - which is also why a converter with a rising treble noise floor scores several decibels better weighted than flat. SNR (5) and the N readout are deliberately left unweighted; they state the raw wideband noise and so carry no A.

The phrase "their leakage zones" hides the detail that decides whether these figures are honest: the band excluded around the fundamental is not a fixed guard. A wide or phase-noisy fundamental spreads a skirt of raised bins to either side; left in the noise sum it would flatter both SNR and ENOB by counting the tone's own spilled energy as noise. So the exclusion is grown to fit the skirt. Starting at the fundamental bin the analyser walks outward, bin by bin, while each neighbour still stands above the noise floor, and ends the walk only when a whole look-ahead window of bins has fallen back to it - a narrow skirt is excluded narrowly, a broad one out to wherever it truly ends. The window is sized to the frame-overlap correlation length, about 1/(1−overlap) bins plus margin: an overlap-averaged floor wobbles on exactly that scale, so a shorter sub-floor dip inside the skirt is floor texture, not the skirt's end, and is stepped over. The level used for the test is the median power of the spectrum's noise bins, which is immune to the very leakage being measured, so the width of the walk does not depend on the chosen SNR band; and the walk is hard-capped at ±½·f₁, so no floor statistic can ever extend the exclusion to the second harmonic. A second, bounded pass then sweeps the same ±½·f₁ zone around the fundamental and drops any isolated phase-noise bump still standing above the floor - a skirt broken by a gap longer than the look-ahead would otherwise leak its outer islands back in. That zone is capped at half the fundamental frequency so it can never reach the second harmonic at 2·f₁, which must stay counted as distortion, not excluded as noise.

However wide the exclusions grow, they cannot deflate the noise figure, because excluded bins are not counted as silence: the noise integral is rescaled by the ratio of band bins to bins actually summed, which estimates the noise hidden under the skirt and the harmonics at the level of the surrounding floor - the only honest assumption, since the true noise under a tone is unobservable.

Two-tone (intermodulation)

With two tones present, the products of interest are not harmonics of one tone but combinations a·f₁ + b·f₂. The analyser locates the second tone's own sub-bin position κ₂ from a clean block (where it is still at full level - coherent cancellation only bites in the average), then de-rotates the whole intermodulation grid at each product's true frequency a·Φ₁ + b·Φ₂. Snapping every product to the nearest harmonic of one tone instead would rotate some products ~180° apart between blocks and cancel them - the cause of an earlier "IMD products vanish" bug. The right de-rotation self-selects: the one that leaves a product strongest is the correct one.

Calibration and plot-time corrections

The averaging accumulator holds the raw spectrum. Everything corrective is applied when the curve is drawn, so toggling a correction re-plots instantly without discarding the average:

dBr belongs to that last item, and to nothing else. It is a display transform: the plotted curve is offset so that the fundamental reads exactly 0, which is a subtraction in the log domain of one number from every bin.

LdBr(k) = LdBFS(k) − LdBFS(f₀)(9)

Two properties follow from doing it here rather than earlier. The reference is the fundamental as displayed - the de-embedded value, or a manually pinned one when that is in force - and it is re-read on every repaint, so a level that drifts carries its whole spectrum with it and the harmonics stay at the same dBr while their absolute level moves. And because the offset never reaches the accumulator, nothing is ever integrated against it: the averaged data stays raw, switching to dBr and back costs no accumulated blocks, and the noise and distortion metrics - which are computed from the raw spectrum, not from what the axis happens to show - are identical in every unit.

Frequency-response compensation: the red and blue dots

A measurement through any real chain - a notch filter, an attenuator, the soundcard's own roll-off - reads each harmonic through that chain's frequency response. Loaded calibration files (measured by the frequency-response module, e.g. the twin-T notch's own curve) undo this: at display time the chain's response is divided out of the spectrum, and when several calibrations are loaded they compose in order.

The view shows both worlds at once:

The measurement table follows the red dots. After the compensation is applied, every derived figure - fundamental level, harmonic levels and percentages, THD, THD+N, SNR - is recomputed from the corrected spectrum, so the table states the device's numbers, not the chain's. One subtlety: on a deep average the blue dots are re-derived from the pinned coherent frequency rather than each tick's jittering single-tick estimate - otherwise the calibration would be looked up at a slightly different frequency every tick and the blue dots would visibly dance on a steep notch flank.

Pinning the fundamental by hand on a steep notch

The fundamental is the one level the notch calibration recovers least reliably. A passive twin-T notch's flank around its null is extraordinarily steep - in one measured filter the response is −82.46 dB at 1001.9 Hz but −81.73 dB at 1002.23 Hz: about 0.7 dB of correction across a third of a hertz. So the slightest error in the looked-up frequency - or the slightest drift of the notch itself - turns into a large error in the de-embedded fundamental level. And the twin-T's null does drift, by tenths of a hertz as its components warm and cool, so over a long run the recovered fundamental wanders even though the source is rock-steady.

Because every THD / THD+N / SNR figure is referenced to the fundamental level, that wander shows up as instability in the distortion readings even when the device under test has not changed. The cure is to stop de-embedding the fundamental and state it by hand: its amplitude is the part you actually know - you dialled it on the generator, or measured it without the notch in circuit - whereas the filter's null is the uncertain part. With the manual fundamental enabled, the distortion metrics are referenced against that fixed, known value, and only the harmonics are de-embedded - and they are robust to the very same drift: at the 2nd harmonic (≈2004 Hz) a 2/3 Hz shift of the notch moves the correction by under 0.01 dB, and at the 3rd (≈3006 Hz) even a 1 Hz shift stays under 0.01 dB, because those frequencies sit far up the notch's gentle flank rather than in its steep null. Only the fundamental, down in the null, is fragile. The THD reading then holds steady no matter how the notch drifts.

What all of this buys

Each mechanism in this chapter removes one way a long complex average can rot: bin alignment to ±0.05 ppm removes the leakage, exact per-block de-rotation removes the phase drift the residual would cause, pooled phase references keep the lock when the fundamental is notched away, and strict block exclusion keeps overruns and discontinuities out of the statistics. Together they make the average stable over very long runs - 8 to 12 hours of continuous coherent averaging - and that endurance is the requirement behind the headline figure: a noise floor driven low enough to state THD at the 10⁻⁸ % level with confidence, not as a one-off lucky readout.

That the de-rotation can hold a complex average together for that long without quietly corrupting the harmonic levels is not assumed - it is worked out in the de-rotation accuracy chapter, which shows the rotation-angle error stays at about 0.023°, contributes only micro-decibels to the levels, and - the key point - does not accumulate over hours of averaging.

Where it sits

The analyser shares one capture session and ring buffer with the oscilloscope. When the generator runs an internal tone it can take the commanded frequency as a starting hint and, with snap-to-bin active, feeds a frequency trim back to the generator so the emitted tone lands exactly on a bin - the tightest possible coherence. Its harmonic measurements also feed the generator's compensated-sine distortion cancellation.

References

  1. 1. Window functions and leakage - Wikipedia: Window function; the HFT (flat-top) family: G. Heinzel et al., "Spectrum and spectral density estimation by the DFT, including a comprehensive list of window functions", 2002.
  2. 2. Dolph-Chebyshev window (uniform side-lobe floor) - Wikipedia: Dolph-Chebyshev window.
  3. 3. Median and median-absolute-deviation as a robust threshold - Wikipedia: Median absolute deviation.
  4. 4. SINAD, ENOB and the 6.02 N + 1.76 dB law - Wikipedia: Effective number of bits; Analog Devices tutorial MT-003, "Understand SINAD, ENOB, SNR, THD, THD + N, and SFDR".
  5. 5. A-weighting - Wikipedia: A-weighting (IEC 61672).
  6. 6. S. L. Gjelstrup, "FFT Analysis (Fast Fourier Transform): The Ultimate Guide to Frequency Analysis", Dewesoft, 2026 - recommended reading on the foundations (windows, leakage, averaging, scaling); local summary.

◀ Oscilloscope · Theory of operation · next: De-rotation accuracy ▶