-
feat(tempo-lens): measure the tempo the room FEELS, not the one setcps declares · 156444d6
#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.PLN (Algolia) authored156444d6
×