const MAX_TAPS: usize = 4;
const TIMEOUT_MS: u32 = 2000;
const MIN_INTERVAL_MS: u32 = 300;
const MIN_BPM: u16 = 30;
const MAX_BPM: u16 = 200;
#[derive(Debug, Clone)]
pub struct TapTempo {
taps: [u32; MAX_TAPS],
count: u8,
}
impl Default for TapTempo {
fn default() -> Self {
Self::new()
}
}
impl TapTempo {
pub fn new() -> Self {
Self {
taps: [0; MAX_TAPS],
count: 0,
}
}
pub fn tap(&mut self, now_ms: u32) -> Option<u16> {
if self.count > 0 {
let last_idx = (self.count - 1).min(MAX_TAPS as u8 - 1) as usize;
let last = self.taps[last_idx];
let elapsed = now_ms.wrapping_sub(last);
if elapsed > TIMEOUT_MS {
self.count = 0;
} else if elapsed < MIN_INTERVAL_MS {
return None;
}
}
if (self.count as usize) < MAX_TAPS {
self.taps[self.count as usize] = now_ms;
self.count += 1;
} else {
self.taps.rotate_left(1);
self.taps[MAX_TAPS - 1] = now_ms;
}
if (self.count as usize) < MAX_TAPS {
return None;
}
let n = self.count as usize;
let total_interval = self.taps[n - 1].wrapping_sub(self.taps[0]);
let avg_interval = total_interval / (n as u32 - 1);
if avg_interval == 0 {
return None;
}
let bpm = (60_000 / avg_interval) as u16;
let bpm = bpm.clamp(MIN_BPM, MAX_BPM);
Some(bpm)
}
pub fn reset(&mut self) {
self.count = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_tap_returns_none() {
let mut tt = TapTempo::new();
assert_eq!(tt.tap(0), None);
}
#[test]
fn three_taps_returns_none() {
let mut tt = TapTempo::new();
tt.tap(0);
assert_eq!(tt.tap(500), None);
assert_eq!(tt.tap(1000), None);
}
#[test]
fn four_taps_at_120bpm() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(500);
tt.tap(1000);
assert_eq!(tt.tap(1500), Some(120));
}
#[test]
fn uneven_taps_averages() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(400);
tt.tap(900);
assert_eq!(tt.tap(1400), Some(128));
}
#[test]
fn timeout_resets() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(500);
tt.tap(1000);
tt.tap(1500); assert_eq!(tt.tap(5500), None); assert_eq!(tt.tap(6000), None);
assert_eq!(tt.tap(6500), None);
assert_eq!(tt.tap(7000), Some(120)); }
#[test]
fn clamps_at_max_bpm() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(300);
tt.tap(600);
assert_eq!(tt.tap(900), Some(200));
}
#[test]
fn clamps_at_min_bpm() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(1900);
tt.tap(3800);
assert_eq!(tt.tap(5700), Some(31));
}
#[test]
fn more_than_max_taps_shifts_window() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(500);
tt.tap(1000);
tt.tap(1500);
assert_eq!(tt.tap(2000), Some(120));
}
#[test]
fn reset_clears_state() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(500);
tt.tap(1000);
tt.tap(1500);
tt.reset();
assert_eq!(tt.tap(2000), None); }
#[test]
fn ignores_bounce_tap() {
let mut tt = TapTempo::new();
tt.tap(0);
assert_eq!(tt.tap(50), None);
tt.tap(500);
tt.tap(1000);
assert_eq!(tt.tap(1500), Some(120));
}
#[test]
fn ignores_bounce_mid_sequence() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(500);
tt.tap(1000);
assert_eq!(tt.tap(1050), None);
assert_eq!(tt.tap(1500), Some(120));
}
}