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_set().len() != self.unique_pitch_names().len()
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_names().len() == 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_names().len() == 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.forte_address() != Some(address) {
236            return false;
237        }
238        if !self.is_triad() || self.has_any_enharmonic_spelled_pitches() {
239            return false;
240        }
241        let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
242        else {
243            return false;
244        };
245        semitones_above(root, third) == third_semitones
246            && semitones_above(root, fifth) == fifth_semitones
247    }
248
249    pub(super) fn is_incomplete_triad_of_type(
250        &self,
251        address: (u8, u8),
252        third_semitones: u8,
253    ) -> bool {
254        if self
255            .forte_address()
256            .is_none_or(|(card, index, _)| (card, index) != address)
257        {
258            return false;
259        }
260        let (Some(root), Some(_)) = (self.root(), self.third()) else {
261            return false;
262        };
263        self.pitch_refs()
264            .all(|pitch| [0, third_semitones].contains(&semitones_above(root, pitch)))
265    }
266
267    /// Returns whether the chord is an Italian, French, German or Swiss
268    /// augmented sixth. Each must be in its conventional inversion unless
269    /// `permit_any_inversion` is set.
270    pub fn is_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
271        match self.pitch_class_cardinality() {
272            3 => self.is_italian_augmented_sixth(permit_any_inversion, false),
273            4 => {
274                self.is_french_augmented_sixth(permit_any_inversion)
275                    || self.is_german_augmented_sixth(permit_any_inversion)
276                    || self.is_swiss_augmented_sixth(permit_any_inversion)
277            }
278            _ => false,
279        }
280    }
281
282    /// Returns whether the chord is an Italian augmented sixth, such as
283    /// `A- C F#`.
284    pub fn is_italian_augmented_sixth(
285        &self,
286        permit_any_inversion: bool,
287        restrict_doublings: bool,
288    ) -> bool {
289        if !self.is_augmented_sixth_of_type(
290            (3, 8, 1),
291            1,
292            permit_any_inversion,
293            &AUGMENTED_SIXTHS.italian,
294        ) {
295            return false;
296        }
297        if !restrict_doublings {
298            return true;
299        }
300        let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
301        else {
302            return false;
303        };
304        self.pitch_refs().all(|pitch| {
305            pitch.name() == fifth.name() || std::ptr::eq(pitch, third) || std::ptr::eq(pitch, root)
306        })
307    }
308
309    /// Returns whether the chord is a French augmented sixth, such as
310    /// `A- C D F#`.
311    pub fn is_french_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
312        self.is_augmented_sixth_of_type(
313            (4, 25, 0),
314            2,
315            permit_any_inversion,
316            &AUGMENTED_SIXTHS.french,
317        )
318    }
319
320    /// Returns whether the chord is a German augmented sixth, such as
321    /// `A- C E- F#`.
322    pub fn is_german_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
323        self.is_augmented_sixth_of_type(
324            (4, 27, -1),
325            1,
326            permit_any_inversion,
327            &AUGMENTED_SIXTHS.german,
328        )
329    }
330
331    /// Returns whether the chord is a Swiss augmented sixth, such as
332    /// `A- C D# F#`.
333    pub fn is_swiss_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
334        self.is_augmented_sixth_of_type(
335            (4, 27, -1),
336            2,
337            permit_any_inversion,
338            &AUGMENTED_SIXTHS.swiss,
339        )
340    }
341
342    /// Returns whether the chord is five distinct pitch names with a third,
343    /// fifth, seventh and ninth above the root.
344    pub fn is_ninth(&self) -> bool {
345        self.unique_pitch_names().len() == 5
346            && self.third().is_some()
347            && self.fifth().is_some()
348            && self.seventh().is_some()
349            && self.chord_step(2).is_some()
350    }
351
352    /// Returns whether some transposition other than the octave maps the
353    /// pitch-class set onto itself. With `require_intervallic_evenness` only
354    /// the evenly spaced sets count, the way Straus defines the property.
355    pub fn is_transpositionally_symmetrical(&self, require_intervallic_evenness: bool) -> bool {
356        let Some((card, index, _)) = self.forte_address() else {
357            return self.notes.is_empty();
358        };
359        if card == 1 {
360            return require_intervallic_evenness;
361        }
362        const EVEN: [(u8, u8); 5] = [(2, 6), (3, 12), (4, 28), (6, 35), (12, 1)];
363        const UNEVEN: [(u8, u8); 10] = [
364            (4, 9),
365            (4, 25),
366            (6, 7),
367            (6, 20),
368            (6, 30),
369            (8, 9),
370            (8, 25),
371            (8, 28),
372            (9, 12),
373            (10, 6),
374        ];
375        EVEN.contains(&(card, index))
376            || (!require_intervallic_evenness && UNEVEN.contains(&(card, index)))
377    }
378
379    /// Returns whether two pitches share a letter under different
380    /// accidentals, such as `E` and `E-`.
381    pub fn has_any_repeated_diatonic_note(&self) -> bool {
382        let steps = self
383            .pitch_refs()
384            .map(Pitch::step)
385            .collect::<std::collections::BTreeSet<_>>();
386        steps.len() != self.unique_pitch_names().len()
387    }
388
389    pub(super) fn is_augmented_sixth_of_type(
390        &self,
391        address: (u8, u8, i8),
392        required_inversion: u8,
393        permit_any_inversion: bool,
394        intervals: &[[Interval; 2]],
395    ) -> bool {
396        if self.forte_address() != Some(address) || self.has_any_enharmonic_spelled_pitches() {
397            return false;
398        }
399        if !permit_any_inversion && self.inversion() != Some(required_inversion) {
400            return false;
401        }
402        let Some(root) = self.root() else {
403            return false;
404        };
405        let steps = [self.third(), self.fifth(), self.seventh()];
406        intervals.iter().zip(steps).all(|(accepted, step)| {
407            step.and_then(|pitch| Interval::between_pitches(root, pitch).ok())
408                .is_some_and(|interval| {
409                    accepted.iter().any(|candidate| {
410                        candidate.directed_simple_key() == interval.directed_simple_key()
411                    })
412                })
413        })
414    }
415
416    pub(super) fn has_intervals_above_root(&self, intervals: &[u8]) -> bool {
417        let Some(root_pitch) = self.find_root_pitch() else {
418            return false;
419        };
420        let root_pc = root::pitch_class(root_pitch);
421        let chord_pcs = self.pitch_class_set();
422        intervals
423            .iter()
424            .all(|interval| chord_pcs.contains(&((root_pc + interval) % 12)))
425    }
426
427    /// Returns the interval from the root to the first pitch lying on the
428    /// given chord step, compound intervals included, so the third of
429    /// `C3 G3 E4 C5` is a major tenth. `None` when the chord has no root or
430    /// no pitch on that step.
431    pub fn interval_from_chord_step(&self, step: u8) -> Option<Interval> {
432        let root = self.root()?;
433        self.pitch_refs()
434            .filter_map(|pitch| Interval::between_pitches(root, pitch).ok())
435            .find(|interval| interval.mod7() == IntegerType::from(step))
436    }
437}
438
439/// The quality of the triad above a chord's root, as music21's
440/// `Chord.quality` reports it.
441#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
442#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
443pub enum TriadQuality {
444    /// A major third with a perfect fifth, or a major third alone.
445    Major,
446    /// A minor third with a perfect fifth, or a minor third alone.
447    Minor,
448    /// A major third with an augmented fifth.
449    Augmented,
450    /// A minor third with a diminished fifth.
451    Diminished,
452    /// Anything else, including a missing third or a repeated chord step.
453    Other,
454}
455
456impl TriadQuality {
457    /// Returns music21's lowercase name for the quality.
458    pub fn as_str(self) -> &'static str {
459        match self {
460            Self::Major => "major",
461            Self::Minor => "minor",
462            Self::Augmented => "augmented",
463            Self::Diminished => "diminished",
464            Self::Other => "other",
465        }
466    }
467}
468
469impl Display for TriadQuality {
470    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
471        f.write_str(self.as_str())
472    }
473}
474
475pub(super) fn fold_chord_step(step: u8) -> u8 {
476    if step >= 8 { step - 7 } else { step }
477}
478
479pub(super) fn diatonic_steps_above(root: &Pitch, pitch: &Pitch) -> u8 {
480    ((root::step_num(pitch) - root::step_num(root)).rem_euclid(7) + 1) as u8
481}
482
483pub(super) fn semitones_above(root: &Pitch, pitch: &Pitch) -> u8 {
484    (root::pitch_class(pitch) + 12 - root::pitch_class(root)) % 12
485}
486
487pub(super) struct AugmentedSixthIntervals {
488    italian: [[Interval; 2]; 2],
489    french: [[Interval; 2]; 3],
490    german: [[Interval; 2]; 3],
491    swiss: [[Interval; 2]; 3],
492}
493
494/// The intervals each augmented-sixth type stacks above its root, as music21
495/// spells them: the third and fifth (and seventh) may each be written either
496/// way up.
497pub(super) static AUGMENTED_SIXTHS: LazyLock<AugmentedSixthIntervals> = LazyLock::new(|| {
498    let pair = |up: &str, down: &str| {
499        [
500            Interval::from_name(up).expect("augmented sixth intervals parse"),
501            Interval::from_name(down).expect("augmented sixth intervals parse"),
502        ]
503    };
504    AugmentedSixthIntervals {
505        italian: [pair("d3", "A-6"), pair("d5", "A-4")],
506        french: [pair("M3", "m-6"), pair("d5", "A-4"), pair("m7", "M-2")],
507        german: [pair("d3", "A-6"), pair("d5", "A-4"), pair("d7", "A-2")],
508        swiss: [pair("m3", "M-6"), pair("dd5", "AA-4"), pair("d7", "A-2")],
509    }
510});