const MAX_TAPS: usize = 4;
const TIMEOUT_MS: u32 = 2000;
const MIN_BPM: u16 = 30;
const MAX_BPM: u16 = 300;
#[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 = self.taps[(self.count - 1) as usize];
if now_ms.wrapping_sub(last) > TIMEOUT_MS {
self.count = 0;
}
}
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 < 2 {
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 two_taps_at_120bpm() {
let mut tt = TapTempo::new();
tt.tap(0);
assert_eq!(tt.tap(500), Some(120));
}
#[test]
fn four_taps_averages_intervals() {
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);
assert_eq!(tt.tap(900), Some(133));
}
#[test]
fn timeout_resets() {
let mut tt = TapTempo::new();
tt.tap(0);
tt.tap(500); assert_eq!(tt.tap(5500), None); assert_eq!(tt.tap(6000), Some(120)); }
#[test]
fn clamps_at_max_bpm() {
let mut tt = TapTempo::new();
tt.tap(0);
assert_eq!(tt.tap(100), Some(300));
}
#[test]
fn clamps_at_min_bpm() {
let mut tt = TapTempo::new();
tt.tap(0);
assert_eq!(tt.tap(1999), Some(30));
}
#[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.reset();
assert_eq!(tt.tap(1000), None); }
}