1use crate::util::{self, mel_filterbank, next_pow2};
14use ferraus_core::AudioBuffer;
15use serde::Serialize;
16
17#[derive(Clone, Debug)]
19pub struct TempoOptions {
20 pub min_bpm: f64,
22 pub max_bpm: f64,
24 pub prior_bpm: f64,
26 pub prior_octaves: f64,
28 pub tightness: f64,
30}
31
32impl Default for TempoOptions {
33 fn default() -> Self {
34 TempoOptions { min_bpm: 60.0, max_bpm: 200.0, prior_bpm: 120.0, prior_octaves: 1.0, tightness: 100.0 }
35 }
36}
37
38#[derive(Clone, Debug, Serialize)]
40pub struct TempoCandidate {
41 pub bpm: f64,
42 pub score: f64,
43}
44
45#[derive(Clone, Debug, Serialize)]
47pub struct TempoResult {
48 pub bpm: f64,
50 pub confidence: f64,
52 pub beats: Vec<f64>,
54 pub downbeats: Vec<f64>,
56 pub downbeat_confidence: f64,
58 pub time_signature: String,
60 pub time_signature_confidence: f64,
62 pub alternatives: Vec<TempoCandidate>,
64}
65
66impl TempoResult {
67 fn empty() -> TempoResult {
68 TempoResult {
69 bpm: 0.0,
70 confidence: 0.0,
71 beats: Vec::new(),
72 downbeats: Vec::new(),
73 downbeat_confidence: 0.0,
74 time_signature: "4/4".into(),
75 time_signature_confidence: 0.0,
76 alternatives: Vec::new(),
77 }
78 }
79}
80
81#[derive(Clone, Debug)]
84pub struct OnsetEnvelope {
85 pub frame_rate: f64,
86 pub values: Vec<f32>,
88 pub low: Vec<f32>,
90 pub low_level_db: Vec<f32>,
92}
93
94pub fn onset_envelope(buf: &AudioBuffer) -> OnsetEnvelope {
96 let sr = buf.sample_rate as f64;
97 let x = buf.mono_mix();
98 let hop = ((sr / 100.0).round() as usize).max(1);
99 let frame_rate = sr / hop as f64;
100 if x.len() < hop * 4 {
101 return OnsetEnvelope {
102 frame_rate,
103 values: vec![0.0; x.len() / hop + 1],
104 low: vec![0.0; x.len() / hop + 1],
105 low_level_db: vec![-80.0; x.len() / hop + 1],
106 };
107 }
108 let n_fft = next_pow2((sr * 0.023) as usize).max(256);
109 let n_mels = 40;
110 let bank = mel_filterbank(sr, n_fft, n_mels, 30.0, 8000.0_f64.min(sr / 2.0));
111 let low_bands = (0..n_mels)
112 .filter(|m| {
113 let c = bank[*m].iter().map(|(k, _)| *k).max().unwrap_or(0) as f64 * sr / n_fft as f64;
114 c < 200.0
115 })
116 .count()
117 .max(1);
118 let frames = util::stft_mag(&x, n_fft, hop);
119 let mut mel: Vec<Vec<f32>> = frames
121 .iter()
122 .map(|f| {
123 let p: Vec<f32> = f.iter().map(|m| m * m).collect();
124 util::apply_bank(&bank, &p).into_iter().map(|e| 10.0 * (e.max(1e-12)).log10()).collect()
125 })
126 .collect();
127 let top = mel.iter().flat_map(|f| f.iter()).fold(f32::NEG_INFINITY, |m, v| m.max(*v));
128 for f in mel.iter_mut() {
129 for v in f.iter_mut() {
130 *v = v.max(top - 80.0);
131 }
132 }
133 let n = mel.len();
134 let low_level_db: Vec<f32> = mel
135 .iter()
136 .map(|f| {
137 let e: f32 = f[..low_bands].iter().map(|db| 10f32.powf(db / 10.0)).sum();
138 10.0 * e.max(1e-12).log10() - top
139 })
140 .collect();
141 let mut flux = vec![0.0f32; n];
142 let mut low = vec![0.0f32; n];
143 for t in 1..n {
144 let mut s = 0.0;
145 let mut sl = 0.0;
146 for (m, (cur, prev)) in mel[t].iter().zip(&mel[t - 1]).enumerate() {
147 let d = (cur - prev).max(0.0);
148 s += d;
149 if m < low_bands {
150 sl += d;
151 }
152 }
153 flux[t] = s / n_mels as f32;
154 low[t] = sl / low_bands as f32;
155 }
156 let values = normalise_onset(&flux, frame_rate);
157 let low = normalise_onset(&low, frame_rate);
158 OnsetEnvelope { frame_rate, values, low, low_level_db }
159}
160
161fn normalise_onset(x: &[f32], frame_rate: f64) -> Vec<f32> {
163 let n = x.len();
164 let w = (frame_rate * 0.5).round() as usize;
165 let mut cs = vec![0.0f64; n + 1];
166 for i in 0..n {
167 cs[i + 1] = cs[i] + x[i] as f64;
168 }
169 let mut out: Vec<f32> = (0..n)
170 .map(|i| {
171 let a = i.saturating_sub(w);
172 let b = (i + w + 1).min(n);
173 let m = (cs[b] - cs[a]) / (b - a) as f64;
174 (x[i] as f64 - m).max(0.0) as f32
175 })
176 .collect();
177 let v: Vec<f64> = out.iter().map(|v| *v as f64).collect();
178 let sd = util::std_dev(&v);
179 if sd > 1e-9 {
180 for o in out.iter_mut() {
181 *o = (*o as f64 / sd) as f32;
182 }
183 }
184 out
185}
186
187fn autocorr(x: &[f32], max_lag: usize) -> Vec<f64> {
189 let n = x.len();
190 let m = x.iter().map(|v| *v as f64).sum::<f64>() / n.max(1) as f64;
191 let y: Vec<f64> = x.iter().map(|v| *v as f64 - m).collect();
192 let mut ac = vec![0.0f64; max_lag + 1];
193 for (lag, a) in ac.iter_mut().enumerate() {
194 if lag >= n {
195 break;
196 }
197 let s: f64 = y[..n - lag].iter().zip(&y[lag..]).map(|(a, b)| a * b).sum();
198 *a = s / (n - lag) as f64;
199 }
200 let a0 = ac[0];
201 if a0 > 0.0 {
202 for a in ac.iter_mut() {
203 *a /= a0;
204 }
205 }
206 ac
207}
208
209fn interp(v: &[f64], pos: f64) -> f64 {
210 if pos < 0.0 {
211 return 0.0;
212 }
213 let i = pos.floor() as usize;
214 if i + 1 >= v.len() {
215 return 0.0;
216 }
217 let f = pos - i as f64;
218 v[i] * (1.0 - f) + v[i + 1] * f
219}
220
221pub fn detect_tempo(buf: &AudioBuffer, opts: &TempoOptions) -> TempoResult {
223 let env = onset_envelope(buf);
224 detect_tempo_from_onsets(&env, opts)
225}
226
227pub fn detect_tempo_from_onsets(env: &OnsetEnvelope, opts: &TempoOptions) -> TempoResult {
229 let fr = env.frame_rate;
230 let o = &env.values;
231 let n = o.len();
232 let (min_bpm, max_bpm) = (opts.min_bpm.max(20.0).min(opts.max_bpm), opts.max_bpm.max(opts.min_bpm));
233 if n < (fr * 1.5) as usize || o.iter().all(|v| *v <= 0.0) {
234 return TempoResult::empty();
235 }
236 let max_lag = ((60.0 * fr / (min_bpm / 3.0)).ceil() as usize).min(n - 1);
238 let ac = autocorr(o, max_lag);
239 let prior = |bpm: f64| -> f64 {
240 let z = (bpm / opts.prior_bpm).log2() / opts.prior_octaves;
241 (-0.5 * z * z).exp()
242 };
243 let tps = |bpm: f64| -> f64 {
244 let lag = 60.0 * fr / bpm;
245 let v = interp(&ac, lag).max(interp(&ac, lag - 0.5)).max(interp(&ac, lag + 0.5));
247 prior(bpm) * v.max(0.0)
248 };
249 let score = |bpm: f64| -> f64 { tps(bpm) + 0.5 * tps(bpm / 2.0) + 0.25 * tps(bpm / 3.0) };
250 let step = 0.25;
251 let mut grid = Vec::new();
252 let mut b = min_bpm;
253 while b <= max_bpm + 1e-9 {
254 grid.push((b, score(b)));
255 b += step;
256 }
257 let best_score = grid.iter().map(|g| g.1).fold(0.0, f64::max);
258 if best_score <= 0.0 {
259 return TempoResult::empty();
260 }
261 let mut peaks: Vec<(f64, f64)> = (0..grid.len())
263 .filter(|&i| {
264 let l = if i > 0 { grid[i - 1].1 } else { f64::NEG_INFINITY };
265 let r = if i + 1 < grid.len() { grid[i + 1].1 } else { f64::NEG_INFINITY };
266 grid[i].1 >= l && grid[i].1 > r
267 })
268 .map(|i| grid[i])
269 .collect();
270 peaks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
271 let coarse_bpm = peaks.first().map(|p| p.0).unwrap_or(120.0);
272
273 let period = 60.0 * fr / coarse_bpm;
274 let beats_f = track_beats(o, period, opts.tightness);
275 if beats_f.len() < 2 {
276 return TempoResult::empty();
277 }
278 let bias = 0.0;
280 let beats: Vec<f64> = beats_f.iter().map(|b| *b as f64 / fr + bias).collect();
281
282 let bpm = refine_bpm(&beats).unwrap_or(coarse_bpm);
284
285 let lm: Vec<f64> = (0..n).map(|i| local_max(o, i, 1) as f64).collect();
287 let mean_all = util::mean(&lm);
288 let at_beats = beats_f.iter().map(|&b| lm[b]).sum::<f64>() / beats_f.len() as f64;
289 let salience = if mean_all > 0.0 { ((at_beats / mean_all - 1.0) / 2.0).clamp(0.0, 1.0) } else { 0.0 };
290 let lag = 60.0 * fr / bpm;
291 let periodicity = (interp(&ac, lag).max(interp(&ac, lag - 0.5)).max(interp(&ac, lag + 0.5)) / 0.5).clamp(0.0, 1.0);
292 let ibis: Vec<f64> = beats.windows(2).map(|w| w[1] - w[0]).collect();
293 let mean_ibi = util::mean(&ibis);
294 let regularity = if mean_ibi > 0.0 { (-(util::std_dev(&ibis) / mean_ibi) / 0.1).exp() } else { 0.0 };
295 let confidence = ((salience * periodicity).sqrt() * regularity).clamp(0.0, 1.0);
296
297 let alternatives: Vec<TempoCandidate> = peaks
298 .iter()
299 .filter(|p| (p.0 - coarse_bpm).abs() > 3.0)
300 .take(3)
301 .map(|p| TempoCandidate { bpm: p.0, score: p.1 / best_score })
302 .collect();
303
304 let meter = analyse_meter(env, &beats_f, 60.0 * fr / bpm);
305 let downbeats = if meter.downbeat_confidence >= 0.35 && confidence > 0.3 {
306 beats.iter().enumerate().filter(|(i, _)| i % meter.beats_per_bar == meter.phase).map(|(_, t)| *t).collect()
307 } else {
308 Vec::new()
309 };
310 TempoResult {
311 bpm,
312 confidence,
313 beats,
314 downbeats,
315 downbeat_confidence: meter.downbeat_confidence,
316 time_signature: meter.signature,
317 time_signature_confidence: meter.confidence,
318 alternatives,
319 }
320}
321
322fn local_max(x: &[f32], i: usize, r: usize) -> f32 {
323 let a = i.saturating_sub(r);
324 let b = (i + r + 1).min(x.len());
325 x[a..b].iter().fold(0.0f32, |m, v| m.max(*v))
326}
327
328fn refine_bpm(beats: &[f64]) -> Option<f64> {
331 if beats.len() < 4 {
332 return None;
333 }
334 let ibis: Vec<f64> = beats.windows(2).map(|w| w[1] - w[0]).collect();
335 let med = util::median(&ibis);
336 if med <= 0.0 {
337 return None;
338 }
339 let mut idx = vec![0.0f64];
341 for w in ibis.iter() {
342 let k = (w / med).round().max(1.0);
343 idx.push(idx.last().unwrap() + k);
344 }
345 let n = beats.len() as f64;
346 let mx = idx.iter().sum::<f64>() / n;
347 let my = beats.iter().sum::<f64>() / n;
348 let (mut sxy, mut sxx) = (0.0, 0.0);
349 for (x, y) in idx.iter().zip(beats) {
350 sxy += (x - mx) * (y - my);
351 sxx += (x - mx) * (x - mx);
352 }
353 if sxx <= 0.0 {
354 return None;
355 }
356 let slope = sxy / sxx;
357 if slope <= 0.0 {
358 None
359 } else {
360 Some(60.0 / slope)
361 }
362}
363
364fn track_beats(onset: &[f32], period: f64, tightness: f64) -> Vec<usize> {
366 let n = onset.len();
367 if period < 1.0 || n < 2 {
368 return Vec::new();
369 }
370 let sigma = (period / 32.0).max(0.5);
372 let half = (sigma * 4.0).ceil() as isize;
373 let kernel: Vec<f64> = (-half..=half).map(|k| (-0.5 * (k as f64 / sigma).powi(2)).exp()).collect();
374 let local: Vec<f64> = (0..n as isize)
375 .map(|i| {
376 let mut s = 0.0;
377 for (j, w) in kernel.iter().enumerate() {
378 let idx = i + j as isize - half;
379 if idx >= 0 && (idx as usize) < n {
380 s += onset[idx as usize] as f64 * w;
381 }
382 }
383 s
384 })
385 .collect();
386 let lo = (period / 2.0).round() as usize;
387 let hi = (2.0 * period).round() as usize;
388 let mut cum = vec![0.0f64; n];
389 let mut back = vec![usize::MAX; n];
390 let cost: Vec<f64> = (0..=hi)
392 .map(|d| if d == 0 { f64::NEG_INFINITY } else { -tightness * ((d as f64 / period).ln()).powi(2) })
393 .collect();
394 for t in 0..n {
395 let mut best = f64::NEG_INFINITY;
396 let mut arg = usize::MAX;
397 if t >= lo {
398 let start = t.saturating_sub(hi);
399 for tau in start..=t - lo.max(1) {
400 let v = cum[tau] + cost[t - tau];
401 if v > best {
402 best = v;
403 arg = tau;
404 }
405 }
406 }
407 if arg != usize::MAX && best > 0.0 {
409 cum[t] = local[t] + best;
410 back[t] = arg;
411 } else {
412 cum[t] = local[t];
413 }
414 }
415 let maxima: Vec<usize> =
417 (1..n.saturating_sub(1)).filter(|&i| cum[i] > cum[i - 1] && cum[i] >= cum[i + 1]).collect();
418 if maxima.is_empty() {
419 return Vec::new();
420 }
421 let vals: Vec<f64> = maxima.iter().map(|&i| cum[i]).collect();
422 let med = util::median(&vals);
423 let tail_start = n.saturating_sub((2.0 * period) as usize + 1);
425 let mut last = *maxima.iter().rev().find(|&&i| cum[i] >= 0.5 * med).unwrap_or(maxima.last().unwrap());
426 let mut best_tail = f64::NEG_INFINITY;
427 for &i in maxima.iter().filter(|&&i| i >= tail_start) {
428 if cum[i] > best_tail {
429 best_tail = cum[i];
430 last = i;
431 }
432 }
433 let mut beats = vec![last];
434 let mut cur = last;
435 while back[cur] != usize::MAX {
436 cur = back[cur];
437 beats.push(cur);
438 }
439 beats.reverse();
440 trim_weak_beats(&local, beats)
441}
442
443fn trim_weak_beats(local: &[f64], mut beats: Vec<usize>) -> Vec<usize> {
445 if beats.len() < 3 {
446 return beats;
447 }
448 let vals: Vec<f64> = beats.iter().map(|&b| local[b]).collect();
449 let rms = (vals.iter().map(|v| v * v).sum::<f64>() / vals.len() as f64).sqrt();
450 let thr = 0.5 * rms;
451 while beats.len() > 2 && local[beats[0]] < thr {
452 beats.remove(0);
453 }
454 while beats.len() > 2 && local[*beats.last().unwrap()] < thr {
455 beats.pop();
456 }
457 beats
458}
459
460struct Meter {
461 signature: String,
462 beats_per_bar: usize,
463 phase: usize,
464 confidence: f64,
465 downbeat_confidence: f64,
466}
467
468fn analyse_meter(env: &OnsetEnvelope, beats: &[usize], period: f64) -> Meter {
469 let default =
470 Meter { signature: "4/4".into(), beats_per_bar: 4, phase: 0, confidence: 0.0, downbeat_confidence: 0.0 };
471 if beats.len() < 8 {
472 return default;
473 }
474 let r = ((period / 8.0).round() as usize).max(1);
475 let zscore = |v: Vec<f64>| -> Vec<f64> {
477 let m = util::mean(&v);
478 let sd = util::std_dev(&v).max(1e-9);
479 v.into_iter().map(|x| (x - m) / sd).collect()
480 };
481 let level = zscore(
482 beats
483 .iter()
484 .map(|&b| {
485 let a = b.saturating_sub(1);
486 let e = (b + r + 1).min(env.low_level_db.len());
487 env.low_level_db[a..e].iter().fold(-200.0f32, |m, v| m.max(*v)) as f64
488 })
489 .collect(),
490 );
491 let strength = zscore(beats.iter().map(|&b| local_max(&env.values, b, r) as f64).collect());
492 let acc: Vec<f64> = level.iter().zip(&strength).map(|(l, s)| 0.7 * l + 0.3 * s).collect();
493 let sd = util::std_dev(&acc).max(1e-9);
494 let fold = |m: usize| -> Vec<f64> {
495 (0..m)
496 .map(|phi| {
497 let (mut on, mut non, mut off, mut noff) = (0.0, 0usize, 0.0, 0usize);
498 for (i, a) in acc.iter().enumerate() {
499 if i % m == phi {
500 on += a;
501 non += 1;
502 } else {
503 off += a;
504 noff += 1;
505 }
506 }
507 if non == 0 || noff == 0 {
508 0.0
509 } else {
510 (on / non as f64 - off / noff as f64) / sd
511 }
512 })
513 .collect()
514 };
515 let c3 = fold(3);
516 let c4 = fold(4);
517 let best = |c: &[f64]| -> (usize, f64, f64) {
518 let mut idx: Vec<usize> = (0..c.len()).collect();
519 idx.sort_by(|a, b| c[*b].partial_cmp(&c[*a]).unwrap_or(std::cmp::Ordering::Equal));
520 (idx[0], c[idx[0]], c[idx[1]])
521 };
522 let (p3, s3, s3b) = best(&c3);
523 let (p4, s4, s4b) = best(&c4);
524 let o = &env.values;
526 let (mut tern, mut bin, mut cnt) = (0.0f64, 0.0f64, 0usize);
527 for w in beats.windows(2) {
528 let (a, b) = (w[0] as f64, w[1] as f64);
529 let d = b - a;
530 if d < 3.0 {
531 continue;
532 }
533 let at = |pos: f64| local_max(o, pos.round() as usize, 1) as f64;
534 tern += 0.5 * (at(a + d / 3.0) + at(a + 2.0 * d / 3.0));
535 bin += at(a + d / 2.0);
536 cnt += 1;
537 }
538 let ternary = cnt > 0 && tern > 1.5 * bin && tern / cnt as f64 > 0.3;
539 let (m, phase, s, sb, other) = if s3 > s4 * 1.15 { (3usize, p3, s3, s3b, s4) } else { (4usize, p4, s4, s4b, s3) };
540 let confidence = if s > 0.0 { ((s - other.max(0.0)) / s).clamp(0.0, 1.0) } else { 0.0 };
541 let downbeat_confidence = if s > 0.0 { ((s - sb.max(0.0)) / s).clamp(0.0, 1.0) * (s / 1.5).min(1.0) } else { 0.0 };
542 let signature = if ternary && m == 4 {
543 "6/8".to_string()
544 } else if m == 3 {
545 "3/4".to_string()
546 } else {
547 "4/4".to_string()
548 };
549 Meter { signature, beats_per_bar: m, phase, confidence, downbeat_confidence }
550}
551
552pub fn transient_density(buf: &AudioBuffer) -> f64 {
554 let env = onset_envelope(buf);
555 let o = &env.values;
556 if o.len() < 3 || buf.duration_s() <= 0.0 {
557 return 0.0;
558 }
559 let mut count = 0usize;
560 let min_gap = (env.frame_rate * 0.05) as usize;
561 let mut last: Option<usize> = None;
562 for i in 1..o.len() - 1 {
563 if o[i] > 1.5 && o[i] >= o[i - 1] && o[i] > o[i + 1] {
564 if let Some(l) = last {
565 if i - l < min_gap {
566 continue;
567 }
568 }
569 count += 1;
570 last = Some(i);
571 }
572 }
573 count as f64 / buf.duration_s()
574}
575
576#[cfg(test)]
577pub(crate) mod tests {
578 use super::*;
579 use ferraus_core::testing;
580 use std::f64::consts::PI;
581
582 pub(crate) struct Rng(u64);
584 impl Rng {
585 pub(crate) fn new(seed: u64) -> Rng {
586 Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1)
587 }
588 pub(crate) fn next(&mut self) -> f64 {
589 self.0 ^= self.0 << 13;
590 self.0 ^= self.0 >> 7;
591 self.0 ^= self.0 << 17;
592 (self.0 >> 11) as f64 / (1u64 << 53) as f64
593 }
594 pub(crate) fn sym(&mut self) -> f64 {
595 self.next() * 2.0 - 1.0
596 }
597 }
598
599 fn add_kick(x: &mut [f32], sr: f64, t: f64, amp: f64) {
600 let s = (t * sr) as usize;
601 let len = (0.25 * sr) as usize;
602 let mut ph = 0.0;
603 for i in 0..len {
604 if s + i >= x.len() {
605 break;
606 }
607 let tt = i as f64 / sr;
608 let f = 50.0 + 100.0 * (-tt / 0.03).exp();
609 ph += 2.0 * PI * f / sr;
610 x[s + i] += (amp * (-tt / 0.12).exp() * ph.sin()) as f32;
611 }
612 }
613
614 fn add_noise_hit(x: &mut [f32], sr: f64, t: f64, amp: f64, decay: f64, hp: bool, rng: &mut Rng) {
615 let s = (t * sr) as usize;
616 let len = (decay * 6.0 * sr) as usize;
617 let mut prev = 0.0;
618 for i in 0..len {
619 if s + i >= x.len() {
620 break;
621 }
622 let tt = i as f64 / sr;
623 let n = rng.sym();
624 let v = if hp { n - prev } else { n };
625 prev = n;
626 x[s + i] += (amp * (-tt / decay).exp() * v) as f32;
627 }
628 }
629
630 pub(crate) fn drum_track(bpm: f64, secs: f64, swing: f64, jitter_s: f64, seed: u64) -> (AudioBuffer, Vec<f64>) {
633 let sr = 22050.0;
634 let n = (secs * sr) as usize;
635 let mut x = vec![0.0f32; n];
636 let mut rng = Rng::new(seed);
637 let beat = 60.0 / bpm;
638 let start = 0.25;
639 let mut truth = Vec::new();
640 let mut i = 0usize;
641 loop {
642 let t0 = start + i as f64 * beat;
643 if t0 > secs - 0.3 {
644 break;
645 }
646 truth.push(t0);
647 let j = |r: &mut Rng| r.sym() * jitter_s;
648 match i % 4 {
649 0 => add_kick(&mut x, sr, t0 + j(&mut rng), 0.9),
650 2 => add_kick(&mut x, sr, t0 + j(&mut rng), 0.6),
651 _ => add_noise_hit(&mut x, sr, t0 + j(&mut rng), 0.5, 0.05, false, &mut rng),
652 }
653 add_noise_hit(&mut x, sr, t0 + j(&mut rng), 0.15, 0.015, true, &mut rng);
654 let off = t0 + beat * (0.5 + swing) + j(&mut rng);
655 add_noise_hit(&mut x, sr, off, 0.1, 0.015, true, &mut rng);
656 i += 1;
657 }
658 for v in x.iter_mut() {
659 *v += (rng.sym() * 0.02) as f32;
660 }
661 (AudioBuffer::from_mono(sr as u32, x), truth)
662 }
663
664 pub(crate) fn beat_hits(truth: &[f64], det: &[f64], tol: f64) -> f64 {
667 let score = |r: &[f64], d: &[f64]| -> f64 {
668 let hits = r.iter().filter(|t| d.iter().any(|b| (*b - **t).abs() <= tol)).count();
669 hits as f64 / r.len().max(1) as f64
670 };
671 let straight = score(truth, det);
672 let half_a: Vec<f64> = truth.iter().step_by(2).copied().collect();
673 let half_b: Vec<f64> = truth.iter().skip(1).step_by(2).copied().collect();
674 let dbl_a: Vec<f64> = det.iter().step_by(2).copied().collect();
675 let dbl_b: Vec<f64> = det.iter().skip(1).step_by(2).copied().collect();
676 let rd = det.len() as f64 / truth.len().max(1) as f64;
677 if rd < 0.7 {
678 score(&half_a, det).max(score(&half_b, det))
679 } else if rd > 1.4 {
680 straight.max(score(truth, &dbl_a)).max(score(truth, &dbl_b))
681 } else {
682 straight
683 }
684 }
685
686 fn tempo_ok(want: f64, got: f64) -> bool {
687 [1.0, 2.0, 0.5, 1.5, 2.0 / 3.0, 3.0, 1.0 / 3.0].iter().any(|m| (got - want * m).abs() <= 2.0)
688 }
689
690 #[test]
691 fn tempo_on_synthetic_drum_tracks() {
692 let tempi = [85.0, 100.0, 120.0, 128.0, 140.0, 174.0, 92.0, 110.0, 160.0, 70.0];
693 let mut ok = 0;
694 let mut exact = 0;
695 let mut total_hits = 0.0;
696 let mut total_beats = 0.0;
697 for (k, bpm) in tempi.iter().enumerate() {
698 let swing = if k % 3 == 1 { 0.12 } else { 0.0 };
699 let (buf, truth) = drum_track(*bpm, 10.0, swing, 0.008, k as u64 + 1);
700 let r = detect_tempo(&buf, &TempoOptions::default());
701 let good = tempo_ok(*bpm, r.bpm);
702 if good {
703 ok += 1;
704 }
705 if (r.bpm - bpm).abs() <= 2.0 {
706 exact += 1;
707 }
708 let h = beat_hits(&truth, &r.beats, 0.07);
709 eprintln!(
710 "bpm {bpm}: got {:.2} conf {:.2} beats {} hits {:.2} sig {} db {:?}",
711 r.bpm,
712 r.confidence,
713 r.beats.len(),
714 h,
715 r.time_signature,
716 r.downbeats.first()
717 );
718 total_hits += h * truth.len() as f64;
719 total_beats += truth.len() as f64;
720 }
721 assert!(ok as f64 >= 0.9 * tempi.len() as f64, "tempo ok {ok}/{}", tempi.len());
722 assert!(exact >= 7, "exact tempo {exact}/{}", tempi.len());
723 assert!(total_hits / total_beats >= 0.85, "beat accuracy {:.3}", total_hits / total_beats);
724 }
725
726 #[test]
727 fn click_track_and_range() {
728 let b = testing::clicks(22050, 0.5, 8.0, 0.9);
729 let r = detect_tempo(&b, &TempoOptions::default());
730 assert!((r.bpm - 120.0).abs() < 1.0, "{}", r.bpm);
731 assert!(r.confidence > 0.5, "{}", r.confidence);
732 let r = detect_tempo(&b, &TempoOptions { min_bpm: 180.0, max_bpm: 260.0, ..Default::default() });
734 assert!((r.bpm - 240.0).abs() < 2.0 || (r.bpm - 120.0).abs() < 1.0, "{}", r.bpm);
735 let b = testing::add(&testing::clicks(48000, 0.6, 9.0, 0.8), &testing::noise(48000, 9.0, 0.01, 5));
737 let r = detect_tempo(&b, &TempoOptions::default());
738 assert!((r.bpm - 100.0).abs() < 1.0, "{}", r.bpm);
739 assert!(r.beats.iter().any(|b| (b - 1.2).abs() < 0.03), "{:?}", &r.beats[..3]);
740 }
741
742 #[test]
743 fn silence_and_noise() {
744 let s = AudioBuffer::new(22050, 1, 22050 * 3);
745 let r = detect_tempo(&s, &TempoOptions::default());
746 assert_eq!(r.bpm, 0.0);
747 assert!(r.beats.is_empty());
748 let short = testing::sine(22050, 440.0, 0.2, 0.5);
749 let r = detect_tempo(&short, &TempoOptions::default());
750 assert!(r.beats.is_empty());
751 let nz = testing::noise(22050, 5.0, 0.3, 3);
752 let r = detect_tempo(&nz, &TempoOptions::default());
753 assert!(r.confidence < 0.5, "noise conf {}", r.confidence);
754 }
755
756 #[test]
757 fn three_four_accent() {
758 let sr = 22050.0;
760 let bpm = 120.0;
761 let secs = 12.0;
762 let mut x = vec![0.0f32; (secs * sr) as usize];
763 let mut rng = Rng::new(9);
764 let mut truth_db = Vec::new();
765 for i in 0..23 {
766 let t = 0.3 + i as f64 * 60.0 / bpm;
767 if i % 3 == 0 {
768 add_kick(&mut x, sr, t, 0.9);
769 truth_db.push(t);
770 }
771 add_noise_hit(&mut x, sr, t, 0.3, 0.03, true, &mut rng);
772 }
773 let r = detect_tempo(&AudioBuffer::from_mono(sr as u32, x), &TempoOptions::default());
774 assert_eq!(r.time_signature, "3/4", "{r:?}");
775 assert!(!r.downbeats.is_empty());
776 assert!(beat_hits(&truth_db, &r.downbeats, 0.07) > 0.8);
777 }
778
779 #[test]
780 fn density_counts_onsets() {
781 let b = testing::clicks(22050, 0.25, 6.0, 0.9);
782 let d = transient_density(&b);
783 assert!((d - 4.0).abs() < 0.6, "{d}");
784 }
785}