redact_composer_musical/
interval.rs

1use std::iter::Sum;
2use std::ops::{Add, AddAssign};
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "redact-composer")]
8use redact_composer_core::derive::Element;
9
10/// A pitch difference, measured in half-steps/semitones.
11#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[cfg_attr(feature = "redact-composer", derive(Element))]
14pub struct Interval(pub u8);
15
16#[allow(non_upper_case_globals)]
17impl Interval {
18    /// Perfect Unison (0 semitones)
19    pub const P1: Interval = Interval(0);
20    /// Minor 2nd (1 semitone)
21    pub const m2: Interval = Interval(1);
22    /// Major 2nd (2 semitones)
23    pub const M2: Interval = Interval(2);
24    /// Minor 3rd (3 semitones)
25    pub const m3: Interval = Interval(3);
26    /// Major 3rd (4 semitones)
27    pub const M3: Interval = Interval(4);
28    /// Perfect 4th (5 semitones)
29    pub const P4: Interval = Interval(5);
30    /// Tritone (6 semitones)
31    pub const TT: Interval = Interval(6);
32    /// Augmented 4th (6 semitones)
33    pub const A4: Interval = Interval(6);
34    /// Diminished 5th (6 semitones)
35    pub const d5: Interval = Interval(6);
36    /// Perfect 5th (7 semitones)
37    pub const P5: Interval = Interval(7);
38    /// Minor 6th (8 semitones)
39    pub const m6: Interval = Interval(8);
40    /// Augmented 5th (8 semitones)
41    pub const A5: Interval = Interval(8);
42    /// Major 6th (9 semitones)
43    pub const M6: Interval = Interval(9);
44    /// Diminished 7th (9 semitones)
45    pub const d7: Interval = Interval(9);
46    /// Minor 7th (10 semitones)
47    pub const m7: Interval = Interval(10);
48    /// Major 7th (11 semitones)
49    pub const M7: Interval = Interval(11);
50    /// Perfect 8th (12 semitones)
51    pub const P8: Interval = Interval(12);
52    /// Octave (12 semitones)
53    pub const Octave: Interval = Self::P8;
54    /// Minor 9th (13 semitones)
55    pub const m9: Interval = Interval(13);
56    /// Major 9th (13 semitones)
57    pub const M9: Interval = Interval(14);
58    /// Minor 10th (15 semitones)
59    pub const m10: Interval = Interval(15);
60    /// Major 10th (16 semitones)
61    pub const M10: Interval = Interval(16);
62    /// Perfect 11th (17 semitones)
63    pub const P11: Interval = Interval(17);
64    /// Perfect 12th (19 semitones)
65    pub const P12: Interval = Interval(19);
66    /// Minor 13th (20 semitones)
67    pub const m13: Interval = Interval(20);
68    /// Major 13th (21 semitones)
69    pub const M13: Interval = Interval(21);
70
71    /// Returns `true` if this is a simple interval (up to one octave).
72    /// ```
73    /// # use redact_composer_musical::Interval;
74    /// assert!(Interval::P5.is_simple());
75    /// ```
76    pub fn is_simple(&self) -> bool {
77        self.0 <= 12
78    }
79
80    /// Return the simple interval counterpart.
81    /// Note: This function will reduce [`Interval::Octave`] to [`Interval::P1`].
82    /// ```
83    /// # use redact_composer_musical::Interval;
84    /// assert_eq!(Interval::m9.to_simple(), Interval::m2);
85    /// ```
86    pub fn to_simple(self) -> Interval {
87        Interval(self.0 % 12)
88    }
89
90    /// Returns `true` if this is a compound interval (larger than one octave).
91    /// ```
92    /// # use redact_composer_musical::Interval;
93    /// assert!(Interval::m9.is_compound());
94    /// ```
95    pub fn is_compound(&self) -> bool {
96        !self.is_simple()
97    }
98
99    /// Return the compound interval counterpart (added octave). Does nothing if the interval is already compound.
100    /// ```
101    /// # use redact_composer_musical::Interval;
102    /// assert_eq!(Interval::m2.to_compound(), Interval::m9);
103    /// ```
104    pub fn to_compound(self) -> Interval {
105        if self.is_simple() {
106            Interval(self.0 + 12)
107        } else {
108            self
109        }
110    }
111
112    /// Returns the interval's inversion.
113    /// ```
114    /// # use redact_composer_musical::Interval;
115    /// assert_eq!(Interval::P5.inversion(), Interval::P4);
116    /// ```
117    pub fn inversion(&self) -> Interval {
118        if self.is_simple() {
119            Interval(12 - self.0)
120        } else {
121            let octaves = self.0 / 12 + 1;
122
123            Interval(12 * octaves - self.0)
124        }
125    }
126}
127
128impl Add for Interval {
129    type Output = Interval;
130
131    fn add(self, rhs: Self) -> Self::Output {
132        Interval(self.0 + rhs.0)
133    }
134}
135
136impl AddAssign for Interval {
137    fn add_assign(&mut self, rhs: Self) {
138        self.0 += rhs.0;
139    }
140}
141
142impl Sum for Interval {
143    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
144        iter.fold(Interval::default(), |i1, i2| i1 + i2)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::Interval as I;
151
152    #[test]
153    fn check_simple_interval() {
154        assert!(I(0).is_simple());
155        assert!(I(12).is_simple());
156        assert!(!I(13).is_simple());
157    }
158
159    #[test]
160    fn check_compound_interval() {
161        assert!(I(13).is_compound());
162        assert!(I(24).is_compound());
163        assert!(!I(12).is_compound());
164    }
165
166    #[test]
167    fn check_simple_inversions() {
168        assert_eq!(I::P1.inversion(), I::P8);
169        assert_eq!(I::P8.inversion(), I::P1);
170
171        assert_eq!(I::m2.inversion(), I::M7);
172        assert_eq!(I::M7.inversion(), I::m2);
173
174        assert_eq!(I::M2.inversion(), I::m7);
175        assert_eq!(I::m7.inversion(), I::M2);
176
177        assert_eq!(I::m3.inversion(), I::M6);
178        assert_eq!(I::M6.inversion(), I::m3);
179
180        assert_eq!(I::M3.inversion(), I::m6);
181        assert_eq!(I::m6.inversion(), I::M3);
182
183        assert_eq!(I::P4.inversion(), I::P5);
184        assert_eq!(I::P5.inversion(), I::P4);
185
186        assert_eq!(I::A4.inversion(), I::A4);
187    }
188
189    #[test]
190    fn check_compound_inversions() {
191        assert_eq!(I::m9.inversion(), I::M7);
192        assert_eq!(I::M9.inversion(), I::m7);
193        assert_eq!(I::m10.inversion(), I::M6);
194        assert_eq!(I::M10.inversion(), I::m6);
195        assert_eq!(I::P11.inversion(), I::P5);
196        assert_eq!(I::P12.inversion(), I::P4);
197        assert_eq!(I::m13.inversion(), I::M3);
198        assert_eq!(I::M13.inversion(), I::m3);
199    }
200}