Skip to main content

music21_rs/figuredbass/
resolution.rs

1//! How a dominant seventh, a diminished seventh or an augmented sixth
2//! resolves: music21's `figuredBass.resolution`.
3//!
4//! Each function takes a possibility spelling one of those chords -- a pitch
5//! per part, highest part first -- and moves every pitch by the step its
6//! place in the chord calls for, so the possibility it answers is the chord
7//! it resolves to, voiced part for part. A pitch the rule says nothing about
8//! stays where it is.
9//!
10//! The chord's notes are worked out from the possibility unless the caller
11//! hands them over as a [`ChordInfo`], which a realizer does, since it
12//! resolves many voicings of one chord.
13
14use std::sync::LazyLock;
15
16use crate::chord::Chord;
17use crate::error::{Error, Result};
18use crate::interval::Interval;
19use crate::pitch::Pitch;
20
21/// A chord's bass, root, third, fifth and seventh, whichever it has: what a
22/// resolution is worked out from.
23#[derive(Clone, Debug, Default, PartialEq)]
24pub struct ChordInfo {
25    /// The chord's bass.
26    pub bass: Option<Pitch>,
27    /// Its root.
28    pub root: Option<Pitch>,
29    /// Its third.
30    pub third: Option<Pitch>,
31    /// Its fifth.
32    pub fifth: Option<Pitch>,
33    /// Its seventh.
34    pub seventh: Option<Pitch>,
35}
36
37impl ChordInfo {
38    /// The notes of `chord`, where it has them.
39    pub fn of(chord: &Chord) -> Self {
40        Self {
41            bass: chord.bass().cloned(),
42            root: chord.root().cloned(),
43            third: chord.chord_step(3).cloned(),
44            fifth: chord.chord_step(5).cloned(),
45            seventh: chord.chord_step(7).cloned(),
46        }
47    }
48}
49
50/// The three augmented sixths that resolve by a rule of their own. An
51/// Italian sixth has no fourth note to place, and is resolved as any chord
52/// is.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub enum AugmentedSixth {
55    /// French: music21's type 1.
56    French,
57    /// German: music21's type 2.
58    German,
59    /// Swiss: music21's type 3.
60    Swiss,
61}
62
63impl AugmentedSixth {
64    /// The kind of augmented sixth `chord` is.
65    ///
66    /// # Errors
67    ///
68    /// A chord that is no augmented sixth, or an Italian one.
69    pub fn of(chord: &Chord) -> Result<Self> {
70        if !chord.is_augmented_sixth(false) {
71            return Err(Error::FiguredBass(
72                "Possibility is not an augmented sixth chord.".to_string(),
73            ));
74        }
75        if chord.is_italian_augmented_sixth(false, false) {
76            return Err(Error::FiguredBass(
77                "Italian augmented sixth resolution not supported in this method.".to_string(),
78            ));
79        }
80        if chord.is_french_augmented_sixth(false) {
81            Ok(Self::French)
82        } else if chord.is_german_augmented_sixth(false) {
83            Ok(Self::German)
84        } else if chord.is_swiss_augmented_sixth(false) {
85            Ok(Self::Swiss)
86        } else {
87            Err(Error::FiguredBass("Unknown augSixthType: None".to_string()))
88        }
89    }
90
91    /// music21's number for the kind: 1, 2 or 3.
92    pub fn number(self) -> u8 {
93        match self {
94            Self::French => 1,
95            Self::German => 2,
96            Self::Swiss => 3,
97        }
98    }
99
100    /// The kind music21 numbers so.
101    pub fn from_number(number: u8) -> Option<Self> {
102        match number {
103            1 => Some(Self::French),
104            2 => Some(Self::German),
105            3 => Some(Self::Swiss),
106            _ => None,
107        }
108    }
109}
110
111/// Every step a resolution moves a pitch by, read once.
112static STEPS: LazyLock<Vec<(&'static str, Interval)>> = LazyLock::new(|| {
113    ["P1", "m2", "-m2", "M2", "-M2", "A1", "d1", "d2", "P4"]
114        .into_iter()
115        .map(|name| (name, Interval::from_name(name).expect("a resolution step")))
116        .collect()
117});
118
119fn step(name: &str) -> &'static Interval {
120    STEPS
121        .iter()
122        .find(|(known, _)| *known == name)
123        .map(|(_, interval)| interval)
124        .expect("every resolution step is in the table")
125}
126
127/// A rule of a resolution: which pitches it moves, and by what.
128type Rule<'a> = (Box<dyn Fn(&Pitch) -> bool + 'a>, &'static str);
129
130/// Moves each pitch of `possibility` by the step of the first rule it
131/// answers to, leaving it where it is when it answers to none.
132fn resolve(possibility: &[Pitch], rules: &[Rule<'_>]) -> Result<Vec<Pitch>> {
133    possibility
134        .iter()
135        .map(|pitch| {
136            let name = rules
137                .iter()
138                .find(|(applies, _)| applies(pitch))
139                .map_or("P1", |(_, step)| *step);
140            pitch.transpose(step(name))
141        })
142        .collect()
143}
144
145/// Whether `pitch` is spelled as `note`, where there is such a note.
146fn named<'a>(note: Option<&'a Pitch>) -> impl Fn(&Pitch) -> bool + 'a {
147    move |pitch| note.is_some_and(|note| pitch.name() == note.name())
148}
149
150/// Moves each named note of `possibility` by its step, and the rest nowhere.
151fn move_named(
152    possibility: &[Pitch],
153    moves: &[(Option<&Pitch>, &'static str)],
154) -> Result<Vec<Pitch>> {
155    let rules: Vec<Rule<'_>> = moves
156        .iter()
157        .map(|&(note, step)| -> Rule<'_> { (Box::new(named(note)), step) })
158        .collect();
159    resolve(possibility, &rules)
160}
161
162/// The chord's notes as a resolution of `possibility` needs them, read off
163/// the possibility where the caller has not given them, after `check` has
164/// said the chord is of the right kind.
165fn info_or_read(
166    possibility: &[Pitch],
167    info: Option<&ChordInfo>,
168    check: impl Fn(&Chord) -> Result<()>,
169) -> Result<ChordInfo> {
170    match info {
171        Some(info) => Ok(info.clone()),
172        None => {
173            let chord = Chord::new(possibility)?;
174            check(&chord)?;
175            Ok(ChordInfo::of(&chord))
176        }
177    }
178}
179
180/// The notes of an augmented sixth, and which kind it is.
181fn augmented_sixth_notes(
182    possibility: &[Pitch],
183    kind: Option<AugmentedSixth>,
184    info: Option<&ChordInfo>,
185) -> Result<(AugmentedSixth, ChordInfo)> {
186    let mut kind = kind;
187    let info = match info {
188        Some(info) => info.clone(),
189        None => {
190            let chord = Chord::new(possibility)?;
191            if kind.is_none() {
192                kind = Some(AugmentedSixth::of(&chord)?);
193            } else if !chord.is_augmented_sixth(false) {
194                return Err(Error::FiguredBass(
195                    "Possibility is not an augmented sixth chord.".to_string(),
196                ));
197            }
198            ChordInfo::of(&chord)
199        }
200    };
201    let kind = kind.ok_or_else(|| Error::FiguredBass("Unknown augSixthType: None".to_string()))?;
202    Ok((kind, info))
203}
204
205/// The bass, root, fifth and fourth note of an augmented sixth, read as its
206/// kind reads them: the fourth note stands where a French or Swiss sixth has
207/// its root and where a German sixth has its seventh.
208fn augmented_sixth_parts(kind: AugmentedSixth, info: &ChordInfo) -> [Option<&Pitch>; 4] {
209    match kind {
210        AugmentedSixth::French | AugmentedSixth::Swiss => [
211            info.bass.as_ref(),
212            info.third.as_ref(),
213            info.seventh.as_ref(),
214            info.root.as_ref(),
215        ],
216        AugmentedSixth::German => [
217            info.bass.as_ref(),
218            info.root.as_ref(),
219            info.fifth.as_ref(),
220            info.seventh.as_ref(),
221        ],
222    }
223}
224
225/// Resolves an augmented sixth to the dominant: music21's
226/// `augmentedSixthToDominant`. The bass and the fifth fall a semitone and
227/// the root rises one; a German sixth's seventh falls a semitone too, and a
228/// Swiss sixth's doubly augmented fourth is respelled where it stands.
229///
230/// `kind` and `info` are read off the possibility where not given.
231///
232/// # Errors
233///
234/// A possibility that is no augmented sixth, or an Italian one, when they
235/// are not given.
236pub fn augmented_sixth_to_dominant(
237    possibility: &[Pitch],
238    kind: Option<AugmentedSixth>,
239    info: Option<&ChordInfo>,
240) -> Result<Vec<Pitch>> {
241    resolve_augmented_sixth(
242        possibility,
243        kind,
244        info,
245        "-m2",
246        &[
247            (AugmentedSixth::Swiss, "d1"),
248            (AugmentedSixth::German, "-m2"),
249        ],
250    )
251}
252
253/// Resolves an augmented sixth to the major tonic in second inversion:
254/// music21's `augmentedSixthToMajorTonic`.
255///
256/// # Errors
257///
258/// As [`augmented_sixth_to_dominant`].
259pub fn augmented_sixth_to_major_tonic(
260    possibility: &[Pitch],
261    kind: Option<AugmentedSixth>,
262    info: Option<&ChordInfo>,
263) -> Result<Vec<Pitch>> {
264    resolve_augmented_sixth(
265        possibility,
266        kind,
267        info,
268        "P1",
269        &[
270            (AugmentedSixth::French, "M2"),
271            (AugmentedSixth::German, "A1"),
272            (AugmentedSixth::Swiss, "m2"),
273        ],
274    )
275}
276
277/// Resolves an augmented sixth to the minor tonic in second inversion:
278/// music21's `augmentedSixthToMinorTonic`.
279///
280/// # Errors
281///
282/// As [`augmented_sixth_to_dominant`].
283pub fn augmented_sixth_to_minor_tonic(
284    possibility: &[Pitch],
285    kind: Option<AugmentedSixth>,
286    info: Option<&ChordInfo>,
287) -> Result<Vec<Pitch>> {
288    resolve_augmented_sixth(
289        possibility,
290        kind,
291        info,
292        "P1",
293        &[
294            (AugmentedSixth::French, "m2"),
295            (AugmentedSixth::Swiss, "d2"),
296        ],
297    )
298}
299
300/// Resolves an augmented sixth: the bass falls and the root rises a minor
301/// second, the fifth moves by `fifth`, and the last note moves as `other`
302/// says for the kind of sixth it is, staying put for a kind not listed.
303fn resolve_augmented_sixth(
304    possibility: &[Pitch],
305    kind: Option<AugmentedSixth>,
306    info: Option<&ChordInfo>,
307    fifth: &'static str,
308    other: &[(AugmentedSixth, &'static str)],
309) -> Result<Vec<Pitch>> {
310    let (kind, info) = augmented_sixth_notes(possibility, kind, info)?;
311    let [bass, root, fifth_note, other_note] = augmented_sixth_parts(kind, &info);
312    let mut moves = vec![(bass, "-m2"), (root, "m2"), (fifth_note, fifth)];
313    if let Some(&(_, step)) = other.iter().find(|(listed, _)| *listed == kind) {
314        moves.push((other_note, step));
315    }
316    move_named(possibility, &moves)
317}
318
319fn dominant_seventh(chord: &Chord) -> Result<()> {
320    if chord.is_dominant_seventh() {
321        Ok(())
322    } else {
323        Err(Error::FiguredBass(
324            "Possibility is not a dominant seventh chord.".to_string(),
325        ))
326    }
327}
328
329fn dominant_seventh_in_root_position(chord: &Chord) -> Result<()> {
330    dominant_seventh(chord)?;
331    if chord.inversion() == Some(0) {
332        Ok(())
333    } else {
334        Err(Error::FiguredBass(
335            "Possibility must be in root position.".to_string(),
336        ))
337    }
338}
339
340fn diminished_seventh(chord: &Chord) -> Result<()> {
341    if chord.is_diminished_seventh() {
342        Ok(())
343    } else {
344        Err(Error::FiguredBass(
345            "Possibility is not a fully diminished seventh chord.".to_string(),
346        ))
347    }
348}
349
350/// The five notes of a seventh chord, each of which a rule may need.
351fn seventh_notes(info: &ChordInfo) -> [Option<&Pitch>; 5] {
352    [
353        info.bass.as_ref(),
354        info.root.as_ref(),
355        info.third.as_ref(),
356        info.fifth.as_ref(),
357        info.seventh.as_ref(),
358    ]
359}
360
361/// Resolves a dominant seventh to the major tonic: music21's
362/// `dominantSeventhToMajorTonic`. The root in the bass rises a fourth, the
363/// third rises a semitone, and the fifth and seventh fall -- or, taking a
364/// V4/3 to I6, rise a tone.
365///
366/// # Errors
367///
368/// A possibility that is no dominant seventh, when `info` is not given.
369pub fn dominant_seventh_to_major_tonic(
370    possibility: &[Pitch],
371    resolve_v43_to_i6: bool,
372    info: Option<&ChordInfo>,
373) -> Result<Vec<Pitch>> {
374    dominant_seventh_to_tonic(possibility, resolve_v43_to_i6, info, "M2", "-M2", "-m2")
375}
376
377/// Resolves a dominant seventh to the minor tonic: music21's
378/// `dominantSeventhToMinorTonic`.
379///
380/// # Errors
381///
382/// As [`dominant_seventh_to_major_tonic`].
383pub fn dominant_seventh_to_minor_tonic(
384    possibility: &[Pitch],
385    resolve_v43_to_i6: bool,
386    info: Option<&ChordInfo>,
387) -> Result<Vec<Pitch>> {
388    dominant_seventh_to_tonic(possibility, resolve_v43_to_i6, info, "m2", "-M2", "-M2")
389}
390
391fn dominant_seventh_to_tonic(
392    possibility: &[Pitch],
393    resolve_v43_to_i6: bool,
394    info: Option<&ChordInfo>,
395    fifth_up: &'static str,
396    fifth_down: &'static str,
397    seventh_down: &'static str,
398) -> Result<Vec<Pitch>> {
399    let info = info_or_read(possibility, info, dominant_seventh)?;
400    let [bass, root, third, fifth, seventh] = seventh_notes(&info);
401    let rules: Vec<Rule<'_>> = vec![
402        (
403            Box::new(move |pitch: &Pitch| named(root)(pitch) && bass == Some(pitch)),
404            "P4",
405        ),
406        (Box::new(named(third)), "m2"),
407        (
408            Box::new(move |pitch: &Pitch| resolve_v43_to_i6 && named(fifth)(pitch)),
409            fifth_up,
410        ),
411        (Box::new(named(fifth)), fifth_down),
412        (
413            Box::new(move |pitch: &Pitch| resolve_v43_to_i6 && named(seventh)(pitch)),
414            "M2",
415        ),
416        (Box::new(named(seventh)), seventh_down),
417    ];
418    resolve(possibility, &rules)
419}
420
421/// Resolves a dominant seventh in root position deceptively, to the major
422/// submediant: music21's `dominantSeventhToMajorSubmediant`.
423///
424/// # Errors
425///
426/// A possibility that is no dominant seventh in root position, when `info`
427/// is not given.
428pub fn dominant_seventh_to_major_submediant(
429    possibility: &[Pitch],
430    info: Option<&ChordInfo>,
431) -> Result<Vec<Pitch>> {
432    let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
433    let [_, root, third, fifth, seventh] = seventh_notes(&info);
434    move_named(
435        possibility,
436        &[
437            (root, "m2"),
438            (third, "m2"),
439            (fifth, "-M2"),
440            (seventh, "-M2"),
441        ],
442    )
443}
444
445/// Resolves a dominant seventh in root position deceptively, to the minor
446/// submediant: music21's `dominantSeventhToMinorSubmediant`.
447///
448/// # Errors
449///
450/// As [`dominant_seventh_to_major_submediant`].
451pub fn dominant_seventh_to_minor_submediant(
452    possibility: &[Pitch],
453    info: Option<&ChordInfo>,
454) -> Result<Vec<Pitch>> {
455    let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
456    let [_, root, third, fifth, seventh] = seventh_notes(&info);
457    move_named(
458        possibility,
459        &[
460            (root, "M2"),
461            (third, "m2"),
462            (fifth, "-M2"),
463            (seventh, "-m2"),
464        ],
465    )
466}
467
468/// Resolves a dominant seventh in root position to the major subdominant:
469/// music21's `dominantSeventhToMajorSubdominant`.
470///
471/// # Errors
472///
473/// As [`dominant_seventh_to_major_submediant`].
474pub fn dominant_seventh_to_major_subdominant(
475    possibility: &[Pitch],
476    info: Option<&ChordInfo>,
477) -> Result<Vec<Pitch>> {
478    let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
479    let [_, root, third, fifth, _] = seventh_notes(&info);
480    move_named(possibility, &[(root, "M2"), (third, "m2"), (fifth, "-M2")])
481}
482
483/// Resolves a dominant seventh in root position to the minor subdominant:
484/// music21's `dominantSeventhToMinorSubdominant`.
485///
486/// # Errors
487///
488/// As [`dominant_seventh_to_major_submediant`].
489pub fn dominant_seventh_to_minor_subdominant(
490    possibility: &[Pitch],
491    info: Option<&ChordInfo>,
492) -> Result<Vec<Pitch>> {
493    let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
494    let [_, root, third, fifth, _] = seventh_notes(&info);
495    move_named(possibility, &[(root, "m2"), (third, "m2"), (fifth, "-M2")])
496}
497
498/// Resolves a diminished seventh to the major tonic: music21's
499/// `diminishedSeventhToMajorTonic`. With `doubled_root`, the third falls to
500/// double the tonic rather than rising to its third.
501///
502/// # Errors
503///
504/// A possibility that is no fully diminished seventh, when `info` is not
505/// given.
506pub fn diminished_seventh_to_major_tonic(
507    possibility: &[Pitch],
508    doubled_root: bool,
509    info: Option<&ChordInfo>,
510) -> Result<Vec<Pitch>> {
511    diminished_seventh_to_tonic(possibility, doubled_root, info, "M2", "-m2")
512}
513
514/// Resolves a diminished seventh to the minor tonic: music21's
515/// `diminishedSeventhToMinorTonic`.
516///
517/// # Errors
518///
519/// As [`diminished_seventh_to_major_tonic`].
520pub fn diminished_seventh_to_minor_tonic(
521    possibility: &[Pitch],
522    doubled_root: bool,
523    info: Option<&ChordInfo>,
524) -> Result<Vec<Pitch>> {
525    diminished_seventh_to_tonic(possibility, doubled_root, info, "m2", "-M2")
526}
527
528fn diminished_seventh_to_tonic(
529    possibility: &[Pitch],
530    doubled_root: bool,
531    info: Option<&ChordInfo>,
532    third_up: &'static str,
533    fifth_down: &'static str,
534) -> Result<Vec<Pitch>> {
535    let info = info_or_read(possibility, info, diminished_seventh)?;
536    let [_, root, third, fifth, seventh] = seventh_notes(&info);
537    let rules: Vec<Rule<'_>> = vec![
538        (Box::new(named(root)), "m2"),
539        (
540            Box::new(move |pitch: &Pitch| doubled_root && named(third)(pitch)),
541            "-M2",
542        ),
543        (Box::new(named(third)), third_up),
544        (Box::new(named(fifth)), fifth_down),
545        (Box::new(named(seventh)), "-m2"),
546    ];
547    resolve(possibility, &rules)
548}
549
550/// Resolves a diminished seventh to the major subdominant: music21's
551/// `diminishedSeventhToMajorSubdominant`.
552///
553/// # Errors
554///
555/// As [`diminished_seventh_to_major_tonic`].
556pub fn diminished_seventh_to_major_subdominant(
557    possibility: &[Pitch],
558    info: Option<&ChordInfo>,
559) -> Result<Vec<Pitch>> {
560    let info = info_or_read(possibility, info, diminished_seventh)?;
561    let [_, root, third, _, seventh] = seventh_notes(&info);
562    move_named(
563        possibility,
564        &[(root, "m2"), (third, "-M2"), (seventh, "A1")],
565    )
566}
567
568/// Resolves a diminished seventh to the minor subdominant: music21's
569/// `diminishedSeventhToMinorSubdominant`.
570///
571/// # Errors
572///
573/// As [`diminished_seventh_to_major_tonic`].
574pub fn diminished_seventh_to_minor_subdominant(
575    possibility: &[Pitch],
576    info: Option<&ChordInfo>,
577) -> Result<Vec<Pitch>> {
578    let info = info_or_read(possibility, info, diminished_seventh)?;
579    let [_, root, third, _, _] = seventh_notes(&info);
580    move_named(possibility, &[(root, "m2"), (third, "-M2")])
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    /// music21's own French, German and Swiss sixths over B-2.
588    fn sixths() -> [Vec<Pitch>; 3] {
589        let chord = |names: [&str; 4]| names.map(|name| Pitch::from_name(name).unwrap()).to_vec();
590        [
591            chord(["G#4", "E4", "D4", "B-2"]),
592            chord(["G#4", "F4", "D4", "B-2"]),
593            chord(["G#4", "E#4", "D4", "B-2"]),
594        ]
595    }
596
597    fn names(pitches: Result<Vec<Pitch>>) -> Vec<String> {
598        pitches
599            .unwrap()
600            .iter()
601            .map(Pitch::name_with_octave)
602            .collect()
603    }
604
605    #[test]
606    fn every_augmented_sixth_resolves_as_music21_resolves_it() {
607        for sixth in sixths() {
608            assert_eq!(
609                names(augmented_sixth_to_dominant(&sixth, None, None)),
610                ["A4", "E4", "C#4", "A2"]
611            );
612            assert_eq!(
613                names(augmented_sixth_to_major_tonic(&sixth, None, None)),
614                ["A4", "F#4", "D4", "A2"]
615            );
616            assert_eq!(
617                names(augmented_sixth_to_minor_tonic(&sixth, None, None)),
618                ["A4", "F4", "D4", "A2"]
619            );
620        }
621    }
622}