midi_controller/
tap_tempo.rs1const MAX_TAPS: usize = 4;
7const TIMEOUT_MS: u32 = 2000;
8const MIN_INTERVAL_MS: u32 = 300;
9const MIN_BPM: u16 = 30;
10const MAX_BPM: u16 = 200;
11
12#[derive(Debug, Clone)]
14pub struct TapTempo {
15 taps: [u32; MAX_TAPS],
17 count: u8,
19}
20
21impl Default for TapTempo {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl TapTempo {
28 pub fn new() -> Self {
29 Self {
30 taps: [0; MAX_TAPS],
31 count: 0,
32 }
33 }
34
35 pub fn tap(&mut self, now_ms: u32) -> Option<u16> {
38 if self.count > 0 {
40 let last_idx = (self.count - 1).min(MAX_TAPS as u8 - 1) as usize;
41 let last = self.taps[last_idx];
42 let elapsed = now_ms.wrapping_sub(last);
43 if elapsed > TIMEOUT_MS {
44 self.count = 0;
45 } else if elapsed < MIN_INTERVAL_MS {
46 return None;
48 }
49 }
50
51 if (self.count as usize) < MAX_TAPS {
53 self.taps[self.count as usize] = now_ms;
54 self.count += 1;
55 } else {
56 self.taps.rotate_left(1);
58 self.taps[MAX_TAPS - 1] = now_ms;
59 }
60
61 if (self.count as usize) < MAX_TAPS {
63 return None;
64 }
65
66 let n = self.count as usize;
68 let total_interval = self.taps[n - 1].wrapping_sub(self.taps[0]);
69 let avg_interval = total_interval / (n as u32 - 1);
70
71 if avg_interval == 0 {
72 return None;
73 }
74
75 let bpm = (60_000 / avg_interval) as u16;
77 let bpm = bpm.clamp(MIN_BPM, MAX_BPM);
78
79 Some(bpm)
80 }
81
82 pub fn reset(&mut self) {
84 self.count = 0;
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn first_tap_returns_none() {
94 let mut tt = TapTempo::new();
95 assert_eq!(tt.tap(0), None);
96 }
97
98 #[test]
99 fn three_taps_returns_none() {
100 let mut tt = TapTempo::new();
101 tt.tap(0);
102 assert_eq!(tt.tap(500), None);
103 assert_eq!(tt.tap(1000), None);
104 }
105
106 #[test]
107 fn four_taps_at_120bpm() {
108 let mut tt = TapTempo::new();
109 tt.tap(0);
110 tt.tap(500);
111 tt.tap(1000);
112 assert_eq!(tt.tap(1500), Some(120));
114 }
115
116 #[test]
117 fn uneven_taps_averages() {
118 let mut tt = TapTempo::new();
119 tt.tap(0);
120 tt.tap(400);
121 tt.tap(900);
122 assert_eq!(tt.tap(1400), Some(128));
124 }
125
126 #[test]
127 fn timeout_resets() {
128 let mut tt = TapTempo::new();
129 tt.tap(0);
130 tt.tap(500);
131 tt.tap(1000);
132 tt.tap(1500); assert_eq!(tt.tap(5500), None); assert_eq!(tt.tap(6000), None);
136 assert_eq!(tt.tap(6500), None);
137 assert_eq!(tt.tap(7000), Some(120)); }
139
140 #[test]
141 fn clamps_at_max_bpm() {
142 let mut tt = TapTempo::new();
143 tt.tap(0);
144 tt.tap(300);
145 tt.tap(600);
146 assert_eq!(tt.tap(900), Some(200));
148 }
149
150 #[test]
151 fn clamps_at_min_bpm() {
152 let mut tt = TapTempo::new();
153 tt.tap(0);
154 tt.tap(1900);
155 tt.tap(3800);
156 assert_eq!(tt.tap(5700), Some(31));
158 }
159
160 #[test]
161 fn more_than_max_taps_shifts_window() {
162 let mut tt = TapTempo::new();
163 tt.tap(0);
164 tt.tap(500);
165 tt.tap(1000);
166 tt.tap(1500);
167 assert_eq!(tt.tap(2000), Some(120));
169 }
170
171 #[test]
172 fn reset_clears_state() {
173 let mut tt = TapTempo::new();
174 tt.tap(0);
175 tt.tap(500);
176 tt.tap(1000);
177 tt.tap(1500);
178 tt.reset();
179 assert_eq!(tt.tap(2000), None); }
181
182 #[test]
183 fn ignores_bounce_tap() {
184 let mut tt = TapTempo::new();
185 tt.tap(0);
186 assert_eq!(tt.tap(50), None);
188 tt.tap(500);
189 tt.tap(1000);
190 assert_eq!(tt.tap(1500), Some(120));
192 }
193
194 #[test]
195 fn ignores_bounce_mid_sequence() {
196 let mut tt = TapTempo::new();
197 tt.tap(0);
198 tt.tap(500);
199 tt.tap(1000);
200 assert_eq!(tt.tap(1050), None);
202 assert_eq!(tt.tap(1500), Some(120));
204 }
205}