Skip to content
Mateusz Sawka All writing

Signal analysis / verification

the machine says 115.
the track is 172.

I built a tempo detector to check a running playlist. It read a drum and bass track as 115 bpm and was more confident about that than about the right answer. Here is why, and what it took to actually know the number.

Track: Mefjus & InsideInfo, Repentance / 30 s preview 55 to 400 bpm / vertical scale square root
Candidate 114.7bpm
Correlation says 0.93
Spectrum says 0.00
Verdict reading
Jump to drag on the plot
Track Show

01 / What the machine hears

it never hears music. it hears a list of moments when something started.

Before anything can guess a tempo it has to reduce the audio to onsets: for every slice of time, how much new energy appeared since the last slice. A drum hit is a spike. A note dying away is nothing. What comes out is a strip of impulses, and every tempo estimate in existence is a guess about how regularly those impulses repeat.

Below is that strip for the track itself, scrolling as it plays, with the metronome drawn on the same timeline. Start it and switch between the two candidates. This panel was meant to be the easy demonstration, the one where the wrong tempo audibly slides off the drums. It is not, and the readout underneath is the evidence. Carry that through the next two sections, because it turns out to be the same wall seen from a different side.

Play it against the candidate Mefjus & InsideInfo, Repentance
Clicking at 114.7bpm
Click to nearest hit, best phase ·ms
Verdict press play
Click at

Both candidates land on real hits: about 4 ms at 172 and about 9 at 114.7, against roughly 39 ms for a tempo unrelated to this track. Each is placed at the best phase it can achieve, so neither is handicapped. What you hear at 114.7 is a three-against-two cross-rhythm sitting on every third hit, which is why your ear calls it wrong while this measurement cannot.

This exact recording on Spotify and Deezer, matched by ISRC rather than by name. Searching the title alone returns a different edition of Billie Jean than the one measured here, which is section 04 happening on this page.

02 / The ladder

the standard method asks the wrong question

Autocorrelation slides the strip of onsets against a delayed copy of itself and asks how well they line up. Slide by exactly one beat and every hit lands on a hit. Big score. That is the whole idea, and it works on most music.

It fails here because drum and bass pulses on a dense, even grid. Slide by one grid unit and the hits line up. Slide by two, three, five, nine. Every rung of the ladder scores about the same, and only some of those rungs are the beat.

Set the plot to 114.7 and read the blue number: 0.93. Now set it to 172, the real tempo: 0.88. The wrong answer scores higher than the right one, and as far as correlation is concerned nothing is wrong.

One honest qualification, because it is the first thing a careful reader will check. The detector was searching inside the range a runner's feet actually operate in, roughly 110 to 220, since picking a running tempo was the entire point. Inside that window correlation prefers 114.7. Let it look at the whole range instead and its tallest peak on this track is 86, which halves neatly into 172 and would have been right. So the failure is not that autocorrelation is blind, it is that autocorrelation stops being able to tell you anything the moment you constrain it to the answer you need. That is a worse property, not a better one, because the constraint is not optional: the number I want is a running cadence.

a method that cannot fail is not the same as a method that is right.

And the error matters in a specific way. Confusing 172 with 86 is harmless for a runner, because a track at half tempo asks the same thing of your feet. Confusing 172 with 114.7 is not harmless. That is a three-against-two error, it does not fold away, and a playlist built on it puts you a full stride out.

03 / The spectrum

ask a different question and the mirage disappears

Instead of sliding the signal against itself, measure how much energy it carries at each frequency. The strip of onsets is not a bare grid: it repeats, accents and all, every two beats, once per kick and snare pair. A signal that repeats 86 times a minute has energy at 86 and at whole multiples of 86, and close to nothing anywhere else.

That is a prediction you can check against the plot rather than a story. 86, 172, 258 and 344 are all multiples of 86 and all read substantial: 0.218, 0.117, 0.146, 1.000, with the grid unit at 344 carrying the most because that is where the hits are. 114.7 and 229.3 are not multiples of 86, and they read 0.002 and 0.015. The mirage lives in the gap between two harmonics, which is exactly where correlation cannot see and the spectrum can.

Put the plot on 114.7 again and read the orange number: 0.006. At 172 it reads 0.117. Correlation separated those two candidates by six percent, and in the wrong direction. The spectrum separates them by a factor of twenty, in the right one.

Neither curve is right on its own. The spectrum loves 344, the grid unit, which is not a tempo anyone would name. Correlation loves anything that repeats. Press Product of both and the surviving peaks are the ones both methods agree are real, at which point most of what is left is which octave to call it, and octaves are the harmless kind for a runner.

Most, not all. The fold that makes octaves harmless does nothing to a three-against-two relation, which is the whole reason the mirage was worth a page. And the failure log further down has a row where the gap is five to four, which is neither an octave nor a third and which this estimator has no way to climb back from. Calling the remaining ambiguity octaves would be tidier than it is true.

Switch the track to Billie Jean and watch the failure not happen. Its strongest correlation peak sits at 58.5, half the true tempo, which folds back to 117 and costs nothing. There is no three-against-two mirage to fall into. That is why a detector can pass every test you throw at it on pop and rock and still be quietly, systematically wrong on the genre you actually run to.

04 / The harder bug

the number was never the main problem

Fixing the estimator took an afternoon. The bug underneath it was worse, and it had been sitting there the whole time.

The playlist was assembled by searching a music service for each track by artist and title. Search returns a recording, and the recording it returns is whichever edition the engine liked: a live cut, a remaster, a radio edit. Tempo was then looked up for the track I asked for, while the track that actually landed in the playlist was a different one. Every number in that pipeline was checked. None of them described the audio that would come out of the headphones.

The fix is an identifier that names a recording rather than a song. ISRC does that, it survives re-releases, and once every track is pinned to one the tempo readings finally describe the thing you are going to hear. Of 134 tracks, 128 pinned cleanly.

Worth saying plainly: this class of bug produces no error. Nothing crashes, nothing looks wrong, and the output is a tidy table of verified numbers about the wrong objects.

05 / Three sources

each one was individually caught being wrong

One scope note before the good part, because the sentence I wrote first was too flattering. What follows describes the audit, the thing that goes back over a playlist and checks it. The builder that assembles a playlist in the first place still picks tracks on a single database number, and wiring it to the audit is work I have not done. So this is a story about catching my own mistakes, not about a tool that cannot make them.

The audit trusts nothing. Three readings for every recording: a music database's stored figure, an open archive's analysis by a different library on a different copy of the audio, and my own measurement of the preview. Fold them all into one octave, then take the majority.

That is what I built, and it is not quite what saved me. What actually holds up is narrower and worth stating exactly: pin identity by ISRC before measuring anything, cross-check a measurement against a figure produced by a different method, and count a majority only when its members can fail independently of each other. The third clause is the one I had to learn, and the next section is how.

That is not defensive paranoia. Over the run, every one of the three turned out to be the odd one out on some track:

Failure log / one per source 134 tracks audited
SourceTrackIt saidAgreed cadenceHow it failed
My measurement Battles, The Yabba 205.0 164.0 Off by five to four, which is not a metrical level this estimator can reach and so not a failure it can recover from on its own. Outvoted two to one.
Open archive Morphine, I Know You (Pt. II) 114.8 172.3 The same three-against-two mirage, from a different algorithm. Not an isolated slip: it repeats on six drum and bass tracks. See the next section.
Music database Leftfield, Melt 170.14 varies Reports one figure for the studio cut, the live cut and a remix. It stores tempo per song, not per recording.
Music database 2,222 library tracks no value measurable Coverage was 28 percent. The other 72 percent were invisible to the builder entirely.

05b / Where the vote fails

a majority cannot see a shared blind spot

Here is the part that took the longest to admit. The archive's analysis is not independent of mine in the way the argument needs. It reads the audio with a different library, written by different people, and it makes the same three-against-two error on the same kind of track. On the drum and bass in this library it lands on 114.7 exactly where my own estimator used to.

So for a while the pipeline shipped with a rule that was supposed to correct my estimator against a second opinion, and it applied that correction without checking which of the two readings was the suspect. On the track this page is named after, my measurement was right at 172.2, the archive was on the mirage, and the rule pulled my correct number down onto the wrong one. The two of them then outvoted the database, which had been right the whole time. The audit output that came out of that run looked healthier than the one from the version with the bug fixed. Six tracks, all of them drum and bass, silently reclassified.

A vote counts agreements. It cannot tell an agreement that comes from two instruments seeing the same thing apart from one that comes from two instruments failing the same way. Nothing in the tally distinguishes them, and the tally is what everybody looks at.

What actually caught it was neither of those three sources. It was a runner's watch: the archive's figure for one track implied 110 steps per minute, and his own feet had done 165 during it. The correction is one line and lives in the repo. The lesson does not fit in one line.

Most of the 134 tracks close on two readings that agree within 3 bpm. A handful rest on one reading and are labelled that way rather than rounded up to a fact, and a handful sit outside the tempo their playlist promises.

I had exact counts here and I have taken them out, which is worth explaining because it is the same lesson again. Those numbers move between runs without the code changing, because one of the three sources is a frozen archive that is sometimes simply down, and a track it does not answer for is a track with one fewer opinion. A count like that is a measurement of somebody else's uptime wearing the clothes of a fact about my library. The audit writes a dated file on every run and that file is the honest artefact; a number lifted out of it and printed here without its date is not.

And the part I would rather not print. Before any of this existed I had a list of six suspects, produced by the broken detector. Three of them were innocent. I had already said so out loud.

06 / Ground truth

the only test that counts is whether your feet agree

All of the above is a machine checking a machine. The actual question is whether a number derived from 30 seconds of compressed audio predicts what a runner does at kilometre nine.

So I aligned one run against the tracks that played during it, using the cadence the watch recorded second by second. Eleven tracks had enough data and eight are in the table below. On seven of those eight the measured tempo and the recorded footfall land within two steps per minute of each other, which is the tightest claim that survives either of my two cadence implementations. The eighth is eight steps out, and it stays in the table rather than being tidied away: it is the one track here that no database had a tempo for at all, so my measurement was the only source and nothing was available to check it against.

One caveat about that table, because it is the same class of problem as everything else here. Two of my own scripts compute the cadence for a track from the same watch stream and disagree by up to half a step per minute, depending on how they align the window to the track boundary. The figures below come from the committed tool rather than the throwaway one, and are given to the nearest whole step, because the tenth of a step I originally printed was precision I do not have.

There is a caveat, and it is a caveat about the caveat. I originally wrote that cadence follows track tempo with a slope near 0.65, so a faster track buys less than its full tempo. Fit a line to the rows above and you get a slope whose confidence interval comfortably contains 1.0, which means these eight tracks cannot tell "a faster track buys less" apart from "cadence follows tempo one for one" at all. The tempos here span about six steps and the residuals reach eight, so there is nothing to fit. The honest version is that I do not know the slope, and one run of one person is never going to.

One run / 2026-08-24 / measured tempo against recorded cadence whole steps per minute

07 / The point

this was never about running music

Every system I get called into has a number like this in it somewhere. A figure that arrives from an API, gets cached, gets averaged, gets put on a chart, and gets decided on. Nobody checked it, not because they are careless but because it never announced itself as a thing that could be wrong. It had no error state. It just had a value.

The three habits that caught this one are not clever and they are not specific to audio. Pin identity before you measure anything, because a correct measurement of the wrong object is the failure mode that leaves no trace. Get a second reading from a method that shares no assumptions with the first, and then go and check whether it really shares none, because I believed that about mine and was wrong. Two tools that fail the same way will agree, and their agreement will look exactly like confirmation. And when you cannot get a second reading, say so in the output rather than rounding it up to a fact.

the moat is not access to ai. everyone has that. it is knowing which outputs to trust and which to verify.

An AI will happily write you the autocorrelation version. It is the textbook answer, it is fifty lines, and it passes every test you would think to write. It will not tell you that it is silently wrong on an entire genre. That part is still the job.

I build AI systems and automations for companies. Most of the work is not the model. It is deciding which outputs can be trusted, which have to be checked, and then actually checking them, so that what gets shipped is a number somebody stands behind.

If something in your stack is producing numbers nobody has ever verified, that is usually where I start.

komai.pl