I needed to read a track's BPM and musical key in seconds, offline, on a
phone. No ML black box, no server. Here's the signal-processing pipeline I ended
up with — hand-rolled, since the usual packages weren't an option.
#
Tempo
Tempo is periodicity in the track's energy. The steps:
Spectral flux → onsets. Sum the positive frame-to-frame magnitude increases of the STFT. Peaks are onsets (kick, snare, synth attacks). That's the onset envelope.
Autocorrelation. The lag where the onset envelope best matches itself is the beat period. #
Comb over multiples. Weight candidate periods by their multiples (½, 2×) so tempo isn't confused with its harmonics. #
Octave folding + prior. Fold candidates into 60–180 BPM and softly prefer typical dance tempos — kills the "174 vs 87" error. #
Median smoothing for stability.
For the beat grid, I use dynamic programming (Ellis-style): reward beats that land on onsets, penalize deviation from a steady interval. The DP gives a grid
that doesn't drift.
#
Key
Chromagram. Fold the spectrum (~55–2000 Hz) into 12 pitch classes, accumulated with a leaky integrator (~8 s memory) → an averaged chroma vector. #
Krumhansl–Schmuckler correlation. Correlate against 24 reference key
profiles (12 major + 12 minor); best match wins. #
Map to Camelot for harmonic mixing. This lineage goes back to libKeyFinder.
#
Honest limits
- Atonal/dense harmony blurs the chroma vector → less certain key.
- Mid-track modulation → it reports the dominant key.
- Weak percussion/rubato → fewer onsets, shakier BPM.
#
Sources
- Krumhansl, Cognitive Foundations of Musical Pitch (1990)
- Sha'ath, Estimation of Key in Digital Music Recordings (2011)
- Ellis, Beat Tracking by Dynamic Programming (2007)
The pipeline ships in BeatScope, a free offline analyzer for macOS/iPhone:
https://beatscope.pro — happy to answer DSP questions in the comments.