bpm_analyzer/
types.rs

1//! Core types for BPM detection results.
2
3/// A single beat timing event.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct BeatTiming {
6    /// Time in seconds since analysis started
7    pub time_seconds: f64,
8    /// Strength of the beat (0.0 to 1.0)
9    pub strength: f32,
10}
11
12impl BeatTiming {
13    /// Creates a new beat timing.
14    pub fn new(time_seconds: f64, strength: f32) -> Self {
15        Self {
16            time_seconds,
17            strength,
18        }
19    }
20}
21
22/// A BPM detection candidate with its confidence value.
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct BpmCandidate {
25    /// The detected tempo in beats per minute
26    pub bpm: f32,
27    /// Confidence value from autocorrelation (higher = more confident)
28    pub confidence: f32,
29}
30
31impl BpmCandidate {
32    /// Creates a new BPM candidate.
33    pub fn new(bpm: f32, confidence: f32) -> Self {
34        Self { bpm, confidence }
35    }
36
37    /// Returns true if this candidate's BPM is within the given range.
38    pub fn in_range(&self, min: f32, max: f32) -> bool {
39        self.bpm >= min && self.bpm <= max
40    }
41}
42
43impl From<(f32, f32)> for BpmCandidate {
44    fn from((bpm, confidence): (f32, f32)) -> Self {
45        Self::new(bpm, confidence)
46    }
47}
48
49/// Result from BPM detection containing up to 5 candidates sorted by confidence.
50#[derive(Debug, Clone)]
51pub struct BpmDetection {
52    candidates: Vec<BpmCandidate>,
53    /// Recent beat timings (up to last 8 beats)
54    beat_timings: Vec<BeatTiming>,
55}
56
57impl BpmDetection {
58    /// Creates a new detection result from an array of candidates.
59    pub fn from_array(arr: [(f32, f32); 5]) -> Self {
60        let candidates = arr
61            .into_iter()
62            .filter(|(bpm, _)| *bpm > 0.0) // Filter out padding zeros
63            .map(BpmCandidate::from)
64            .collect();
65        Self {
66            candidates,
67            beat_timings: Vec::new(),
68        }
69    }
70
71    /// Creates a new detection result with beat timings.
72    pub fn with_beats(arr: [(f32, f32); 5], beat_timings: Vec<BeatTiming>) -> Self {
73        let candidates = arr
74            .into_iter()
75            .filter(|(bpm, _)| *bpm > 0.0)
76            .map(BpmCandidate::from)
77            .collect();
78        Self {
79            candidates,
80            beat_timings,
81        }
82    }
83
84    /// Returns the top BPM candidate (highest confidence).
85    pub fn top_candidate(&self) -> Option<&BpmCandidate> {
86        self.candidates.first()
87    }
88
89    /// Returns all candidates.
90    pub fn candidates(&self) -> &[BpmCandidate] {
91        &self.candidates
92    }
93
94    /// Returns the most likely BPM value.
95    pub fn bpm(&self) -> Option<f32> {
96        self.top_candidate().map(|c| c.bpm)
97    }
98
99    /// Returns candidates within a specific BPM range.
100    pub fn in_range(&self, min: f32, max: f32) -> Vec<BpmCandidate> {
101        self.candidates
102            .iter()
103            .filter(|c| c.in_range(min, max))
104            .copied()
105            .collect()
106    }
107
108    /// Returns the detected beat timings.
109    pub fn beat_timings(&self) -> &[BeatTiming] {
110        &self.beat_timings
111    }
112
113    /// Returns the most recent beat timing, if available.
114    pub fn last_beat(&self) -> Option<&BeatTiming> {
115        self.beat_timings.last()
116    }
117
118    /// Returns the time interval between the last two beats, if available.
119    pub fn last_beat_interval(&self) -> Option<f64> {
120        if self.beat_timings.len() >= 2 {
121            let len = self.beat_timings.len();
122            Some(self.beat_timings[len - 1].time_seconds - self.beat_timings[len - 2].time_seconds)
123        } else {
124            None
125        }
126    }
127}