"I found a strong peak at 49.7 Hz and confidently reported a new vibration mode. It was the 50 Hz mains, smeared sideways because my window did not end where it began."
A Leaky AI Agent
Prerequisites. This section builds directly on the sampling and discrete-time notation of Chapter 3: a sensor stream is \(x[n] = x(nT_s)\) sampled at rate \(f_s = 1/T_s\), and the Nyquist limit \(f_s/2\) caps the frequencies we can resolve. It also uses complex exponentials \(e^{j\theta} = \cos\theta + j\sin\theta\) and the geometric-series sum, both reviewed in Appendix A. No prior Fourier machinery is assumed; the continuous transform and the sampling theorem that justify treating the DFT as a spectrum estimate are developed in Chapter 2 and Chapter 3. The moving-average and convolution ideas from Chapter 6 reappear here as windows.
Why the spectrum is the second language of sensing
Almost every physical signal a machine perceives is, at some level, a sum of oscillations: the 60 Hz hum of a motor, the 1 to 2 Hz sway of a walking gait, the resonant ring of a cracked bearing, the carrier of a radar return. The Discrete Fourier Transform (DFT) is the tool that turns a block of samples into "how much energy sits at each frequency," and the Fast Fourier Transform (FFT) is the algorithm that makes it cheap enough to run inside a wristband or a microcontroller. This section is the foundation of the entire chapter, and of much of the feature engineering in Chapter 8. It also carries a warning that trips up more practitioners than any other single mistake in spectral analysis: the peaks you see are not always where the energy really is. That distortion is called spectral leakage, and understanding it is the difference between reading a spectrum and being fooled by one.
The DFT: projecting a block of samples onto pure tones
What the DFT does is take \(N\) real or complex samples \(x[0], \dots, x[N-1]\) and return \(N\) complex numbers \(X[k]\), each measuring how strongly a pure tone of a particular frequency is present in the block:
$$X[k] = \sum_{n=0}^{N-1} x[n]\, e^{-j 2\pi k n / N}, \qquad k = 0, 1, \dots, N-1.$$Why this works is that the complex exponentials \(e^{-j2\pi kn/N}\) form an orthogonal basis: multiplying the signal by tone \(k\) and summing acts as a correlation, large when \(x\) contains that tone and near zero when it does not. The magnitude \(|X[k]|\) is the amplitude at bin \(k\) and the angle \(\arg X[k]\) is its phase. How bins map to real frequencies is the one formula you must never forget: bin \(k\) corresponds to
$$f_k = \frac{k}{N}\, f_s \ \text{Hz}, \qquad \text{so the bin spacing is} \quad \Delta f = \frac{f_s}{N} = \frac{1}{N T_s}.$$The frequency resolution \(\Delta f\) is set entirely by the observation time \(N T_s\), not by the sample rate. To tell 49.5 Hz apart from 50.0 Hz you need \(\Delta f \le 0.5\) Hz, hence at least two seconds of data, no matter how fast you sample. Sampling faster raises the Nyquist ceiling; it does not sharpen closely spaced peaks. For real-valued sensor signals the spectrum is conjugate-symmetric, so only the first \(N/2 + 1\) bins, from DC up to Nyquist, carry unique information.
Resolution comes from duration, sensitivity from rate
Two knobs govern every DFT and they do different jobs. The record length \(N T_s\) sets how finely you can separate neighboring frequencies (\(\Delta f = 1/(NT_s)\)); the sample rate \(f_s\) sets how high in frequency you can see before aliasing (Nyquist \(=f_s/2\)). Confusing them is the classic beginner error: someone unable to resolve two close tones cranks the sample rate and wonders why the peaks still merge. They needed a longer window, not a faster ADC. Keep this split in mind through the whole chapter, because the short-time transform of Section 7.2 is precisely the art of trading window duration against time localization.
The FFT: from \(N^2\) to \(N\log N\)
Computed straight from the definition, the DFT costs \(O(N^2)\) complex multiplies: for a 1-second window at 48 kHz that is over two billion operations, hopeless on an embedded target. The FFT is a family of algorithms (the radix-2 Cooley-Tukey version is the famous one) that exploits the symmetry and periodicity of the exponentials to factor the computation recursively, splitting even and odd samples, and driving the cost down to \(O(N\log N)\). For \(N = 65536\) that is roughly a four-thousand-fold speedup. The FFT computes exactly the same numbers as the DFT; it is an accelerator, not an approximation. One practical residue survives: classic radix-2 implementations want \(N\) to be a power of two, which is why sensor pipelines so often chop or pad blocks to 256, 1024, or 4096 samples. Modern libraries handle arbitrary \(N\) but run fastest on smooth, highly composite lengths.
Spectral leakage: the price of a finite window
Here is the trap. The DFT does not analyze your infinite signal; it analyzes the \(N\) samples you handed it, and it implicitly assumes that block repeats forever, wrapping end to start. When a sinusoid completes a whole number of cycles inside the window, that wrap is seamless and its energy lands cleanly in a single bin. When the frequency falls between bins, so the last sample does not continue smoothly into a copy of the first, the periodic extension has a jump, and a jump contains energy at every frequency. The result is that a single true tone smears its power across many bins, with slowly decaying side skirts. That smearing is spectral leakage. A related effect, scalloping loss, means a tone sitting exactly between two bins reads up to about 36 percent (3.9 dB) low in amplitude, because no bin sits on its peak. Leakage is not noise and it is not a bug in the FFT; it is the unavoidable consequence of looking through a finite rectangular window, and it can bury a weak signal under the skirts of a strong neighbor.
The cure is windowing: before transforming, multiply the block by a smooth taper \(w[n]\) that eases both ends to zero, so the periodic extension no longer jumps. Applying a window before the FFT is the standard workflow:
import numpy as np
fs, N = 1000, 1024 # 1 kHz sample rate, ~1.024 s window
t = np.arange(N) / fs
# A tone at 200.0 Hz lands ON a bin; 200.5 Hz lands BETWEEN bins.
x = np.sin(2 * np.pi * 200.5 * t) # deliberately off-grid
# Rectangular (no window) vs a Hann taper.
X_rect = np.fft.rfft(x)
X_hann = np.fft.rfft(x * np.hann(N) if hasattr(np, "hann") else x * np.hanning(N))
freqs = np.fft.rfftfreq(N, 1 / fs)
def peak_report(X, label):
mag = np.abs(X)
k = mag.argmax()
# Fraction of energy OUTSIDE the 3 bins around the peak = leakage.
total = (mag**2).sum()
near = (mag[max(0, k-1):k+2]**2).sum()
print(f"{label:11s} peak {freqs[k]:6.1f} Hz leaked {100*(1-near/total):4.1f}%")
peak_report(X_rect, "rectangular")
peak_report(X_hann, "hann")
The code above makes leakage measurable rather than merely visible: the off-grid tone bleeds a large share of its energy across the spectrum under a rectangular window, and the Hann taper confines it. That confinement is not free, and the tradeoff is the real content of windowing. A window trades a wider main lobe (worse resolution, since energy spreads over more bins) for lower side lobes (less leakage into distant bins). A rectangular window has the narrowest main lobe but side lobes only 13 dB down; the Hann window widens the main lobe to about two bins but pushes side lobes to 31 dB and rolls them off steeply; a Blackman-Harris window buys 90-plus dB of side-lobe suppression at the cost of a lobe four bins wide. There is no universally best window, only the right point on this resolution-versus-dynamic-range curve for your problem.
Right tool: windows and scaling you should not hand-roll
Choosing, generating, and correctly normalizing a window is fiddly, and getting the amplitude scaling wrong silently biases every downstream feature. SciPy packages the whole catalog and the corrections:
from scipy.signal import get_window
w = get_window(("kaiser", 8.6), N) # tunable beta trades lobe width vs leakage
X = np.fft.rfft(x * w)
X *= 2 / w.sum() # coherent-gain scaling -> true amplitude
get_window exposes Hann, Hamming, Blackman-Harris, flat-top, and the parametric Kaiser; the 2/w.sum() factor restores true peak amplitude on a one-sided spectrum.The library owns the exact taper coefficients, the coherent-gain factor (amplitude-accurate) versus the noise-equivalent-bandwidth factor (power-accurate), and the special flat-top window used when you need the peak height read correctly rather than its frequency. Only write your own window on a microcontroller with no SciPy, where a precomputed Hann table in flash is the pragmatic choice.
The phantom bearing tone on a factory gearbox
A predictive-maintenance team, of the kind we return to in Chapter 35, ran a raw FFT on 0.5-second accelerometer blocks from a gearbox and flagged a growing peak near 118 Hz as an emerging bearing fault. Maintenance opened a healthy gearbox and found nothing. The "fault" was the skirt of the dominant gear-mesh line at 120 Hz, leaking sideways because the 0.5-second rectangular window gave only 2 Hz resolution and the strong 120 Hz tone fell slightly off-grid. Two changes fixed it. Lengthening the window to 2 seconds sharpened resolution to 0.5 Hz, and applying a Hann taper dropped the mesh line's side lobes below the real fault features. The phantom 118 Hz peak vanished; a genuine, much weaker sideband at 122.7 Hz, the actual signature of the developing defect, emerged from under the leakage. The lesson generalizes: before trusting any peak near a strong neighbor, ask whether it is a real tone or the skirt of a leaked one, and whether your window is long enough and smooth enough to tell them apart.
Exercise 7.1
You sample a vibration signal at \(f_s = 2000\) Hz and must distinguish a suspected fault tone at 122.7 Hz from a strong structural line at 120.0 Hz. (a) What is the minimum window length \(N\) (and duration in seconds) so that \(\Delta f\) can separate them? (b) With that \(N\), which power-of-two FFT length would you actually use, and what \(\Delta f\) does it give? (c) The 120 Hz line is 40 dB stronger than the fault. Explain why a rectangular window might still hide the fault even at adequate resolution, and name a window whose side-lobe level would reveal it. (d) A colleague proposes zero-padding a 0.25-second block up to your target \(N\) instead of collecting more data. Explain in one sentence why that does not recover the resolution you need.
Self-check
1. Two tones 0.4 Hz apart merge into one peak. Do you increase the sample rate or the window length, and why does the other knob not help?
2. In what precise sense does the DFT "assume" your finite block repeats forever, and how does that assumption produce spectral leakage?
3. State the fundamental tradeoff a window makes, and give one situation favoring a rectangular window and one favoring a Blackman-Harris window.
What's Next
In Section 7.2, we confront the DFT's biggest limitation: it gives one spectrum for an entire block and cannot say when a frequency appeared. By sliding a short window along the signal and transforming each position, the Short-Time Fourier Transform turns a single spectrum into a spectrogram, a time-frequency picture, and the window-length choice we just met becomes the central knob trading time resolution against frequency resolution.