rstmt_nrt/types/
lpr.rs

1/*
2    Appellation: lpr <module>
3    Created At: 2025.12.25:17:21:05
4    Contrib: @FL03
5*/
6use crate::error::TriadError;
7use crate::traits::{Relative, TriadRepr, TriadType};
8use crate::triad::TriadBase;
9use num_traits::{FromPrimitive, One};
10use rstmt_core::{Dirac, PitchMod};
11
12/// The [`LPR`] implementation enumerates the primary transformations considered within the
13/// Neo-Riemannian theory. Each transformation is its own inverse (meaning consecutive
14/// applications of the same transformation will return the original triad) and may be chained
15/// together in both disctrete and continuous space to explore relationships between triads.
16/// These transformation act on the given triad based on the quality of the interval between
17/// the first and second notes (i.e., the root and third chord factors) meaning the effect of
18/// the transformation is dictated by whether the triad starts with a major or minor third.
19///
20/// With The transformations are:
21///
22/// - Leading (L):
23///   - [Major] decrement the root by a semitone; move to the fifth
24///   - [Minor] increment the fifth by a semitone; move to the root
25/// - Parallel (P):
26///   - [Major] decrement the third by a semitone
27///   - [Minor] increment the third by a semitone
28/// - Relative (R):
29///   - [Major] add a whole tone to the fifth; move to the root
30///   - [Minor] subtract a whole tone from the root; move to the fifth
31///
32/// Using category theory we could define these transformations to be contravariant functors
33/// mapping between _categories_ of triads.
34#[derive(
35    Clone,
36    Copy,
37    Debug,
38    Default,
39    Eq,
40    Hash,
41    Ord,
42    PartialEq,
43    PartialOrd,
44    strum::AsRefStr,
45    strum::Display,
46    strum::EnumCount,
47    strum::EnumIs,
48    strum::EnumIter,
49    strum::EnumString,
50    strum::VariantArray,
51    strum::VariantNames,
52)]
53#[cfg_attr(
54    feature = "serde",
55    derive(serde::Deserialize, serde::Serialize),
56    serde(rename_all = "lowercase")
57)]
58#[strum(serialize_all = "lowercase")]
59pub enum LPR {
60    /// Leading (L) transformation
61    #[default]
62    #[cfg_attr(feature = "serde", serde(alias = "L", alias = "l", alias = "lead"))]
63    Leading = 0,
64    /// Parallel (P) transformation
65    #[cfg_attr(feature = "serde", serde(alias = "P", alias = "p", alias = "par"))]
66    Parallel = 1,
67    /// Relative (R) transformation
68    #[cfg_attr(feature = "serde", serde(alias = "R", alias = "r", alias = "rel"))]
69    Relative = 2,
70}
71
72impl LPR {
73    /// a functional constructor returning a [`Leading`](LPR::Leading) transformation
74    pub const fn leading() -> Self {
75        LPR::Leading
76    }
77    /// a functional constructor returning a [`Parallel`](LPR::Parallel) transformation
78    pub const fn parallel() -> Self {
79        LPR::Parallel
80    }
81    /// a functional constructor returning a [`Relative`](LPR::Relative) transformation
82    pub const fn relative() -> Self {
83        LPR::Relative
84    }
85    pub fn iter() -> LPRIter {
86        use strum::IntoEnumIterator;
87        <LPR as IntoEnumIterator>::iter()
88    }
89    /// a convenience method for applying a transformation onto a triad, panicking on failure.
90    pub fn apply<X, Y>(self, triad: X) -> Y
91    where
92        Self: Dirac<X, Output = Y>,
93    {
94        <Self as Dirac<X>>::apply(self, triad)
95    }
96
97    fn dirac<S, T, K, R>(self, rhs: &TriadBase<S, K, T>) -> TriadBase<S, R, T>
98    where
99        K: TriadType<Rel = R>,
100        R: TriadType,
101        S: TriadRepr<Elem = T>,
102        T: Copy
103            + FromPrimitive
104            + One
105            + PitchMod<Output = T>
106            + core::ops::Add<Output = T>
107            + core::ops::Sub<Output = T>,
108    {
109        let major = rhs.class().root() == 4;
110        let two = T::from_u8(2).unwrap();
111
112        let &x = rhs.chord().root();
113        let &y = rhs.chord().third();
114        let &z = rhs.chord().fifth();
115
116        let notes: [T; 3] = if major {
117            match self {
118                LPR::Leading => [y, z, (x - T::one()).pmod()],
119                LPR::Parallel => [x, (y - T::one()).pmod(), z],
120                LPR::Relative => [(z + two).pmod(), x, y],
121            }
122        } else {
123            match self {
124                LPR::Leading => [(z + T::one()).pmod(), x, y],
125                LPR::Parallel => [x, (y + T::one()).pmod(), z],
126                LPR::Relative => [y, z, (x - two).pmod()],
127            }
128        };
129
130        TriadBase {
131            chord: S::from_arr(notes),
132            class: <K as Relative>::rel(&rhs.class()),
133            octave: *rhs.octave(),
134        }
135    }
136}
137
138impl<S, T, K> Dirac<TriadBase<S, K, T>> for LPR
139where
140    K: TriadType,
141    K::Rel: TriadType,
142    S: TriadRepr<Elem = T>,
143    T: Copy
144        + FromPrimitive
145        + One
146        + PitchMod<Output = T>
147        + core::ops::Add<Output = T>
148        + core::ops::Sub<Output = T>,
149{
150    type Output = TriadBase<S, K::Rel, T>;
151
152    fn apply(self, rhs: TriadBase<S, K, T>) -> Self::Output {
153        self.dirac(&rhs)
154    }
155}
156
157impl<S, T, K> Dirac<TriadBase<S, K, T>> for &LPR
158where
159    K: TriadType,
160    K::Rel: TriadType,
161    S: TriadRepr<Elem = T>,
162    T: Copy
163        + FromPrimitive
164        + One
165        + PitchMod<Output = T>
166        + core::ops::Add<Output = T>
167        + core::ops::Sub<Output = T>,
168{
169    type Output = TriadBase<S, K::Rel, T>;
170
171    fn apply(self, rhs: TriadBase<S, K, T>) -> Self::Output {
172        self.dirac(&rhs)
173    }
174}
175
176impl<S, T, K> Dirac<TriadBase<S, K, T>> for &mut LPR
177where
178    K: TriadType,
179    K::Rel: TriadType,
180    S: TriadRepr<Elem = T>,
181    T: Copy
182        + FromPrimitive
183        + One
184        + PitchMod<Output = T>
185        + core::ops::Add<Output = T>
186        + core::ops::Sub<Output = T>,
187{
188    type Output = TriadBase<S, K::Rel, T>;
189
190    fn apply(self, rhs: TriadBase<S, K, T>) -> Self::Output {
191        self.dirac(&rhs)
192    }
193}
194
195impl<S, T, K> Dirac<&TriadBase<S, K, T>> for LPR
196where
197    K: TriadType,
198    K::Rel: TriadType,
199    S: TriadRepr<Elem = T>,
200    T: Copy
201        + FromPrimitive
202        + One
203        + PitchMod<Output = T>
204        + core::ops::Add<Output = T>
205        + core::ops::Sub<Output = T>,
206{
207    type Output = TriadBase<S, K::Rel, T>;
208
209    fn apply(self, rhs: &TriadBase<S, K, T>) -> Self::Output {
210        self.dirac(rhs)
211    }
212}
213
214impl<S, T, K> Dirac<&TriadBase<S, K, T>> for &LPR
215where
216    K: TriadType,
217    K::Rel: TriadType,
218    S: TriadRepr<Elem = T>,
219    T: Copy
220        + FromPrimitive
221        + One
222        + PitchMod<Output = T>
223        + core::ops::Add<Output = T>
224        + core::ops::Sub<Output = T>,
225{
226    type Output = TriadBase<S, K::Rel, T>;
227
228    fn apply(self, rhs: &TriadBase<S, K, T>) -> Self::Output {
229        self.dirac(rhs)
230    }
231}
232
233impl<S, T, K> Dirac<&mut TriadBase<S, K, T>> for LPR
234where
235    K: TriadType,
236    K::Rel: TriadType,
237    S: TriadRepr<Elem = T>,
238    T: Copy
239        + FromPrimitive
240        + One
241        + PitchMod<Output = T>
242        + core::ops::Add<Output = T>
243        + core::ops::Sub<Output = T>,
244{
245    type Output = TriadBase<S, K::Rel, T>;
246
247    fn apply(self, rhs: &mut TriadBase<S, K, T>) -> Self::Output {
248        self.dirac(rhs)
249    }
250}
251
252impl TryFrom<char> for LPR {
253    type Error = TriadError;
254    fn try_from(value: char) -> Result<Self, Self::Error> {
255        use LPR::*;
256        match value.to_ascii_lowercase() {
257            'l' => Ok(Leading),
258            'p' => Ok(Parallel),
259            'r' => Ok(Relative),
260            v => Err(TriadError::TransformationParseCharError(v)),
261        }
262    }
263}
264
265macro_rules! impl_from_lpr {
266    ($($T:ty),* $(,)?) => {
267        $(impl_from_lpr! { @impl $T })*
268    };
269    (@impl $T:ty) => {
270        impl From<LPR> for $T {
271            fn from(value: LPR) -> Self {
272                value as $T
273            }
274        }
275
276        impl From<$T> for LPR {
277            fn from(value: $T) -> Self {
278                match value % 3 {
279                    0 => LPR::Leading,
280                    1 => LPR::Parallel,
281                    2 => LPR::Relative,
282                    _ => unreachable!("Modular arithmetic failed"),
283                }
284            }
285        }
286    };
287}
288
289impl_from_lpr! { u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize }