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