# FFT for mortals > What a spectrum actually shows, how Sonarish builds it from microphone data, and how to read peaks from fans and motors. 2026-01-07 · 19 min read · sonarish, dsp · by ntan (ntan) for uranashel Canonical HTML: https://uranashel.com/blog/fft-made-readable.html --- Sound is pressure changing over time. Your ear and brain are extraordinarily good at noticing when that pressure repeats at a steady rate, and the sensation gets a name: pitch. Machinery rarely produces a single pure tone. It produces stacks of periodic components plus hiss, and the Fourier transform is the mathematical tool that answers a deceptively simple question: if I record a few milliseconds of microphone data, which frequencies are present and how loud is each one? [Sonarish](https://uranashel.com/apps/sonarish.html) builds its spectrum analyzer and scrolling spectrogram on that question, running entirely on your phone without sending audio to a server. Most FFT explanations open with complex exponentials and lose the reader by the second page. This one runs the other way. It starts from what the output numbers mean, keeps the algebra to four short formulas, and spends the reclaimed space on the parts textbooks skip: windows, leakage, threading and the specific ways a phone microphone quietly lies. ## What the transform actually computes The discrete Fourier transform takes N consecutive audio samples and produces N complex numbers, each representing the sine and cosine contribution at one of N equally spaced frequencies. The recipe for output bin k is `X[k] = Σ x[n]·e^(−i2πkn/N)`, summed over n from 0 to N−1. Strip the notation away and it is a correlation. You multiply the signal, sample by sample, against a probe sinusoid that completes exactly k cycles inside the block, then add everything up. When the signal contains that frequency the products line up in phase and the sum grows large. When it does not, positive and negative products cancel and the sum hovers near zero. Bin k sits at frequency `f_k = k·fs/N`, where fs is the sample rate. We rarely display the full complex result. For noise and fault work the quantity that matters is magnitude, `|X[k]| = √(Re² + Im²)`, a plain measure of how strong each frequency bin is, and Sonarish converts it to decibels with `20·log10(|X[k]|/ref)` so quiet and loud components share one readable axis. Phase is real information too. It just answers questions about timing and alignment that a noise survey never asks. One structural fact makes phone implementations cheaper. Real-valued input produces a symmetric spectrum: bin N−k is the complex conjugate of bin k, so everything above half the sample rate mirrors what sits below it. Libraries exploit the symmetry and compute only N/2+1 useful bins, which is why the loop in the code further down stops at 2048 rather than 4096. ## Why the fast version deserves the name Evaluating the sum directly costs N multiply-adds per bin, and there are N bins, so the bill is N². At 4096 points that is roughly 16.8 million complex multiplies for one frame. The fast Fourier transform reorganizes the work instead of shrinking it: split the block into even and odd samples, transform each half, then stitch the halves together with a single pass of butterfly operations. Applied recursively, the cost collapses to `(N/2)·log2(N)` multiplies. For 4096 points that is 24,576, a factor of nearly 700 cheaper. Gauss had the trick in 1805 and filed it in a private notebook, which may be the most mathematician move on record. Cooley and Tukey republished it in 1965 and made real-time spectral analysis practical. Bench note from our side: across 3 phones (an iPhone 13, a Pixel 7 and a 2019 Galaxy A50) we timed 10,000 consecutive 4096-point real FFTs and averaged 19 µs, 46 µs and 210 µs per transform. Even the slowest device spends well under 1% of one core on the roughly 47 transforms per second that Sonarish's default settings ask for. ## Chopping time into windows A single FFT snapshot tells you what happened in one short slice of time. Real machines and rooms change slowly but not infinitely slowly, so Sonarish uses a short-time Fourier transform: chop the continuous microphone stream into overlapping windows, FFT each window, and plot magnitude against time as a scrolling waterfall. Window length trades frequency resolution against time resolution. No setting wins both. The default grabs 2048 samples at the 48 kHz capture rate, 42.7 ms of sound, and zero-pads each block to a 4096-point transform, which lands the plotted bins about 11.7 Hz apart. That spacing separates a 120 Hz mains hum from its second harmonic at 240 Hz with room to spare. Zero-padding deserves an honest footnote here: the padded zeros interpolate a smoother curve through the spectrum but cannot manufacture resolving power, which stays fixed by the 2048 real samples underneath. Switching to a full 4096-sample window halves the bin width but reacts more slowly to sudden clicks, because each click gets averaged across 85 ms of context. ## Leakage, and why every block gets a Hann The transform silently assumes your N-sample block repeats forever, end spliced to beginning. A tone that completes a whole number of cycles inside the block splices cleanly. A tone that does not, which in the field is nearly every tone, hits the seam mid-cycle, and the transform reads that discontinuity as energy smeared into neighboring bins. This is spectral leakage, and it is worst exactly when a tone's frequency falls between bin centers. A knife-sharp harmonic turns into a low pyramid wide enough to bury a quieter neighbor. The fix is to stop pretending the block edges carry meaning. We multiply each window by a Hann function, `w[n] = 0.5·(1 − cos(2πn/N))`, which tapers both ends smoothly to zero so the imaginary seam disappears. The price is a mainlobe roughly twice as wide; the reward is sidelobes some 30 dB lower. For tonal machinery with sharp harmonics, Hann is a sensible default, and it is what Sonarish ships. Consecutive windows hop forward by 50% of their length, which keeps the scrolling waterfall visually smooth without doubling CPU cost unnecessarily. We tried 75% overlap in a bench build once. Nobody could pick it out of a blind scroll test, so the extra transforms went back on the shelf. ## Reading peaks like an engineer A peak at 120 Hz in a country with 60 Hz mains is usually twice-line-frequency magnetic hum, while a companion line just under 60 Hz marks a two-pole induction motor spinning near 3600 RPM, because mechanical rotation is electrical frequency divided by pole pairs. Bearing wear announces itself differently. It introduces sidebands that are not simple multiples of shaft speed, and energy appearing at unexpected offsets is sometimes the first audible clue before vibration analysts confirm fault frequencies in a formal report. Broadband hiss rising across many bins suggests turbulence, brush arcing, or loose panels rattling without a single dominant tone. Sonarish labels peaks automatically when signal-to-noise ratio exceeds about 6 dB above a locally estimated noise floor. The floor estimate is a median over the neighboring bins, roughly an octave to each side, so one loud neighbor cannot hide a genuine peak. The app does not pretend to diagnose your washing machine. It shows the spectrum honestly and lets experience, or our [baseline comparison workflow](https://uranashel.com/blog/machine-fault-baseline.html), interpret change over time. Recording a known-good spectrum today is cheap insurance for an argument with a repair technician in 18 months. ## None of it touches the audio thread FFT work never runs inside the audio capture callback. That path is reserved for copying samples into a lock-free ring buffer and returning, for the reasons laid out in [life on the audio thread](https://uranashel.com/blog/dsp-audio-thread.html). A background queue pulls blocks from the ring, applies the window and the transform using platform libraries (Accelerate vDSP on iOS, a compact radix-2 implementation on Android) and hands finished magnitude arrays to the UI thread. The whole consumer loop fits on one screen: ``` // background queue; the audio callback only feeds the ring loop: wait until ring.available >= HOP // HOP = 1024 samples ring.peek_latest(frame, 2048) // newest full window ring.advance(HOP) for n in 0..2047: buf[n] = frame[n] * hann[n] // taper for n in 2048..4095: buf[n] = 0 // zero-pad to 4096 fft_real_4096(buf, re, im) // vDSP / radix-2 for k in 0..2048: mag = sqrt(re[k]*re[k] + im[k]*im[k]) db[k] = 20 * log10(max(mag, EPS)) ema_update(db_smooth, db, 0.6) // calm the flicker if now() - last_draw >= 33 ms: // cap near 30 Hz post_to_ui(db_smooth) last_draw = now() ``` Even when transforms complete faster, spectrum redraws are capped at 15 to 30 Hz. Humans cannot interpret a bar chart flickering at 60 Hz, and battery matters in a factory walk-through. The per-bin exponential moving average exists for the same reason; a raw spectrum bounces enough that peak labels would strobe. ## Pitfalls that fool smart people Aliasing is the classic trap. Any component above the Nyquist limit of `fs/2`, which is 24 kHz at our capture rate, folds back down and impersonates a lower frequency. The phone's audio hardware applies anti-alias filtering ahead of the converter, which largely handles the problem for ordinary sources, but you still should not point a mic at a whistle near the Nyquist limit and trust the graph blindly. The second trap is units. Decibels relative to digital full scale measure headroom inside the recording chain and say nothing about acoustic pressure in the room. Confusing dBFS with dB SPL invalidates any comparison to environmental law, and our [A-weighting and decibels post](https://uranashel.com/blog/a-weighting-decibels.html) walks through the distinction plus the calibration story behind it. The third trap arrives dressed as a feature request. Shrinking the window to make peaks look sharper actually widens the bins, and adjacent harmonics merge into one fat bar — pretty UI, worse physics. ## Where to poke at it yourself [Phyzix](https://uranashel.com/apps/phyzix.html) includes a live microphone FFT view built for classroom demos, where students clap, whistle and watch bins light up in real time. Wheria has no use for spectral analysis, yet the same sampling discipline (respect Nyquist, never block real-time capture) runs through every uranashel app that touches a microphone or accelerometer at high rate. The transform itself is 200-year-old mathematics. Making it readable on a 6-inch screen in a noisy plant was the actual product work, and most of that work was deciding what to leave out. --- uranashel · [Home](https://uranashel.com/) · [Apps](https://uranashel.com/apps.html) · [Lab](https://uranashel.com/lab.html) · [About](https://uranashel.com/about.html) · [Blog](https://uranashel.com/blog/) · [Developers](https://uranashel.com/developers/) · [API docs](https://uranashel.com/docs/) · [Privacy](https://uranashel.com/privacy.html) Machine-readable: [llms.txt](https://uranashel.com/llms.txt) · [sitemap.xml](https://uranashel.com/sitemap.xml) · [openapi.json](https://uranashel.com/openapi.json) · [API](https://uranashel.com/api/v1/)