Skip to main content

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