Skip to main content

acorde_core/model/
duration.rs

1use serde::{Deserialize, Serialize};
2
3/// Note duration as a power-of-two fraction of a whole note.
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
5pub enum Duration {
6    /// Longa (four whole notes).
7    Longa,
8    /// Double whole note.
9    Breve,
10    Whole,
11    Half,
12    Quarter,
13    Eighth,
14    Sixteenth,
15    ThirtySecond,
16    SixtyFourth,
17    HundredTwentyEighth,
18}
19
20impl Duration {
21    /// Exact fraction relative to a whole note = 1.
22    pub fn as_fraction(&self) -> (u32, u32) {
23        match self {
24            Duration::Longa => (4, 1),
25            Duration::Breve => (2, 1),
26            Duration::Whole => (1, 1),
27            Duration::Half => (1, 2),
28            Duration::Quarter => (1, 4),
29            Duration::Eighth => (1, 8),
30            Duration::Sixteenth => (1, 16),
31            Duration::ThirtySecond => (1, 32),
32            Duration::SixtyFourth => (1, 64),
33            Duration::HundredTwentyEighth => (1, 128),
34        }
35    }
36
37    /// Duration in ticks when divisions = 480 (ticks per quarter note).
38    pub fn to_ticks(&self, dot_count: u8) -> u32 {
39        let (num, den) = self.as_fraction();
40        let base_ticks = 4 * 480 * num / den;
41        let mut ticks = base_ticks;
42        let mut dot_value = base_ticks / 2;
43        for _ in 0..dot_count {
44            ticks += dot_value;
45            dot_value /= 2;
46        }
47        ticks
48    }
49
50    /// MusicXML `<type>` string.
51    pub fn to_musicxml_type(&self) -> &'static str {
52        match self {
53            Duration::Longa => "long",
54            Duration::Breve => "breve",
55            Duration::Whole => "whole",
56            Duration::Half => "half",
57            Duration::Quarter => "quarter",
58            Duration::Eighth => "eighth",
59            Duration::Sixteenth => "16th",
60            Duration::ThirtySecond => "32nd",
61            Duration::SixtyFourth => "64th",
62            Duration::HundredTwentyEighth => "128th",
63        }
64    }
65
66    /// Duration in beats (quarter = 1.0), accounting for dots.
67    pub fn beats(&self, dot_count: u8) -> f64 {
68        let (num, den) = self.as_fraction();
69        let base = 4.0 * num as f64 / den as f64;
70        let mut total = base;
71        let mut dot = base / 2.0;
72        for _ in 0..dot_count {
73            total += dot;
74            dot /= 2.0;
75        }
76        total
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn quarter_ticks() {
86        assert_eq!(Duration::Quarter.to_ticks(0), 480);
87    }
88
89    #[test]
90    fn dotted_quarter_ticks() {
91        assert_eq!(Duration::Quarter.to_ticks(1), 720);
92    }
93
94    #[test]
95    fn whole_beats() {
96        assert!((Duration::Whole.beats(0) - 4.0).abs() < 1e-9);
97    }
98
99    #[test]
100    fn dotted_half_beats() {
101        assert!((Duration::Half.beats(1) - 3.0).abs() < 1e-9);
102    }
103
104    #[test]
105    fn musicxml_type_strings() {
106        assert_eq!(Duration::Longa.to_musicxml_type(), "long");
107        assert_eq!(Duration::Longa.to_ticks(0), 7680);
108        assert!((Duration::Longa.beats(0) - 16.0).abs() < 1e-9);
109        assert_eq!(Duration::Sixteenth.to_musicxml_type(), "16th");
110        assert_eq!(Duration::ThirtySecond.to_musicxml_type(), "32nd");
111    }
112}