rstmt_nrt/
triad.rs

1/*
2    Appellation: triad <module>
3    Created At: 2025.12.20:11:38:53
4    Contrib: @FL03
5*/
6//! this module defines the [`Triad`] struct along with additional types and traits supporting
7//! the representation of triads and their operations w.r.t. the neo-riemannian theory.
8//!
9//! # Overview
10//!
11//! A triad is defined to be a chord, composed of three notes, each of which maintain certain
12//! intervallic relationships with one another. More specifically, the distance between the
13//! first and second as well as the second and third notes is defined to be a major or minor
14//! third, whilst the distance between the first and third notes is some variant of a _fifth_.
15//!
16//! These compositional contraints lead to four possible triadic chord types, namely: major,
17//! minor, augmented, and diminished. Each of these chord types can be represented as a 3-tuple
18//! containing the pitch classes of each note within the chord. For example, a C-major triad
19//! can be represented as the tuple (0, 4, 7), where `0` represents the root note C, `4` the
20//! major third E, and `7` as the perfect fifth G.
21//!
22//! Additionally, triads may be transformed using any one of three transformations defined as:
23//! leading, parallel, and relative. The behavior of these transformations is determined by the
24//! interval between the first two notes of the triad, i.e. a major or minor third. Each of
25//! these transformations is capable of being chained together into discrete and continuous
26//! sequences or spaces and is its own inverse. This means that any consecutive applications of
27//! any one particular transformation simply reverts the object back into its original state.
28//!
29//! That being said, augmented and diminished traids _can_ be transformed, however, the exact
30//! nature of their responses is not as predictable as with major / minor triads.  
31//!
32//! ## Examples
33//!
34//! ### _Basic Usage_
35//!
36//! Initialize a C-major triad and verify its composition
37//!
38//! ```rust
39//! use rstmt_nrt::Triad;
40//! // create new triad
41//! let c_major = Triad::major(0);
42//! // verify the composition
43//! assert_eq! { c_major, [0, 4, 7] }
44//! ```
45//!
46//! ### _Transformations_
47//!
48//! Initialize a C-major triad before applying each of the three transformations:
49//!
50//! ```rust
51//! use rstmt_nrt::Triad;
52//! // create new triad
53//! let c_major = Triad::major(0);
54//! // apply leading transformation
55//! assert_eq! { c_major.leading(), Triad::minor(4) }
56//! // apply parallel transformation
57//! assert_eq! { c_major.parallel(), Triad::minor(0) }
58//! // apply relative transformation
59//! assert_eq! { c_major.relative(), Triad::minor(9) }
60//! // confirm inverses
61//! assert_eq! { c_major.leading().leading(), c_major }
62//! assert_eq! { c_major.parallel().parallel(), c_major }
63//! assert_eq! { c_major.relative().relative(), c_major }
64//! ```
65//!
66//! # References
67//!
68//! - [Continuous Transformations](https://www.mtosmt.org/issues/mto.04.10.3/mto.04.10.3.callender.pdf)
69//! - [Neo-Riemannian Theory](https://en.wikipedia.org/wiki/Neo-Riemannian_theory)
70
71use crate::traits::{TriadRepr, TriadType};
72use crate::types::Triads;
73use rspace_traits::RawSpace;
74use rstmt_core::{Major, Octave};
75
76/// The default representation of a triadic chord
77pub type TriChord<T = isize> = [T; 3];
78/// A type alias for a [`TriadBase`] instance configured to use the [`TriChord`] as
79/// its storage
80pub type Triad<K = Triads, T = usize> = TriadBase<TriChord<T>, K, T>;
81/// A type alias for a [`Triad`] using a dynamic classifier of type [`Triads`]
82pub type DynTriad<T = usize> = Triad<Triads, T>;
83
84/// The [`TriadBase`] is an implementation of a triad generic over the chord, or storage, its
85/// classification, and the element type used to represent a note within the triadic chord.
86#[derive(Clone, Copy, Default, Eq, Hash, PartialOrd)]
87#[cfg_attr(
88    feature = "serde",
89    derive(serde::Deserialize, serde::Serialize),
90    serde(rename_all = "snake_case")
91)]
92pub struct TriadBase<S = TriChord, K = Major, T = <S as RawSpace>::Elem>
93where
94    K: TriadType,
95    S: TriadRepr<Elem = T>,
96{
97    pub(crate) chord: S,
98    pub(crate) class: K,
99    pub(crate) octave: Octave<T>,
100}
101
102#[cfg(test)]
103mod tests {
104    use super::Triad;
105
106    #[test]
107    /// Test: test initialization routines using a single root note, `C(0)`
108    fn test_triad_create() {
109        assert_eq! { Triad::major(0), [0, 4, 7] }
110        assert_eq! { Triad::minor(0), [0, 3, 7] }
111        assert_eq! { Triad::augmented(0), [0, 4, 8] }
112        assert_eq! { Triad::diminished(0), [0, 3, 6] }
113    }
114
115    #[test]
116    /// Test: test initialization routines using various root notes
117    fn test_triad_create_with_n() {
118        for root in 0..12 {
119            let major = Triad::major(root);
120            let minor = Triad::minor(root);
121            let augmented = Triad::augmented(root);
122            let diminished = Triad::diminished(root);
123            assert! { major.is_major() && !major.is_minor() }
124            assert! { minor.is_minor() && !minor.is_major() }
125            assert! { augmented.is_augmented() }
126            assert! { diminished.is_diminished() }
127        }
128    }
129
130    #[test]
131    fn test_triad_properties() {
132        let fsharp_minor = Triad::minor(6);
133        assert! { fsharp_minor.is_minor() && !fsharp_minor.is_major() }
134        assert_eq! { fsharp_minor.class(), &rstmt_core::Minor }
135    }
136
137    #[test]
138    fn test_triad_transform_c_major() {
139        let c_major = Triad::major(0);
140        let leading = Triad::minor(4);
141        let parallel = Triad::minor(0);
142        let relative = Triad::minor(9);
143        // leading
144        assert! {
145            c_major.leading() == leading &&
146            leading.leading() == c_major &&
147            c_major.leading().leading() == leading.leading()
148        }
149        // parallel
150        assert! {
151            c_major.parallel() == parallel &&
152            parallel.parallel() == c_major &&
153            c_major.parallel().parallel() == parallel.parallel()
154        }
155        // relative
156        assert! {
157            c_major.relative() == relative &&
158            relative.relative() == c_major &&
159            c_major.relative().relative() == relative.relative()
160        }
161    }
162}