Skip to main content

music21_rs/chord/
quality.rs

1//! What kind of chord it is: the chord steps above the root, the triad
2//! and seventh predicates, the augmented sixths and the quality they add
3//! up to.
4
5use super::*;
6
7impl Chord {
8    /// Returns the first pitch lying at the given chord step above the root,
9    /// so `3` is the third and `7` the seventh. Steps of eight and above are
10    /// folded down by an octave, so `9` finds a second.
11    pub fn chord_step(&self, step: u8) -> Option<&Pitch> {
12        self.chord_step_from(step, self.root()?)
13    }
14
15    /// Returns the third above the root, if the chord has one.
16    pub fn third(&self) -> Option<&Pitch> {
17        self.chord_step(3)
18    }
19
20    /// Returns the fifth above the root, if the chord has one.
21    pub fn fifth(&self) -> Option<&Pitch> {
22        self.chord_step(5)
23    }
24
25    /// Returns the seventh above the root, if the chord has one.
26    pub fn seventh(&self) -> Option<&Pitch> {
27        self.chord_step(7)
28    }
29
30    /// Returns the semitones from the root to the given chord step, within an
31    /// octave, if the chord has that step.
32    pub fn semitones_from_chord_step(&self, step: u8) -> Option<u8> {
33        let root = self.root()?;
34        let pitch = self.chord_step_from(step, root)?;
35        Some(semitones_above(root, pitch))
36    }
37
38    /// Returns whether the chord has the given step spelled two different
39    /// ways, such as both `E` and `E-` above `C`.
40    pub fn has_repeated_chord_step(&self, step: u8) -> bool {
41        let Some(root) = self.root() else {
42            return false;
43        };
44        let step = fold_chord_step(step);
45        let Some(first) = self
46            .chord_step_from(step, root)
47            .map(|pitch| semitones_above(root, pitch))
48        else {
49            return false;
50        };
51        self.pitch_refs().any(|pitch| {
52            diatonic_steps_above(root, pitch) == step && semitones_above(root, pitch) != first
53        })
54    }
55
56    /// Returns whether two pitches share a pitch class under different names,
57    /// such as `C#` and `D-`.
58    pub fn has_any_enharmonic_spelled_pitches(&self) -> bool {
59        self.pitch_class_cardinality() != self.unique_pitch_name_count()
60    }
61
62    /// Returns whether the chord is exactly three distinct pitch names with a
63    /// third and a fifth above the root, of any quality.
64    pub fn is_triad(&self) -> bool {
65        self.unique_pitch_name_count() == 3 && self.third().is_some() && self.fifth().is_some()
66    }
67
68    /// Returns whether the chord is exactly four distinct pitch names with a
69    /// third, fifth and seventh above the root, of any quality.
70    pub fn is_seventh(&self) -> bool {
71        self.unique_pitch_name_count() == 4
72            && self.third().is_some()
73            && self.fifth().is_some()
74            && self.seventh().is_some()
75    }
76
77    /// Returns whether the chord is a correctly spelled major triad.
78    pub fn is_major_triad(&self) -> bool {
79        self.is_triad_of_type((3, 11, -1), 4, 7)
80    }
81
82    /// Returns whether the chord is a correctly spelled minor triad.
83    pub fn is_minor_triad(&self) -> bool {
84        self.is_triad_of_type((3, 11, 1), 3, 7)
85    }
86
87    /// Returns whether the chord is a correctly spelled diminished triad.
88    pub fn is_diminished_triad(&self) -> bool {
89        self.is_triad_of_type((3, 10, 0), 3, 6)
90    }
91
92    /// Returns whether the chord is a correctly spelled augmented triad.
93    pub fn is_augmented_triad(&self) -> bool {
94        self.is_triad_of_type((3, 12, 0), 4, 8)
95    }
96
97    /// Returns whether the chord is a seventh chord whose pitches all lie at
98    /// the given semitone offsets above the root.
99    pub fn is_seventh_of_type(&self, semitones: &[u8]) -> bool {
100        if !self.is_seventh() {
101            return false;
102        }
103        let Some(root) = self.root() else {
104            return false;
105        };
106        self.pitch_refs()
107            .all(|pitch| semitones.contains(&semitones_above(root, pitch)))
108    }
109
110    /// Returns whether the chord is a dominant seventh: a major triad with a
111    /// minor seventh.
112    pub fn is_dominant_seventh(&self) -> bool {
113        self.is_seventh_of_type(&[0, 4, 7, 10])
114    }
115
116    /// Returns whether the chord is a half-diminished seventh.
117    pub fn is_half_diminished_seventh(&self) -> bool {
118        self.is_seventh_of_type(&[0, 3, 6, 10])
119    }
120
121    /// Returns whether the chord is a fully diminished seventh.
122    pub fn is_diminished_seventh(&self) -> bool {
123        self.is_seventh_of_type(&[0, 3, 6, 9])
124    }
125
126    /// Returns whether the chord is only a root and a major third above it.
127    pub fn is_incomplete_major_triad(&self) -> bool {
128        self.is_incomplete_triad_of_type((2, 4), 4)
129    }
130
131    /// Returns whether the chord is only a root and a minor third above it.
132    pub fn is_incomplete_minor_triad(&self) -> bool {
133        self.is_incomplete_triad_of_type((2, 3), 3)
134    }
135
136    /// Returns whether the chord has a third and a fifth above its root. A
137    /// dominant seventh is not a triad but contains one.
138    pub fn contains_triad(&self) -> bool {
139        self.third().is_some() && self.fifth().is_some()
140    }
141
142    /// Returns whether the chord contains a triad and a seventh above its root.
143    pub fn contains_seventh(&self) -> bool {
144        self.contains_triad() && self.seventh().is_some()
145    }
146
147    /// Returns the quality of the triad above the root, following music21's
148    /// `Chord.quality`: incomplete triads still count, and a chord with a
149    /// repeated or missing chord step is [`TriadQuality::Other`].
150    pub fn quality(&self) -> TriadQuality {
151        let Some(third) = self.semitones_from_chord_step(3) else {
152            return TriadQuality::Other;
153        };
154        // A third or fifth a fraction of a semitone off is neither major nor
155        // minor, as music21 reads it.
156        if self.root().is_some_and(|root| {
157            [3, 5].into_iter().any(|step| {
158                self.chord_step_from(step, root)
159                    .is_some_and(|pitch| ((pitch.ps() - root.ps()) * 100.0).round() % 100.0 != 0.0)
160            })
161        }) {
162            return TriadQuality::Other;
163        }
164        if self.has_repeated_chord_step(1) || self.has_repeated_chord_step(3) {
165            return TriadQuality::Other;
166        }
167        let Some(fifth) = self.semitones_from_chord_step(5) else {
168            return match third {
169                4 => TriadQuality::Major,
170                3 => TriadQuality::Minor,
171                _ => TriadQuality::Other,
172            };
173        };
174        if self.has_repeated_chord_step(5) {
175            return TriadQuality::Other;
176        }
177        match (third, fifth) {
178            (4, 7) => TriadQuality::Major,
179            (3, 7) => TriadQuality::Minor,
180            (4, 8) => TriadQuality::Augmented,
181            (3, 6) => TriadQuality::Diminished,
182            _ => TriadQuality::Other,
183        }
184    }
185
186    /// Returns whether the chord is consonant in the common-practice sense:
187    /// one pitch name, two whose closed-position interval is consonant, or a
188    /// major or minor triad not in second inversion.
189    pub fn is_consonant(&self) -> bool {
190        let distinct = self.remove_redundant_pitch_names();
191        match distinct.notes.len() {
192            1 => true,
193            2 => {
194                let closed = self.closed_position(None, false).remove_redundant_pitches();
195                Interval::between_pitches(&closed.notes[0].pitch, &closed.notes[1].pitch)
196                    .is_ok_and(|interval| interval.is_consonant())
197            }
198            3 => (self.is_major_triad() || self.is_minor_triad()) && self.inversion() != Some(2),
199            _ => false,
200        }
201    }
202
203    /// The first pitch at the given chord step above a root the caller
204    /// decided on, rather than the one the chord infers: music21's
205    /// `getChordStep(step, testRoot)`.
206    pub fn chord_step_with_root(&self, step: u8, root: &Pitch) -> Option<&Pitch> {
207        self.chord_step_from(step, root)
208    }
209
210    /// The semitone distance from a caller-supplied root to the pitch at the
211    /// given chord step: music21's `semitonesFromChordStep(step, testRoot)`.
212    pub fn semitones_from_chord_step_with_root(&self, step: u8, root: &Pitch) -> Option<u8> {
213        let pitch = self.chord_step_from(step, root)?;
214        Some(semitones_above(root, pitch))
215    }
216
217    /// The inversion measured from a root the caller decided on: music21's
218    /// `inversion(testRoot=...)`.
219    pub fn inversion_from_root(&self, root: &Pitch) -> Option<u8> {
220        self.inversion_with_root(root)
221    }
222
223    pub(crate) fn chord_step_from(&self, step: u8, root: &Pitch) -> Option<&Pitch> {
224        let step = fold_chord_step(step);
225        self.pitch_refs()
226            .find(|pitch| diatonic_steps_above(root, pitch) == step)
227    }
228
229    pub(super) fn is_triad_of_type(
230        &self,
231        address: (u8, u8, i8),
232        third_semitones: u8,
233        fifth_semitones: u8,
234    ) -> bool {
235        if !self.is_triad() || self.has_any_enharmonic_spelled_pitches() {
236            return false;
237        }
238        let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
239        else {
240            return false;
241        };
242        let spelled = semitones_above(root, third) == third_semitones
243            && semitones_above(root, fifth) == fifth_semitones;
244        // music21 asks the chord tables first. Three names with no two of
245        // them one pitch class, a third and a fifth at these distances: that
246        // *is* the set class, so the search through the tables -- the dear
247        // part of this, and one `common_name` has already made -- would only
248        // say so again. The tests hold it to that.
249        debug_assert!(!spelled || self.forte_address() == Some(address));
250        spelled
251    }
252
253    pub(super) fn is_incomplete_triad_of_type(
254        &self,
255        address: (u8, u8),
256        third_semitones: u8,
257    ) -> bool {
258        if self
259            .forte_address()
260            .is_none_or(|(card, index, _)| (card, index) != address)
261        {
262            return false;
263        }
264        let (Some(root), Some(_)) = (self.root(), self.third()) else {
265            return false;
266        };
267        self.pitch_refs()
268            .all(|pitch| [0, third_semitones].contains(&semitones_above(root, pitch)))
269    }
270
271    /// Returns whether the chord is an Italian, French, German or Swiss
272    /// augmented sixth. Each must be in its conventional inversion unless
273    /// `permit_any_inversion` is set.
274    pub fn is_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
275        match self.pitch_class_cardinality() {
276            3 => self.is_italian_augmented_sixth(permit_any_inversion, false),
277            4 => {
278                self.is_french_augmented_sixth(permit_any_inversion)
279                    || self.is_german_augmented_sixth(permit_any_inversion)
280                    || self.is_swiss_augmented_sixth(permit_any_inversion)
281            }
282            _ => false,
283        }
284    }
285
286    /// Returns whether the chord is an Italian augmented sixth, such as
287    /// `A- C F#`.
288    pub fn is_italian_augmented_sixth(
289        &self,
290        permit_any_inversion: bool,
291        restrict_doublings: bool,
292    ) -> bool {
293        if !self.is_augmented_sixth_of_type(
294            (3, 8, 1),
295            1,
296            permit_any_inversion,
297            &AUGMENTED_SIXTHS.italian,
298        ) {
299            return false;
300        }
301        if !restrict_doublings {
302            return true;
303        }
304        let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
305        else {
306            return false;
307        };
308        self.pitch_refs().all(|pitch| {
309            pitch.name() == fifth.name() || std::ptr::eq(pitch, third) || std::ptr::eq(pitch, root)
310        })
311    }
312
313    /// Returns whether the chord is a French augmented sixth, such as
314    /// `A- C D F#`.
315    pub fn is_french_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
316        self.is_augmented_sixth_of_type(
317            (4, 25, 0),
318            2,
319            permit_any_inversion,
320            &AUGMENTED_SIXTHS.french,
321        )
322    }
323
324    /// Returns whether the chord is a German augmented sixth, such as
325    /// `A- C E- F#`.
326    pub fn is_german_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
327        self.is_augmented_sixth_of_type(
328            (4, 27, -1),
329            1,
330            permit_any_inversion,
331            &AUGMENTED_SIXTHS.german,
332        )
333    }
334
335    /// Returns whether the chord is a Swiss augmented sixth, such as
336    /// `A- C D# F#`.
337    pub fn is_swiss_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
338        self.is_augmented_sixth_of_type(
339            (4, 27, -1),
340            2,
341            permit_any_inversion,
342            &AUGMENTED_SIXTHS.swiss,
343        )
344    }
345
346    /// Returns whether the chord is five distinct pitch names with a third,
347    /// fifth, seventh and ninth above the root.
348    pub fn is_ninth(&self) -> bool {
349        self.unique_pitch_name_count() == 5
350            && self.third().is_some()
351            && self.fifth().is_some()
352            && self.seventh().is_some()
353            && self.chord_step(2).is_some()
354    }
355
356    /// Returns whether some transposition other than the octave maps the
357    /// pitch-class set onto itself. With `require_intervallic_evenness` only
358    /// the evenly spaced sets count, the way Straus defines the property.
359    pub fn is_transpositionally_symmetrical(&self, require_intervallic_evenness: bool) -> bool {
360        let Some((card, index, _)) = self.forte_address() else {
361            return self.notes.is_empty();
362        };
363        if card == 1 {
364            return require_intervallic_evenness;
365        }
366        const EVEN: [(u8, u8); 5] = [(2, 6), (3, 12), (4, 28), (6, 35), (12, 1)];
367        const UNEVEN: [(u8, u8); 10] = [
368            (4, 9),
369            (4, 25),
370            (6, 7),
371            (6, 20),
372            (6, 30),
373            (8, 9),
374            (8, 25),
375            (8, 28),
376            (9, 12),
377            (10, 6),
378        ];
379        EVEN.contains(&(card, index))
380            || (!require_intervallic_evenness && UNEVEN.contains(&(card, index)))
381    }
382
383    /// Returns whether two pitches share a letter under different
384    /// accidentals, such as `E` and `E-`.
385    pub fn has_any_repeated_diatonic_note(&self) -> bool {
386        let steps = self
387            .pitch_refs()
388            .map(Pitch::step)
389            .collect::<std::collections::BTreeSet<_>>();
390        steps.len() != self.unique_pitch_name_count()
391    }
392
393    pub(super) fn is_augmented_sixth_of_type(
394        &self,
395        address: (u8, u8, i8),
396        required_inversion: u8,
397        permit_any_inversion: bool,
398        intervals: &[[Interval; 2]],
399    ) -> bool {
400        if self.forte_address() != Some(address) || self.has_any_enharmonic_spelled_pitches() {
401            return false;
402        }
403        if !permit_any_inversion && self.inversion() != Some(required_inversion) {
404            return false;
405        }
406        let Some(root) = self.root() else {
407            return false;
408        };
409        let steps = [self.third(), self.fifth(), self.seventh()];
410        intervals.iter().zip(steps).all(|(accepted, step)| {
411            step.and_then(|pitch| Interval::between_pitches(root, pitch).ok())
412                .is_some_and(|interval| {
413                    accepted.iter().any(|candidate| {
414                        candidate.directed_simple_key() == interval.directed_simple_key()
415                    })
416                })
417        })
418    }
419
420    pub(super) fn has_intervals_above_root(&self, intervals: &[u8]) -> bool {
421        let Some(root_pitch) = self.find_root_pitch() else {
422            return false;
423        };
424        let root_pc = root::pitch_class(root_pitch);
425        let chord_pcs = self.pitch_class_set();
426        intervals
427            .iter()
428            .all(|interval| chord_pcs.contains(&((root_pc + interval) % 12)))
429    }
430
431    /// Returns the interval from the root to the first pitch lying on the
432    /// given chord step, compound intervals included, so the third of
433    /// `C3 G3 E4 C5` is a major tenth. `None` when the chord has no root or
434    /// no pitch on that step.
435    pub fn interval_from_chord_step(&self, step: u8) -> Option<Interval> {
436        let root = self.root()?;
437        self.pitch_refs()
438            .filter_map(|pitch| Interval::between_pitches(root, pitch).ok())
439            .find(|interval| interval.mod7() == IntegerType::from(step))
440    }
441}
442
443/// The quality of the triad above a chord's root, as music21's
444/// `Chord.quality` reports it.
445#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
446#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
447pub enum TriadQuality {
448    /// A major third with a perfect fifth, or a major third alone.
449    Major,
450    /// A minor third with a perfect fifth, or a minor third alone.
451    Minor,
452    /// A major third with an augmented fifth.
453    Augmented,
454    /// A minor third with a diminished fifth.
455    Diminished,
456    /// Anything else, including a missing third or a repeated chord step.
457    Other,
458}
459
460impl TriadQuality {
461    /// Returns music21's lowercase name for the quality.
462    pub fn as_str(self) -> &'static str {
463        match self {
464            Self::Major => "major",
465            Self::Minor => "minor",
466            Self::Augmented => "augmented",
467            Self::Diminished => "diminished",
468            Self::Other => "other",
469        }
470    }
471}
472
473impl Display for TriadQuality {
474    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
475        f.write_str(self.as_str())
476    }
477}
478
479pub(super) fn fold_chord_step(step: u8) -> u8 {
480    if step >= 8 { step - 7 } else { step }
481}
482
483pub(super) fn diatonic_steps_above(root: &Pitch, pitch: &Pitch) -> u8 {
484    ((root::step_num(pitch) - root::step_num(root)).rem_euclid(7) + 1) as u8
485}
486
487pub(super) fn semitones_above(root: &Pitch, pitch: &Pitch) -> u8 {
488    (root::pitch_class(pitch) + 12 - root::pitch_class(root)) % 12
489}
490
491pub(super) struct AugmentedSixthIntervals {
492    italian: [[Interval; 2]; 2],
493    french: [[Interval; 2]; 3],
494    german: [[Interval; 2]; 3],
495    swiss: [[Interval; 2]; 3],
496}
497
498/// The intervals each augmented-sixth type stacks above its root, as music21
499/// spells them: the third and fifth (and seventh) may each be written either
500/// way up.
501pub(super) static AUGMENTED_SIXTHS: LazyLock<AugmentedSixthIntervals> = LazyLock::new(|| {
502    let pair = |up: &str, down: &str| {
503        [
504            Interval::from_name(up).expect("augmented sixth intervals parse"),
505            Interval::from_name(down).expect("augmented sixth intervals parse"),
506        ]
507    };
508    AugmentedSixthIntervals {
509        italian: [pair("d3", "A-6"), pair("d5", "A-4")],
510        french: [pair("M3", "m-6"), pair("d5", "A-4"), pair("m7", "M-2")],
511        german: [pair("d3", "A-6"), pair("d5", "A-4"), pair("d7", "A-2")],
512        swiss: [pair("m3", "M-6"), pair("dd5", "AA-4"), pair("d7", "A-2")],
513    }
514});