1#![deny(
6 clippy::unwrap_used,
7 clippy::expect_used,
8 clippy::indexing_slicing,
9 clippy::panic,
10 clippy::unreachable
11)]
12
13use super::WINDOW_SIZE;
14
15const MAG_BINS: usize = WINDOW_SIZE / 2;
16const HISTORY: usize = 43;
19const REFRACTORY_HOPS: u32 = 9;
21const THRESHOLD_K: f32 = 1.5;
23const ABS_FLOOR: f32 = 1e-6;
26
27pub struct OnsetDetector {
29 prev: [f32; MAG_BINS],
30 have_prev: bool,
31 history: [f32; HISTORY],
32 hist_pos: usize,
33 hist_len: usize,
34 refractory: u32,
35}
36
37impl OnsetDetector {
38 pub fn new() -> Self {
40 Self {
41 prev: [0.0; MAG_BINS],
42 have_prev: false,
43 history: [0.0; HISTORY],
44 hist_pos: 0,
45 hist_len: 0,
46 refractory: 0,
47 }
48 }
49
50 #[allow(
52 clippy::indexing_slicing,
53 reason = "hist_pos < HISTORY (kept modulo HISTORY), a valid index into the ring history"
54 )]
55 pub fn process(&mut self, mags: &[f32; MAG_BINS]) -> (f32, bool) {
56 let mut flux = 0.0f32;
58 if self.have_prev {
59 for (m, p) in mags.iter().zip(self.prev.iter()) {
60 flux += (m - p).max(0.0);
61 }
62 flux /= MAG_BINS as f32;
63 }
64 self.prev.copy_from_slice(mags);
65 self.have_prev = true;
66
67 let (mean, std) = self.history_stats();
70 let over_threshold = flux > mean + THRESHOLD_K * std && flux > ABS_FLOOR;
71 let beat = self.refractory == 0 && over_threshold;
72 if beat {
73 self.refractory = REFRACTORY_HOPS;
74 } else {
75 self.refractory = self.refractory.saturating_sub(1);
76 }
77
78 self.history[self.hist_pos] = flux;
79 self.hist_pos = (self.hist_pos + 1) % HISTORY;
80 self.hist_len = (self.hist_len + 1).min(HISTORY);
81
82 (flux, beat)
83 }
84
85 #[allow(
86 clippy::indexing_slicing,
87 reason = "hist_len <= HISTORY, so history[..hist_len] is always in range"
88 )]
89 fn history_stats(&self) -> (f32, f32) {
90 if self.hist_len == 0 {
91 return (0.0, 0.0);
92 }
93 let n = self.hist_len as f32;
94 let slice = &self.history[..self.hist_len];
95 let mean = slice.iter().sum::<f32>() / n;
96 let var = slice.iter().map(|f| (f - mean) * (f - mean)).sum::<f32>() / n;
97 (mean, var.sqrt())
98 }
99}
100
101impl Default for OnsetDetector {
102 fn default() -> Self {
103 Self::new()
104 }
105}