midi_controller/
tap_tempo.rs1const MAX_TAPS: usize = 4;
7const TIMEOUT_MS: u32 = 2000;
8const MIN_BPM: u16 = 30;
9const MAX_BPM: u16 = 300;
10
11#[derive(Debug, Clone)]
13pub struct TapTempo {
14 taps: [u32; MAX_TAPS],
16 count: u8,
18}
19
20impl Default for TapTempo {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl TapTempo {
27 pub fn new() -> Self {
28 Self {
29 taps: [0; MAX_TAPS],
30 count: 0,
31 }
32 }
33
34 pub fn tap(&mut self, now_ms: u32) -> Option<u16> {
37 if self.count > 0 {
39 let last = self.taps[(self.count - 1) as usize];
40 if now_ms.wrapping_sub(last) > TIMEOUT_MS {
41 self.count = 0;
42 }
43 }
44
45 if (self.count as usize) < MAX_TAPS {
47 self.taps[self.count as usize] = now_ms;
48 self.count += 1;
49 } else {
50 self.taps.rotate_left(1);
52 self.taps[MAX_TAPS - 1] = now_ms;
53 }
54
55 if self.count < 2 {
57 return None;
58 }
59
60 let n = self.count as usize;
62 let total_interval = self.taps[n - 1].wrapping_sub(self.taps[0]);
63 let avg_interval = total_interval / (n as u32 - 1);
64
65 if avg_interval == 0 {
66 return None;
67 }
68
69 let bpm = (60_000 / avg_interval) as u16;
71 let bpm = bpm.clamp(MIN_BPM, MAX_BPM);
72
73 Some(bpm)
74 }
75
76 pub fn reset(&mut self) {
78 self.count = 0;
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn first_tap_returns_none() {
88 let mut tt = TapTempo::new();
89 assert_eq!(tt.tap(0), None);
90 }
91
92 #[test]
93 fn two_taps_at_120bpm() {
94 let mut tt = TapTempo::new();
95 tt.tap(0);
96 assert_eq!(tt.tap(500), Some(120));
98 }
99
100 #[test]
101 fn four_taps_averages_intervals() {
102 let mut tt = TapTempo::new();
103 tt.tap(0);
104 tt.tap(500); tt.tap(1000); assert_eq!(tt.tap(1500), Some(120));
108 }
109
110 #[test]
111 fn uneven_taps_averages() {
112 let mut tt = TapTempo::new();
113 tt.tap(0);
114 tt.tap(400);
115 assert_eq!(tt.tap(900), Some(133));
117 }
118
119 #[test]
120 fn timeout_resets() {
121 let mut tt = TapTempo::new();
122 tt.tap(0);
123 tt.tap(500); assert_eq!(tt.tap(5500), None); assert_eq!(tt.tap(6000), Some(120)); }
128
129 #[test]
130 fn clamps_at_max_bpm() {
131 let mut tt = TapTempo::new();
132 tt.tap(0);
133 assert_eq!(tt.tap(100), Some(300));
135 }
136
137 #[test]
138 fn clamps_at_min_bpm() {
139 let mut tt = TapTempo::new();
140 tt.tap(0);
141 assert_eq!(tt.tap(1999), Some(30));
143 }
144
145 #[test]
146 fn more_than_max_taps_shifts_window() {
147 let mut tt = TapTempo::new();
148 tt.tap(0);
149 tt.tap(500);
150 tt.tap(1000);
151 tt.tap(1500);
152 assert_eq!(tt.tap(2000), Some(120));
154 }
155
156 #[test]
157 fn reset_clears_state() {
158 let mut tt = TapTempo::new();
159 tt.tap(0);
160 tt.tap(500);
161 tt.reset();
162 assert_eq!(tt.tap(1000), None); }
164}