feat(tempo-lens): measure the tempo the room FEELS, not the one setcps declares
#12 (order the set into a BPM arc) has been blocked on three tempo questions, and
they were framed as things PLN had to decide. Two of them are measurable.
FIRST, ONE DISSOLVED ON INSPECTION. you_my_sunshine was recorded as "144 measured vs
166 written", a 22 BPM gap big enough to INVERT the planned rising finish
Mafia(160)->Sunshine(166). The file has two setcps lines and the 144 one is COMMENTED
OUT. The active tempo is 166, the backlog was right all along, and the "mismatch" was
a grep reading a comment. That is the parsers-over-copy lesson again: a parser miss
must never masquerade as a data conflict. (My own first re-extraction then mangled
every number by using `tr -d '/60'`, which deletes the digits 6 and 0 — 160 became 1.
Two parsing bugs in one sitting, on four-line shell one-liners.)
SECOND, THE REAL QUESTION NEEDS AUDIO. gimme_acid declares 80 while the backlog calls
it 160, and `setcps` cannot settle it: setcps(80/60/4) declares four beats per cycle
at 80, but whether that is HEARD as 80 or 160 depends on what the patterns put inside
the cycle. Half-time notation is standard in dnb. This decides whether the track sits
beside the 160 BPM peak or is the second-slowest thing in the set.
So: spectral-flux novelty curve -> autocorrelation -> the fastest pulse that explains
the signal, reported together with its half/double family, because tempo from audio
is only ever determined up to a factor of two and pretending otherwise is the error.
BUILT THE KATANA FIRST, and it needed four passes — every one caught on synthetic
click tracks of KNOWN tempo before the tool informed any decision:
1. Inter-onset-interval MODE. Failed on the first real track: gimme_acid's d1 is a
continuous 303 line whose flux peaks every ~80 ms of internal movement, so the
modal gap gave "750 events/min". Counting gaps between events cannot find a beat
when the events are not beats. -> autocorrelation, which asks about PERIODICITY
and is unbothered by extra onsets inside each period.
2. Octave errors: a clean 124 click read 61.9, a clean 160 read 80.0 — exactly half,
correlation 0.85+, confidently wrong. Cause: at 100 Hz frames the true lag 48.39
must round to 48, which misaligns every later beat, while exactly 2x landed on a
whole frame. -> 200 Hz frames + 15 ms Gaussian smoothing, so a fractional period
still matches itself.
3. A uniform +1.8% bias (~2 BPM at techno tempo — enough to swap a 124 and a 127 in
a set order). Suspected the unnormalised overlap in np.correlate, fixed that too,
and the bias did not budge. The actual cause: the shortest-lag-within-12% rule,
written to choose between octaves, was also sliding down the LEFT FLANK of the
correct peak — comparing points on one hill as if they were different hills.
-> candidates restricted to local maxima, plus parabolic interpolation.
Result: 13/13 synthetic cases within 0.03%, including jittered, noisy, offbeat-8ths,
and the half-time case (80 BPM with 16th kicks correctly reads 160 felt).
A tempo number that is 2% wrong looks perfectly reasonable, which is exactly why this
had to be calibrated against known truth instead of eyeballed against real audio.
Showing
tools/tempo-lens.py
0 → 100644
This diff is collapsed.
Click to expand it.
Please
register
or
sign in
to comment