use core::fmt;
use std::time::Instant;
use crate::ringbuf::RingBuf;
#[derive(Clone, Debug)]
pub struct Tapper {
buf: RingBuf<f32, { Self::MAX_CAP as usize }>,
cap: u16,
bounded: bool,
last_tap: Option<Instant>,
}
impl Tapper {
pub const MAX_CAP: u16 = 0x1000;
#[must_use]
pub const fn new(cap: u16, bounded: bool) -> Self {
Self {
buf: RingBuf::new(),
cap,
bounded,
last_tap: None,
}
}
pub fn tap(&mut self) {
let now = Instant::now();
if let Some(last) = self.last_tap.replace(now) {
let elapsed = now.saturating_duration_since(last).as_secs_f32();
let bpm = 60.0 / elapsed;
self.buf.push(bpm);
self.sync_cap();
}
}
pub fn clear(&mut self) {
self.buf.clear();
self.last_tap = None;
}
pub fn resize(&mut self, new_cap: u16) {
self.cap = new_cap;
self.sync_cap();
}
pub fn toggle_bounded(&mut self) {
self.bounded ^= true;
self.sync_cap();
}
#[must_use]
pub fn bpm(&self) -> f32 {
let count = self.count();
if count == 0 {
return 0.0;
}
self.buf.iter().sum::<f32>() / f32::from(count)
}
#[must_use]
pub const fn count(&self) -> u16 {
self.buf.len()
}
#[must_use]
pub fn cap(&self) -> u16 {
self.cap.min(Self::MAX_CAP)
}
#[must_use]
pub const fn is_recording(&self) -> bool {
self.last_tap.is_some()
}
#[must_use]
pub const fn is_bounded(&self) -> bool {
self.bounded
}
fn sync_cap(&mut self) {
if self.bounded {
self.buf.truncate_back(self.cap());
}
}
}
impl fmt::Display for Tapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut dbg = f.debug_list();
for bpm in self.buf.iter().rev() {
dbg.entry(bpm);
}
dbg.finish()
}
}