Skip to content

Commit 31835b5

Browse files
committed
Measure the tracker on Ballroom, and close Q4
HANDOFF §8's last Phase 4 item. 698 clips, 6.1 hours, scored with mir_eval at its 70 ms tolerance: beat F-measure 0.8941, downbeat 0.8575, in 95 seconds. Ballroom is a set BeatNet+ was trained on — upstream's own README says `| Ballroom | 699 | Train |`, and §A.2 agrees — so that number says the port reproduces what the model can do over six hours of real audio, and says nothing about generalisation. It cannot be held against §A.1's 80.62, which is GTZAN and GTZAN alone. evaluate.py now recognises the training sets and prints that warning itself, so the mistake cannot be made quietly. Three findings worth the run on their own. §5.3's default holds, and the margin is in the downbeats: generic 0.894/0.858, af-non-percussive 0.879/0.755, generic-main 0.793/0.581. §5.5's octave fold is not free, and §5.5 does not say so. A fold cannot change a beat F-measure at all — it relabels the tempo and the beats still fire where the filter puts them, and the beat and downbeat scores came out identical to four decimal places with the fold off, at 70-140 and at 88-176. Tempo is the only measure it can appear in, so --out now carries the published tempo as a third column and the harness scores MIREX's two tempo accuracies. There the fold cuts both ways: octave- tolerant accuracy rises 0.936 -> 0.980, exact accuracy falls 0.769 -> 0.691, because Ballroom spans 87 to 204 BPM and no 2:1 window holds all of it. Per genre it is stark: Samba 0.419 -> 0.953 and Waltz 0.509 -> 0.936 inside the window, Quickstep and Viennese Waltz 1.000 -> 0.000 outside it. The fold is a promise about the material, not a safety net, and Phase 5's UI has to show that. And the worst cases are not ours. On the hardest Samba files the tracker calls 7 beats where the annotation has 51; the same activations through upstream's own particle_filtering_cascade give 7, 7 and 8. It is BeatNet+'s behaviour file for file — the filter only emits when the median particle sits in the first four states of its beat, and a cloud can agree on the tempo while spread across the phase. Upstream also divides by a zero weight sum on that material and emits a NaN warning; this port guards that case. Q4, the DSP fallback: no. The neural path costs 86 us per 20 ms hop, scores 0.89 on real audio and gives downbeat and meter, which no classical ODF tracker does. The scaffold Q4 suggested building was never built, so there is nothing to remove.
1 parent 1fab2c0 commit 31835b5

9 files changed

Lines changed: 981 additions & 20 deletions

‎README.md‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ detected meter as the quantum, timed through Link's own regression on the audio
5050
sample counter), a generic OSC namespace on any number of targets, and MIDI beat clock
5151
at 24 PPQN.
5252

53+
Measured on the 698-clip Ballroom set — 6.1 hours, scored with mir_eval at its 70 ms
54+
tolerance — the whole chain gets **0.894 beat and 0.858 downbeat F-measure**. Ballroom is
55+
a set BeatNet+ was *trained* on, so that says the port reproduces what the model can do
56+
and nothing about how the model generalises; it is not comparable to BeatNet+'s published
57+
GTZAN figures. [tests/data/tracking/evaluation/](tests/data/tracking/evaluation/) has the
58+
numbers, the per-genre breakdown, what the octave fold costs as well as buys, and why the
59+
worst cases are BeatNet+'s behaviour rather than this port's.
60+
5361
`takt4 --version` prints what it was built with, and the window opens; the UI does not
5462
show any of this yet.
5563

@@ -140,18 +148,23 @@ python tools/pf_reference.py # regenerate the tra
140148
until this project's restatement of the particle filter has been held against the real
141149
one.
142150

143-
`tools/evaluate.py` is the odd one out: it produces nothing committed. It measures beat
144-
and downbeat F-measure against an annotated dataset by running every file through
145-
`takt4-cli track` and scoring with mir_eval, so the number is the shipped C++ scored by
146-
the reference implementation the published figures come from. No dataset is vendored —
147-
they are large and their licences are their own — and the layouts of Ballroom, SMC and
148-
GTZAN work as they come:
151+
`tools/evaluate.py` measures beat and downbeat F-measure, and tempo accuracy, against an
152+
annotated dataset — by running every file through `takt4-cli track` and scoring with
153+
mir_eval, so the number is the shipped C++ scored by the reference implementation the
154+
published figures come from. No dataset is vendored — they are large and their licences
155+
are their own — and the layouts of Ballroom, SMC and GTZAN work as they come:
149156

150157
```sh
151158
pip install -r tools/requirements-eval.txt
152159
python tools/evaluate.py path/to/audio --annotations path/to/annotations --report eval.json
153160
```
154161

162+
It warns when the dataset it was pointed at is one BeatNet+ was trained on, because a
163+
score on those cannot be compared with the published ones. The trimmed reports of the
164+
runs behind the numbers above are committed in
165+
[tests/data/tracking/evaluation/](tests/data/tracking/evaluation/), which also says
166+
exactly where to get the Ballroom audio and annotations.
167+
155168
Two steps because madmom builds from source and its `setup.py` imports numpy and
156169
Cython. Full-length source tracks belong outside git; `references/` is ignored for
157170
that purpose.

‎src/cli/main.cpp‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ void printUsage(std::ostream& out) {
113113
<< " --latency MS added to every beat's timestamp; negative fires early\n"
114114
<< " --confidence T hold the last tempo below this, default 0.15\n"
115115
<< " --seed N the particle filter's seed, default 1\n"
116-
<< " --out FILE write every beat as <seconds> TAB <beat in bar>, the\n"
117-
<< " format the beat-tracking datasets annotate in\n"
116+
<< " --out FILE write every beat as <seconds> TAB <beat in bar> TAB\n"
117+
<< " <BPM>; the first two columns are the format the\n"
118+
<< " beat-tracking datasets annotate in\n"
118119
<< " --link join the Ableton Link network as tempo master\n"
119120
<< " --osc H:P send the generic namespace there; repeat for more\n"
120121
<< " --osc-prefix P that namespace's prefix, default /takt4\n"
@@ -864,8 +865,12 @@ class Chain {
864865
}
865866
}
866867

867-
/// Writes every beat as "<seconds>\t<beat in bar>", the format the beat-tracking
868-
/// datasets annotate in, so an estimate and a reference are the same kind of file.
868+
/// Writes every beat as "<seconds>\t<beat in bar>\t<BPM>". The first two columns are
869+
/// the format the beat-tracking datasets annotate in, so an estimate and a reference
870+
/// are the same kind of file and the same reader handles both; the third is what the
871+
/// tempo state machine was publishing at that beat, which is the only thing §5.5's
872+
/// octave fold can move. A fold cannot change a beat time, so it cannot show up in
873+
/// beat F-measure — the tempo column is where its effect is visible at all.
869874
void writeBeatsTo(const std::filesystem::path& path) {
870875
beatsOut_.open(path, std::ios::trunc);
871876
if (!beatsOut_) {
@@ -888,7 +893,7 @@ class Chain {
888893
}
889894
std::cout << beatLine(*event, tempo_.state()) << '\n';
890895
if (beatsOut_.is_open()) {
891-
beatsOut_ << event->time << '\t' << event->beatInBar << '\n';
896+
beatsOut_ << event->time << '\t' << event->beatInBar << '\t' << event->bpm << '\n';
892897
}
893898

894899
if (osc_) {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Offline evaluation
2+
3+
HANDOFF §8's last Phase 4 item: *"Offline evaluation against annotated audio; compare F1
4+
with §A.1's published numbers. Decide the fate of the DSP fallback (Q4)."*
5+
6+
The five files here are `tools/evaluate.py`'s reports over the **Ballroom** set — 698
7+
clips, 6.1 hours — with the per-file scores stripped out; the configuration, the summary
8+
and the per-genre means are what a reader needs. Regenerate the full detail with
9+
`--report`. The audio and annotations are not vendored: see §"Reproducing" below.
10+
11+
Every number is mir_eval's, at its default 70 ms tolerance, with its convention of
12+
trimming the first five seconds off both sides.
13+
14+
## Read this before quoting any of it
15+
16+
**Ballroom is a set BeatNet+ was trained on.** Upstream's own README lists it as
17+
`| Ballroom | 699 | Train |`, and HANDOFF §A.2 says the same. So the scores below say
18+
that this port reproduces what the model can do, over six hours of real audio — which is
19+
the strongest end-to-end check in the project, and the reason for running it — and they
20+
say **nothing** about how the model generalises.
21+
22+
They must not be compared with §A.1's 80.62 beat / 56.51 downbeat. Those are GTZAN,
23+
which §A.2 records as *"test-only and never seen in training"*. A number that could be
24+
held against them needs GTZAN, and getting it is a separate decision (see the handoff).
25+
`tools/evaluate.py` prints this warning itself when it recognises the dataset, so the
26+
mistake cannot be made quietly.
27+
28+
## What the tracker does
29+
30+
`generic` weights, no octave fold — the configuration a published evaluation uses,
31+
because a fold needs a tempo range no evaluation may assume:
32+
33+
| | mean | median |
34+
|---|---|---|
35+
| Beat F-measure | **0.8941** | 0.9223 |
36+
| Downbeat F-measure | **0.8575** | 0.9040 |
37+
| Tempo accuracy 1 | 0.7693 | 1.0000 |
38+
| Tempo accuracy 2 | 0.9355 | 1.0000 |
39+
| Cemgil | 0.8001 | 0.8274 |
40+
| Correct Metric Level Total | 0.7711 | 0.8196 |
41+
| P-score | 0.8615 | 0.9038 |
42+
43+
Per genre, beat F-measure: Tango 0.961 and Viennese Waltz 0.961 at the top, **Samba
44+
0.796** at the bottom, with Rumba-Misc 0.836 next. 698 files in 95 seconds on the
45+
development machine, eight at a time.
46+
47+
### The weight set: §5.3's default holds
48+
49+
§5.3 makes `generic` the default and offers the other two by use case. On this material
50+
that is the right call by a wide margin, and the margin is in the downbeats:
51+
52+
| weights | beat F1 | downbeat F1 |
53+
|---|---|---|
54+
| **generic** | **0.8941** | **0.8575** |
55+
| af-non-percussive | 0.8785 | 0.7546 |
56+
| generic-main | 0.7932 | 0.5805 |
57+
58+
`af-non-percussive` is interesting rather than bad: its *median* beat F1 is 0.954,
59+
higher than `generic`'s 0.922, so it is better on most files and much worse on a few.
60+
`generic-main` — which upstream's README calls the main branch — is the weakest here on
61+
both measures, and its worst genre is Waltz at 0.574.
62+
63+
### The octave fold is not free, and §5.5 does not say so
64+
65+
§5.5: *"User sets an expected BPM range. Estimates outside it are ×2 / ÷2 folded into
66+
range before being considered. **This alone eliminates most real-world failures.**"*
67+
68+
Two things are true and neither is obvious.
69+
70+
**A fold cannot change a beat F-measure at all.** It relabels the tempo; the beats still
71+
fire wherever the particle filter puts them. Beat and downbeat F-measure came out
72+
*identical to four decimal places* with the fold off, at 70–140 and at 88–176. Tempo
73+
accuracy is the only measure a fold can appear in, which is why `--out` carries the
74+
published tempo as a third column and why the harness scores it.
75+
76+
**And in tempo it cuts both ways:**
77+
78+
| fold | Tempo accuracy 1 | Tempo accuracy 2 |
79+
|---|---|---|
80+
| off | **0.7693** | 0.9355 |
81+
| 88–176 | 0.6390 | 0.9628 |
82+
| 70–140 | 0.6905 | **0.9799** |
83+
84+
Accuracy 2 forgives octave errors, and the fold helps it: fewer wild readings. Accuracy 1
85+
does not, and the fold *hurts* it — because Ballroom spans 87 to 204 BPM and no 2:1
86+
window contains all of that. Folding a correct answer out of range makes it wrong.
87+
88+
Per genre, with the 70–140 fold, the mechanism is unmistakable:
89+
90+
| genre | reference BPM | accuracy 1, no fold | accuracy 1, fold 70–140 |
91+
|---|---|---|---|
92+
| Rumba-Misc | 99 | 0.200 | **0.950** |
93+
| Samba | 101 | 0.419 | **0.953** |
94+
| Waltz | 87 | 0.509 | **0.936** |
95+
| Rumba-International | 100 | 0.627 | **1.000** |
96+
| ChaChaCha | 125 | 0.964 | 0.991 |
97+
| Rumba-American (7 files) | 128 | 1.000 | 0.857 |
98+
| Tango | 128 | 1.000 | 1.000 |
99+
| Jive | 171 | 0.967 | **0.100** |
100+
| Viennese Waltz | 178 | 1.000 | **0.000** |
101+
| Quickstep | 204 | 1.000 | **0.000** |
102+
103+
Inside the window it is transformative — the slow genres are where the tracker doubles
104+
the tempo, and folding fixes almost all of it. Outside the window it is catastrophic:
105+
Quickstep and Viennese Waltz go from perfect to nothing.
106+
107+
So the fold is a promise the operator makes about their material, not a safety net. For
108+
one DJ playing one genre it does what §5.5 claims. Across a set that spans more than an
109+
octave it destroys tempo readings that were already right. **Phase 5's UI has to make
110+
the window and its consequence visible** rather than leaving it as a slider with a
111+
plausible default — and it is worth considering whether the tracker should say when its
112+
unfolded estimate is far outside the window, because that is exactly when the fold is
113+
doing something drastic.
114+
115+
### Where it fails, and whether that is ours
116+
117+
Samba is the worst genre, and the worst individual files are Samba: on
118+
`Albums-Latin_Jam3-04` the tracker calls 7 beats where the annotation has 51. That looked
119+
like a port bug, so it was checked against upstream directly — the same activations
120+
through `particle_filtering_cascade`, three seeds:
121+
122+
| file | annotated | takt4 | upstream |
123+
|---|---|---|---|
124+
| Albums-Latin_Jam3-04 | 51 | 7 | 7 / 7 / 8 |
125+
| Albums-Macumba-06 | 50 | 18 | 13 / 18 / 19 |
126+
| Albums-Macumba-08 | 50 | 11 | 10 / 7 / 12 |
127+
| Albums-Cafe_Paradiso-05 | 63 | 38 | 37 / 35 / 35 |
128+
| Media-103804 | 59 | 52 | 52 / 51 / 50 |
129+
130+
It is BeatNet+'s behaviour, file for file. The cause is structural: the filter emits only
131+
when the *median* particle sits in the first four states of its beat, and a cloud that
132+
agrees on the tempo can still be spread across the phase — the tempo agreement on
133+
`Albums-Macumba-06` is 0.91 while nineteen seconds pass without a beat. Nothing in the
134+
port causes it and nothing in §5.4 asks for it to be fixed, but it is the failure an
135+
operator would notice, and it is the first thing to look at if beat *recall* ever becomes
136+
a goal.
137+
138+
Upstream also emits `RuntimeWarning: invalid value encountered in divide` on this
139+
material — `weights / sum(weights)` with a zero sum. takt4 guards that case explicitly
140+
(`particle_filter.cpp`, "every hypothesis is impossible"), so the port is strictly more
141+
robust than the reference here.
142+
143+
### Q4, the DSP fallback
144+
145+
§3's Q4 asks whether to ship a classical tracker as a low-CPU alternative, and §8 puts
146+
the decision here. **No** — and the numbers make it easy. The neural path costs 86 µs of
147+
one core per 20 ms hop, scores 0.89 on real audio, and gives downbeat and meter, which no
148+
classical ODF tracker does. The recommendation in Q4 was to build BTrack as the Phase 1
149+
bring-up scaffold and then decide; the scaffold was never built (§6), so there is nothing
150+
to remove and no second tuning surface to support. If a machine is ever found that cannot
151+
afford 0.43% of a core, that is the time to revisit it.
152+
153+
## Reproducing
154+
155+
Neither the audio nor the annotations are vendored — they are large and their terms are
156+
their own. Both live under the git-excluded `references/`:
157+
158+
```sh
159+
# annotations: 698 .beats files, "<time> TAB <beat in bar>", downbeat = 1
160+
git clone https://github.com/CPJKU/BallroomAnnotations references/ballroom-annotations
161+
162+
# audio: 698 clips of 30 s, 1,453,888,083 bytes. Note https — the http:// URL in the
163+
# annotations README, and the page it points at, both return 403 now.
164+
curl -L -o references/ballroom-data1.tar.gz \
165+
https://mtg.upf.edu/ismir2004/contest/tempoContest/data1.tar.gz
166+
mkdir -p references/ballroom-audio
167+
tar -xzf references/ballroom-data1.tar.gz -C references/ballroom-audio
168+
169+
pip install -r tools/requirements-eval.txt
170+
python tools/evaluate.py references/ballroom-audio \
171+
--annotations references/ballroom-annotations --jobs 8 \
172+
--report references/ballroom.json
173+
```
174+
175+
The annotations ask to be cited as Krebs, Böck & Widmer, ISMIR 2013. The audio is
176+
commercial ballroom-dance CDs distributed for the 2004 tempo contest with no licence
177+
attached and no redistribution right; decoding it locally to compute an F-measure is the
178+
ordinary research use, and `references/` is excluded by both `.gitignore` and
179+
`.git/info/exclude` so it cannot reach the repository.
180+
181+
SMC MIREX was the other candidate and was not used: 217 deliberately-hard excerpts, but
182+
there is no public download — the MIREX pages use it as a private evaluation set and
183+
every trail ends at "contact the authors".
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
{
2+
"annotations": "CPJKU/BallroomAnnotations @ 1db08914a8ae15edb01f104046e30bad88effe67",
3+
"beatnet_plus_trained_on_this": [
4+
"ballroom"
5+
],
6+
"cli": "C:\\build\\takt4\\windows-core\\bin\\Release\\takt4-cli.exe",
7+
"confidence_threshold": 0.15,
8+
"dataset": "Ballroom (references/ballroom-audio), not vendored",
9+
"failures": {},
10+
"files": 698,
11+
"octave_fold": "off",
12+
"per_group": {
13+
"ChaChaCha": {
14+
"Downbeat F-measure": 0.7140272805229392,
15+
"F-measure": 0.865484617617433,
16+
"files": 111
17+
},
18+
"Jive": {
19+
"Downbeat F-measure": 0.7070053064722093,
20+
"F-measure": 0.9641259985587269,
21+
"files": 60
22+
},
23+
"Quickstep": {
24+
"Downbeat F-measure": 0.8785437033167688,
25+
"F-measure": 0.9455639375724022,
26+
"files": 82
27+
},
28+
"Rumba-American": {
29+
"Downbeat F-measure": 0.904469420618489,
30+
"F-measure": 0.9628032494166475,
31+
"files": 7
32+
},
33+
"Rumba-International": {
34+
"Downbeat F-measure": 0.777536290619455,
35+
"F-measure": 0.8740771300010524,
36+
"files": 51
37+
},
38+
"Rumba-Misc": {
39+
"Downbeat F-measure": 0.6249991997758554,
40+
"F-measure": 0.8055485400371593,
41+
"files": 40
42+
},
43+
"Samba": {
44+
"Downbeat F-measure": 0.6713311131343048,
45+
"F-measure": 0.8614618691401944,
46+
"files": 86
47+
},
48+
"Tango": {
49+
"Downbeat F-measure": 0.8881759168687798,
50+
"F-measure": 0.9443039662683405,
51+
"files": 86
52+
},
53+
"VienneseWaltz": {
54+
"Downbeat F-measure": 0.9482297364364638,
55+
"F-measure": 0.9594863498421483,
56+
"files": 65
57+
},
58+
"Waltz": {
59+
"Downbeat F-measure": 0.602012420821619,
60+
"F-measure": 0.732150688292278,
61+
"files": 110
62+
}
63+
},
64+
"platform": "Windows-10-10.0.19045-SP0",
65+
"seed": 1,
66+
"summary": {
67+
"Any Metric Level Continuous": {
68+
"files": 698,
69+
"mean": 0.6951814521085492,
70+
"median": 0.7763888888888889
71+
},
72+
"Any Metric Level Total": {
73+
"files": 698,
74+
"mean": 0.8241006099217918,
75+
"median": 0.9056603773584906
76+
},
77+
"Cemgil": {
78+
"files": 698,
79+
"mean": 0.7787013110631436,
80+
"median": 0.8474384972867772
81+
},
82+
"Cemgil Best Metric Level": {
83+
"files": 698,
84+
"mean": 0.8004541192278413,
85+
"median": 0.8482039622121319
86+
},
87+
"Correct Metric Level Continuous": {
88+
"files": 698,
89+
"mean": 0.6701903681613056,
90+
"median": 0.7604347826086957
91+
},
92+
"Correct Metric Level Total": {
93+
"files": 698,
94+
"mean": 0.786349848833477,
95+
"median": 0.9047619047619048
96+
},
97+
"Downbeat F-measure": {
98+
"files": 698,
99+
"mean": 0.7545522044331278,
100+
"median": 0.8571428571428572
101+
},
102+
"F-measure": {
103+
"files": 698,
104+
"mean": 0.8784973404362127,
105+
"median": 0.9540229885057471
106+
},
107+
"Goto": {
108+
"files": 698,
109+
"mean": 0.505730659025788,
110+
"median": 1.0
111+
},
112+
"Information gain": {
113+
"files": 698,
114+
"mean": 0.4792289410951436,
115+
"median": 0.4733952287018425
116+
},
117+
"P-score": {
118+
"files": 698,
119+
"mean": 0.8588938011025233,
120+
"median": 0.9272727272727272
121+
}
122+
},
123+
"unmatched": [],
124+
"weights": "af-non-percussive"
125+
}

0 commit comments

Comments
 (0)