tempo_tapper 0.5.5

terminal tempo tapper (archive)
// tempo: terminal tempo tapper
// copyright (C) 2022-2023 Nissa <and-nissa@protonmail.com>
// licensed under MIT OR Apache-2.0

use core::fmt;
use std::time::Instant;

use crate::ringbuf::RingBuf;

/// Tempo tapper which measures the average BPM between taps.
#[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;

    /// Return a new `Tapper` with its buffer capped to `cap`.
    #[must_use]
    pub const fn new(cap: u16, bounded: bool) -> Self {
        Self {
            buf: RingBuf::new(),
            cap,
            bounded,
            last_tap: None,
        }
    }

    /// Record the interval since the last tap.
    pub fn tap(&mut self) {
        let now = Instant::now();

        // update bpm
        if let Some(last) = self.last_tap.replace(now) {
            let elapsed = now.saturating_duration_since(last).as_secs_f32();

            // push a new bpm
            let bpm = 60.0 / elapsed;
            self.buf.push(bpm);

            // remove old elements
            self.sync_cap();
        }
    }

    /// Clear the buffer of all samples and forget the last tap.
    pub fn clear(&mut self) {
        self.buf.clear();
        self.last_tap = None;
    }

    /// Restrict the buffer to at most `new_cap` samples.
    pub fn resize(&mut self, new_cap: u16) {
        self.cap = new_cap;
        self.sync_cap();
    }

    /// Toggle whether the buffer is restricted to [`cap`](Self::cap) samples.
    pub fn toggle_bounded(&mut self) {
        self.bounded ^= true;
        self.sync_cap();
    }

    /// Return the average BPM between recorded taps.
    #[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)
    }

    /// Return the number of samples held by the buffer.
    #[must_use]
    pub const fn count(&self) -> u16 {
        self.buf.len()
    }

    /// Return the capacity of the buffer when bounded.
    #[must_use]
    pub fn cap(&self) -> u16 {
        self.cap.min(Self::MAX_CAP)
    }

    /// Return `true` if a tap has been recorded.
    #[must_use]
    pub const fn is_recording(&self) -> bool {
        self.last_tap.is_some()
    }

    /// Return `true` if the buffer is bounded.
    #[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()
    }
}