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