06.BPM / key / LUFS batch analysis
Three native Rust analyzers (onset-autocorrelation BPM, Krumhansl-Kessler key, BS.1770-style LUFS), a batch queue you can pause/resume, per-file caches, and a direct pipeline into smart playlists.
Where the analysis lives
Analysis is a post-scan enrichment pass that fills the bpm, key, and lufs columns on the audio library table. Frontend triggers are in frontend/js/audio.js; backend algorithms are in src-tauri/src/bpm.rs, src-tauri/src/key_detect.rs, and src-tauri/src/lufs.rs. The database columns are updated through db_update_bpm, db_update_key, db_update_lufs, and the one-shot db_update_analysis.
Kick off a batch
- Palette action —
Cmd+K→ Start BPM/Key/LUFS Analysis. - Shortcut —
Cmd+Shift+Vto start,Cmd+Shift+Cto stop. - Auto on startup — Settings → Auto Analyze Startup. When enabled, the app runs the analysis pass on all unanalyzed rows shortly after boot.
- Per-file — context menu on any Samples row, or the analysis button inside the expanded metadata panel.
The queue targets every row where bpm, key, or lufs is NULL. It's a stream — you can stop mid-batch and resume from where you left off without losing progress.
The IPC surface
estimate_bpm({ filePath })→f32.detect_audio_key({ filePath })→"C Major","F# Minor", etc.measure_lufs({ filePath })→f64(dBFS-like LUFS).batch_analyze({ paths })— runs all three in one round trip per file.db_get_analysis,db_unanalyzed_paths,db_backfill_audio_meta— backfill helpers.
BPM — onset-strength autocorrelation
src-tauri/src/bpm.rs. Supported formats: WAV, AIFF, MP3, FLAC, OGG, M4A, AAC, OPUS. The pipeline:
- Decode PCM via Symphonia, mixing to mono (first channel).
- Cap the signal at roughly 30 seconds at 44.1 kHz so the whole pipeline stays bounded in memory.
- Compute an energy envelope / onset-strength function.
- Autocorrelation over the 50–220 BPM range to find the dominant period.
- Convert the period back to beats-per-minute and return
Some(bpm)orNone.
No external deps, no model download, no Python. Works offline.
Key — 12-bin chromagram + Krumhansl-Kessler
src-tauri/src/key_detect.rs. The classic musical-key-detection pipeline:
- Decode first ~30 s of PCM.
- Frame with a Hann window.
- For each frame, run Goertzel against 84 target frequencies — C1 (32.7 Hz) through B7 (3951 Hz), 7 octaves × 12 pitch classes.
- Sum into a 12-bin chromagram (C, C#, D, …, B).
- Normalize by the maximum bin.
- Match against 24 key profiles (12 major + 12 minor Krumhansl-Kessler) using Pearson correlation.
- Return the highest-correlated key name, e.g. "D Minor".
Accuracy is good on tonal music; noisy percussive loops will sometimes come back with a close but not perfect guess.
LUFS — simplified BS.1770
src-tauri/src/lufs.rs. A simplified ITU-R BS.1770 implementation — integrated loudness without the K-weighting filter, computed across the first 60 seconds of the file:
- Read PCM, mix to mono.
- Compute mean square of samples (
Σx² / N). - LUFS =
-0.691 + 10 × log10(mean_sq). - Clamp silence to
-70.0 LUFS. - Round to one decimal place.
This gives you a comparable loudness number across your library — good enough for sorting loud vs quiet samples and building smart playlists like "LUFS > -14 AND BPM BETWEEN 120 AND 128". It is not a broadcast-certified EBU R128 measurement.
Caching & stopping
Results are written to SQLite immediately as they come in (db_update_bpm etc.), so stopping mid-batch never loses progress. The in-memory caches _bpmCache[path], _keyCache[path], and _lufsCache[path] are also updated so the UI reflects new values instantly.
To wipe analysis entirely and re-run from scratch, use Settings → Clear analysis cache (Cmd+Shift+9), then trigger the batch again.
Consuming the analysis
- Sort the Samples table by BPM, key, or LUFS (click column headers).
- Build Smart playlists with
bpm_range,key, or LUFS-based rules. - View the BPM distribution and Key distribution cards in the Heatmap dashboard.
- Compare analysis snapshots across scan runs in the History tab.
ui-idle.js will lower the analysis priority when you're interacting with the app, so you won't feel a frame drop.