#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::pedantic, clippy::nursery)]
use std::collections::VecDeque;
use std::time::Instant;
#[derive(Clone, Copy, Debug)]
pub(crate) struct CacheF32 {
val: Option<f32>,
sadness: usize,
max_sadness: usize,
}
impl CacheF32 {
pub const fn new(max_sadness: usize) -> Self {
Self {
sadness: 0,
val: None,
max_sadness,
}
}
pub fn invalidate(&mut self) {
self.val = None;
self.sadness = 0;
}
pub fn get<F: FnOnce() -> f32>(&mut self, f: F) -> f32 {
if let Some(val) = self.val {
val
} else {
let val = f();
self.val = Some(val);
val
}
}
pub fn try_mut<F: FnOnce(&mut f32)>(&mut self, f: F) -> Option<f32> {
self.cry();
self.val.as_mut().map(|val| {
f(val);
*val
})
}
pub fn try_add(&mut self, add: f32) -> Option<f32> {
self.try_mut(|val| *val += add)
}
pub fn try_sub(&mut self, sub: f32) -> Option<f32> {
self.try_mut(|val| *val -= sub)
}
fn cry(&mut self) -> bool {
if self.sadness == self.max_sadness {
self.invalidate();
true
} else {
self.sadness += 1;
false
}
}
}
#[derive(Clone, Debug)]
pub struct Tapper {
buf: VecDeque<f32>, cap: u16,
bounded: bool,
last_tap: Option<Instant>,
buf_sum: CacheF32, }
impl Tapper {
const BUF_SUM_MAX_SADNESS: usize = 256;
#[must_use]
pub fn new(cap: u16, bounded: bool) -> Self {
Self {
buf: VecDeque::with_capacity(usize::from(cap) + 1),
cap,
bounded,
last_tap: None,
buf_sum: CacheF32::new(Self::BUF_SUM_MAX_SADNESS),
}
}
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 = elapsed.recip() * 60.0;
self.buf.push_back(bpm);
self.buf_sum.try_add(bpm);
self.sync_cap();
}
}
pub fn clear(&mut self) {
self.buf_sum.invalidate();
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(&mut self) -> f32 {
let count = self.count();
if count == 0 {
return 0.0;
}
let sum = self.buf_sum.get(|| self.buf.iter().sum());
sum / f32::from(count)
}
#[allow(clippy::missing_panics_doc)]
#[inline]
#[must_use]
pub fn count(&self) -> u16 {
self.buf.len().try_into().unwrap() }
#[inline]
#[must_use]
pub const fn cap(&self) -> u16 {
self.cap
}
#[inline]
#[must_use]
pub const fn is_recording(&self) -> bool {
self.last_tap.is_some()
}
#[inline]
#[must_use]
pub const fn is_bounded(&self) -> bool {
self.bounded
}
fn sync_cap(&mut self) {
if self.bounded {
while usize::from(self.cap) < self.buf.len() {
let bpm = self.buf.pop_front().unwrap();
self.buf_sum.try_sub(bpm);
}
}
}
}