Designs by Duhart All writing

·3 min read·bpm · musictech · machinelearning · audioprocessing · spotify

Finding the BPM of Any Song (and the Half-Time Trap Every Detector Falls Into)

Beat tracking, beat-interval refinement and the octave error, run on 3,141 real tracks: 64.6% have a runner-up tempo at half or double the winner.

Cover slide: Finding the BPM of Any Song (and the Half-Time Trap Every Detector Falls Into). Bottom left, an "Interviewing at" badge with the logos of Spotify, Apple Music, YouTube Music, SoundCloud.

Spotify stopped serving tempo. I compute it myself.

When Spotify closed its audio features endpoint to new apps, tempo went with it. I run my own streaming platform, Billboard, and its recommendation engine needs tempo for every track, so the signal engine computes it from raw audio. This post is part 8 of 14 in my feature engineering series, and it is about the one stage every music app thinks is easy: finding the BPM.

Here is the result that made me write it. My engine's record for Another One Bites the Dust says 110.0 BPM. The same record says the strongest peak in the song's tempogram is 55.0 BPM, exactly half. Neither number is a bug.

BPM detection in 40 seconds

Every number on screen comes from my engine's stored records for 3,141 tracks.
Opening frame of the video: Finding the BPM of Any Song (and the Half-Time Trap Every Detector Falls Into)
Every number on screen comes from my engine's stored records for 3,141 tracks. Watch the video: https://designsbyduhart.org/blog/finding-the-bpm-half-time-trap/

Step 1: a beat grid on the onset envelope

The engine analyses a 60 second excerpt from the middle of each track at 22,050 Hz. librosa computes an onset envelope, how much the spectrum jumps from one frame to the next, and its dynamic-programming beat tracker places beats on it.

The tracker's tempo is a tempogram bin, about 2% wide around 120 BPM. The beats it places are finer than that, so I refine: take the beat intervals, keep the steady ones (within 20% of the median), and convert their mean to BPM. If the refined value agrees with the bin within 8%, it wins. A trimmed mean, not a median, because beat frames are quantised to the hop (23 ms) and the intervals alternate around the true value.

Beat-interval refinement, from bpm_analysis.py in my signal engine

python
tempo, beats = librosa.beat.beat_track(onset_envelope=onset_env, sr=sr, hop_length=hop,
                                       start_bpm=120.0, tightness=100, trim=False, units="frames")
bpm = float(np.atleast_1d(tempo)[0])
beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=hop)
if len(beat_times) >= 8:
    ivl_all = np.diff(beat_times)
    med = float(np.median(ivl_all))
    steady = ivl_all[np.abs(ivl_all / med - 1.0) < 0.2]
    if len(steady) >= 4:
        refined = 60.0 / float(steady.mean())
        if abs(refined / bpm - 1.0) <= 0.08:
            bpm = refined

Step 2: the half-time trap

The octave error, 120 against 60 against 240, is the mistake every tempo estimator makes. My engine keeps a runner-up: the next tempogram peak that is not within 4% of the winner. On real songs:

  • Another One Bites the Dust (Queen): stored 110.0 BPM, runner-up 55.0, strongest tempogram peak 55.0
  • Get Lucky: stored 116.4 BPM, runner-up 76.0, strongest tempogram peak 234.9
  • Lose Yourself (Eminem): stored 171.4 BPM, runner-up 86.1, strongest tempogram peak 172.3
  • We Will Rock You: stored 81.5 BPM, runner-up 161.5, strongest tempogram peak 80.7

Across the 3,134 tracks that have a runner-up tempo, 64.6% put it at half or double the winner, within 4%. The ambiguity is not an edge case. It is the normal state of a song.

Four songs, three tempo readings

Slide 4 of 9 of "Finding the BPM of Any Song (and the Half-Time Trap Every Detector Falls Into)", near-black background, white Inter type with mono labels. Real songs, real disagreements. BEAT = what the engine stores. 2ND = runner-up tempo. PEAK = strongest tempogram peak. Red = an octave away. Chart from the video at its final state.
BEAT is the stored BPM, 2ND the runner-up, PEAK the strongest tempogram peak. Red marks a reading an octave away from the stored BPM.

Step 3: two confidences, because there are two questions

confidence is the share of local tempo estimates within 4% of the stored BPM. octave_agreement is the same share counting half and double as agreement. A consumer who only cares about "fast or slow" can read the second; a DJ tool that needs the exact grid reads the first. Median confidence on my catalogue: 0.82. The two differ by more than 0.2 on 2.7% of tracks.

beat_strength is the onset strength at the chosen beats relative to the loudest onset: a four-on-the-floor kick reads near 1, a rubato ballad reads low. Median 0.36.

Every track in my catalogue, by BPM

Tracks per 5 BPM bin, 3,141 tracks. Hover for counts. Open full screen
Slide 6 of 9 of "Finding the BPM of Any Song (and the Half-Time Trap Every Detector Falls Into)", near-black background, white Inter type with mono labels. The catalogue: median 121.4 BPM. Middle half of tracks: 108 to 134 BPM. Classes: 1,228 moderate, 1,254 upbeat, 411 fast, 235 slow. Chart from the video at its final state.
Tracks per 5 BPM bin, 3,141 tracks. Hover for counts. Interactive version: https://designsbyduhart.org/blog/finding-the-bpm-half-time-trap/

The honest part: agreement, not accuracy

Not one track in my catalogue came with a BPM. The engine is the first writer of that column, so its evaluation compares with the catalogue where it can (duration) and says plainly that it cannot score BPM. What it can test is a known answer: a synthetic 120 BPM click track must read 120 within 3%, with beat regularity above 0.9 and tempo class "upbeat". That check runs on every test run.

The catalogue median is 121.4 BPM, the middle half sits between 108 and 134. The next step is a small hand-labelled set to count real octave errors. Until then the octave ambiguity is stored next to the BPM, where the recommender can see it.

Part 8 of 14, next: Spotify's Danceability Score Doesn't Match What People Feel. Here's What Rhythm Really Measures


Part 8 of 14, next: Spotify's Danceability Score Doesn't Match What People Feel. Here's What Rhythm Really Measures

More: LinkedIn · Instagram. Portfolio and case studies: designsbyduhart.org.

If any of this saved you an afternoon, Buy me a coffee.