rstmt_nrt/types/
kinds.rs

1/*
2    Appellation: classes <module>
3    Contrib: @FL03
4*/
5use crate::traits::TriadType;
6use num_traits::{FromPrimitive, ToPrimitive};
7use rstmt::PitchMod;
8
9/// The [`Triads`] implementation enumerates the allowed triad classifications determined
10/// by the intervals between the chord factors.
11#[derive(
12    Clone,
13    Copy,
14    Debug,
15    Default,
16    Eq,
17    Hash,
18    Ord,
19    PartialOrd,
20    strum::AsRefStr,
21    strum::Display,
22    strum::EnumIs,
23    strum::EnumIter,
24    strum::EnumString,
25    strum::VariantArray,
26    strum::VariantNames,
27)]
28#[cfg_attr(
29    feature = "serde",
30    derive(serde::Deserialize, serde::Serialize),
31    serde(untagged, rename_all = "lowercase")
32)]
33#[strum(serialize_all = "lowercase")]
34pub enum Triads {
35    #[default]
36    Major = 0,
37    Minor = 1,
38    Augmented = 2,
39    Diminished = 3,
40}
41
42impl Triads {
43    pub fn from_class<C>(class: C) -> Self
44    where
45        C: TriadType,
46    {
47        if class.is_major() {
48            Self::Major
49        } else if class.is_minor() {
50            Self::Minor
51        } else if class.is_augmented() {
52            Self::Augmented
53        } else if class.is_diminished() {
54            Self::Diminished
55        } else {
56            panic!("invalid triad class")
57        }
58    }
59    pub fn is<T: TriadType>(&self, class: T) -> bool {
60        match self {
61            Triads::Major => class.is_major(),
62            Triads::Minor => class.is_minor(),
63            Triads::Augmented => class.is_augmented(),
64            Triads::Diminished => class.is_diminished(),
65        }
66    }
67    /// a functional constructor for the [`Major`](Triads::Major) variant
68    pub const fn major() -> Self {
69        Self::Major
70    }
71    /// a functional constructor for the [`Minor`](Triads::Minor) variant
72    pub const fn minor() -> Self {
73        Self::Minor
74    }
75    /// a functional constructor for the [`Augmented`](Triads::Augmented) variant
76    pub const fn augmented() -> Self {
77        Self::Augmented
78    }
79    /// a functional constructor for the [`Diminished`](Triads::Diminished) variant
80    pub const fn diminished() -> Self {
81        Self::Diminished
82    }
83    /// try to derive a classification for a triad from three notes
84    pub fn try_from_notes(a: isize, b: isize, c: isize) -> crate::Result<Self> {
85        Self::try_from_arr([a, b, c])
86    }
87    #[cfg(feature = "alloc")]
88    /// try to determine the class of a triad from an array of three notes
89    pub fn try_from_arr(notes: [isize; 3]) -> crate::Result<Self> {
90        use itertools::Itertools;
91        let intervals = notes
92            .iter()
93            .combinations(2)
94            .map(|v| (v[1] - v[0]).pmod())
95            .collect::<Vec<_>>();
96        match intervals[..] {
97            [4, 7, 3] => Ok(Self::Major),
98            [3, 7, 4] => Ok(Self::Minor),
99            [4, 8, 4] => Ok(Self::Augmented),
100            [3, 6, 3] => Ok(Self::Diminished),
101            _ => Err(crate::TriadError::InvalidTriad),
102        }
103    }
104    // TODO: ensure augmented & diminished are handled correctly
105    /// returns the class _relative_ to the current variant; for major and minor triads its
106    /// rather straightforward
107    pub const fn relative(&self) -> Self {
108        match self {
109            Triads::Major => Triads::Minor,
110            Triads::Minor => Triads::Major,
111            Triads::Augmented => Triads::Diminished,
112            Triads::Diminished => Triads::Augmented,
113        }
114    }
115    /// returns the intervals corresponding to the triad type as arrays of three `usize` values
116    /// ordered as: [root_to_third, root_to_fifth, third_to_fifth]
117    pub const fn intervals(&self) -> [usize; 3] {
118        match self {
119            Triads::Major => [4, 7, 3],
120            Triads::Minor => [3, 7, 4],
121            Triads::Augmented => [4, 8, 4],
122            Triads::Diminished => [3, 6, 3],
123        }
124    }
125    /// returns the two third intervals defining the current variant
126    pub const fn thirds(&self) -> (usize, usize) {
127        (self.root(), self.third())
128    }
129    /// returns the **interval** between the root and third chord factors
130    pub const fn root(&self) -> usize {
131        match self {
132            Triads::Major => 4,
133            Triads::Minor => 3,
134            Triads::Augmented => 4,
135            Triads::Diminished => 3,
136        }
137    }
138    /// returns a reference to the **interval** between the root and third chord factors
139    pub const fn root_ref(&self) -> &usize {
140        match self {
141            Triads::Major => &4,
142            Triads::Minor => &3,
143            Triads::Augmented => &4,
144            Triads::Diminished => &3,
145        }
146    }
147    /// returns the **interval** between the third and fifth chord factors
148    pub const fn third(&self) -> usize {
149        match self {
150            Triads::Major => 3,
151            Triads::Minor => 4,
152            Triads::Augmented => 4,
153            Triads::Diminished => 3,
154        }
155    }
156    /// returns a reference to the **interval** between the third and fifth chord factors
157    pub const fn third_ref(&self) -> &usize {
158        match self {
159            Triads::Major => &3,
160            Triads::Minor => &4,
161            Triads::Augmented => &4,
162            Triads::Diminished => &3,
163        }
164    }
165    /// returns the **interval** between the root and fifth chord factors
166    pub const fn fifth(&self) -> usize {
167        match self {
168            Triads::Major => 7,
169            Triads::Minor => 7,
170            Triads::Augmented => 8,
171            Triads::Diminished => 6,
172        }
173    }
174    /// returns a reference to the **interval** between the root and fifth chord factors
175    pub const fn fifth_ref(&self) -> &usize {
176        match self {
177            Triads::Major => &7,
178            Triads::Minor => &7,
179            Triads::Augmented => &8,
180            Triads::Diminished => &6,
181        }
182    }
183    /// returns true if the given chord factors satisfy the requirements of the current class
184    pub fn is_valid(&self, root: usize, third: usize, fifth: usize) -> bool {
185        let [rt, rf, tf] = self.intervals();
186        (third - root).pmod() == rt && (fifth - root).pmod() == rf && (fifth - third).pmod() == tf
187    }
188    /// validate a chord's composition satisfies the requirements of the current class
189    pub fn validate<T>(&self, &[r, t, f]: &[T; 3]) -> bool
190    where
191        T: Copy
192            + PartialEq
193            + FromPrimitive
194            + ToPrimitive
195            + PitchMod<Output = T>
196            + core::ops::Sub<Output = T>,
197    {
198        let a = T::from_usize(self.root()).unwrap();
199        let b = T::from_usize(self.fifth()).unwrap();
200        let c = T::from_usize(self.third()).unwrap();
201        (t - r).pmod() == a && (f - t).pmod() == c && (f - r).pmod() == b
202    }
203}
204
205macro_rules! impl_from_triad_class {
206    ($($T:ty),* $(,)?) => {
207        $(
208            impl From<$T> for Triads {
209                fn from(value: $T) -> Self {
210                    match value % 4 {
211                        0 => Triads::Major,
212                        1 => Triads::Minor,
213                        2 => Triads::Augmented,
214                        3 => Triads::Diminished,
215                        _ => unreachable! { "invalid modulo operation" },
216                    }
217                }
218            }
219
220            impl From<Triads> for $T {
221                fn from(value: Triads) -> Self {
222                    value as $T
223                }
224            }
225        )*
226    };
227}
228
229impl_from_triad_class! { u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize }
230
231impl core::ops::Index<super::Factors> for Triads {
232    type Output = usize;
233
234    fn index(&self, index: super::Factors) -> &Self::Output {
235        match index {
236            super::Factors::Root => self.root_ref(),
237            super::Factors::Third => self.third_ref(),
238            super::Factors::Fifth => self.fifth_ref(),
239        }
240    }
241}
242
243macro_rules! interval_to_class {
244    (@impl $T:ident) => {
245            // impl PartialEq<rstmt_core::$T> for Triads {
246            //     fn eq(&self, _other: &rstmt_core::$T) -> bool {
247            //         matches! { self, Triads::$T }
248            //     }
249            // }
250
251            impl From<rstmt_core::$T> for Triads {
252                fn from(_value: rstmt_core::$T) -> Self {
253                    Triads::$T
254                }
255            }
256
257            impl TryFrom<Triads> for rstmt_core::$T {
258                type Error = $crate::TriadError;
259
260                fn try_from(value: Triads) -> Result<Self, Self::Error> {
261                    if matches!(value, Triads::$T) {
262                        Ok(Self)
263                    } else {
264                        Err($crate::TriadError::IncompatibleTriadClasses)
265                    }
266                }
267            }
268    };
269    ($($T:ident),* $(,)?) => {
270        $(interval_to_class! { @impl $T })*
271    };
272}
273
274interval_to_class! { Major, Minor, Augmented, Diminished }
275
276impl<C: crate::TriadType> PartialEq<C> for Triads {
277    fn eq(&self, other: &C) -> bool {
278        match self {
279            Triads::Major => other.is_major(),
280            Triads::Minor => other.is_minor(),
281            Triads::Augmented => other.is_augmented(),
282            Triads::Diminished => other.is_diminished(),
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_class_creation() -> crate::Result<()> {
293        let class = Triads::try_from_arr([0, 4, 7])?;
294        assert!(class.is_major());
295        let class = Triads::try_from_arr([0, 3, 7])?;
296        assert!(class.is_minor());
297        let class = Triads::try_from_arr([0, 4, 8])?;
298        assert!(class.is_augmented());
299        let class = Triads::try_from_arr([0, 3, 6])?;
300        assert!(class.is_diminished());
301
302        assert!(Triads::try_from_arr([0, 7, 4]).is_err());
303        assert!(Triads::try_from_arr([0, 5, 9]).is_err());
304
305        Ok(())
306    }
307}