eryon_nrt/transform/
kinds.rs

1/*
2    Appellation: transform <module>
3    Contrib: @FL03
4*/
5use crate::{MusicError, PitchMod, Triad, Triads};
6
7/// Enumerates primary available transformations in Neo-Riemannian theory.
8///
9/// Each transformation is invertible, meaning that consecutive applications of any
10/// transformation will return the original triad. Furthermore, LPR transformations may be
11/// chained together in discrete or continuous sequences to create complex harmonic
12/// progressions.
13///
14/// The transformations are:
15///
16/// - Leading (L):
17///   - [Major] given a major triad, subtract a semitone from the root and move it to the fifth
18///   - [Minor] given a minor triad, add a semitone to the fifth and move it to the root
19/// - Parallel (P):
20///   - [Major] given a major triad, subtract a semitone from the third
21///   - [Minor] given a minor triad, add a semitone to the third
22/// - Relative (R):
23///   - [Major] given a major triad, add a tone to the fifth and move it to the root
24///   - [Minor] given a minor triad, subtract a tone from the root and move it to the fifth
25///
26/// These transformations can be described categorically as morphisms between various triads.
27/// More specifically, they are contravariant functors between categories of triads.
28///
29#[derive(
30    Clone,
31    Copy,
32    Debug,
33    Default,
34    Eq,
35    Hash,
36    Ord,
37    PartialEq,
38    PartialOrd,
39    strum::AsRefStr,
40    strum::Display,
41    strum::EnumCount,
42    strum::EnumIs,
43    strum::EnumIter,
44    strum::EnumString,
45    strum::VariantArray,
46    strum::VariantNames,
47)]
48#[cfg_attr(
49    feature = "serde",
50    derive(serde_derive::Deserialize, serde_derive::Serialize),
51    serde(rename_all = "lowercase")
52)]
53#[strum(serialize_all = "lowercase")]
54pub enum LPR {
55    /// Leading (L) transformation
56    #[default]
57    #[cfg_attr(feature = "serde", serde(alias = "L", alias = "l", alias = "lead"))]
58    Leading,
59    /// Parallel (P) transformation
60    #[cfg_attr(feature = "serde", serde(alias = "P", alias = "p", alias = "par"))]
61    Parallel,
62    /// Relative (R) transformation
63    #[cfg_attr(feature = "serde", serde(alias = "R", alias = "r", alias = "rel"))]
64    Relative,
65}
66
67impl LPR {
68    pub fn leading() -> Self {
69        LPR::Leading
70    }
71
72    pub fn parallel() -> Self {
73        LPR::Parallel
74    }
75
76    pub fn relative() -> Self {
77        LPR::Relative
78    }
79    pub fn apply(&self, triad: &Triad) -> Triad {
80        self.try_apply(triad).unwrap()
81    }
82    /// Apply a transformation to a triad
83    pub fn try_apply(&self, triad: &Triad) -> Result<Triad, MusicError> {
84        let [x, y, z] = triad.notes;
85
86        let notes: [usize; 3];
87        let class: Triads;
88        match triad.class() {
89            Triads::Major => match self {
90                LPR::Leading => {
91                    notes = [y, z, (x as isize - 1).pmod() as usize];
92                    class = Triads::Minor;
93                }
94                LPR::Parallel => {
95                    notes = [x, (y as isize - 1).pmod() as usize, z];
96                    class = Triads::Minor;
97                }
98                LPR::Relative => {
99                    notes = [(z + 2).pmod(), x, y];
100                    class = Triads::Minor;
101                }
102            },
103            Triads::Minor => match self {
104                LPR::Leading => {
105                    notes = [(z + 1).pmod(), x, y];
106                    class = Triads::Major;
107                }
108                LPR::Parallel => {
109                    notes = [x, (y + 1).pmod(), z];
110                    class = Triads::Major;
111                }
112                LPR::Relative => {
113                    notes = [y, z, (x as isize - 2).pmod() as usize];
114                    class = Triads::Major;
115                }
116            },
117            _ => return Err(MusicError::InvalidTriadClass),
118        };
119
120        Ok(Triad {
121            notes,
122            class,
123            octave: triad.octave,
124        })
125    }
126}
127
128impl From<char> for LPR {
129    fn from(value: char) -> Self {
130        match value.to_ascii_lowercase() {
131            'l' => LPR::Leading,
132            'p' => LPR::Parallel,
133            'r' => LPR::Relative,
134            _ => panic!("Invalid LPR transformation; character must be 'L', 'P', or 'R'"),
135        }
136    }
137}
138
139impl From<usize> for LPR {
140    fn from(value: usize) -> Self {
141        use strum::EnumCount;
142        match value % Self::COUNT {
143            0 => LPR::Leading,
144            1 => LPR::Parallel,
145            2 => LPR::Relative,
146            _ => unreachable!(),
147        }
148    }
149}
150
151impl From<LPR> for usize {
152    fn from(value: LPR) -> Self {
153        match value {
154            LPR::Leading => 0,
155            LPR::Parallel => 1,
156            LPR::Relative => 2,
157        }
158    }
159}