Constant-Q Spectrogram

log-frequency STFT + live harmonic-stack overlay
Play
Contrast
FFT
Pick a sound above (or enable the microphone).
The vertical axis is musical: each octave is spaced equally (x2 in Hz).
Watch the amber / teal guide lines snap onto the real partials.
Renderer -
Source idle
Fundamental(s) -
Bins / octave (approx CQT) -
FFT resolution -
The math you are looking at

1. STFT: sound -> a stack of spectra over time

The AnalyserNode runs a Short-Time Fourier Transform: it chops the signal into overlapping windows and takes an FFT of each. One FFT gives you the energy at every frequency bin at one instant. Stack those spectra left-to-right in time and you get a spectrogram. Each vertical strip you see is one FFT frame turned into color.

bin spacing: delta_f = fs / N (fs = sample rate, N = FFT size)

With N = 4096 at 48 kHz, delta_f ~= 11.7 Hz. That is the same absolute spacing everywhere -- great up high, but crude down low where whole semitones are only a few Hz apart.

2. Why a LOG frequency axis = musical pitch

Pitch is logarithmic. Going up one octave always multiplies frequency by 2 (A2=110, A3=220, A4=440). So equal musical intervals must map to equal vertical distance only if the axis is logarithmic. This explorer maps screen height by:

freq(y) = f_min * (f_max / f_min) ^ y (y in 0..1, bottom..top)

That is why C1, C2, C3 ... are drawn evenly spaced. A linear-Hz spectrogram crams all the music into the top and wastes the bottom.

3. Constant-Q: equal resolution per octave

A true Constant-Q Transform picks a bank of filters whose quality factor is constant across the whole range:

Q = f / delta_f = f_k / (f_(k+1) - f_k) = 1 / (2^(1/b) - 1)

where b is the number of bins per octave. Constant Q means the window length scales with frequency -- long windows (fine resolution) for low notes, short windows for high notes:

N_k = Q * fs / f_k (samples in the analysis window for bin k)

Honest disclosure of what THIS tool does: it takes one fixed-size FFT and resamples it onto a log axis (the fragment shader reads the linear FFT texture at log-spaced positions). That gives the correct musical layout and the harmonic combs line up perfectly, but the underlying time/frequency resolution is still the FFT's constant delta_f, not a true CQT's constant Q. So low notes look slightly smeared and highs slightly over-sharp compared with a real CQT. The "bins/octave" readout reports the effective log-axis density, not a genuine filterbank.

4. The harmonic series = integer multiples

A vibrating string or reed produces a fundamental f plus overtones at 2f, 3f, 4f, ... -- exact integer multiples. That is why the sawtooth shows a full evenly-stacked comb (on this log axis the partials get closer together going up, because 2f/f is an octave but 3f/2f is only a fifth), while the pure sine shows a single line. The overlay draws those integer multiples so you can watch them land on the bright bands.

5. Why a fifth (3:2) "locks" and beats

Play 220 Hz and 330 Hz together (ratio 3:2). Their harmonic combs share partials: the 3rd harmonic of the low note (660) equals the 2nd of the high note (660); 1320 is shared too. Shared partials = consonance. When the ratio is slightly off, those shared partials sit a few Hz apart and their sum rises and falls at the difference frequency -- that slow throb is beating, and it is how the interval is tuned by ear.

Self-contained, offline, no external code. Learn-from lineage: FFT-frame-as-texture WebGL spectrograms and Constant-Q visual guides -- reimplemented from scratch here.