Skip to main content

music21_rs/analysis/
harmonic_function.rs

1//! Harmonic function labels and the Roman numerals they stand for:
2//! music21's `analysis.harmonicFunction`.
3//!
4//! A label is a function — tonic, subdominant or dominant — in major
5//! (capital) or minor (small), optionally with its Parallelklang (`p`/`P`,
6//! a third below) or Gegenklang (`g`/`G`, a third above):
7//!
8//! ```text
9//!   T  I     Tp vi     Tg iii
10//!   S  IV    Sp ii     Sg vi
11//!   D  V     Dp iii    Dg bvii
12//! ```
13//!
14//! Where the figure depends on the mode — the third, sixth and seventh
15//! degrees — a minor key has its own table.
16
17use crate::{
18    error::{Error, Result},
19    key::Key,
20    roman::RomanNumeral,
21};
22
23/// The mode whose table is used where a figure differs by mode.
24const MINOR: &str = "minor";
25
26/// One of the eighteen harmonic function labels.
27#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum HarmonicFunction {
30    /// `T`
31    TonicMajor,
32    /// `Tp`
33    TonicMajorParallelklangMinor,
34    /// `Tg`
35    TonicMajorGegenklangMinor,
36    /// `t`
37    TonicMinor,
38    /// `tP`
39    TonicMinorParallelklangMajor,
40    /// `tG`
41    TonicMinorGegenklangMajor,
42    /// `S`
43    SubdominantMajor,
44    /// `Sp`
45    SubdominantMajorParallelklangMinor,
46    /// `Sg`
47    SubdominantMajorGegenklangMinor,
48    /// `s`
49    SubdominantMinor,
50    /// `sP`
51    SubdominantMinorParallelklangMajor,
52    /// `sG`
53    SubdominantMinorGegenklangMajor,
54    /// `D`
55    DominantMajor,
56    /// `Dp`
57    DominantMajorParallelklangMinor,
58    /// `Dg`
59    DominantMajorGegenklangMinor,
60    /// `d`
61    DominantMinor,
62    /// `dP`
63    DominantMinorParallelklangMajor,
64    /// `dG`
65    DominantMinorGegenklangMajor,
66}
67
68use HarmonicFunction::*;
69
70/// The figures that are the same in either mode, in music21's order.
71const KEY_NEUTRAL: [(HarmonicFunction, &str); 11] = [
72    (TonicMajor, "I"),
73    (TonicMinor, "i"),
74    (SubdominantMinorGegenklangMajor, "bII"),
75    (SubdominantMajorParallelklangMinor, "ii"),
76    (SubdominantMajor, "IV"),
77    (SubdominantMinor, "iv"),
78    (DominantMajor, "V"),
79    (DominantMinor, "v"),
80    (TonicMajorParallelklangMinor, "vi"),
81    (SubdominantMajorGegenklangMinor, "vi"),
82    (DominantMajorGegenklangMinor, "bvii"),
83];
84
85/// The figures a major key has of its own, listed before the neutral ones
86/// as music21 merges them: the first match wins a reverse lookup, so `iii`
87/// reads as `Dp` and not `Tg`.
88const MAJOR_ONLY: [(HarmonicFunction, &str); 7] = [
89    (TonicMinorParallelklangMajor, "bIII"),
90    (DominantMinorGegenklangMajor, "bIII"),
91    (DominantMajorParallelklangMinor, "iii"),
92    (TonicMajorGegenklangMinor, "iii"),
93    (SubdominantMinorParallelklangMajor, "bVI"),
94    (TonicMinorGegenklangMajor, "bVI"),
95    (DominantMinorParallelklangMajor, "bVII"),
96];
97
98/// The figures a minor key has of its own, likewise.
99const MINOR_ONLY: [(HarmonicFunction, &str); 7] = [
100    (TonicMinorParallelklangMajor, "III"),
101    (DominantMinorGegenklangMajor, "III"),
102    (DominantMajorParallelklangMinor, "#iii"),
103    (TonicMajorGegenklangMinor, "#iii"),
104    (SubdominantMinorParallelklangMajor, "VI"),
105    (TonicMinorGegenklangMajor, "VI"),
106    (DominantMinorParallelklangMajor, "VII"),
107];
108
109/// Whether a lookup keeps the whole label or only its main function.
110#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum FunctionDetail {
113    /// The whole label: `sP`.
114    #[default]
115    Full,
116    /// Only the Hauptfunktion, its first letter: `s`. music21's
117    /// `onlyHauptHarmonicFunction`.
118    MainOnly,
119}
120
121impl HarmonicFunction {
122    /// All eighteen, in music21's order.
123    pub const ALL: [Self; 18] = [
124        TonicMajor,
125        TonicMajorParallelklangMinor,
126        TonicMajorGegenklangMinor,
127        TonicMinor,
128        TonicMinorParallelklangMajor,
129        TonicMinorGegenklangMajor,
130        SubdominantMajor,
131        SubdominantMajorParallelklangMinor,
132        SubdominantMajorGegenklangMinor,
133        SubdominantMinor,
134        SubdominantMinorParallelklangMajor,
135        SubdominantMinorGegenklangMajor,
136        DominantMajor,
137        DominantMajorParallelklangMinor,
138        DominantMajorGegenklangMinor,
139        DominantMinor,
140        DominantMinorParallelklangMajor,
141        DominantMinorGegenklangMajor,
142    ];
143
144    /// The label: `T`, `Tp`, `sG` and so on.
145    pub fn symbol(self) -> &'static str {
146        match self {
147            TonicMajor => "T",
148            TonicMajorParallelklangMinor => "Tp",
149            TonicMajorGegenklangMinor => "Tg",
150            TonicMinor => "t",
151            TonicMinorParallelklangMajor => "tP",
152            TonicMinorGegenklangMajor => "tG",
153            SubdominantMajor => "S",
154            SubdominantMajorParallelklangMinor => "Sp",
155            SubdominantMajorGegenklangMinor => "Sg",
156            SubdominantMinor => "s",
157            SubdominantMinorParallelklangMajor => "sP",
158            SubdominantMinorGegenklangMajor => "sG",
159            DominantMajor => "D",
160            DominantMajorParallelklangMinor => "Dp",
161            DominantMajorGegenklangMinor => "Dg",
162            DominantMinor => "d",
163            DominantMinorParallelklangMajor => "dP",
164            DominantMinorGegenklangMajor => "dG",
165        }
166    }
167
168    /// music21's member name: `TONIC_MAJOR`, `SUBDOMINANT_MINOR_GEGENKLANG_MAJOR`.
169    pub fn music21_name(self) -> &'static str {
170        match self {
171            TonicMajor => "TONIC_MAJOR",
172            TonicMajorParallelklangMinor => "TONIC_MAJOR_PARALLELKLANG_MINOR",
173            TonicMajorGegenklangMinor => "TONIC_MAJOR_GEGENKLANG_MINOR",
174            TonicMinor => "TONIC_MINOR",
175            TonicMinorParallelklangMajor => "TONIC_MINOR_PARALLELKLANG_MAJOR",
176            TonicMinorGegenklangMajor => "TONIC_MINOR_GEGENKLANG_MAJOR",
177            SubdominantMajor => "SUBDOMINANT_MAJOR",
178            SubdominantMajorParallelklangMinor => "SUBDOMINANT_MAJOR_PARALLELKLANG_MINOR",
179            SubdominantMajorGegenklangMinor => "SUBDOMINANT_MAJOR_GEGENKLANG_MINOR",
180            SubdominantMinor => "SUBDOMINANT_MINOR",
181            SubdominantMinorParallelklangMajor => "SUBDOMINANT_MINOR_PARALLELKLANG_MAJOR",
182            SubdominantMinorGegenklangMajor => "SUBDOMINANT_MINOR_GEGENKLANG_MAJOR",
183            DominantMajor => "DOMINANT_MAJOR",
184            DominantMajorParallelklangMinor => "DOMINANT_MAJOR_PARALLELKLANG_MINOR",
185            DominantMajorGegenklangMinor => "DOMINANT_MAJOR_GEGENKLANG_MINOR",
186            DominantMinor => "DOMINANT_MINOR",
187            DominantMinorParallelklangMajor => "DOMINANT_MINOR_PARALLELKLANG_MAJOR",
188            DominantMinorGegenklangMajor => "DOMINANT_MINOR_GEGENKLANG_MAJOR",
189        }
190    }
191
192    /// The function a label names, case and all.
193    ///
194    /// # Errors
195    ///
196    /// A label that is none of the eighteen, `TPG` say.
197    pub fn from_symbol(symbol: &str) -> Result<Self> {
198        Self::ALL
199            .into_iter()
200            .find(|function| function.symbol() == symbol)
201            .ok_or_else(|| Error::Value(format!("'{symbol}' is not a valid HarmonicFunction")))
202    }
203
204    /// The main function alone, the label's first letter: `sP` is `s`.
205    pub fn main_function(self) -> Self {
206        match self.symbol().chars().next() {
207            Some('T') => TonicMajor,
208            Some('t') => TonicMinor,
209            Some('S') => SubdominantMajor,
210            Some('s') => SubdominantMinor,
211            Some('D') => DominantMajor,
212            _ => DominantMinor,
213        }
214    }
215
216    /// The Roman numeral figure this function has in a key of `mode`:
217    /// `sP` is `bVI` in major and `VI` in minor.
218    pub fn figure(self, mode: &str) -> &'static str {
219        table(mode)
220            .find(|(function, _)| *function == self)
221            .map(|(_, figure)| figure)
222            .unwrap_or_default()
223    }
224
225    /// The Roman numeral this function is in `key`: music21's
226    /// `functionToRoman`.
227    ///
228    /// ```
229    /// use music21_rs::{Key, analysis::harmonic_function::HarmonicFunction};
230    ///
231    /// let minor = Key::from_tonic_mode("a", "minor")?;
232    /// let numeral = HarmonicFunction::SubdominantMinorParallelklangMajor.to_roman(&minor)?;
233    /// assert_eq!(numeral.figure(), "VI");
234    /// # Ok::<(), music21_rs::Error>(())
235    /// ```
236    ///
237    /// # Errors
238    ///
239    /// None in practice: every figure in the tables is one a Roman numeral
240    /// reads.
241    pub fn to_roman(self, key: &Key) -> Result<RomanNumeral> {
242        RomanNumeral::new(self.figure(key.mode()), key.clone())
243    }
244
245    /// The function a Roman numeral has, read off its numeral and front
246    /// alteration alone, so inversions and figures do not count: music21's
247    /// `romanToFunction`. Where two functions share a numeral, the first in
248    /// music21's table wins: `vi` is `Tp`. Nothing for a numeral no function
249    /// has.
250    pub fn from_roman(numeral: &RomanNumeral, detail: FunctionDetail) -> Option<Self> {
251        let written = numeral.roman_numeral();
252        let (found, _) = table(numeral.key().mode()).find(|(_, figure)| *figure == written)?;
253
254        match detail {
255            FunctionDetail::Full => Some(found),
256            FunctionDetail::MainOnly => Some(found.main_function()),
257        }
258    }
259}
260
261/// The figures a key of `mode` reads its functions as, in music21's order:
262/// the mode's own first, then the neutral ones.
263fn table(mode: &str) -> impl Iterator<Item = (HarmonicFunction, &'static str)> {
264    let own = if mode == MINOR {
265        MINOR_ONLY
266    } else {
267        MAJOR_ONLY
268    };
269    own.into_iter().chain(KEY_NEUTRAL)
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    fn key(tonic: &str, mode: &str) -> Key {
277        Key::from_tonic_mode(tonic, mode).unwrap()
278    }
279
280    fn numeral(figure: &str, tonic: &str, mode: &str) -> RomanNumeral {
281        RomanNumeral::new(figure, key(tonic, mode)).unwrap()
282    }
283
284    #[test]
285    fn every_function_has_a_figure_in_either_mode() {
286        for function in HarmonicFunction::ALL {
287            assert!(!function.figure("major").is_empty(), "{function:?}");
288            assert!(!function.figure(MINOR).is_empty(), "{function:?}");
289            assert_eq!(
290                HarmonicFunction::from_symbol(function.symbol()).unwrap(),
291                function
292            );
293        }
294        assert!(HarmonicFunction::from_symbol("TPG").is_err());
295    }
296
297    #[test]
298    fn functions_read_as_music21s_numerals() {
299        let c = key("C", "major");
300        assert_eq!(TonicMajor.to_roman(&c).unwrap().figure(), "I");
301        assert_eq!(TonicMinor.to_roman(&c).unwrap().figure(), "i");
302        assert_eq!(
303            SubdominantMinorParallelklangMajor
304                .to_roman(&c)
305                .unwrap()
306                .figure(),
307            "bVI"
308        );
309        assert_eq!(
310            SubdominantMinorParallelklangMajor
311                .to_roman(&key("a", MINOR))
312                .unwrap()
313                .figure(),
314            "VI"
315        );
316        assert_eq!(
317            TonicMajorParallelklangMinor.to_roman(&c).unwrap().figure(),
318            "vi"
319        );
320        assert_eq!(
321            SubdominantMajorGegenklangMinor
322                .to_roman(&c)
323                .unwrap()
324                .figure(),
325            "vi"
326        );
327    }
328
329    #[test]
330    fn numerals_read_as_music21s_functions() {
331        let full = FunctionDetail::Full;
332        let vi = numeral("vi", "C", "major");
333        assert_eq!(
334            HarmonicFunction::from_roman(&vi, full),
335            Some(TonicMajorParallelklangMinor)
336        );
337
338        let sixth = numeral("VI", "a", MINOR);
339        assert_eq!(
340            HarmonicFunction::from_roman(&sixth, full),
341            Some(SubdominantMinorParallelklangMajor)
342        );
343        assert_eq!(
344            HarmonicFunction::from_roman(&sixth, FunctionDetail::MainOnly),
345            Some(SubdominantMinor)
346        );
347
348        let neapolitan = numeral("bII6", "g", MINOR);
349        assert_eq!(
350            HarmonicFunction::from_roman(&neapolitan, full),
351            Some(SubdominantMinorGegenklangMajor)
352        );
353
354        let mediant = numeral("III", "f", MINOR);
355        assert_eq!(
356            HarmonicFunction::from_roman(&mediant, full),
357            Some(TonicMinorParallelklangMajor)
358        );
359        assert_eq!(
360            HarmonicFunction::from_roman(&numeral("i6", "C", "major"), full),
361            Some(TonicMinor)
362        );
363        assert_eq!(
364            HarmonicFunction::from_roman(&numeral("#iv", "C", "major"), full),
365            None
366        );
367    }
368
369    #[test]
370    fn each_label_names_its_main_function() {
371        let mains: Vec<&str> = HarmonicFunction::ALL
372            .into_iter()
373            .map(|function| function.main_function().symbol())
374            .collect();
375        assert_eq!(
376            mains,
377            [
378                "T", "T", "T", "t", "t", "t", "S", "S", "S", "s", "s", "s", "D", "D", "D", "d",
379                "d", "d"
380            ]
381        );
382        assert_eq!(
383            DominantMinorGegenklangMajor.music21_name(),
384            "DOMINANT_MINOR_GEGENKLANG_MAJOR"
385        );
386    }
387}