#[derive(Debug, Clone, Copy)]
pub enum NoteDuration {
Whole,
Half,
Quarter,
Eighth,
Sixteenth,
ThirtySecond,
DottedHalf,
DottedQuarter,
DottedEighth,
QuarterTriplet,
EighthTriplet,
}
impl NoteDuration {
pub fn beats(&self) -> f32 {
match self {
NoteDuration::Whole => 4.0,
NoteDuration::Half => 2.0,
NoteDuration::Quarter => 1.0,
NoteDuration::Eighth => 0.5,
NoteDuration::Sixteenth => 0.25,
NoteDuration::ThirtySecond => 0.125,
NoteDuration::DottedHalf => 3.0,
NoteDuration::DottedQuarter => 1.5,
NoteDuration::DottedEighth => 0.75,
NoteDuration::QuarterTriplet => 2.0 / 3.0,
NoteDuration::EighthTriplet => 1.0 / 3.0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Tempo {
pub bpm: f32, }
impl Tempo {
pub fn new(bpm: f32) -> Self {
let bpm = bpm.clamp(20.0, 500.0);
debug_assert!(bpm > 0.0, "BPM must be positive after clamping");
Self { bpm }
}
pub fn duration_to_seconds(&self, duration: NoteDuration) -> f32 {
let beats = duration.beats();
let seconds_per_beat = 60.0 / self.bpm;
beats * seconds_per_beat
}
pub fn quarter_note(&self) -> f32 {
60.0 / self.bpm
}
pub fn eighth_note(&self) -> f32 {
30.0 / self.bpm
}
pub fn sixteenth_note(&self) -> f32 {
15.0 / self.bpm
}
pub fn whole_note(&self) -> f32 {
240.0 / self.bpm
}
}
impl Default for Tempo {
fn default() -> Self {
Self::new(120.0) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tempo_120bpm() {
let tempo = Tempo::new(120.0);
assert_eq!(tempo.quarter_note(), 0.5); assert_eq!(tempo.eighth_note(), 0.25);
assert_eq!(tempo.sixteenth_note(), 0.125);
}
#[test]
fn test_note_durations() {
let tempo = Tempo::new(60.0); assert_eq!(tempo.duration_to_seconds(NoteDuration::Quarter), 1.0);
assert_eq!(tempo.duration_to_seconds(NoteDuration::Half), 2.0);
assert_eq!(tempo.duration_to_seconds(NoteDuration::Eighth), 0.5);
}
#[test]
fn test_tempo_clamping() {
let tempo = Tempo::new(0.0);
assert_eq!(tempo.bpm, 20.0);
let tempo = Tempo::new(-100.0);
assert_eq!(tempo.bpm, 20.0);
let tempo = Tempo::new(1000.0);
assert_eq!(tempo.bpm, 500.0);
let tempo = Tempo::new(120.0);
assert_eq!(tempo.bpm, 120.0);
}
#[test]
fn test_no_division_by_zero() {
let tempo = Tempo::new(0.0);
let duration = tempo.quarter_note();
assert!(duration.is_finite());
assert!(duration > 0.0);
}
}