Home Pricing Docs Compare SDK Blog Changelog Status Get API Key →
Music API · Migration · Field guide

Migrating from Spotify Audio Features: a Field-by-Field Threshold Guide

May 2026 · 9 min read · FreqBlog

You swapped the host from api.spotify.com to api.freqblog.com. Your GET /v1/audio-features/{id} handler still parses the response unchanged, the unit tests pass. Then your downstream if features.danceability > 0.7 recommender starts surfacing weird tracks. Your acousticness > 0.5 "find the acoustic versions" rule fires on basically everything. Your liveness > 0.8 "skip the live recordings" filter rejects the whole catalog.

Welcome. The migration is byte-compatible at the shape level. It's directional at the value level. Spotify built their audio features by training ML classifiers on a large in-house labelled corpus; we compute ours from signal analysis (librosa for the descriptors, Essentia for BPM and key). Same names, different beasts.

This is the field-by-field guide to re-tuning your thresholds, with real distribution data from our pre-analysed catalog. We'll be honest about which fields work well, which ones are shifted but usable, and which ones currently shouldn't be relied on at all.

If you haven't done the migration yet: start with Spotify Audio Features Is Dead. Here's What to Use Instead in 2026 for the landscape and the host-swap code. This post is the next step — making sure your numbers behave once the calls are flowing.

How the numbers are made (and why they differ)

Spotify's path was model inference. Each field was the output of a classifier trained on a labelled corpus — what "danceable" or "acoustic" meant was set by their labelling team, and the model returned a calibrated probability-like number on [0, 1]. The classifiers themselves were never public, but their training-data behaviour shaped the distributions you got used to.

FreqBlog's path is signal analysis. Each descriptor is a deterministic function of the audio's spectral and temporal statistics, applied to a 30-second window of the preview:

BPM and key are different: those come from Essentia's RhythmExtractor2013 (multifeature method, with a confidence score) and KeyExtractor. Essentia is a mature C++ DSP library; in our experience these two are broadly comparable to Spotify's models in accuracy — sometimes better, as we'll see.

The upshot: if a Spotify field was "ML on labels," our number is "what one specific signal statistic happens to look like." Both can be useful in production — they're just not the same statistic, and the distributions don't line up.

TL;DR threshold map

Catalog medians are over ~8,000 sampled tracks from our pre-analysed catalog as of May 2026.

FieldSpotify (typical)FreqBlog catalog medianWhat to do
bpm / tempo60–200118.7Drop-in. Use bpm_alt for half-time fixes (see below).
key (0–11 / −1)0–11 or −1same scaleDrop-in — returned as key_int in our response.
mode (0/1)0 or 1sameDrop-in.
energy~0.5–0.7 for pop0.73Distribution is shifted up; nudge thresholds ~0.05–0.1 higher.
valencecentred ~0.50.48Closest to Spotify's distribution. Minimal re-tune.
danceabilitycentred ~0.5–0.60.74Shifted up. Old 0.65 ≈ ours 0.80.
acousticness0.05–0.3 pop; > 0.7 acoustic0.98Do not use as "is acoustic." Discriminates near 0.99+.
instrumentalnessbimodal (0 or 1)0.23, narrowDon't use as a vocal-detection classifier; treat as a continuous shape signal.
liveness0.05–0.15 studio; > 0.7 live1.00 (saturated)Not currently diagnostic. Avoid.
speechiness0.03–0.07 music; > 0.66 spoken0.45Shifted up ~5–10×. Old 0.66 ≈ ours > 0.85.
loudness (dB)−7 to −10 mastered pop−143–6 dB lower. 30s window vs full-track integrated, dBFS vs BS.1770. Use relatively, not absolutely.
time_signaturemostly 44 in practiceNot currently diagnostic.

Field by field

bpm / tempo — drop-in, plus a documented improvement

Catalog median 118.7 BPM, P95 162. Comparable scale and accuracy to Spotify. The one wrinkle both APIs share is the half-time / double-time detection error: Essentia (and Spotify's old model) sometimes locks onto half the beat grid for upbeat pop — "Blinding Lights" is the canonical example, where Essentia returns ~85 BPM but the perceived BPM is ~171. We expose an extra field bpm_alt on the rich /lookup response that surfaces the corrected value when our heuristic suggests the detection locked on the wrong level. See Half-Time vs Double-Time BPM Detection: How We Fixed Spotify's Known Accuracy Gap for the algorithm. Net: tempo behaves the same; bpm_alt is bonus, opt-in.

key (string), key_int, mode — drop-in

Essentia's KeyExtractor is in the same accuracy league as the DJ-software incumbents (see Why DJ Platforms Disagree on Key 60% of the Time). The Spotify-shaped /v1/audio-features/{id} route returns key as the integer 0–11 (−1 if undetected) and mode as 0/1 — identical semantics. The rich /lookup response additionally returns the human-readable key ("C#-Minor") plus camelot + open_key notation for harmonic mixing. No re-tune needed.

energy — comparable, slightly higher in our distribution

We compute energy as normalised mean RMS over the analysis window, clipped to [0, 1]. Catalog median 0.73, mean 0.69. Spotify's typical pop sat around 0.5–0.7. If you had thresholds like energy > 0.6 = "high energy", expect more matches under our distribution; nudge it to ~0.7.

valence — closest to Spotify's distribution

Valence is the field where our signal-analysis approximation tracks Spotify's behaviour most closely. Our model: 0.65 if major key else 0.35, weighted with tempo, spectral brightness, and RMS. Catalog median 0.48 (Spotify centred ~0.5). The mode dependence is the biggest single driver in both schemes — major-key tracks score higher, minor lower. Minimal re-tune needed.

danceability — shifted up

Our recipe: a Gaussian curve over BPM peaking at 120, beat-confidence, and inter-beat regularity. Catalog median 0.74 vs Spotify's ~0.55. The reason: beat regularity is very high for nearly all commercial music (compressed mastering produces clean ticks), so the regularity term saturates and pushes the score up. Old danceability > 0.65 filter ≈ ours > 0.80. The relative ordering within a genre is still informative; the absolute scale is shifted.

acousticness — rescaled on 27 July 2026; your Spotify thresholds now transfer

Updated 27 July 2026 — this section previously told you to raise your threshold to ~0.99. Do not do that any more. What this post originally described as a quirk was a bug: the constant we scaled spectral flatness against was far too large (real music sits near 0.003, not the 0.15 we divided by), so 98% of the catalogue came back at roughly 0.98. That is fixed. If you re-tuned your thresholds upward on the old advice, revert them.

Our model is still spectral flatness — low flatness means strong tonal/harmonic peaks — but the result is now mapped onto the same distribution as Spotify's own acousticness, calibrated against a 114,000-track reference set. Catalogue percentiles moved from p25 0.9365 / p50 0.9718 / p75 0.9900 to p25 0.017 / p50 0.169 / p75 0.598, which is Spotify's shape to three decimal places. Mean absolute error against Spotify fell from 0.74 to 0.27. A Spotify threshold like acousticness > 0.7 now selects a comparable slice of your library instead of ~99% of it.

The honest caveat is unchanged in kind, only in degree: this is a spectral measure, not a trained classifier. Rescaling fixes the scale, not the underlying agreement — rank correlation with Spotify is about 0.34, so a highly tonal synth pad can still read as fairly acoustic. Use it to sort and filter across a library, and for confident acoustic-vs-electric tagging on an individual track still prefer instrumentalness + energy + genre together. (The R² figure quoted later in this post, 0.07, was measured on the old saturated scale.)

instrumentalness — signal proxy, not a vocal classifier

Spotify's instrumentalness was a vocal-presence classifier with a notably bimodal output: vocal music near 0, instrumental near 1. Ours is 1 − clip(mean_std(MFCC[1:5]) / 25, 0, 1), which measures whether the formant-band MFCCs are stable (instrumental) or varying (vocal). Catalog median 0.23, P75 0.35. It's a soft, continuous signal — not a confident "is this vocal" yes/no. Don't use instrumentalness > 0.5 as a vocal filter; it will mostly return false. Use it as a relative ordering signal within a genre instead.

liveness — currently saturated

Honest call-out: our liveness formula is clip(noise_floor_ratio × 6.0, 0, 1) where noise_floor_ratio is the 5th-percentile RMS divided by the 95th-percentile RMS over the window. The intuition is that live recordings have an elevated noise floor (audience ambience). The problem: modern mastered music has heavy dynamic-range compression, which compresses the RMS percentiles together — and the ×6 multiplier saturates at 1.0 almost immediately. Catalog median 1.0; the field is not currently useful for distinguishing live recordings.

If your Spotify code used liveness > 0.8 to filter out live recordings, that filter will reject your entire catalog under FreqBlog. Until we ship a better live-detector, drop the filter or use track-name pattern matching for now ("(Live)", "Live at", etc.) — we already do that to keep live recordings out of the catalog wherever the source labels them.

speechiness — shifted up roughly an order of magnitude

Spotify's speechiness sat around 0.03–0.07 for music and crossed 0.66 for spoken word / talk podcasts. Ours is clip(zero_crossing_rate / 0.12, 0, 1). Typical music ZCR is 0.03–0.07, which lands us at 0.25–0.6 — an order of magnitude higher than Spotify's. Catalog median 0.45.

To reproduce Spotify's "is this spoken word" rule, you probably want our threshold around > 0.85 rather than > 0.66. The relative ordering is preserved (rap and spoken intros do score higher than melodic singing), the absolute scale isn't.

loudness — numerically lower, relatively comparable, or use ?calibrate=spotify

Catalog median −14 dB; Spotify's mastered-pop typical range was −7 to −10 dB. Two reasons for the offset: (1) we measure dBFS amplitude on a 30-second analysis window, not the BS.1770 integrated full-track loudness Spotify reported; (2) the window is shorter so loud passages are diluted. The relative ordering ("this track is louder than that one") is preserved; the absolute numbers shift down by 3–6 dB. If you were using loudness > −8 as a "well-mastered modern release" filter, try > −12 under FreqBlog.

Or use the calibrated mode. GET /v1/audio-features/{id}?calibrate=spotify applies a linear regression fitted on a 5,724-track overlap between our catalog and the public maharshipandya/spotify-tracks-dataset:

loudness_spotify_calibrated  =  0.557 × loudness_freqblog  +  0.960

Regression metrics on the 5,724-track sample: R² = 0.43, RMSE = 3.0 dB (down from 8.5 dB raw — 64% improvement). Worked example: Adele "Hello" returns raw loudness = −10.96; calibrated returns −5.14 (cached Spotify value for the same recording: −6.13). The calibrated number won't match Spotify's exactly — same physical quantity measured by different methods on different audio windows — but it's much closer to the scale your existing code is tuned for. Default (omit ?calibrate=) returns raw FreqBlog values so existing integrations stay byte-identical.

Other audio-feature fields don't ship calibration in v1: their heuristic-vs-Spotify-ML methodology gap is too wide to fit a regression honestly. Per-field R² on the same 5,724-track sample: energy 0.28, danceability 0.18, valence 0.005, acousticness 0.07, instrumentalness 0.003, liveness 0.004, speechiness 0.014. See the next section for why — the short version is: when R² is near zero, a linear regression doesn't reveal a calibration, it just averages noise into a number that looks like Spotify's but doesn't correlate with what Spotify would have said.

Why most fields don't ship calibration (the methodology gap)

The honest summary: when R² is near zero, a linear regression doesn't reveal a calibration — it just averages noise into a number that looks like Spotify's but doesn't actually correlate with what Spotify would have said.

Two different things are being measured

Our pipeline computes audio features as deterministic formulas over spectral measurements of a 30-second preview. Here's valence end-to-end (the actual production code, simplified):

valence = (0.65 if major_key else 0.35) * 0.40
        + clip((bpm - 60) / 120, 0, 1)            * 0.20
        + clip(spectral_centroid / (sr*0.3), 0, 1) * 0.20
        + clip(rms_mean / 0.3, 0, 1)              * 0.20

That's the whole function. Pure DSP. Same input → same output, forever. The same shape (weighted sum of bounded spectral statistics) holds for danceability, acousticness, instrumentalness, liveness, speechiness — different inputs, same family of functions.

Spotify's pipeline trained ML classifiers on labelled data: human-tagged tracks, user playlist co-occurrence ("what tracks do people put on 'happy' playlists vs 'sad' playlists"), genre tags, listening behaviour, the lot. Hidden architecture, trained on perception rather than signal properties, updated over years with continuous training.

These are different things being measured, not different measurements of the same thing.

Concrete example: why valence's R² is 0.005

Johnny Cash's cover of "Hurt":

Spotify "knew" Hurt was sad because it sits next to other sad songs in everyone's "introspective" playlists. We can't see that from a 30-second spectrogram. There's no signal in our inputs that distinguishes "sad cover in a major key" from "happy song in a major key."

Run that mismatch across 5,724 tracks and you get R² = 0.005 — essentially no relationship.

What a "calibration" would actually do at R² = 0.005

A linear fit y = 0.197 · x + 0.406 says "for every 1.0 increase in our valence, Spotify's valence increases by 0.197 on average." With R² 0.005, the "on average" is doing almost all the work — the average track's calibrated value lands near Spotify's mean (~0.49), regardless of what our value was.

Shipping that as ?calibrate=spotify would mean:

That's the dishonest part. The raw 0.40 vs 0.78 ordering at least carries some information — major key + tempo + brightness rank-order Hurt and Happy correctly for those acoustic properties. The calibrated 0.485 vs 0.560 carries less: compressed toward the mean by the low slope, the ordering becomes less reliable because the noise dominates the signal.

The decision rule

The R² ≥ 0.30 floor for shipping a calibration is roughly: the regression has to explain more variance than it adds noise. Below that, raw values + the per-field threshold guidance above gives you more information than a regression-laundered number.

Loudness clears the floor (R² 0.43) because it's measuring the same physical quantity as Spotify — just on a different reference (dBFS vs BS.1770) over a different window (30s vs full track). The calibration recovers ~64% of that systematic offset honestly.

For the other fields, getting better numbers means changing the methodology, not calibrating the existing one. Concretely: a TensorFlow vocal classifier for instrumentalness, a learned valence model trained on labelled emotion data, a crowd-noise/applause detector for liveness. Those would be new code paths — not constants applied on top of the heuristic. If your migration depends critically on one of those fields, write — concrete demand is what drives what we work on next.

time_signature — currently always 4

Honest call-out: we ship Essentia's Meter estimator with a fallback to 4 when fewer than four beats are detected in the 30s window. In practice the fallback fires often enough that ~100% of the catalog returns 4. We could expose detected 3/4 and 6/8 if the upstream estimator agreed more often, but right now the field isn't a reliable diagnostic. Don't filter on time_signature; treat it as informational.

What we get right (and where we improve on Spotify)

The honest call-outs

To save you re-reading the warning boxes:

These are honest limitations of the current signal-analysis approach. If your migration depends critically on any of them, write to [email protected] with the use case — concrete demand is what drives what we work on next. (For example: if enough customers care about a real liveness classifier, we'd plug in a TensorFlow-based vocal/ambience model. Without demand we keep the surface area honest.)

A practical migration checklist

  1. Swap the host: api.spotify.com/v1/audio-features/{id}api.freqblog.com/v1/audio-features/{id} (auth becomes X-Api-Key). For reliable coverage, key your lookups by ISRC or by name (/lookup?track=…&artist=…) rather than a raw Spotify ID — a raw Spotify ID only resolves for the minority of tracks we've already mapped to one. The name path searches the full catalog and queues an on-demand fetch + analysis on a miss, so tracks we don't have yet still get ingested and returned shortly.
  2. Run your existing call against a few known tracks, log the response side-by-side with your stored Spotify values from before deprecation if you have them.
  3. Walk the table above: bump danceability / energy thresholds slightly up, push acousticness and speechiness thresholds way up, drop liveness and time_signature from any filters.
  4. Replace any instrumentalness > 0.5 "is instrumental" rule with a combination signal (e.g. low energy + high acousticness + lyrics-absent metadata).
  5. For batches: GET /v1/audio-features?ids=id1,id2,… takes up to 100 comma-separated identifiers and returns the Spotify-shaped {"audio_features": […]}. You’re charged one quota request per ID that returns data — IDs we can’t resolve (an unmapped Spotify ID, or an ISRC with no match) come back null for free, exactly like the single-ID route’s 404. So a batch of raw Spotify IDs only bills for the ones we actually have.

Try the drop-in — free tier, no card

Get an API key in seconds; the first 1,000 lookups per month are free.

Get API Key →

Further reading