sonoglyph
← the manual

chapter 05

Peak detection

Finding the frequencies that matter in a noisy spectrum, and refining them below one bin with parabolic interpolation.

A spectrum is a picture. It is a very good picture: 1,025 magnitudes, one per bin, the whole frequency axis laid out like a skyline. But a recognizer cannot act on a skyline. It needs a claim: there is a tone at 697.1 Hz, and another at 1209.3 Hz, and nothing else that matters. Turning the picture into that short list of claims is peak detection, the pipeline’s first act of interpretation and the first stage that throws information away on purpose.

The first half of the job is almost embarrassingly plain. Walk the bins (skipping DC and Nyquist, which are edges, not peaks) and collect every bin that beats both of its neighbors — a local maximum. Then refuse to be impressed: a bump only counts if it clears a floor, and the floor in detectPeaks is the greater of an absolute minimum and a fraction of the loudest thing in the frame. That relative threshold is what keeps a quiet room from reading as a thousand tiny discoveries; the noise is full of local maxima, and every one of them beats its neighbors by a hair.

The second half is the part with a trick in it. An FFT bin at this pipeline’s defaults is 23.4 Hz wide, and a real tone almost never lands on a bin center. Its energy piles up in the nearest bin, leaks into the neighbors, and the honest answer “the peak is in bin 30” means only “somewhere between 691 and 714 Hz.” That is not good enough. The DTMF recognizer in chapter 07 checks measured frequencies against a ±2% tolerance — about ±14 Hz at 697 Hz — so a bin-center answer could burn most of the budget before the phone line adds a single hertz of trouble.

The trick: near its top, a windowed spectral peak plotted in log magnitude is very nearly a parabola. So take the peak bin and its two neighbors, three points in all, fit the parabola through them, and read off where its vertex sits. The vertex offset, p = ½(a−c)/(a−2b+c) in the code, lands somewhere in the half-bin either side of the winner, and that fraction of a bin is the correction. Three log-magnitudes and one division recover the true frequency to a small fraction of a bin — parabolic interpolation, the whole reason a 23 Hz-resolution FFT can check a ±14 Hz tolerance and pass.

FIG. 1SUB-BIN SIGHTfft: 2048 pt · hann · 23.4 Hz/bin · detector: @sonoglyph/dsp

(1) spectrum · 0–1.6 kHz · detected peaks marked

700.5 Hz
0.06

(2) zoom · five bins around the low peak

2829303132bin 30 · 703.1 Hzfit → 700.2 Hz
binbin centerinterpolatederror vs true
30703.1 Hz700.2 Hz-0.26 Hz
521218.8 Hz1208.8 Hz

(1) the spectrum of one off-bin tone near 700 Hz plus a fixed 1209 Hz partner, with the real detector’s peaks marked — hover for exact values · (2) the five bins around the low peak: bars are what the FFT reports, the curve is the parabola fitted through the top three in log magnitude. Drag the true frequency and watch the interpolated readout track it between bin centers; raise the noise until false peaks appear.

The figure runs the real detector on a DTMF-shaped pair: one tone you can drag between 690 and 710 Hz (deliberately straddling bin centers), plus a fixed partner at 1209 Hz, plus seeded noise. Watch the table as you drag: the bin column barely moves (23 Hz steps), while the interpolated column tracks the slider in half-hertz steps. The zoom shows the machinery: bars are what the FFT reported, the curve is the fitted parabola, and the two vertical lines are the before and after — bin center versus vertex.

Then raise the noise and watch the other failure mode arrive. Noise doesn’t bend the parabola much; interpolation degrades gracefully. But it does grow bumps that clear the floor, and each one becomes a “peak” with a confident-looking interpolated frequency attached. The detector cannot know which claims are real; it can only rank them by magnitude and pass them on. Deciding which peaks mean something is the recognizer’s job, and it brings its own defenses: tolerance bands, twist checks, and the 40 ms persistence rule.

The thresholds are honest knobs, not magic. Set the relative threshold too low and noise becomes signal; too high and a quiet second tone vanishes — a real DTMF failure, since the network attenuates the two groups differently. The defaults here (1% of the loudest bin, at most 16 peaks) are tuned for the pipeline’s reference plugins, and every recognizer is free to re-detect from the raw spectrum with its own settings.

Where this lands in the pipeline: the detector runs once per analysis frame and its output becomes the peaks feature stream (frequency, magnitude, and bin for each survivor, strongest first), which is all the FFT-strategy DTMF recognizer ever reads. Chapter 06 is about that reduction in general; chapter 09 shows the rival philosophy that skips peak-picking entirely and just asks eight pointed questions.