Loading...
Searching...
No Matches
BeatTracker.hpp
1#pragma once
2
3/* SPDX-License-Identifier: GPL-3.0-or-later */
4
5// Real-time beat tracker / tempo follower.
6//
7// Architecture, four layers; only the oscillator talks to the outputs:
8//
9// audio -> [ODF] log-filtered spectral flux (SuperFlux-style), running on
10// its OWN fixed hop (~200 fps), decoupled from the host
11// block size by an input ring buffer.
12// -> [EST] BTrack-style tempo estimator: ACF + shifted comb filter
13// bank + Rayleigh / two-state context prior, updated a few
14// times per second. User Min/Max BPM range instead of the
15// traditional hard-coded 80-160 octave.
16// -> [OSC] cumulative-score beat prediction (Stark DAFx-09 momentum
17// extrapolation) feeding a second-order delay-locked loop
18// (Adriaensen LAC2005/LAC2012): filtered next-beat time and
19// filtered period, continuous and queryable at any sample.
20// + [MON] confidence monitor (IBT-style 3 s window / 1 s hop)
21// driving the loop bandwidth ladder, auto-hold and
22// re-induction.
23//
24// Output shaping: a rate, not a position (phase corrected through the rate,
25// Mixxx-style caps), future event times (Next beat + Lookahead), and origin
26// tracking so the follower does not chase tempo changes it caused itself.
27//
28// The output triple {Timecode-ish phase, Speed, Valid} is shaped to drop into
29// TimecodeSynchronizer's {Timecode, Speed, Validity} inlets, and Speed can
30// drive the interval Speed inlet directly.
31
32#include <ossia/audio/fft.hpp>
33#include <ossia/detail/math.hpp>
34#include <ossia/math/filters.hpp>
35#include <ossia/network/value/value.hpp>
36
37#include <halp/audio.hpp>
38#include <halp/callback.hpp>
39#include <halp/controls.hpp>
40#include <halp/layout.hpp>
41#include <halp/meta.hpp>
42#include <halp/sample_accurate_controls.hpp>
43
44#include <algorithm>
45#include <cmath>
46#include <cstdint>
47#include <optional>
48#include <vector>
49
50namespace avnd_tools
51{
52namespace btrk
53{
54
55static constexpr double default_min_bpm = 80.;
56static constexpr double default_max_bpm = 160.;
57static constexpr double absolute_min_bpm = 30.;
58static constexpr double absolute_max_bpm = 300.;
59
60//
61// [ODF] Log-filtered spectral flux with a maximum filter across frequency
62// (SuperFlux, Boeck & Widmer DAFx-13), on a fixed internal hop.
63//
64// - ~200 frames per second whatever the host buffer size: the input is ring-
65// buffered and frames are cut every `hop` samples. (GistState gets this
66// wrong: its frame rate follows the audio settings.)
67// - log-magnitude on a quarter-tone triangular filterbank: gain-robust, since
68// log(a) - log(b) is a ratio (Boeck ISMIR 2012, LogFiltSpecFlux).
69// - difference against frame n-2 with a 3-bin maximum filter across frequency
70// only, strictly causal (SuperFlux mu=2).
71// - optional adaptive whitening (Stowell): P(n,k) = max(|S|, r, m*P(n-1,k)).
72// - band emphasis: the flux is summed with per-band weights so the tracker
73// can listen to the kick, the snare, transients, or everything.
74//
76{
77 enum class band_mode
78 {
79 full,
80 kick, // ~30-120 Hz
81 snare, // ~150-400 Hz
82 transient, // ~2-5 kHz
83 kick_snare
84 };
85
87 {
88 int first_bin{};
89 std::vector<float> weights; // triangular, normalized to sum 1
90 float center_hz{};
91 };
92
93 double rate{};
94 int fft_size{};
95 int hop{};
96 double fps{}; // rate / hop, ~200
97
98 ossia::fft fft{16};
99 std::vector<float> window; // Hann
100 std::vector<float> ring; // input ring, fft_size samples
101 int ring_pos{}; // write position
102 int hop_fill{}; // samples since last frame
103 int64_t total_samples{}; // absolute sample clock of the last sample
104 int64_t frame_count{}; // ODF frames emitted so far
105
106 std::vector<float> mags; // |S|, fft_size/2+1
107 std::vector<float> whitening_peaks;
108 bool whitening{false};
109 float whitening_floor{0.01f};
110 float whitening_relax{}; // per-frame memory coefficient
111
112 std::vector<filter_band> bands;
113 std::vector<float> band_weights; // band emphasis, [0,1] per band
114 // last 3 filterbank frames (log domain), ring of 3
115 std::vector<float> band_frames; // 3 * bands.size()
116 int band_frame_head{};
117 int band_frames_filled{};
118
119 band_mode mode{band_mode::full};
120
121 void configure(double sample_rate)
122 {
123 rate = sample_rate;
124 fft_size = rate > 50000. ? 4096 : 2048;
125 hop = std::max(1, (int)std::lround(rate / 200.));
126 fps = rate / hop;
127
128 fft.reset(fft_size);
129 window.resize(fft_size);
130 for(int i = 0; i < fft_size; i++)
131 window[i] = 0.5f - 0.5f * std::cos(2.f * float(M_PI) * i / float(fft_size - 1));
132
133 ring.assign(fft_size, 0.f);
134 ring_pos = 0;
135 hop_fill = 0;
136 total_samples = 0;
137 frame_count = 0;
138
139 mags.assign(fft_size / 2 + 1, 0.f);
140 whitening_peaks.assign(fft_size / 2 + 1, whitening_floor);
141 // 60 dB of relaxation over 25.6 s (Stowell's defaults)
142 whitening_relax = std::pow(10.f, -3.f / float(25.6 * fps));
143
144 build_filterbank();
145 band_frames.assign(3 * bands.size(), 0.f);
146 band_frame_head = 0;
147 band_frames_filled = 0;
148 set_band_mode(mode);
149 }
150
151 void build_filterbank()
152 {
153 bands.clear();
154 const double fmax = std::min(17000., rate * 0.45);
155 const double bin_hz = rate / double(fft_size);
156
157 // quarter-tone spaced center frequencies, deduplicated per bin
158 std::vector<double> centers;
159 int last_bin = -1;
160 for(int i = 0;; i++)
161 {
162 const double f = 27.5 * std::pow(2., i / 24.);
163 if(f > fmax)
164 break;
165 const int b = (int)std::lround(f / bin_hz);
166 if(b != last_bin && b >= 1)
167 {
168 centers.push_back(f);
169 last_bin = b;
170 }
171 }
172 if(centers.size() < 3)
173 return;
174
175 for(std::size_t i = 1; i + 1 < centers.size(); i++)
176 {
177 const double lo = centers[i - 1] / bin_hz;
178 const double c = centers[i] / bin_hz;
179 const double hi = centers[i + 1] / bin_hz;
180 filter_band band;
181 band.center_hz = float(centers[i]);
182 band.first_bin = std::max(1, (int)std::ceil(lo));
183 const int end_bin = std::min(fft_size / 2, (int)std::floor(hi));
184 if(end_bin < band.first_bin)
185 continue;
186 band.weights.resize(end_bin - band.first_bin + 1);
187 float sum = 0.f;
188 for(int b = band.first_bin; b <= end_bin; b++)
189 {
190 double w = b <= c ? (b - lo) / std::max(1e-9, c - lo)
191 : (hi - b) / std::max(1e-9, hi - c);
192 w = std::max(0., w);
193 band.weights[b - band.first_bin] = float(w);
194 sum += float(w);
195 }
196 if(sum <= 0.f)
197 continue;
198 for(auto& w : band.weights)
199 w /= sum;
200 bands.push_back(std::move(band));
201 }
202 }
203
204 void set_band_mode(band_mode m)
205 {
206 mode = m;
207 band_weights.assign(bands.size(), 0.f);
208 auto in = [](float f, float lo, float hi) { return f >= lo && f <= hi; };
209 for(std::size_t i = 0; i < bands.size(); i++)
210 {
211 const float f = bands[i].center_hz;
212 switch(m)
213 {
214 case band_mode::full:
215 band_weights[i] = 1.f;
216 break;
217 case band_mode::kick:
218 band_weights[i] = in(f, 30.f, 120.f) ? 1.f : 0.f;
219 break;
220 case band_mode::snare:
221 band_weights[i] = in(f, 150.f, 400.f) ? 1.f : 0.f;
222 break;
223 case band_mode::transient:
224 band_weights[i] = in(f, 2000.f, 5000.f) ? 1.f : 0.f;
225 break;
226 case band_mode::kick_snare:
227 band_weights[i] = (in(f, 30.f, 120.f) || in(f, 150.f, 400.f)) ? 1.f : 0.f;
228 break;
229 }
230 }
231 }
232
233 // Push n mono samples; calls on_frame(odf_value, end_sample_index) for every
234 // completed hop. end_sample_index is the absolute index of the sample just
235 // after the frame, on the same clock as total_samples.
236 template <typename F>
237 void process(const float* in, int n, F&& on_frame)
238 {
239 for(int i = 0; i < n; i++)
240 {
241 ring[ring_pos] = in[i];
242 ring_pos = ring_pos + 1 == fft_size ? 0 : ring_pos + 1;
243 total_samples++;
244 if(++hop_fill >= hop)
245 {
246 hop_fill = 0;
247 on_frame(compute_frame(), total_samples);
248 }
249 }
250 }
251
252 double compute_frame()
253 {
254 // time-order the ring into the FFT input, windowed
255 auto* input = fft.input();
256 int idx = ring_pos; // oldest sample
257 for(int i = 0; i < fft_size; i++)
258 {
259 input[i] = ring[idx] * window[i];
260 idx = idx + 1 == fft_size ? 0 : idx + 1;
261 }
262 auto* out = fft.execute();
263
264 const int nbins = fft_size / 2 + 1;
265 const float norm = 2.f / float(fft_size);
266 for(int b = 0; b < nbins; b++)
267 {
268 const float re = float(out[b][0]), im = float(out[b][1]);
269 float m = std::sqrt(re * re + im * im) * norm;
270 if(whitening)
271 {
272 float& p = whitening_peaks[b];
273 p = std::max({m, whitening_floor, p * whitening_relax});
274 m = m / p;
275 }
276 mags[b] = m;
277 }
278
279 const int nb = (int)bands.size();
280 if(nb == 0)
281 return 0.;
282 float* cur = band_frames.data() + band_frame_head * nb;
283 for(int k = 0; k < nb; k++)
284 {
285 const auto& band = bands[k];
286 float acc = 0.f;
287 for(std::size_t j = 0; j < band.weights.size(); j++)
288 acc += band.weights[j] * mags[band.first_bin + j];
289 // log compression: gain-robust flux
290 cur[k] = std::log10(1.f + 20.f * acc);
291 }
292
293 double flux = 0.;
294 if(band_frames_filled >= 2)
295 {
296 // frame n-2, with a 3-bin maximum filter across frequency (causal:
297 // only the past frame is widened)
298 const int prev_head = (band_frame_head + 1) % 3; // oldest of the 3
299 const float* prev = band_frames.data() + prev_head * nb;
300 for(int k = 0; k < nb; k++)
301 {
302 if(band_weights[k] <= 0.f)
303 continue;
304 float ref = prev[k];
305 if(k > 0)
306 ref = std::max(ref, prev[k - 1]);
307 if(k + 1 < nb)
308 ref = std::max(ref, prev[k + 1]);
309 const float d = cur[k] - ref;
310 if(d > 0.f)
311 flux += band_weights[k] * d;
312 }
313 }
314
315 band_frame_head = (band_frame_head + 1) % 3;
316 if(band_frames_filled < 3)
317 band_frames_filled++;
318 frame_count++;
319 return flux;
320 }
321};
322
323//
324// [EST] Tempo estimator: detrended ODF window -> autocorrelation -> shifted
325// comb filter bank -> prior -> best beat period. (Davies & Plumbley / Stark
326// BTrack lineage.) Updated every `update_interval` ODF frames.
327//
329{
330 double fps{200.};
331 int min_lag{75}, max_lag{150}; // from the user BPM range
332 static constexpr int window_size = 1024; // ~5 s at 200 fps
333 static constexpr int update_interval = 128;
334
335 std::vector<double> window; // ring of detrended ODF
336 int head{};
337 int filled{};
338 int since_update{};
339
340 std::vector<double> scratch; // linearized window
341 std::vector<double> acf;
342 std::vector<double> comb; // prior-weighted
343 std::vector<double> comb_raw; // prior-free, for the confidence margin
344
345 // two-state context (Davies): after three consistent estimates, swap the
346 // Rayleigh prior for a tight Gaussian around the previous period, which is
347 // what prevents on-beat -> off-beat switches.
348 double history[3]{};
349 int history_n{};
350 bool context_locked{};
351 double context_period{};
352 int context_disagreements{};
353
354 double period{0.}; // best period, in ODF frames; 0 = none yet
355 double confidence{0.}; // margin between best and second-best hypothesis
356
357 void configure(double frames_per_second, double min_bpm, double max_bpm)
358 {
359 fps = frames_per_second;
360 min_bpm = std::clamp(min_bpm, absolute_min_bpm, absolute_max_bpm);
361 max_bpm = std::clamp(max_bpm, min_bpm + 1., absolute_max_bpm);
362 min_lag = std::max(4, (int)std::lround(60. * fps / max_bpm));
363 max_lag = std::min(window_size / 2, (int)std::lround(60. * fps / min_bpm));
364 if(max_lag <= min_lag)
365 max_lag = min_lag + 1;
366 if(window.empty())
367 {
368 window.assign(window_size, 0.);
369 scratch.resize(window_size);
370 acf.resize(window_size);
371 comb.resize(window_size);
372 comb_raw.resize(window_size);
373 }
374 }
375
376 void reset_context()
377 {
378 history_n = 0;
379 context_locked = false;
380 context_disagreements = 0;
381 }
382
383 // Transport / tap seeding: pretend we already saw a consistent period.
384 void seed(double period_frames)
385 {
386 period_frames = std::clamp<double>(period_frames, min_lag, max_lag);
387 period = period_frames;
388 context_period = period_frames;
389 context_locked = true;
390 history_n = 0;
391 }
392
393 // returns true when a new estimate was produced
394 bool push(double detrended_odf)
395 {
396 window[head] = detrended_odf;
397 head = head + 1 == window_size ? 0 : head + 1;
398 if(filled < window_size)
399 filled++;
400 // Wait for a full analysis window: estimates from a half-empty window are
401 // mostly prior, and the two-state context must never lock onto those.
402 if(++since_update < update_interval || filled < window_size)
403 return false;
404 since_update = 0;
405 estimate();
406 return true;
407 }
408
409 void estimate()
410 {
411 const int n = filled;
412 // linearize, oldest first
413 int idx = (head - n + window_size) % window_size;
414 for(int i = 0; i < n; i++)
415 {
416 scratch[i] = window[idx];
417 idx = idx + 1 == window_size ? 0 : idx + 1;
418 }
419
420 // autocorrelation, normalized per-lag by the overlap length
421 const int max_acf_lag = std::min(n - 1, 4 * max_lag + 4);
422 for(int lag = 1; lag <= max_acf_lag; lag++)
423 {
424 double acc = 0.;
425 for(int i = lag; i < n; i++)
426 acc += scratch[i] * scratch[i - lag];
427 acf[lag] = acc / double(n - lag);
428 }
429
430 // shifted comb filter bank with 4 harmonics
431 double best = 0., best_lag = 0.;
432 double best_raw = 0., best_raw_lag = 0.;
433 for(int lag = min_lag; lag <= max_lag; lag++)
434 {
435 double sc = 0.;
436 int harmonics = 0;
437 for(int a = 1; a <= 4; a++)
438 {
439 const int c = a * lag;
440 if(c + (a - 1) > max_acf_lag)
441 break;
442 double h = 0.;
443 for(int b = -(a - 1); b <= a - 1; b++)
444 h += acf[c + b];
445 sc += h / double(2 * a - 1);
446 harmonics++;
447 }
448 if(harmonics == 0)
449 continue;
450 sc = sc / harmonics;
451 comb_raw[lag] = sc;
452 if(sc > best_raw)
453 {
454 best_raw = sc;
455 best_raw_lag = lag;
456 }
457 sc *= prior(lag);
458 comb[lag] = sc;
459 if(sc > best)
460 {
461 best = sc;
462 best_lag = lag;
463 }
464 }
465 if(best <= 0. || best_raw <= 0.)
466 {
467 confidence = 0.;
468 return;
469 }
470
471 // Two-state context sanity check: the tight Gaussian prior can only ever
472 // confirm itself, so verify it against the prior-free evidence. Three
473 // consecutive disagreements with the raw best hypothesis mean the context
474 // was locked onto the wrong period: drop it and re-induce.
475 if(context_locked)
476 {
477 double d = std::abs(best_raw_lag - context_period);
478 // octave-tolerant: agreeing with double/half the period is agreement
479 d = std::min({d, std::abs(2. * best_raw_lag - context_period),
480 std::abs(0.5 * best_raw_lag - context_period)});
481 if(d > 0.12 * context_period)
482 {
483 if(++context_disagreements >= 3)
484 {
485 reset_context();
486 period = best_raw_lag;
487 best_lag = best_raw_lag;
488 }
489 }
490 else
491 {
492 context_disagreements = 0;
493 }
494 }
495
496 // parabolic interpolation around the best integer lag
497 int bl = (int)best_lag;
498 if(bl > min_lag && bl < max_lag)
499 {
500 const double ym = comb[bl - 1], y0 = comb[bl], yp = comb[bl + 1];
501 const double denom = ym - 2 * y0 + yp;
502 if(std::abs(denom) > 1e-12)
503 best_lag = bl + std::clamp(0.5 * (ym - yp) / denom, -0.5, 0.5);
504 }
505
506 // hypothesis margin: best vs the best sufficiently-distant competitor, on
507 // the prior-free comb - measured on the prior-weighted one, a wrongly
508 // locked context prior would suppress every competitor and make the
509 // wrong answer look certain.
510 const double base = comb_raw[std::clamp((int)std::lround(best_lag), min_lag, max_lag)];
511 double second = 0.;
512 for(int lag = min_lag; lag <= max_lag; lag++)
513 {
514 if(std::abs(lag - best_lag) < 0.15 * best_lag)
515 continue;
516 second = std::max(second, comb_raw[lag]);
517 }
518 confidence = base > 0. ? std::clamp(1. - second / base, 0., 1.) : 0.;
519 period = best_lag;
520
521 // two-state context switch, after three consistent beat periods
522 history[history_n % 3] = best_lag;
523 history_n++;
524 if(context_locked)
525 {
526 // track slowly, so the prediction context follows drift without
527 // being able to run away within one update
528 context_period += 0.2 * (best_lag - context_period);
529 }
530 else if(history_n >= 3)
531 {
532 const double a = history[0], b = history[1], c = history[2];
533 const double mx = std::max({a, b, c}), mn = std::min({a, b, c});
534 if(mx - mn < 0.075 * mx)
535 {
536 context_locked = true;
537 context_period = (a + b + c) / 3.;
538 }
539 }
540 }
541
542 double prior(int lag) const
543 {
544 if(context_locked)
545 {
546 // tight Gaussian around the previous prediction, sigma^2 = tau/8
547 const double s2 = std::max(1., context_period / 8.);
548 const double d = lag - context_period;
549 return std::exp(-d * d / (2. * s2));
550 }
551 // Rayleigh, mode at ~107.6 BPM (b = 48 samples at BTrack's 86 fps,
552 // rescaled to our frame rate)
553 const double b = 0.5585 * fps;
554 const double l = lag;
555 return (l / (b * b)) * std::exp(-l * l / (2. * b * b));
556 }
557};
558
559//
560// [OSC-1] Cumulative beat score + causal prediction (Stark DAFx-09):
561// C(m) = (1-a)*D(m) + a*max_v(W1(v) * C(m+v)), v in [-2b, -b/2]
562// and at the fixed instant m0 = last predicted beat + b/2, extrapolate C one
563// period forward with a=1 (pure momentum, no ODF) and pick
564// next = m0 + argmax_{v=1..b}( Cf(m0+v) * W2(v) )
565// so the beat is emitted before the audio evidence arrives.
566//
568{
569 static constexpr double alpha = 0.9;
570 static constexpr double eta = 5.;
571
572 std::vector<float> cs; // ring
573 int cs_size{4096};
574 int64_t frame{}; // current ODF frame index
575 double beta{100.}; // beat period in ODF frames
576 std::vector<float> w1; // transition weights for v in [-2b, -b/2]
577 int w1_lo{}, w1_hi{}; // v range: v = -(w1_lo + i), i.e. [-2b .. -b/2]
578 std::vector<float> future;
579
580 int64_t next_prediction_frame{-1};
581 int64_t last_beat_frame{-1};
582 double last_contrast{};
583
584 void configure()
585 {
586 cs.assign(cs_size, 0.f);
587 frame = 0;
588 next_prediction_frame = -1;
589 last_beat_frame = -1;
590 future.resize(cs_size + 1024);
591 set_period(beta);
592 }
593
594 void set_period(double period_frames)
595 {
596 beta = std::clamp(period_frames, 8., 800.);
597 const int lo = std::max(1, (int)std::lround(beta / 2.));
598 const int hi = std::min(cs_size - 1, (int)std::lround(2. * beta));
599 w1_lo = lo;
600 w1_hi = hi;
601 w1.resize(hi - lo + 1);
602 for(int v = lo; v <= hi; v++)
603 {
604 const double r = eta * std::log(double(v) / beta);
605 w1[v - lo] = float(std::exp(-0.5 * r * r));
606 }
607 }
608
609 float& at(int64_t m) { return cs[(m % cs_size + cs_size) % cs_size]; }
610
611 // returns predicted next-beat frame when a new prediction was made, else -1
612 int64_t push(double detrended_odf)
613 {
614 // C(m) = (1-a) * D(m) + a * max_v( W1(v) * C(m+v) )
615 float cmax = 0.f;
616 const int64_t lim = std::min<int64_t>(frame, w1_hi);
617 for(int64_t v = w1_lo; v <= lim; v++)
618 {
619 const float c = at(frame - v) * w1[v - w1_lo];
620 if(c > cmax)
621 cmax = c;
622 }
623 at(frame) = float((1. - alpha) * detrended_odf + alpha * cmax);
624
625 int64_t predicted = -1;
626 if(next_prediction_frame < 0 && frame > int64_t(2 * beta))
627 next_prediction_frame = frame + 1; // bootstrap
628 if(frame == next_prediction_frame)
629 predicted = predict();
630
631 frame++;
632 return predicted;
633 }
634
635 int64_t predict()
636 {
637 const int b = std::max(2, (int)std::lround(beta));
638 const int past = std::min<int64_t>(frame, w1_hi + b + 1);
639 // linearize the recent past, then extrapolate with alpha = 1
640 for(int i = 0; i < past; i++)
641 future[i] = at(frame - past + 1 + i);
642 for(int i = 0; i < b + 1; i++)
643 {
644 float cmax = 0.f;
645 const int base = past + i;
646 for(int v = w1_lo; v <= w1_hi && v < base; v++)
647 {
648 const float c = future[base - v - 1] * w1[v - w1_lo];
649 if(c > cmax)
650 cmax = c;
651 }
652 future[base] = cmax; // pure momentum: no ODF term
653 }
654
655 // W2: Gaussian centered half a period ahead
656 const double half = beta / 2.;
657 double best = -1.;
658 int best_v = b / 2 + 1;
659 for(int v = 1; v <= b; v++)
660 {
661 const double d = v - half;
662 const double sc = future[past + v] * std::exp(-d * d / (2. * half * half));
663 if(sc > best)
664 {
665 best = sc;
666 best_v = v;
667 }
668 }
669
670 const int64_t beat_frame = frame + best_v;
671 last_beat_frame = beat_frame;
672 next_prediction_frame = beat_frame + std::max<int64_t>(1, (int64_t)half);
673
674 // cumulative-score contrast for the confidence monitor: score at the
675 // predicted beat against the mean magnitude over the last period
676 double mean = 1e-9;
677 const int n = std::min<int64_t>(frame, b);
678 for(int i = 0; i < n; i++)
679 mean += std::abs(at(frame - i));
680 mean /= std::max(1, (int)n);
681 const double contrast = future[past + best_v] / mean;
682 last_contrast = std::clamp(contrast / (contrast + 1.5), 0., 1.);
683
684 return beat_frame;
685 }
686};
687
688//
689// [OSC-2] Delay-locked loop on beat times (Adriaensen, "Using a DLL to filter
690// time", LAC2005), with the LAC2012 improvements: a second-order low-pass on
691// the error ahead of the loop filter at 20x the loop bandwidth, one-shot
692// startup correction followed by ~4 s of elevated bandwidth, and restarts
693// that reuse the previous rate estimate.
694//
695// States: t0 = previous beat time, t1 = filtered next beat time, e2 = filtered
696// beat period, all in seconds. tempo = 60/e2 and phase(t) = (t-t0)/(t1-t0) are
697// continuous and monotonic - the synchronizer's contract.
698//
699// A third-order variant (PipeWire-style extra integrator, z3) is offered for
700// inputs with period drift.
701//
703{
704 double t0{}, t1{}, e2{0.5};
705 double z3{};
706 double bw{1.0}; // loop bandwidth, Hz; 0 = hold (freeze e2, keep t1 running)
707 int order{2}; // 2 or 3
708 bool inited{};
709 int64_t beat_index{-1};
710
711 ossia::one_pole_filter<double> pf1{}, pf2{};
712 double innovation_rms{}; // mean square of filtered innovations, seconds^2
713
714 static constexpr double min_period = 60. / absolute_max_bpm;
715 static constexpr double max_period = 60. / absolute_min_bpm;
716
717 void reset()
718 {
719 inited = false;
720 z3 = 0.;
721 beat_index = -1;
722 innovation_rms = 0.;
723 pf1.reset();
724 pf2.reset();
725 }
726
727 // Hard-correct in one shot (Adriaensen startup). A restart with
728 // keep_rate = true reuses the previous period estimate for fast re-locking.
729 void seed(double next_beat_time, double period, bool keep_rate = false)
730 {
731 if(!(keep_rate && inited))
732 e2 = std::clamp(period, min_period, max_period);
733 t1 = next_beat_time;
734 t0 = t1 - e2;
735 z3 = 0.;
736 pf1.reset();
737 pf2.reset();
738 innovation_rms = 0.;
739 inited = true;
740 }
741
742 // Observation: the next beat is expected at time tb (seconds). Updates the
743 // filtered next-beat time and period. Never rolls the cycle: advance() does.
744 void observe(double tb, double now)
745 {
746 if(!inited)
747 {
748 seed(tb, e2);
749 return;
750 }
751
752 double e = tb - t1;
753 // fold to the nearest beat so a phase-shifted observation nudges instead
754 // of dragging the clock a whole period
755 e -= e2 * std::round(e / e2);
756
757 // LAC2012: second-order low-pass on the error, 20x the loop bandwidth,
758 // sampled at the beat rate, to keep high-frequency error noise from
759 // phase-modulating the clock.
760 const double cutoff = std::max(0.05, 20. * bw);
761 const double a = ossia::lowpass_alpha(cutoff, e2);
762 const double ef = pf2(pf1(e, a), a);
763 innovation_rms = 0.9 * innovation_rms + 0.1 * ef * ef;
764
765 if(bw <= 0.)
766 return; // hold: freeze both phase and period corrections
767
768 // omega = 2*pi*B/F, F = beat rate (JACK2's derivation: coefficient from
769 // the period, so the bandwidth is independent of the period). The
770 // precedent DLLs update at the audio-period rate where B/F is tiny; here
771 // the update rate is the beat rate, so omega must be clamped to stay in
772 // the loop's stable region - beyond ~0.5 rad the per-beat corrections
773 // exceed the error and the loop limit-cycles instead of converging.
774 const double w = std::min(2. * M_PI * bw * e2, 0.5);
775 double dphase, dperiod;
776 if(order <= 2)
777 {
778 dphase = M_SQRT2 * w * ef;
779 dperiod = w * w * ef;
780 }
781 else
782 {
783 dphase = 2. * w * ef;
784 dperiod = 2. * w * w * ef + z3;
785 z3 = std::clamp(z3 + w * w * w * ef, -0.02, 0.02);
786 }
787
788 // never correct more than a fraction of a beat inside one observation:
789 // phase error is spread over the next beats (B-Keeper nudge semantics).
790 // A correction towards `now` may consume at most half the remaining cycle
791 // time - pulling t1 down to now would teleport the phase to 1 in one step.
792 const double max_towards_now = t1 > now ? 0.5 * (t1 - now) : 0.;
793 t1 += std::clamp(dphase, -std::min(0.125 * e2, max_towards_now), 0.125 * e2);
794 e2 = std::clamp(e2 + std::clamp(dperiod, -0.05 * e2, 0.05 * e2), min_period,
795 max_period);
796
797 // keep the clock sane: the pending beat stays in the future, and the
798 // running cycle keeps a positive length
799 if(t1 < now)
800 t1 = now;
801 if(t1 < t0 + 0.1 * e2)
802 t1 = t0 + 0.1 * e2;
803 }
804
805 // Roll the cycle: emits on_beat(beat_time, index) for every beat time
806 // crossed. t1 is strictly increasing through here.
807 template <typename F>
808 void advance(double now, F&& on_beat)
809 {
810 if(!inited)
811 return;
812 int guard = 0;
813 while(now >= t1 && guard++ < 64)
814 {
815 t0 = t1;
816 t1 += e2;
817 ++beat_index;
818 on_beat(t0, beat_index);
819 }
820 }
821
822 double phase(double t) const
823 {
824 if(!inited || t1 <= t0)
825 return 0.;
826 return std::clamp((t - t0) / (t1 - t0), 0., 1.);
827 }
828
829 double tempo() const { return 60. / e2; }
830};
831
832//
833// [MON] Confidence monitor. IBT's streaming result is the whole argument: a
834// tracker with no "I am lost" monitor scored AMLt 32.2% across abrupt
835// transitions vs 100% on the same excerpts alone; automatic re-induction took
836// it to 97.2%. Parameters: 3 s window, 1 s hop, score-drop threshold 0.03.
837//
839{
840 double c_contrast{}, c_margin{}, c_dll{};
841 double combined{};
842
843 double acc{}, acc_t{};
844 double wins[4]{};
845 int wins_n{};
846 bool reinduce_request{};
847
848 void reset()
849 {
850 combined = acc = acc_t = 0.;
851 wins_n = 0;
852 reinduce_request = false;
853 }
854
855 void update(double dt, double contrast, double margin, double dll_rms_rel)
856 {
857 c_contrast = contrast;
858 c_margin = margin;
859 c_dll = std::clamp(1. - dll_rms_rel / 0.2, 0., 1.);
860 combined = std::clamp(
861 0.35 * c_margin + 0.35 * c_contrast + 0.3 * c_dll, 0., 1.);
862
863 acc += combined * dt;
864 acc_t += dt;
865 if(acc_t >= 1.)
866 {
867 const double mean = acc / acc_t;
868 acc = acc_t = 0.;
869 for(int i = 0; i < 3; i++)
870 wins[i] = wins[i + 1];
871 wins[3] = mean;
872 if(wins_n < 4)
873 wins_n++;
874 if(wins_n >= 4)
875 {
876 const double prev = (wins[0] + wins[1] + wins[2]) / 3.;
877 const double cur = (wins[1] + wins[2] + wins[3]) / 3.;
878 if(prev - cur > 0.03)
879 reinduce_request = true;
880 }
881 }
882 }
883};
884
885// Tap tempo, Mixxx semantics: average the last 80 taps, discard the whole
886// series on any interval > 2000 ms, round to 1/12 BPM, minimum 30 BPM.
888{
889 static constexpr int max_taps = 81; // 80 intervals
890 std::vector<double> taps;
891 double last_tap{-1e18};
892
893 // returns the tapped BPM, or 0 if not enough taps yet
894 double tap(double now)
895 {
896 if(now - last_tap > 2.0)
897 taps.clear();
898 last_tap = now;
899 taps.push_back(now);
900 if((int)taps.size() > max_taps)
901 taps.erase(taps.begin());
902 if(taps.size() < 2)
903 return 0.;
904 const double mean = (taps.back() - taps.front()) / double(taps.size() - 1);
905 if(mean <= 0.)
906 return 0.;
907 double bpm = 60. / mean;
908 bpm = std::round(bpm * 12.) / 12.;
909 if(bpm < 30.)
910 return 0.;
911 return bpm;
912 }
913};
914
920inline std::optional<int> beat_number_of(const ossia::value& v) noexcept
921{
922 if(auto* i = v.target<int32_t>())
923 return *i;
924 if(auto* f = v.target<float>())
925 return (int)std::llround(*f);
926 return std::nullopt;
927}
928
929//
930// [EV] Tempo and phase from discrete events - one OSC message per beat, a
931// footswitch, a clock pulse. The audio path has to infer beat times from a
932// noisy onset function; here they are handed to us, so the only real problems
933// left are a dropped or doubled message and the mapping from event rate to
934// beat rate.
935//
937{
938 // Median, not mean: a dropped message doubles one interval and a duplicated
939 // one halves it. A mean smears that across the whole estimate; a median of
940 // five rejects up to two such outliers outright.
941 ossia::median_filter<double, 5> ioi;
942
943 double last_event{-1.};
944 double period{0.}; // seconds per BEAT, already divided by events_per_beat
945 double confidence{0.};
946 int count{};
947
948 void reset()
949 {
950 ioi.reset();
951 last_event = -1.;
952 period = 0.;
953 confidence = 0.;
954 count = 0;
955 }
956
960 bool push(double t, int div, double min_period, double max_period)
961 {
962 if(last_event < 0.)
963 {
964 last_event = t;
965 return false;
966 }
967
968 const double raw = t - last_event;
969 last_event = t;
970 if(raw <= 1e-4)
971 return false; // two messages in the same sample: not an interval
972
973 double beat_ioi = raw * std::max(1, div);
974
975 // Fold octaves into the allowed range: a dropped message reads as a
976 // double-length interval, an extra one as half. Only powers of two are
977 // folded - anything else is left for the median to reject, because
978 // silently rescaling an arbitrary interval would invent a tempo.
979 for(int i = 0; i < 4 && beat_ioi > max_period; i++)
980 beat_ioi *= 0.5;
981 for(int i = 0; i < 4 && beat_ioi < min_period; i++)
982 beat_ioi *= 2.;
983 if(beat_ioi < min_period || beat_ioi > max_period)
984 return false;
985
986 const double m = ioi(beat_ioi);
987 ++count;
988 if(count < 2)
989 return false;
990
991 period = m;
992
993 // Confidence is the agreement between this interval and the median: a
994 // metronome converges to 1, a human tapping settles around 0.6-0.8, a
995 // stream with dropouts stays low and the monitor holds instead of chasing.
996 const double err = m > 0. ? std::abs(beat_ioi - m) / m : 1.;
997 confidence = std::clamp(1. - 4. * err, 0., 1.);
998 return period > 0.;
999 }
1000};
1001
1002} // namespace btrk
1003
1010{
1011 halp_meta(name, "Beat Tracker")
1012 halp_meta(author, "ossia team")
1013 halp_meta(c_name, "avnd_beat_tracker")
1014 halp_meta(category, "Timing/Audio")
1015 halp_meta(description,
1016 "Audio beat tracker / tempo follower: onset detection, tempo "
1017 "estimation and a phase-locked clock with confidence monitoring.")
1018 halp_meta(manual_url, "https://ossia.io/score-docs/processes/beat-tracker.html")
1019 halp_meta(uuid, "8c8c1855-b96f-4231-b40a-453468e7f9ec")
1020
1021 using band_mode = btrk::spectral_flux_odf::band_mode;
1022 enum class clock_filter
1023 {
1024 dll_2nd_order,
1025 dll_3rd_order
1026 };
1027
1031 enum class source_mode
1032 {
1033 audio,
1034 events,
1035 audio_and_events
1036 };
1037
1038 struct ins
1039 {
1040 struct : halp::dynamic_audio_bus<"In", double>
1041 {
1042 halp_meta(
1043 description,
1044 "Audio to track: a microphone on a drummer, a click, a DJ feed, a "
1045 "bus from elsewhere in the score. Channels are summed to mono. "
1046 "Ignored when Source is Events.")
1047 } audio;
1048
1049 struct : halp::combobox_t<"Source", source_mode>
1050 {
1051 halp_meta(
1052 description,
1053 "Where beats come from. Audio: listen to the input and detect onsets. "
1054 "Events: ignore the audio entirely and take beats from the Beat inlet "
1055 "(OSC, a footswitch, a clock). Audio + Events: both, whichever is "
1056 "alive.")
1057 struct range
1058 {
1059 std::string_view values[3]{"Audio", "Events", "Audio + Events"};
1060 source_mode init{source_mode::audio};
1061 };
1062 } source;
1063
1064 // One message per event: an OSC bang, a footswitch, a clock pulse.
1065 //
1066 // The payload type selects the meaning, so one cable covers both cases:
1067 // int / float -> the value IS the beat number. The tracker then knows
1068 // absolute position, so the downbeat lands on the right
1069 // beat of the bar rather than wherever it started.
1070 // anything else (impulse, bool, string, list...) -> a bare beat: phase
1071 // and tempo only.
1072 //
1073 // Timestamps are sample-accurate within the tick - quantising them to the
1074 // block would add up to a whole buffer of jitter per beat, which at 512
1075 // samples / 44.1 kHz is 11.6 ms, comparable to the entire onset detector's
1076 // latency.
1077 struct : halp::accurate<halp::val_port<"Beat", ossia::value>>
1078 {
1079 halp_meta(
1080 description,
1081 "One message per beat, used when Source is Events. Send an int or a "
1082 "float and the value is taken as the beat NUMBER, which also gives "
1083 "the tracker the position in the bar; send anything else (a bang, a "
1084 "string, a list) and it counts as a plain beat, giving tempo and "
1085 "phase only. Timestamps are sample-accurate within the buffer.")
1086 } beat_in;
1087
1088 struct : halp::spinbox_i32<"Events per beat", halp::irange{1, 24, 1}>
1089 {
1090 halp_meta(
1091 description,
1092 "How many messages arrive per beat. 1 for one message per beat, 4 "
1093 "for sixteenth notes in 4/4, 24 for MIDI clock. Only whole beats set "
1094 "the phase; subdivisions only refine the tempo.")
1095 } events_per_beat;
1096
1097 struct : halp::combobox_t<"Band", band_mode>
1098 {
1099 halp_meta(
1100 description,
1101 "Which part of the spectrum to listen to. Full works on a finished "
1102 "mix; Kick (30-120 Hz) and Snare (150-400 Hz) are for a miked drum "
1103 "and ignore everything else; Transient (2-5 kHz) suits clicks and "
1104 "sticks. Narrowing the band is the single most effective way to stop "
1105 "the tracker hearing things that are not the beat.")
1106 struct range
1107 {
1108 std::string_view values[5]{"Full", "Kick", "Snare", "Transient", "Kick+Snare"};
1109 band_mode init{band_mode::full};
1110 };
1111 } band;
1112
1113 // BPM range instead of the traditional hard-coded octave; the toggle is
1114 // the "None" escape - guessing without an off switch is worse than not
1115 // guessing.
1116 struct : halp::toggle<"Limit BPM range", halp::toggle_setup{.init = true}>
1117 {
1118 halp_meta(
1119 description,
1120 "Restrict the search to Min/Max BPM. This is the main defence "
1121 "against half- and double-time errors. Turn it off if you would "
1122 "rather the tracker never second-guessed you.")
1123 } limit_range;
1124 struct : halp::spinbox_f32<"Min BPM", halp::range{30., 300., btrk::default_min_bpm}>
1125 {
1126 halp_meta(
1127 description,
1128 "Slowest tempo considered. Set the pair to bracket the music you "
1129 "expect: a narrow range locks faster and is far less likely to "
1130 "settle on half or double the real tempo.")
1131 } min_bpm;
1132 struct : halp::spinbox_f32<"Max BPM", halp::range{30., 300., btrk::default_max_bpm}>
1133 {
1134 halp_meta(description, "Fastest tempo considered. See Min BPM.")
1135 } max_bpm;
1136
1137 // In a sequencer the tempo is free; a seeded tracker beats a cold one on
1138 // every continuity measure.
1139 struct : halp::toggle<"Transport tempo hint", halp::toggle_setup{.init = true}>
1140 {
1141 halp_meta(
1142 description,
1143 "Start from the score's own tempo instead of from nothing. A tracker "
1144 "given a starting tempo locks in well under a second where a cold "
1145 "one needs several. Leave this on unless you are deliberately "
1146 "testing cold-start behaviour.")
1147 } hint;
1148
1149 struct : halp::combobox_t<"Clock filter", clock_filter>
1150 {
1151 halp_meta(
1152 description,
1153 "The loop that turns detected beats into a smooth clock. 2nd order "
1154 "is the tested default. 3rd order can settle a little flatter on "
1155 "very steady material but has not been measured against it here.")
1156 struct range
1157 {
1158 std::string_view values[2]{"DLL (2nd order)", "DLL (3rd order)"};
1159 clock_filter init{clock_filter::dll_2nd_order};
1160 };
1161 } filter;
1162
1163 struct : halp::knob_f32<"Lookahead (ms)", halp::range{0., 250., 0.}>
1164 {
1165 halp_meta(
1166 description,
1167 "Emit each beat this early, so downstream processing and display "
1168 "latency land on time. Around 100 ms is nearly free in accuracy "
1169 "terms. Leave at 0 when driving from Events, which already arrive "
1170 "on time.")
1171 } lookahead;
1172 struct : halp::knob_f32<"Offset (ms)", halp::range{-250., 250., 0.}>
1173 {
1174 halp_meta(
1175 description,
1176 "Fixed timing trim, applied after Lookahead. Positive delays the "
1177 "beat, negative advances it. Use it to compensate a mic distance or "
1178 "a converter's latency.")
1179 } offset;
1180 struct : halp::knob_f32<"Gate (dB)", halp::range{-90., 0., -60.}>
1181 {
1182 halp_meta(
1183 description,
1184 "Input level below which the signal counts as silence, at which "
1185 "point the clock holds its last tempo rather than chasing noise. "
1186 "Raise it if room tone or bleed is keeping the tracker awake.")
1187 } gate;
1188 struct : halp::toggle<"Whitening", halp::toggle_setup{.init = false}>
1189 {
1190 halp_meta(
1191 description,
1192 "Continuously equalise the spectrum before onset detection, so quiet "
1193 "and loud passages contribute alike. Helps on a full mix or a room "
1194 "mic with a wide dynamic range; unnecessary on a close-miked drum.")
1195 } whitening;
1196 struct : halp::spinbox_i32<"Beats per bar", halp::irange{1, 16, 4}>
1197 {
1198 halp_meta(
1199 description,
1200 "Time signature numerator, used to decide which beats are downbeats "
1201 "and to compute Bar phase. It does not affect tempo tracking.")
1202 } beats_per_bar;
1203
1204 // Manual rescue controls
1205 struct : halp::impulse_button<"Tap">
1206 {
1207 halp_meta(description, "When unlocked, tap in time to set the tempo (taps are averaged; pause for two seconds to start a fresh series). Once locked, a single tap resets the phase so the next beat is now.")
1208 void update(BeatTracker& self) { self.on_tap(); }
1209 } tap;
1210 struct : halp::impulse_button<"Resync">
1211 {
1212 halp_meta(description, "Declare that now is beat one. Snaps the downbeat without touching the tempo.")
1213 void update(BeatTracker& self) { self.on_resync(); }
1214 } resync;
1215 struct : halp::impulse_button<"Nudge -">
1216 {
1217 halp_meta(description, "Momentarily slow down, to pull the clock back in line with a performer who is ahead. Returns to the tracked tempo on release.")
1218 void update(BeatTracker& self) { self.on_nudge(-1); }
1219 } nudge_minus;
1220 struct : halp::impulse_button<"Nudge +">
1221 {
1222 halp_meta(description, "Momentarily speed up, to push the clock forward towards a performer who is behind. Returns to the tracked tempo on release.")
1223 void update(BeatTracker& self) { self.on_nudge(+1); }
1224 } nudge_plus;
1225 struct : halp::impulse_button<"x2">
1226 {
1227 halp_meta(description, "Double the tempo without re-detecting, for when the tracker settled an octave low.")
1228 void update(BeatTracker& self) { self.on_octave(0.5); }
1229 } dbl;
1230 struct : halp::impulse_button<"/2">
1231 {
1232 halp_meta(description, "Halve the tempo without re-detecting, for when the tracker settled an octave high.")
1233 void update(BeatTracker& self) { self.on_octave(2.); }
1234 } hlv;
1235 struct : halp::toggle<"Hold", halp::toggle_setup{.init = false}>
1236 {
1237 halp_meta(
1238 description,
1239 "Freeze the tempo at its current value while still following the "
1240 "beat's phase. For a passage where you trust the tempo but not what "
1241 "the tracker is hearing - a breakdown, a solo, heavy bleed.")
1242 } hold;
1243 struct : halp::toggle<"Follow", halp::toggle_setup{.init = true}>
1244 {
1245 halp_meta(
1246 description,
1247 "Master switch. Off freezes the clock entirely and stops analysing, "
1248 "leaving the last tempo and phase in place.")
1249 } follow;
1250 } inputs;
1251
1252 struct
1253 {
1254 struct : halp::val_port<"Tempo", double>
1255 {
1256 using halp::val_port<"Tempo", double>::operator=;
1257 halp_meta(
1258 description,
1259 "Detected tempo in BPM, smoothed for display. Cable this to an "
1260 "interval's Tempo inlet to drive the timeline.")
1261 } tempo{120.};
1262 struct : halp::val_port<"Speed", double>
1263 {
1264 using halp::val_port<"Speed", double>::operator=;
1265 halp_meta(
1266 description,
1267 "Playback rate multiplier: 1 means the score's tempo already "
1268 "matches, above 1 means speed up to catch the performer. This is the "
1269 "port to feed a synchroniser or an interval's Speed inlet; unlike "
1270 "Tempo it corrects phase as well, gradually and within safe limits.")
1271 } speed{1.};
1272 struct : halp::val_port<"Phase", double>
1273 {
1274 using halp::val_port<"Phase", double>::operator=;
1275 halp_meta(
1276 description,
1277 "Position within the current beat, 0 at the beat and approaching 1 "
1278 "just before the next. Continuous, so it is safe to map to a "
1279 "parameter directly.")
1280 } phase{0.};
1281 struct : halp::val_port<"Bar phase", double>
1282 {
1283 using halp::val_port<"Bar phase", double>::operator=;
1284 halp_meta(
1285 description,
1286 "Position within the current bar, 0 at the downbeat approaching 1. "
1287 "Depends on Beats per bar and on the downbeat being correct - use "
1288 "Resync, or send beat numbers, to place it.")
1289 } bar_phase{0.};
1290 struct : halp::val_port<"Beat index", int>
1291 {
1292 using halp::val_port<"Beat index", int>::operator=;
1293 halp_meta(
1294 description,
1295 "Beats counted since tracking started. Increments by one on every "
1296 "beat and never goes backwards.")
1297 } beat_index{0};
1298 struct : halp::val_port<"Next beat (s)", double>
1299 {
1300 using halp::val_port<"Next beat (s)", double>::operator=;
1301 halp_meta(
1302 description,
1303 "Seconds until the next beat, already advanced by Lookahead. Use "
1304 "this to schedule something ahead of time instead of reacting to "
1305 "the Beat pulse.")
1306 } next_beat{0.};
1307 struct : halp::val_port<"Confidence", double>
1308 {
1309 using halp::val_port<"Confidence", double>::operator=;
1310 halp_meta(
1311 description,
1312 "How much the tracker trusts its own reading, 0 to 1. Falls during "
1313 "breakdowns, silence and material with no clear pulse. Worth mapping "
1314 "to a fallback so the score can react to the tracker losing the "
1315 "plot instead of following it blindly.")
1316 } confidence{0.};
1317 struct : halp::val_port<"Locked", bool>
1318 {
1319 using halp::val_port<"Locked", bool>::operator=;
1320 halp_meta(
1321 description,
1322 "True once the clock has settled: either the evidence is strong or "
1323 "the tempo has simply held steady for a few seconds. While locked "
1324 "the tracker corrects gently; while unlocked it hunts.")
1325 } locked{false};
1326 struct : halp::val_port<"Valid", bool>
1327 {
1328 using halp::val_port<"Valid", bool>::operator=;
1329 halp_meta(
1330 description,
1331 "False when there is nothing to track - the input is below the gate, "
1332 "or no events have arrived. The clock keeps running on its last "
1333 "tempo; this tells you not to trust it.")
1334 } valid{false};
1335
1336 struct : halp::timed_callback<"Beat">
1337 {
1338 halp_meta(
1339 description,
1340 "Fires on every beat, timed to the sample. Advanced by Lookahead and "
1341 "trimmed by Offset.")
1342 } beat;
1343 struct : halp::timed_callback<"Downbeat">
1344 {
1345 halp_meta(
1346 description,
1347 "Fires on the first beat of each bar, according to Beats per bar.")
1348 } downbeat;
1349 struct : halp::timed_callback<"Onset">
1350 {
1351 halp_meta(
1352 description,
1353 "Fires on every detected attack, not only on beats. Useful for "
1354 "triggering off the raw playing rather than the inferred grid.")
1355 } onset;
1356 } outputs;
1357
1358 // Grouped by the question the user is answering, not by the order the
1359 // signal flows: "what am I listening to", "what tempo do I expect", "how
1360 // should the clock behave", "what do I reach for mid-performance".
1361 //
1362 // The audio bus and the Beat inlet are not here: they are cables, not
1363 // controls, and have no widget to place.
1364 struct ui
1365 {
1366 halp_meta(name, "Beat Tracker")
1367 halp_meta(layout, halp::layouts::tabs)
1368 halp_meta(background, halp::colors::background_mid)
1369
1370 struct
1371 {
1372 halp_meta(name, "Source")
1373 halp_meta(layout, halp::layouts::hbox)
1374
1375 struct
1376 {
1377 halp_meta(name, "Input")
1378 halp_meta(layout, halp::layouts::vbox)
1379 halp::item<&ins::source> source;
1380 halp::item<&ins::events_per_beat> events_per_beat;
1381 } input;
1382
1383 halp::spacing sp1{.width = 12, .height = 1};
1384
1385 struct
1386 {
1387 halp_meta(name, "Listening")
1388 halp_meta(layout, halp::layouts::vbox)
1389 halp::item<&ins::band> band;
1390 halp::item<&ins::whitening> whitening;
1391 halp::item<&ins::gate> gate;
1392 } listening;
1393 } source_tab;
1394
1395 struct
1396 {
1397 halp_meta(name, "Tempo")
1398 halp_meta(layout, halp::layouts::hbox)
1399
1400 struct
1401 {
1402 halp_meta(name, "Range")
1403 halp_meta(layout, halp::layouts::vbox)
1404 halp::item<&ins::limit_range> limit_range;
1405 halp::item<&ins::min_bpm> min_bpm;
1406 halp::item<&ins::max_bpm> max_bpm;
1407 } range;
1408
1409 halp::spacing sp1{.width = 12, .height = 1};
1410
1411 struct
1412 {
1413 halp_meta(name, "Musical")
1414 halp_meta(layout, halp::layouts::vbox)
1415 halp::item<&ins::hint> hint;
1416 halp::item<&ins::beats_per_bar> beats_per_bar;
1417 } musical;
1418 } tempo_tab;
1419
1420 struct
1421 {
1422 halp_meta(name, "Clock")
1423 halp_meta(layout, halp::layouts::hbox)
1424
1425 struct
1426 {
1427 halp_meta(name, "Filter")
1428 halp_meta(layout, halp::layouts::vbox)
1429 halp::item<&ins::filter> filter;
1430 } filt;
1431
1432 halp::spacing sp1{.width = 12, .height = 1};
1433
1434 struct
1435 {
1436 halp_meta(name, "Timing")
1437 halp_meta(layout, halp::layouts::vbox)
1438 halp::item<&ins::lookahead> lookahead;
1439 halp::item<&ins::offset> offset;
1440 } timing;
1441 } clock_tab;
1442
1443 struct
1444 {
1445 halp_meta(name, "Performance")
1446 halp_meta(layout, halp::layouts::hbox)
1447
1448 struct
1449 {
1450 halp_meta(name, "Sync")
1451 halp_meta(layout, halp::layouts::vbox)
1452 halp::item<&ins::tap> tap;
1453 halp::item<&ins::resync> resync;
1454 } sync;
1455
1456 halp::spacing sp1{.width = 12, .height = 1};
1457
1458 struct
1459 {
1460 halp_meta(name, "Nudge")
1461 halp_meta(layout, halp::layouts::vbox)
1462 halp::item<&ins::nudge_minus> nudge_minus;
1463 halp::item<&ins::nudge_plus> nudge_plus;
1464 } nudge;
1465
1466 halp::spacing sp2{.width = 12, .height = 1};
1467
1468 struct
1469 {
1470 halp_meta(name, "Octave")
1471 halp_meta(layout, halp::layouts::vbox)
1472 halp::item<&ins::dbl> dbl;
1473 halp::item<&ins::hlv> hlv;
1474 } octave;
1475
1476 halp::spacing sp3{.width = 12, .height = 1};
1477
1478 struct
1479 {
1480 halp_meta(name, "Engage")
1481 halp_meta(layout, halp::layouts::vbox)
1482 halp::item<&ins::hold> hold;
1483 halp::item<&ins::follow> follow;
1484 } engage;
1485 } performance_tab;
1486 };
1487
1488 // --- engine state ---
1492 btrk::beat_dll m_dll;
1494 btrk::tap_tempo m_tap;
1495
1496 ossia::moving_average_filter<double, 16> m_detrend; // ODF moving mean
1497 ossia::one_pole_filter<double> m_tempo_display; // display smoothing
1498
1499 std::vector<float> m_mono;
1500 double m_rate{};
1501 int64_t m_samples{}; // absolute sample clock, matches m_odf.total_samples
1502 double m_now{}; // seconds, end of last block
1503
1504 // onset picking
1505 double m_odf_hist[3]{};
1506 int m_odf_hist_n{};
1507 int64_t m_last_onset_frame{-1000};
1508
1509 // gate
1510 double m_silence_time{1e9};
1511 bool m_gate_open{false};
1512
1513 // event source
1514 btrk::event_estimator m_ev;
1515 double m_last_event_time{-1e9};
1516 int m_cfg_source{-1};
1517
1518 // bandwidth ladder / startup
1519 double m_elevated_until{-1.};
1520 int m_locked_count{};
1521 bool m_is_locked{};
1522
1523 // Tempo stability, for the lock criterion. Absolute ODF contrast is
1524 // material-dependent: a synthetic click reaches 0.75 easily while a real
1525 // mix peaks around 0.45-0.65, so an absolute threshold means "locked" never
1526 // fires on actual music - and since lock drives the bandwidth ladder, the
1527 // loop then never settles to 0.05 Hz and drifts late in a piece. A clock
1528 // that has held the same period for several seconds IS locked, whatever the
1529 // onset function's contrast looks like. This mirrors IBT's monitor, which
1530 // detects a *drop* rather than testing an absolute level.
1531 static constexpr int tempo_hist_n = 64;
1532 double m_tempo_hist[tempo_hist_n]{};
1533 int m_tempo_hist_pos{}, m_tempo_hist_len{};
1534 double m_tempo_accum_t{};
1535 double m_tempo_rel_sd{1.};
1536
1537 // transport hint / feedback-loop break
1538 bool m_seeded{};
1539 double m_prev_transport_tempo{-1.};
1540 double m_last_emitted_tempo{-1.};
1541
1542 // downbeat bookkeeping
1543 int64_t m_downbeat_origin{}; // beat index that is "the one"
1544 int64_t m_last_emitted_beat{-1};
1545
1546 // output shaping (Mixxx constants)
1547 double m_speed_trim{};
1548 static constexpr double sync_adjustment_cap = 0.05; // max +-5% rate correction
1549 static constexpr double sync_delta_cap = 0.02; // max change per callback
1550 static constexpr double sync_error_deadband = 0.01; // beats
1551 static constexpr double sync_p_gain = 0.7;
1552
1553 // cached control state, to detect changes cheaply
1554 float m_cfg_min_bpm{-1.f}, m_cfg_max_bpm{-1.f};
1555 bool m_cfg_limit{true};
1556 int m_cfg_band{-1};
1557
1558 halp::setup setup;
1559
1560 void prepare(halp::setup s)
1561 {
1562 // prepare() is re-invoked at runtime when the buffer grows or the channel
1563 // count changes: only reset on an actual sample rate change.
1564 if(s.rate == m_rate && m_rate > 0)
1565 {
1566 setup = s;
1567 return;
1568 }
1569 setup = s;
1570 m_rate = s.rate;
1571 if(m_rate <= 0)
1572 return;
1573
1574 m_odf.whitening = inputs.whitening;
1575 m_odf.configure(m_rate);
1576 apply_bpm_range();
1577 m_ct.beta = m_est.period > 0 ? m_est.period : 60. / 120. * m_odf.fps;
1578 m_ct.configure();
1579 m_ct.set_period(m_ct.beta);
1580 m_dll.reset();
1581 m_dll.e2 = 0.5;
1582 m_mon.reset();
1583 m_detrend.reset();
1584 m_tempo_display.reset();
1585 m_mono.resize(4 * setup.frames + 16);
1586 m_samples = 0;
1587 m_now = 0;
1588 m_odf_hist_n = 0;
1589 m_last_onset_frame = -1000;
1590 m_silence_time = 1e9;
1591 m_gate_open = false;
1592 m_elevated_until = -1.;
1593 m_locked_count = 0;
1594 m_tempo_hist_len = 0;
1595 m_tempo_hist_pos = 0;
1596 m_tempo_accum_t = 0.;
1597 m_tempo_rel_sd = 1.;
1598 m_is_locked = false;
1599 m_seeded = false;
1600 m_prev_transport_tempo = -1.;
1601 m_speed_trim = 0.;
1602 m_downbeat_origin = 0;
1603 m_last_emitted_beat = -1;
1604 }
1605
1606 void apply_bpm_range()
1607 {
1608 const double lo = inputs.limit_range ? inputs.min_bpm.value : 40.;
1609 const double hi = inputs.limit_range ? inputs.max_bpm.value : 240.;
1610 m_est.configure(m_odf.fps, std::min(lo, hi), std::max(lo, hi));
1611 m_cfg_min_bpm = inputs.min_bpm.value;
1612 m_cfg_max_bpm = inputs.max_bpm.value;
1613 m_cfg_limit = inputs.limit_range;
1614 }
1615
1616 void reconfigure_if_needed()
1617 {
1618 if((int)inputs.band.value != m_cfg_band)
1619 {
1620 m_odf.set_band_mode(inputs.band.value);
1621 m_cfg_band = (int)inputs.band.value;
1622 }
1623 if(inputs.min_bpm.value != m_cfg_min_bpm || inputs.max_bpm.value != m_cfg_max_bpm
1624 || bool(inputs.limit_range) != m_cfg_limit)
1625 {
1626 apply_bpm_range();
1627 m_est.reset_context();
1628 }
1629 if((int)inputs.source.value != m_cfg_source)
1630 {
1631 // Switching source invalidates the interval history: intervals measured
1632 // from onsets and from messages are not the same measurement.
1633 m_ev.reset();
1634 m_last_event_time = -1e9;
1635 m_est.reset_context();
1636 m_elevated_until = m_now + 4.;
1637 m_cfg_source = (int)inputs.source.value;
1638 }
1639 m_odf.whitening = inputs.whitening;
1640 m_dll.order = inputs.filter.value == clock_filter::dll_3rd_order ? 3 : 2;
1641 }
1642
1643 // --- manual rescue controls -------------------------------------------
1644 void on_tap()
1645 {
1646 const double bpm = m_tap.tap(m_now);
1647 if(m_is_locked)
1648 {
1649 // locked: a tap resets the phase to "a beat is now"
1650 if(m_dll.inited)
1651 {
1652 m_dll.t0 = m_now;
1653 m_dll.t1 = m_now + m_dll.e2;
1654 }
1655 }
1656 else if(bpm > 0.)
1657 {
1658 // unlocked: taps teach the tempo
1659 const double period = 60. / bpm;
1660 m_dll.seed(m_now + period, period);
1661 seed_estimator_from_period(period);
1662 m_elevated_until = m_now + 4.;
1663 }
1664 }
1665
1666 void on_resync()
1667 {
1668 // snap the downbeat to the one: the next beat becomes beat 0 of a bar,
1669 // and phase restarts now
1670 if(m_dll.inited)
1671 {
1672 m_dll.t0 = m_now;
1673 m_dll.t1 = m_now + m_dll.e2;
1674 m_downbeat_origin = m_dll.beat_index + 1;
1675 }
1676 }
1677
1678 void on_nudge(int direction)
1679 {
1680 if(m_dll.inited)
1681 {
1682 // 2% of a period per press, spread naturally by the clock
1683 const double d = direction * 0.02 * m_dll.e2;
1684 m_dll.t0 += d;
1685 m_dll.t1 += d;
1686 }
1687 }
1688
1689 void on_octave(double factor)
1690 {
1691 // flip the octave without re-detecting
1692 if(m_dll.inited)
1693 {
1694 const double p = std::clamp(
1695 m_dll.e2 * factor, btrk::beat_dll::min_period, btrk::beat_dll::max_period);
1696 m_dll.e2 = p;
1697 m_dll.t1 = std::min(m_dll.t1, m_dll.t0 + p);
1698 seed_estimator_from_period(p);
1699 }
1700 }
1701
1702 void seed_estimator_from_period(double period_seconds)
1703 {
1704 const double frames = period_seconds * m_odf.fps;
1705 m_est.seed(frames);
1706 m_ct.set_period(std::clamp<double>(frames, m_est.min_lag, m_est.max_lag));
1707 }
1708
1709 // ----------------------------------------------------------------------
1710 using tick = halp::tick_flicks;
1711 void operator()(halp::tick_flicks tk)
1712 {
1713 if(setup.rate <= 0 || tk.frames <= 0)
1714 return;
1715 const double rate = setup.rate;
1716 const int frames = tk.frames;
1717 const double block_start = m_samples / rate;
1718 const double block_dt = frames / rate;
1719
1720 reconfigure_if_needed();
1721 handle_transport_hint(tk);
1722
1723 const auto mode = inputs.source.value;
1724 const bool use_audio = mode != source_mode::events;
1725 const bool use_events = mode != source_mode::audio;
1726
1727 // Events are consumed first so that the gate below can see this block's
1728 // messages: in Events mode there may be no audio at all, and an audio-RMS
1729 // gate would hold the loop forever.
1730 if(use_events)
1731 process_events(rate, block_start, frames);
1732
1733 if(use_audio)
1734 {
1735 // mono mix + block RMS for the gate
1736 if((int)m_mono.size() < frames)
1737 m_mono.resize(frames);
1738 const int chans = inputs.audio.channels;
1739 double rms = 0.;
1740 for(int i = 0; i < frames; i++)
1741 {
1742 float acc = 0.f;
1743 for(int c = 0; c < chans; c++)
1744 acc += float(inputs.audio.samples[c][i]);
1745 if(chans > 1)
1746 acc /= float(chans);
1747 m_mono[i] = acc;
1748 rms += acc * acc;
1749 }
1750 rms = std::sqrt(rms / std::max(1, frames));
1751 const double level_db = 20. * std::log10(rms + 1e-12);
1752 if(level_db > inputs.gate)
1753 {
1754 m_silence_time = 0.;
1755 m_gate_open = true;
1756 }
1757 else
1758 {
1759 m_silence_time += block_dt;
1760 // The hang time must exceed the longest inter-beat gap (40 BPM = 1.5 s),
1761 // otherwise the gate would drop out between beats of slow material.
1762 if(m_silence_time > 2.0)
1763 m_gate_open = false; // silence: valid = false, not confidence = 0
1764 }
1765 }
1766
1767 // A live event source opens the gate on its own; in Events mode it is the
1768 // only thing that can.
1769 const bool events_alive = use_events && (m_now - m_last_event_time) < event_hang();
1770 if(!use_audio)
1771 m_gate_open = events_alive;
1772 else if(events_alive)
1773 m_gate_open = true;
1774
1775 // bandwidth ladder: locked 0.05 / tracking 0.2 / re-inducing 1.0 / hold 0
1776 const bool holding = inputs.hold || !inputs.follow || !m_gate_open;
1777 if(holding)
1778 m_dll.bw = 0.;
1779 else if(m_now < m_elevated_until)
1780 m_dll.bw = 1.0;
1781 else if(m_mon.combined > 0.6)
1782 m_dll.bw = 0.05;
1783 else if(m_mon.combined > 0.3)
1784 m_dll.bw = 0.2;
1785 else
1786 m_dll.bw = 1.0;
1787
1788 // run the analysis chain on the fixed internal hop
1789 if(use_audio)
1790 m_odf.process(m_mono.data(), frames, [&](double odf_v, int64_t end_sample) {
1791 on_odf_frame(odf_v, end_sample, rate, block_start, frames);
1792 });
1793 m_samples += frames;
1794 m_now = m_samples / rate;
1795
1796 // re-induction: reset the estimator context and open the loop, keeping
1797 // the previous rate estimate for fast re-locking
1798 if(m_mon.reinduce_request && !holding)
1799 {
1800 m_mon.reinduce_request = false;
1801 m_est.reset_context();
1802 m_elevated_until = m_now + 4.;
1803 }
1804
1805 emit_beats(rate, block_start, frames);
1806 update_outputs(tk, block_dt);
1807 }
1808
1809 void handle_transport_hint(const halp::tick_flicks& tk)
1810 {
1811 if(!inputs.hint || tk.tempo <= 0.)
1812 return;
1813 if(!m_seeded)
1814 {
1815 // free seed: tempo and downbeat are known in a sequencer
1816 m_seeded = true;
1817 m_prev_transport_tempo = tk.tempo;
1818 const double period = 60. / tk.tempo;
1819 m_dll.seed(m_now + period, period);
1820 seed_estimator_from_period(period);
1821 m_elevated_until = m_now + 4.;
1822 return;
1823 }
1824 if(std::abs(tk.tempo - m_prev_transport_tempo) > 1e-9)
1825 {
1826 // The transport tempo changed. If it changed to (about) the tempo we
1827 // last emitted, it is our own output looping back through a cable:
1828 // reacting to it would close a feedback loop. Only re-seed on genuinely
1829 // external changes.
1830 if(m_last_emitted_tempo < 0.
1831 || std::abs(tk.tempo - m_last_emitted_tempo) > 0.5)
1832 {
1833 seed_estimator_from_period(60. / tk.tempo);
1834 if(!m_is_locked)
1835 m_dll.seed(m_now + 60. / tk.tempo, 60. / tk.tempo, false);
1836 m_elevated_until = m_now + 4.;
1837 }
1838 m_prev_transport_tempo = tk.tempo;
1839 }
1840 }
1841
1842 void on_odf_frame(
1843 double odf_v, int64_t end_sample, double rate, double block_start, int frames)
1844 {
1845 // detrend: subtract the moving mean, half-wave rectify
1846 const double mean = m_detrend(odf_v);
1847 const double d = std::max(0., odf_v - mean);
1848
1849 // onset picking: local maximum above an adaptive threshold
1850 m_odf_hist[0] = m_odf_hist[1];
1851 m_odf_hist[1] = m_odf_hist[2];
1852 m_odf_hist[2] = odf_v;
1853 if(m_odf_hist_n < 3)
1854 m_odf_hist_n++;
1855 const int64_t cur_frame = m_odf.frame_count;
1856 if(m_odf_hist_n >= 3 && m_gate_open && m_odf_hist[1] > m_odf_hist[2]
1857 && m_odf_hist[1] >= m_odf_hist[0] && m_odf_hist[1] > 1.5 * mean + 1e-4
1858 && cur_frame - m_last_onset_frame > (int64_t)(0.03 * m_odf.fps))
1859 {
1860 m_last_onset_frame = cur_frame;
1861 const int64_t onset_sample = end_sample - m_odf.hop;
1862 const int64_t off = onset_sample - (int64_t)(block_start * rate);
1863 outputs.onset(std::clamp<int64_t>(off, 0, frames - 1));
1864 }
1865
1866 // The estimator and the cumulative score always consume the frame, gated
1867 // or not: their timelines must stay contiguous. Skipping frames while a
1868 // gate is closed compresses the apparent beat period by exactly the
1869 // skipped time (a 100 BPM train came out as 114.9 BPM that way). Silence
1870 // contributes zero flux, which is the correct evidence for it. The gate
1871 // only drives validity and the DLL hold.
1872 if(!inputs.follow)
1873 return;
1874
1875 // tempo estimation
1876 if(m_est.push(d) && m_est.period > 0.)
1877 {
1878 m_ct.set_period(m_est.period);
1879 // While not yet locked, adopt the estimator's period directly: the
1880 // DLL's slow, capped corrections are for tracking, not acquisition -
1881 // converging from the default period to a distant one at 5% per beat
1882 // takes tens of seconds.
1883 if(!m_is_locked && m_est.confidence > 0.2 && m_dll.inited && m_dll.bw > 0.)
1884 {
1885 m_dll.e2 = std::clamp(
1886 m_est.period / m_odf.fps, btrk::beat_dll::min_period,
1887 btrk::beat_dll::max_period);
1888 }
1889 }
1890
1891 // cumulative score + beat prediction -> DLL observation
1892 const int64_t predicted_frame = m_ct.push(d);
1893 if(predicted_frame >= 0 && !inputs.hold)
1894 {
1895 // ODF latency: the flux at frame n reflects audio a few hops earlier
1896 // ((3+mu) * hop); compensate the constant part here, the Offset control
1897 // trims the rest.
1898 const double tb = (predicted_frame - 3) * m_odf.hop / rate;
1899 m_dll.observe(tb, (double)end_sample / rate);
1900 }
1901 }
1902
1907 std::pair<double, double> period_bounds() const
1908 {
1909 const double lo = inputs.limit_range ? inputs.min_bpm.value : 40.;
1910 const double hi = inputs.limit_range ? inputs.max_bpm.value : 240.;
1911 return {60. / std::max(1., hi), 60. / std::max(1., lo)};
1912 }
1913
1917 double event_hang() const
1918 {
1919 const double p = m_ev.period > 0. ? m_ev.period : m_dll.e2;
1920 return std::max(2.0, 4. * p);
1921 }
1922
1926 void process_events(double rate, double block_start, int frames)
1927 {
1928 const int div = std::max(1, (int)inputs.events_per_beat);
1929 const double block_end = block_start + frames / rate;
1930 const auto [min_p, max_p] = period_bounds();
1931
1932 for(auto& [off, val] : inputs.beat_in.values)
1933 {
1934 const int64_t o = std::clamp<int64_t>(off, 0, frames > 0 ? frames - 1 : 0);
1935 const double t = block_start + double(o) / rate;
1936
1937 const std::optional<int> pending_number = btrk::beat_number_of(val);
1938
1939 m_last_event_time = t;
1940 outputs.onset(o);
1941
1942 if(!inputs.follow || inputs.hold)
1943 continue;
1944
1945 if(!m_ev.push(t, div, min_p, max_p))
1946 continue;
1947
1948 // Only whole beats carry phase: a subdivision between beats would drag
1949 // the clock onto the subdivision grid.
1950 const bool on_beat = (div == 1) || (pending_number && (*pending_number % div == 0));
1951 if(!on_beat)
1952 continue;
1953
1954 if(!m_dll.inited)
1955 {
1956 m_dll.seed(t + m_ev.period, m_ev.period);
1957 m_elevated_until = m_now + 4.;
1958 }
1959 else
1960 {
1961 // Acquisition: adopt the measured period outright rather than letting
1962 // the loop crawl to it at 5% per beat, same reasoning as the audio path.
1963 if(!m_is_locked && m_ev.confidence > 0.2 && m_dll.bw > 0.)
1964 m_dll.e2 = std::clamp(
1965 m_ev.period, btrk::beat_dll::min_period, btrk::beat_dll::max_period);
1966
1967 m_dll.observe(t + m_dll.e2, std::min(t, block_end));
1968 }
1969
1970 // With a beat number we know absolute position, so the downbeat can be
1971 // placed on the right beat of the bar instead of wherever we started.
1972 if(pending_number)
1973 {
1974 const int bpb = std::max(1, (int)inputs.beats_per_bar);
1975 const int beat_in_bar = ((*pending_number / div) % bpb + bpb) % bpb;
1976 m_downbeat_origin = m_dll.beat_index + 1 - beat_in_bar;
1977 }
1978 }
1979 }
1980
1981 void emit_beats(double rate, double block_start, int frames)
1982 {
1983 if(!m_dll.inited)
1984 return;
1985 const double lookahead = inputs.lookahead * 1e-3;
1986 const double offset = inputs.offset * 1e-3;
1987 const double block_end = block_start + frames / rate;
1988
1989 // Scan the pending beats: beat k (global index beat_index + 1 + k) is due
1990 // at t1 + k*e2; it is emitted `lookahead + offset` early.
1991 for(int k = 0; k < 8; k++)
1992 {
1993 const double tb = m_dll.t1 + k * m_dll.e2;
1994 const double emit_time = tb - lookahead - offset;
1995 if(emit_time >= block_end)
1996 break;
1997 const int64_t index = m_dll.beat_index + 1 + k;
1998 if(index <= m_last_emitted_beat)
1999 continue;
2000 if(emit_time < block_start - 0.5 * m_dll.e2)
2001 continue; // stale
2002 const int64_t off = std::clamp<int64_t>(
2003 (int64_t)std::lround((emit_time - block_start) * rate), 0, frames - 1);
2004 m_last_emitted_beat = index;
2005 outputs.beat(off);
2006 const int bpb = std::max(1, (int)inputs.beats_per_bar);
2007 if(((index - m_downbeat_origin) % bpb + bpb) % bpb == 0)
2008 outputs.downbeat(off);
2009 }
2010
2011 // roll the oscillator state up to now
2012 m_dll.advance(m_now, [](double, int64_t) {});
2013 }
2014
2015 void update_outputs(const halp::tick_flicks& tk, double block_dt)
2016 {
2017 const bool valid = m_gate_open && m_dll.inited;
2018
2019 // confidence. In Events mode the cumulative score and the ACF estimator
2020 // never ran, so their values are stale; interval agreement is the evidence
2021 // we actually have. With both sources, take whichever is more sure.
2022 const double rel_rms
2023 = m_dll.e2 > 0. ? std::sqrt(m_dll.innovation_rms) / m_dll.e2 : 1.;
2024 double contrast = m_ct.last_contrast;
2025 double est_conf = m_est.confidence;
2026 if(inputs.source.value != source_mode::audio)
2027 {
2028 const double ev = (m_now - m_last_event_time) < event_hang() ? m_ev.confidence : 0.;
2029 if(inputs.source.value == source_mode::events)
2030 {
2031 contrast = ev;
2032 est_conf = ev;
2033 }
2034 else
2035 {
2036 contrast = std::max(contrast, ev);
2037 est_conf = std::max(est_conf, ev);
2038 }
2039 }
2040 m_mon.update(block_dt, contrast, est_conf, rel_rms);
2041
2042 // Tempo stability over the last ~4 s, sampled at 16 Hz.
2043 m_tempo_accum_t += block_dt;
2044 if(m_tempo_accum_t >= 1. / 16. && m_dll.inited)
2045 {
2046 m_tempo_accum_t = 0.;
2047 m_tempo_hist[m_tempo_hist_pos] = m_dll.tempo();
2048 m_tempo_hist_pos = (m_tempo_hist_pos + 1) % tempo_hist_n;
2049 m_tempo_hist_len = std::min(m_tempo_hist_len + 1, tempo_hist_n);
2050
2051 if(m_tempo_hist_len >= tempo_hist_n / 2)
2052 {
2053 double mean = 0.;
2054 for(int i = 0; i < m_tempo_hist_len; i++)
2055 mean += m_tempo_hist[i];
2056 mean /= m_tempo_hist_len;
2057 double var = 0.;
2058 for(int i = 0; i < m_tempo_hist_len; i++)
2059 var += (m_tempo_hist[i] - mean) * (m_tempo_hist[i] - mean);
2060 m_tempo_rel_sd
2061 = mean > 1. ? std::sqrt(var / m_tempo_hist_len) / mean : 1.;
2062 }
2063 }
2064
2065 // Locked if the evidence is strong OR the clock has simply been steady:
2066 // holding a period to within 0.5% for four seconds, with small DLL
2067 // innovations, is a lock by any useful definition.
2068 const bool steady = m_tempo_rel_sd < 0.005 && rel_rms < 0.15
2069 && m_tempo_hist_len >= tempo_hist_n / 2;
2070 if((m_mon.combined > 0.6 || steady) && valid)
2071 m_locked_count = std::min(m_locked_count + 1, 1000);
2072 else
2073 m_locked_count = std::max(m_locked_count - 2, 0);
2074 m_is_locked = m_locked_count > 20;
2075
2076 const double raw_tempo = m_dll.inited ? m_dll.tempo() : 120.;
2077 const double alpha = ossia::lag_alpha(0.25, block_dt);
2078 const double display_tempo = m_tempo_display(raw_tempo, alpha);
2079
2080 // Rate output, Mixxx-shaped: emit a rate, not a position; correct phase
2081 // through the rate, capped and slew-limited, spread over the next beats.
2082 const double ref_tempo = (inputs.hint && tk.tempo > 0.) ? tk.tempo : 120.;
2083 const double tempo_ratio = raw_tempo / ref_tempo;
2084 double err_beats = 0.;
2085 if(m_dll.inited && valid)
2086 {
2087 const double our_phase = m_dll.phase(m_now);
2088 const double transport_phase
2089 = tk.end_position_in_quarters - std::floor(tk.end_position_in_quarters);
2090 err_beats = our_phase - transport_phase;
2091 err_beats -= std::round(err_beats); // wrap to +-0.5 beat
2092 if(std::abs(err_beats) < sync_error_deadband)
2093 err_beats = 0.;
2094 }
2095 double trim_target
2096 = std::clamp(sync_p_gain * err_beats, -sync_adjustment_cap, sync_adjustment_cap);
2097 m_speed_trim
2098 += std::clamp(trim_target - m_speed_trim, -sync_delta_cap, sync_delta_cap);
2099
2100 outputs.tempo = display_tempo;
2101 m_last_emitted_tempo = display_tempo;
2102 outputs.speed = valid ? tempo_ratio * (1. + m_speed_trim) : 1.;
2103 outputs.phase = m_dll.phase(m_now);
2104 const int bpb = std::max(1, (int)inputs.beats_per_bar);
2105 const int64_t idx = m_dll.beat_index < 0 ? 0 : m_dll.beat_index;
2106 const int64_t in_bar = ((idx - m_downbeat_origin) % bpb + bpb) % bpb;
2107 outputs.bar_phase = (in_bar + outputs.phase) / bpb;
2108 outputs.beat_index = (int)idx;
2109 const double lookahead = inputs.lookahead * 1e-3;
2110 outputs.next_beat
2111 = m_dll.inited ? std::max(0., (m_dll.t1 - m_now) - lookahead) : 0.;
2112 outputs.confidence = m_mon.combined;
2113 outputs.locked = m_is_locked;
2114 outputs.valid = valid;
2115 }
2116};
2117}
STL namespace.
Definition BeatTracker.hpp:1039
Definition BeatTracker.hpp:1365
Definition BeatTracker.hpp:1010
source_mode
Definition BeatTracker.hpp:1032
std::pair< double, double > period_bounds() const
Definition BeatTracker.hpp:1907
void process_events(double rate, double block_start, int frames)
Definition BeatTracker.hpp:1926
double event_hang() const
Definition BeatTracker.hpp:1917
Definition BeatTracker.hpp:703
Definition BeatTracker.hpp:839
Definition BeatTracker.hpp:568
Definition BeatTracker.hpp:937
bool push(double t, int div, double min_period, double max_period)
Definition BeatTracker.hpp:960
Definition BeatTracker.hpp:76
Definition BeatTracker.hpp:888
Definition BeatTracker.hpp:329
Definition MIDISync.hpp:126