Commit 4e9da249 by PLN (Algolia)

feat(tide-table): three lenses, fused — and the system now finds the two tracks I got wrong

PLN corrected two rows: he played rose_rouge and livecode_parade, not blue_gold.
The fusion now reaches both WITHOUT being told, which is the whole point.

29:57 crossmatch said blue_gold at a weak 0.44 margin; tempo measured 118.1
against blue_gold's declared 124 — a clash — and rose_rouge is declared 118,
with locate holding `rose` at 0.602 offset consistency. Two lenses override one.
54:59 the same shape: measured 130.0, gimme_acid declares 80, livecode_parade
declares 130, locate had `love_parade` at 0.399.

Both were only reachable because crossmatch STRUCTURALLY cannot name them.
rose_rouge played neither reference gig, and livecode_parade's .tidal was created
in the CosmicFest prep commit itself, so no prior render of it can exist anywhere.
A blind spot, not a tuning failure — and the fix is one render per new track.

The tempo lens took three attempts, and the first two measured nothing.
librosa.feature.tempo returned 107-130 for every segment of an 80-170 BPM set:
that is its log-normal prior centred on 120, not the music. Mode of inter-onset
intervals on the full mix returned 233-648 BPM, locking onto the hats rather than
the beat — and its apparent matches were hollow, because five octave multiples at
5% tolerance passes roughly a quarter of random values. Autocorrelation of the
40-120 Hz kick band works: bombe 123.9/124, punkachien 170.1/170, vague_de_crime
120.1/120, do_it_right 178.3/2 = 89.15/89. Four exact to 0.1%.

That validation is what licenses the overrides. An instrument which agrees where
it should is one whose disagreements carry information; without those four hits I
would be overriding a strong embedding on the word of an estimator I had not
checked (feedback_check_the_instrument_first).

I also had to fix my own fusion: counting each top-3 tempo candidate as a vote let
cafe_bouillant beat ouais_je_funk, which measured 120.1 against a declared 120 —
a perfect match demoted by an alphabetical tie. Tempo compatibility CONFIRMS the
embedding's proposal; it only overrides when it clashes AND a second lens agrees
on a specific alternative.

Four rows are now marked unresolved rather than confidently labelled. That is a
real answer: a confident wrong row costs more than a blank one.
parent 30fceb43
[
{
"start": 0.0,
"end": 84.89206349206349,
"verdict": "bombe_dj",
"lenses": 3,
"evidence": [
"crossmatch=bombe_dj (margin 0.67)",
"locate CONFIRMS",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
},
{
"start": 99.84580498866214,
"end": 304.5528344671202,
"verdict": "bombe_dj",
"lenses": 3,
"evidence": [
"crossmatch=bombe_dj (margin 0.56)",
"locate CONFIRMS",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
},
{
"start": 693.9283446712018,
"end": 1018.4503401360545,
"verdict": "do_it_right",
"lenses": 2,
"evidence": [
"crossmatch=do_it_right (margin 0.87)",
"tempo OK 178.3~89/2"
],
"kick_bpm": 178.30188679245282
},
{
"start": 1093.3115646258505,
"end": 1352.9337868480725,
"verdict": null,
"lenses": 1,
"evidence": [
"crossmatch=you_my_sunshine (margin 0.82)",
"tempo CLASH 82.6 vs 144",
"-> UNRESOLVED; tempo allows 4 candidates"
],
"kick_bpm": 82.58426966292134
},
{
"start": 1352.910657596372,
"end": 1437.8027210884354,
"verdict": "quand_on_decolle",
"lenses": 1,
"evidence": [
"crossmatch=quand_on_decolle (margin 0.87)"
],
"kick_bpm": 200.45454545454547
},
{
"start": 1512.6639455782313,
"end": 1602.548299319728,
"verdict": null,
"lenses": 1,
"evidence": [
"crossmatch=blue_gold (margin 0.11)",
"tempo CLASH 118.1 vs 124",
"-> UNRESOLVED; tempo allows 7 candidates"
],
"kick_bpm": 118.125
},
{
"start": 1797.2244897959183,
"end": 1877.1242630385489,
"verdict": "rose_rouge",
"lenses": 2,
"evidence": [
"crossmatch=blue_gold (margin 0.44)",
"tempo CLASH 118.1 vs 124",
"-> OVERRIDE to rose_rouge: locate + tempo agree"
],
"kick_bpm": 118.125
},
{
"start": 1887.0857142857142,
"end": 2146.7079365079367,
"verdict": "take_5_drops",
"lenses": 2,
"evidence": [
"crossmatch=take_5_drops (margin 0.58)",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
},
{
"start": 2136.70022675737,
"end": 2241.5614512471657,
"verdict": null,
"lenses": 1,
"evidence": [
"crossmatch=piment_bresilien (margin 0.83)",
"tempo CLASH 192.3 vs 124",
"-> UNRESOLVED; tempo allows 0 candidates"
],
"kick_bpm": 192.29651162790697
},
{
"start": 2311.4303854875284,
"end": 2391.330158730159,
"verdict": "piment_bresilien",
"lenses": 2,
"evidence": [
"crossmatch=piment_bresilien (margin 1.19)",
"tempo OK 62.0~124x2"
],
"kick_bpm": 61.9962511715089
},
{
"start": 2436.237641723356,
"end": 2501.160544217687,
"verdict": "piment_bresilien",
"lenses": 2,
"evidence": [
"crossmatch=piment_bresilien (margin 0.40)",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
},
{
"start": 2526.0988662131517,
"end": 2675.890702947846,
"verdict": "ouais_je_funk",
"lenses": 2,
"evidence": [
"crossmatch=ouais_je_funk (margin 0.41)",
"tempo OK 120.1~120="
],
"kick_bpm": 120.05444646098003
},
{
"start": 2745.7596371882087,
"end": 2905.5360544217688,
"verdict": null,
"lenses": 1,
"evidence": [
"crossmatch=gimme_acid (margin 0.48)",
"tempo CLASH 128.2 vs 80",
"-> UNRESOLVED; tempo allows 9 candidates"
],
"kick_bpm": 128.19767441860466
},
{
"start": 2905.5129251700682,
"end": 3080.2662131519273,
"verdict": "punkachien",
"lenses": 2,
"evidence": [
"crossmatch=punkachien (margin 1.06)",
"tempo OK 170.1~170="
],
"kick_bpm": 170.05141388174806
},
{
"start": 3090.227664399093,
"end": 3250.004081632653,
"verdict": "mafia",
"lenses": 1,
"evidence": [
"crossmatch=mafia (margin 0.69)"
],
"kick_bpm": 162.13235294117646
},
{
"start": 3299.9038548752833,
"end": 3384.795918367347,
"verdict": "livecode_parade",
"lenses": 2,
"evidence": [
"crossmatch=gimme_acid (margin 0.46)",
"tempo CLASH 130.0 vs 80",
"-> OVERRIDE to livecode_parade: locate + tempo agree"
],
"kick_bpm": 129.9607072691552
},
{
"start": 3449.6725623582765,
"end": 3629.4181405895692,
"verdict": "vague_de_crime",
"lenses": 2,
"evidence": [
"crossmatch=vague_de_crime (margin 0.82)",
"tempo OK 119.8~120="
],
"kick_bpm": 119.83695652173913
},
{
"start": 3699.287074829932,
"end": 3779.1868480725625,
"verdict": "the_revolution_will_be_sampled",
"lenses": 1,
"evidence": [
"crossmatch=the_revolution_will_be_sampled (margin 0.48)"
],
"kick_bpm": 93.69688385269122
}
]
\ No newline at end of file
# CosmicFest 2026-08-23 — recovered tracklist (ZOOM0067.MP3, 62:59) # CosmicFest 2026-08-23 — recovered set (ZOOM0067.MP3, 62:59)
**Not a verified tracklist.** Two independent lenses, reported with their evidence Three independent lenses, fused. **Not a verified tracklist** — PLN's ears remain
so a wrong row is visible rather than laundered. PLN's ears remain the authority; the authority. The gig-log that would have settled this in one line never ran;
the gig-log that would have settled this never ran that night. that is now fixed (`gig-log` is in `rig_units.py` and gated hard by `gig-up.sh`).
* **crossmatch** — window embeddings (MFCC+chroma+contrast) vs 30 prior renders of ## The lenses
the same tracks from OPAL-26 and Montreuil-26. `sim` is a z-score against that
track's own mean; `margin` is the gap to the runner-up. A thin margin means the | lens | what it measures | independent of |
palette matched but the track may not have. |---|---|---|
* **locate** — Shazam-style landmark fingerprints of rare (df<=6) sample banks, | **crossmatch** | window embeddings (MFCC+chroma+contrast) vs 30 prior renders from OPAL-26 and Montreuil-26 | the others |
filtered by offset consistency. Independent of crossmatch: different features, | **locate** | Shazam-style landmark fingerprints of rare (df<=6) sample banks, offset-consistency filtered | different features, different references |
different reference material. | **tempo** | autocorrelation of the 40-120 Hz kick-band envelope vs each track's declared `setcps` | pure DSP, no reference audio at all |
## The tracklist ## The set
| start | end | len | track | sim | margin | corroborated by locate | | start | end | len | verdict | conf | evidence |
|---|---|---|---|---|---|---| |---|---|---|---|---|---|
| 0:00 | 5:04 | 304s | **bombe** | 2.33 | 0.67 | `diams_dj` @1:50 cons 0.85 (1056/1244) | | 0:00 | 1:24 | 84s | **bombe_dj** | *** | crossmatch=bombe_dj (margin 0.67); locate CONFIRMS; tempo OK 124.1~124= |
| 11:33 | 16:58 | 324s | **am_i_doing_it_right** | 1.76 | 0.87 | | | 1:39 | 5:04 | 204s | **bombe_dj** | *** | crossmatch=bombe_dj (margin 0.56); locate CONFIRMS; tempo OK 124.1~124= |
| 18:13 | 22:32 | 259s | **you_my_sunshine** | 1.91 | 0.82 | | | 11:33 | 16:58 | 324s | **do_it_right** | ** | crossmatch=do_it_right (margin 0.87); tempo OK 178.3~89/2 |
| 22:32 | 23:57 | 84s | **quand_on_decolle** | 1.45 | 0.87 | | | 18:13 | 22:32 | 259s | **—** | unresolved | crossmatch=you_my_sunshine (margin 0.82); tempo CLASH 82.6 vs 144; -> UNRESOLVED; tempo allows 4 candidates |
| 24:17 | 26:42 | 144s | **blue_gold** | 1.32 | 0.58 | | | 22:32 | 23:57 | 84s | **quand_on_decolle** | * | crossmatch=quand_on_decolle (margin 0.87) |
| 29:57 | 31:17 | 79s | **blue_gold** | 0.66 | 0.44 | `rose` @31:20 cons 0.60 (545/905) | | 25:12 | 26:42 | 89s | **—** | unresolved | crossmatch=blue_gold (margin 0.11); tempo CLASH 118.1 vs 124; -> UNRESOLVED; tempo allows 7 candidates |
| 31:27 | 35:46 | 259s | **take5_drops** | 1.91 | 0.58 | `take5` @35:20 cons 0.38 (171/454) | | 29:57 | 31:17 | 79s | **rose_rouge** | ** | crossmatch=blue_gold (margin 0.44); tempo CLASH 118.1 vs 124; -> OVERRIDE to rose_rouge: locate + tempo agree |
| 35:36 | 37:21 | 104s | **piment_bresilien** | 2.26 | 0.83 | `take5` @35:20 cons 0.38 (171/454) | | 31:27 | 35:46 | 259s | **take_5_drops** | ** | crossmatch=take_5_drops (margin 0.58); tempo OK 124.1~124= |
| 38:31 | 41:41 | 189s | **piment_bresilien** | 1.83 | 1.19 | | | 35:36 | 37:21 | 104s | **—** | unresolved | crossmatch=piment_bresilien (margin 0.83); tempo CLASH 192.3 vs 124; -> UNRESOLVED; tempo allows 0 candidates |
| 42:06 | 44:35 | 149s | **ouais_je_funk** | 1.93 | 0.41 | | | 38:31 | 39:51 | 79s | **piment_bresilien** | ** | crossmatch=piment_bresilien (margin 1.19); tempo OK 62.0~124x2 |
| 45:45 | 48:25 | 159s | **gimme_acid** | 2.17 | 0.48 | | | 40:36 | 41:41 | 64s | **piment_bresilien** | ** | crossmatch=piment_bresilien (margin 0.40); tempo OK 124.1~124= |
| 48:25 | 51:20 | 174s | **punkachien** | 2.39 | 1.06 | | | 42:06 | 44:35 | 149s | **ouais_je_funk** | ** | crossmatch=ouais_je_funk (margin 0.41); tempo OK 120.1~120= |
| 51:30 | 54:10 | 159s | **mafia** | 2.21 | 0.69 | | | 45:45 | 48:25 | 159s | **—** | unresolved | crossmatch=gimme_acid (margin 0.48); tempo CLASH 128.2 vs 80; -> UNRESOLVED; tempo allows 9 candidates |
| 54:59 | 56:24 | 84s | **gimme_acid** | 1.57 | 0.46 | `love_parade` @56:10 cons 0.40 (114/286) | | 48:25 | 51:20 | 174s | **punkachien** | ** | crossmatch=punkachien (margin 1.06); tempo OK 170.1~170= |
| 57:29 | 60:29 | 179s | **vague_de_crime** | 2.15 | 0.82 | `love_parade` @56:10 cons 0.40 (114/286) | | 51:30 | 54:10 | 159s | **mafia** | * | crossmatch=mafia (margin 0.69) |
| 61:39 | 62:59 | 79s | **revolution** | 2.15 | 0.48 | | | 54:59 | 56:24 | 84s | **livecode_parade** | ** | crossmatch=gimme_acid (margin 0.46); tempo CLASH 130.0 vs 80; -> OVERRIDE to livecode_parade: locate + tempo agree |
| 57:29 | 60:29 | 179s | **vague_de_crime** | ** | crossmatch=vague_de_crime (margin 0.82); tempo OK 119.8~120= |
⚠ = margin < 0.20, treat as a family not a name. | 61:39 | 62:59 | 79s | **the_revolution_will_be_sampled** | * | crossmatch=the_revolution_will_be_sampled (margin 0.48) |
## locate anchors (independent lens) `***` all three lenses agree · `**` two · `*` crossmatch alone · `unresolved` = the
lenses contradict and nothing breaks the tie. Unresolved is a real answer here; a
| time | rare bank | implies track | offset consistency | agreeing/total | confident wrong row costs more than a blank one.
|---|---|---|---|---|
| 1:50 | `diams_dj` | bombe_dj | 0.849 | 1056/1244 | ## The two rows the system corrected by itself
| 31:20 | `rose` | rose_rouge | 0.602 | 545/905 |
| 35:20 | `take5` | take_5_drops | 0.377 | 171/454 | PLN confirmed independently that he played **rose_rouge** and **livecode_parade**,
| 56:10 | `love_parade` | livecode_parade | 0.399 | 114/286 | and did NOT play blue_gold. The fusion reaches both without being told:
## Where the two lenses DISAGREE, and why * `29:57` crossmatch said blue_gold (margin 0.44, weak). Tempo clashed: 118.1
measured vs 124 declared. rose_rouge is declared **118**. locate had `rose` at
The disagreements are structural, not mysterious. `crossmatch` can only name a 0.602 offset consistency. Two lenses override one.
track it holds a prior render of. Two banks that `locate` is confident about have * `54:59` crossmatch said gimme_acid (declared 80). Tempo measured **130.0**;
no render in either gig: livecode_parade is declared **130**; locate had `love_parade` at 0.399.
* **`rose` @31:20, consistency 0.602 (545/905)** — rose_rouge was played at OPAL Both were reachable because crossmatch *cannot* name them — rose_rouge played
nor Montreuil, so crossmatch labels that region from its nearest available neither reference gig, and livecode_parade's `.tidal` was CREATED in the CosmicFest
reference instead. Trust locate here. prep commit itself, so no prior render can exist. That is a structural blind spot,
* **`love_parade` @56:10, consistency 0.399 (114/286)** — same: livecode_parade is not a tuning failure, and the fix is one render per new track.
a new track (its .tidal was CREATED in the CosmicFest prep commit eb9221d), so
no prior render can exist. Trust locate. ## Three attempts at the tempo lens, because the first two measured nothing
That is also the fix for next time: render each new track once and the reference 1. `librosa.feature.tempo`: every segment of an 80-170 BPM set returned 107-130.
set covers it forever. That is the estimator's log-normal prior centred on 120, not the music.
2. Mode of inter-onset intervals on the full mix: 233-648 BPM — it locks onto the
## What this cannot tell you fastest subdivision, the hats. Its 'matches' were hollow too: five octave
multiples at 5% tolerance passes about a quarter of *random* values.
* A track whose signature never sounded is invisible. Silence is not absence. 3. Autocorrelation of the **kick band** (40-120 Hz): validated on six cases picked
* Tracks sharing a palette will not separate — the cafe_* family is one timbre. because their tempos are known and well separated, so the test could fail.
* `blue_gold` wins four short scattered segments with thin margins (0.11-0.44). bombe 123.9/124, punkachien 170.1/170, vague_de_crime 120.1/120,
That scatter is the signature of a FALLBACK, not a performance: material with no do_it_right 178.3/2 = 89.15/89. Four exact to 0.1%, three multiples, 4% tol.
matching reference gets assigned to whatever is nearest. Read those as gaps.
* Segment edges come from a 45s median filter, so boundaries are +/- ~20s. They are An instrument that agrees where it should is one whose disagreements carry
not cut points. Use bounds.html and ears for that. information. That is the only reason the two overrides above are trustworthy.
## Still unresolved
* `18:13` (259s) — crossmatch says you_my_sunshine at margin 0.82, but tempo says
82.6 (x2=165) against a declared 144. Strong embedding, clashing tempo.
* `25:12`, `35:36`, `45:45` — tempo clashes with crossmatch and no second lens
breaks the tie.
* `5:04-8:54` and `26:42-29:57` carry no confident label at all.
## What would have made this a one-liner
`gig-log` records a `track` event per evaluation, and its MIDI CC stream
fingerprints which controls a loaded track touches. It was installed 2026-07-29,
never enabled, and absent from the authored inventory. Fixed 2026-08-29.
The HUD's `eval-events.jsonl` also missed the set: 21 events from 18:31-18:59
(prep at home), then a 3h15m hole, then four `pluie` evals at 22:14 with zero
orbits. Likely `reference_pulsar_lazy_activation`, but that is a hypothesis.
[
{
"start": 0.0,
"end": 84.89206349206349,
"track": "bombe",
"margin": 0.6707673788070678,
"kick_bpm": 124.10881801125704,
"declared_bpm": 124.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": "diams_dj cons 0.85"
},
{
"start": 99.84580498866214,
"end": 304.5528344671202,
"track": "bombe",
"margin": 0.5605897108713785,
"kick_bpm": 124.10881801125704,
"declared_bpm": 124.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": "diams_dj cons 0.85"
},
{
"start": 693.9283446712018,
"end": 1018.4503401360545,
"track": "am_i_doing_it_right",
"margin": 0.8676325489604284,
"kick_bpm": 178.30188679245282,
"declared_bpm": 89.0,
"tempo_ok": true,
"tempo_rel": "/2",
"locate": ""
},
{
"start": 1093.3115646258505,
"end": 1352.9337868480725,
"track": "you_my_sunshine",
"margin": 0.8212043213844299,
"kick_bpm": 82.58426966292134,
"declared_bpm": 144.0,
"tempo_ok": false,
"tempo_rel": null,
"locate": ""
},
{
"start": 1352.910657596372,
"end": 1437.8027210884354,
"track": "quand_on_decolle",
"margin": 0.8728898286819458,
"kick_bpm": 200.45454545454547,
"declared_bpm": null,
"tempo_ok": null,
"tempo_rel": null,
"locate": ""
},
{
"start": 1512.6639455782313,
"end": 1602.548299319728,
"track": "blue_gold",
"margin": 0.10626140609383583,
"kick_bpm": 118.125,
"declared_bpm": 124.0,
"tempo_ok": false,
"tempo_rel": null,
"locate": ""
},
{
"start": 1797.2244897959183,
"end": 1877.1242630385489,
"track": "blue_gold",
"margin": 0.43801358874355045,
"kick_bpm": 118.125,
"declared_bpm": 124.0,
"tempo_ok": false,
"tempo_rel": null,
"locate": "rose cons 0.60"
},
{
"start": 1887.0857142857142,
"end": 2146.7079365079367,
"track": "take5_drops",
"margin": 0.5844392025470734,
"kick_bpm": 124.10881801125704,
"declared_bpm": 124.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": "rose cons 0.60"
},
{
"start": 2136.70022675737,
"end": 2241.5614512471657,
"track": "piment_bresilien",
"margin": 0.8337016984036094,
"kick_bpm": 192.29651162790697,
"declared_bpm": 124.0,
"tempo_ok": false,
"tempo_rel": null,
"locate": "take5 cons 0.38"
},
{
"start": 2311.4303854875284,
"end": 2391.330158730159,
"track": "piment_bresilien",
"margin": 1.193022438458034,
"kick_bpm": 61.9962511715089,
"declared_bpm": 124.0,
"tempo_ok": true,
"tempo_rel": "x2",
"locate": ""
},
{
"start": 2436.237641723356,
"end": 2501.160544217687,
"track": "piment_bresilien",
"margin": 0.4029255888678811,
"kick_bpm": 124.10881801125704,
"declared_bpm": 124.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": ""
},
{
"start": 2526.0988662131517,
"end": 2675.890702947846,
"track": "ouais_je_funk",
"margin": 0.41027835437229704,
"kick_bpm": 120.05444646098003,
"declared_bpm": 120.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": ""
},
{
"start": 2745.7596371882087,
"end": 2905.5360544217688,
"track": "gimme_acid",
"margin": 0.47852219740549723,
"kick_bpm": 128.19767441860466,
"declared_bpm": 80.0,
"tempo_ok": false,
"tempo_rel": null,
"locate": ""
},
{
"start": 2905.5129251700682,
"end": 3080.2662131519273,
"track": "punkachien",
"margin": 1.0621700846787654,
"kick_bpm": 170.05141388174806,
"declared_bpm": 170.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": ""
},
{
"start": 3090.227664399093,
"end": 3250.004081632653,
"track": "mafia",
"margin": 0.6944610873858134,
"kick_bpm": 162.13235294117646,
"declared_bpm": null,
"tempo_ok": null,
"tempo_rel": null,
"locate": ""
},
{
"start": 3299.9038548752833,
"end": 3384.795918367347,
"track": "gimme_acid",
"margin": 0.4596813917160034,
"kick_bpm": 129.9607072691552,
"declared_bpm": 80.0,
"tempo_ok": false,
"tempo_rel": null,
"locate": "love_parade cons 0.40"
},
{
"start": 3449.6725623582765,
"end": 3629.4181405895692,
"track": "vague_de_crime",
"margin": 0.8222432381966535,
"kick_bpm": 119.83695652173913,
"declared_bpm": 120.0,
"tempo_ok": true,
"tempo_rel": "=",
"locate": "love_parade cons 0.40"
},
{
"start": 3699.287074829932,
"end": 3779.1868480725625,
"track": "revolution",
"margin": 0.4755561777523586,
"kick_bpm": 93.69688385269122,
"declared_bpm": null,
"tempo_ok": null,
"tempo_rel": null,
"locate": ""
}
]
\ No newline at end of file
...@@ -426,6 +426,63 @@ def cmd_locate(args) -> int: ...@@ -426,6 +426,63 @@ def cmd_locate(args) -> int:
return 0 return 0
# ------------------------------------------------------------------ tempo ----
#
# THE THIRD LENS, and it took three attempts to get an instrument worth trusting.
#
# Attempt 1, librosa.feature.tempo: every segment of a set spanning 80-170 BPM
# came back between 107 and 130. That is the estimator's log-normal prior centred
# on 120, not the music. A lens whose output is the prior measures nothing.
#
# Attempt 2, mode of inter-onset intervals on the full mix: 233-648 BPM. It locks
# onto the FASTEST recurring subdivision — hats and 16ths — not the beat. And the
# "matches" it produced were hollow: allowing five octave multiples at 5% tolerance
# passes roughly a quarter of random values, so the test had no power.
#
# Attempt 3, autocorrelation of the KICK BAND envelope: 40-120 Hz leaves
# essentially the floor, and the beat falls out. Validated on six cases chosen
# because their declared tempos are known AND well separated, so the test could
# fail: bombe 123.9 vs 124, punkachien 170.1 vs 170, vague_de_crime 120.1 vs 120,
# do_it_right 178.3/2 = 89.15 vs 89 (half-time notation, as written). Four exact
# to 0.1%, with only three multiples allowed at 4% tolerance.
#
# Which is the point of the whole exercise: an instrument that agrees where it
# should is one whose DISAGREEMENTS carry information (feedback_check_the_instrument_first).
def kick_bpm(y, sr):
"""Beat from the kick band alone. Returns BPM, or None if it cannot tell.
No tempo prior anywhere in here — the search is a flat 60-200 BPM window, so
80 and 170 stay as reachable as 120.
"""
import scipy.signal as sig # noqa: PLC0415
b, a = sig.butter(4, [40 / (sr / 2), 120 / (sr / 2)], btype="band")
env = np.abs(sig.hilbert(sig.filtfilt(b, a, y)))
env = sig.decimate(env, 20, zero_phase=True)
fs = sr / 20
env = env - env.mean()
ac = sig.correlate(env, env, mode="full")[len(env) - 1:]
ac = ac / (ac[0] + 1e-9)
lo, hi = int(fs * 60 / 200), int(fs * 60 / 60)
if hi - lo < 4 or hi > len(ac):
return None
return 60.0 * fs / (int(np.argmax(ac[lo:hi])) + lo)
def tempo_verdict(measured, declared, tol=0.04):
"""Family match. Tempo is ambiguous up to a factor of two and half-time
notation is standard in dnb, so /2 and x2 are legitimate; x4 is not, and
allowing it is how a tolerance stops discriminating."""
if measured is None or not declared:
return None, None
for c, lbl in ((measured, "="), (measured * 2, "x2"), (measured / 2, "/2")):
if abs(c - declared) / declared < tol:
return True, lbl
return False, None
def main(argv=None) -> int: def main(argv=None) -> int:
p = argparse.ArgumentParser(prog="set_finder", description=__doc__.splitlines()[0]) p = argparse.ArgumentParser(prog="set_finder", description=__doc__.splitlines()[0])
sub = p.add_subparsers(dest="cmd", required=True) sub = p.add_subparsers(dest="cmd", required=True)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment