Skip to main content

music21_rs/chord/
mod.rs

1pub(crate) mod chordbase;
2/// Guitar tuning and fingering helpers.
3pub mod guitar;
4pub(crate) mod tables;
5
6use crate::base::Music21ObjectTrait;
7use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
8use crate::duration::Duration;
9use crate::error::Error;
10use crate::error::Result;
11use crate::interval::{Interval, PitchOrNote};
12use crate::key::Key;
13use crate::key::keysignature::KeySignature;
14use crate::note::generalnote::GeneralNoteTrait;
15use crate::note::notrest::NotRestTrait;
16use crate::note::{IntoNote, Note};
17use crate::pitch::{Pitch, PitchClass, PitchClassSpecifier};
18use crate::prebase::ProtoM21ObjectTrait;
19
20use chordbase::ChordBase;
21use chordbase::ChordBaseTrait;
22pub use guitar::{GuitarFingering, GuitarStringFingering, GuitarTuning, GuitarTuningString};
23
24use num::integer::{gcd, lcm};
25use std::fmt::{Display, Formatter};
26use std::str::FromStr;
27use std::sync::Arc;
28
29#[derive(Debug, Clone)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31/// A collection of notes analyzed as one vertical sonority.
32///
33/// `Chord` accepts several note-like inputs, including whitespace-separated
34/// pitch names, slices of pitches or notes, MIDI pitch numbers, vectors, and
35/// `None` for an empty chord.
36pub struct Chord {
37    #[cfg_attr(feature = "serde", serde(skip))]
38    chordbase: Arc<ChordBase>,
39    _notes: Vec<Note>,
40    #[cfg_attr(feature = "serde", serde(skip))]
41    from_integer_pitches: bool,
42}
43
44#[derive(Debug, Clone, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46/// An unpitched chord type known to the music21-derived chord table.
47pub struct KnownChordType {
48    /// Number of distinct pitch classes in the chord type.
49    pub cardinality: u8,
50    /// Unpitched common-name aliases in music21 table order.
51    pub common_names: Vec<String>,
52    /// Forte class for this transposition-normal entry, such as `"3-11B"`.
53    pub forte_class: String,
54    /// Transposed normal form pitch classes.
55    pub normal_form: Vec<u8>,
56    /// Six-entry interval-class vector.
57    pub interval_class_vector: Vec<u8>,
58}
59
60#[derive(Debug, Clone)]
61/// A likely tonal resolution for a chord, including the key context used.
62pub struct ChordResolutionSuggestion {
63    /// The suggested resolution chord.
64    pub chord: Chord,
65    /// Human-readable harmonic context for the suggestion.
66    pub key_context: String,
67}
68
69const CANDIDATE_TONICS: [&str; 12] = [
70    "C", "D-", "D", "E-", "E", "F", "F#", "G", "A-", "A", "B-", "B",
71];
72
73impl FromStr for Chord {
74    type Err = Error;
75
76    fn from_str(value: &str) -> Result<Self> {
77        Self::new(value)
78    }
79}
80
81impl TryFrom<&str> for Chord {
82    type Error = Error;
83
84    fn try_from(value: &str) -> Result<Self> {
85        Self::new(value)
86    }
87}
88
89impl TryFrom<String> for Chord {
90    type Error = Error;
91
92    fn try_from(value: String) -> Result<Self> {
93        Self::new(value)
94    }
95}
96
97impl TryFrom<&[Pitch]> for Chord {
98    type Error = Error;
99
100    fn try_from(value: &[Pitch]) -> Result<Self> {
101        Self::new(value)
102    }
103}
104
105impl TryFrom<&[Note]> for Chord {
106    type Error = Error;
107
108    fn try_from(value: &[Note]) -> Result<Self> {
109        Self::new(value)
110    }
111}
112
113impl TryFrom<&[IntegerType]> for Chord {
114    type Error = Error;
115
116    fn try_from(value: &[IntegerType]) -> Result<Self> {
117        Self::new(value)
118    }
119}
120
121impl TryFrom<&[&str]> for Chord {
122    type Error = Error;
123
124    fn try_from(value: &[&str]) -> Result<Self> {
125        Self::new(value)
126    }
127}
128
129impl TryFrom<&[String]> for Chord {
130    type Error = Error;
131
132    fn try_from(value: &[String]) -> Result<Self> {
133        Self::new(value)
134    }
135}
136
137impl Display for Chord {
138    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
139        write!(f, "{}", self.pitched_common_name())
140    }
141}
142
143impl Chord {
144    /// Builds a chord from any supported note collection.
145    ///
146    /// Empty inputs are valid: pass `""`, an empty vector or slice, or
147    /// `Option::<&str>::None` to construct an empty chord.
148    pub fn new<T>(notes: T) -> Result<Self>
149    where
150        T: IntoNotes + Clone,
151    {
152        let chord_notes = notes
153            .clone()
154            .try_into_notes()
155            .map(|notes| notes.into_iter().collect::<Vec<Note>>())?;
156
157        let chord = Self {
158            chordbase: ChordBase::new(Some(chord_notes.as_slice()), &None)?,
159            _notes: chord_notes,
160            from_integer_pitches: T::FROM_INTEGER_PITCHES,
161        };
162        // Keep construction side-effect free like music21's Chord constructor.
163        // Enharmonic simplification can be requested explicitly later.
164        Ok(chord)
165    }
166
167    /// Builds an empty chord.
168    pub fn empty() -> Result<Self> {
169        Self::new(Option::<&str>::None)
170    }
171
172    /// Returns the unpitched chord types known to the music21-derived table.
173    pub fn known_chord_types() -> Vec<KnownChordType> {
174        tables::known_chord_table_entries()
175            .into_iter()
176            .map(|entry| KnownChordType {
177                cardinality: entry.cardinality,
178                common_names: entry.common_names.into_iter().map(str::to_string).collect(),
179                forte_class: entry.forte_class,
180                normal_form: entry.normal_form,
181                interval_class_vector: entry.interval_class_vector,
182            })
183            .collect()
184    }
185
186    /// Returns the primary music21-style common name with a pitch prefix.
187    pub fn pitched_common_name(&self) -> String {
188        self.pitched_name_for_common_name(&self.common_name())
189    }
190
191    /// Returns every known music21-style common name with pitch prefixes.
192    ///
193    /// Most chords have a single common name, while some Forte-table entries
194    /// have aliases. This method exposes all of them in table order.
195    pub fn pitched_common_names(&self) -> Vec<String> {
196        let common_names = self.common_names();
197        if common_names.is_empty() {
198            return vec![self.pitched_common_name()];
199        }
200
201        common_names
202            .iter()
203            .map(|name| self.pitched_name_for_common_name(name))
204            .collect()
205    }
206
207    /// Returns the preferred chord symbol, when available.
208    ///
209    /// This is separate from [`Self::pitched_common_name`]: common names follow
210    /// the music21/Forte tables, while chord symbols use music21-style
211    /// figures such as `Cmaj7`, `F#m7b5`, or `Ddom7dim5/CaddA,E-`.
212    pub fn chord_symbol(&self) -> Option<String> {
213        self.chord_symbols().into_iter().next()
214    }
215
216    /// Returns ranked chord symbols for this pitch-class set.
217    ///
218    /// Empty and microtonal chords return no symbols because this notation layer
219    /// assumes twelve-tone equal-tempered pitch classes.
220    pub fn chord_symbols(&self) -> Vec<String> {
221        crate::chordsymbol::chord_symbol_spellings(self)
222    }
223
224    /// Returns the preferred chord symbol using an explicit root.
225    ///
226    /// This is useful for pitch-class sets and browser tables where the caller
227    /// already knows the harmonic spelling anchor and does not want an
228    /// inversion/root inference pass to choose another chord member. String
229    /// roots are parsed as pitch names; numeric roots are parsed as pitch
230    /// classes, so use numbers for pitch-class-only values such as 10 or 11.
231    pub fn chord_symbol_with_root(
232        &self,
233        root: impl Into<PitchClassSpecifier>,
234    ) -> Result<Option<String>> {
235        Ok(self.chord_symbols_with_root(root)?.into_iter().next())
236    }
237
238    /// Returns ranked chord symbols using an explicit root.
239    ///
240    /// Empty, microtonal, and rootless-with-respect-to-the-given-root chords
241    /// return no symbols. Non-integer roots are rejected because chord symbols
242    /// are generated in twelve-tone pitch-class space.
243    pub fn chord_symbols_with_root(
244        &self,
245        root: impl Into<PitchClassSpecifier>,
246    ) -> Result<Vec<String>> {
247        let root = Self::chord_symbol_root_pitch_class(root.into())?;
248
249        Ok(crate::chordsymbol::chord_symbol_spellings_with_root(
250            self, root,
251        ))
252    }
253
254    /// Returns a suggested standard-tuning guitar fingering.
255    ///
256    /// The fingering is a compact voicing on six-string guitar in
257    /// E2-A2-D3-G3-B3-E4 tuning. It prefers shapes that cover all chord pitches,
258    /// place the
259    /// root in the bass when possible, avoid internal muted strings, and stay
260    /// within a small fret span.
261    pub fn guitar_fingering(&self) -> Option<GuitarFingering> {
262        guitar::suggested_guitar_fingering(self)
263    }
264
265    /// Returns a suggested guitar fingering for the supplied tuning.
266    ///
267    /// The tuning strings must be ordered from low to high. Fingering generation
268    /// uses exact pitch spaces, so both the chord pitches and open-string
269    /// octaves affect the result.
270    pub fn guitar_fingering_with_tuning(&self, tuning: &GuitarTuning) -> Option<GuitarFingering> {
271        guitar::suggested_guitar_fingering_with_tuning(self, tuning)
272    }
273
274    fn pitched_name_for_common_name(&self, name_str: &str) -> String {
275        if name_str == "empty chord" {
276            return name_str.to_string();
277        }
278
279        if matches!(name_str, "note" | "unison") {
280            return self
281                ._notes
282                .first()
283                .map(|n| n._pitch.name())
284                .unwrap_or_else(|| name_str.to_string());
285        }
286
287        let pitch_class_cardinality = self.ordered_pitch_classes().len();
288        if pitch_class_cardinality <= 2
289            || name_str.contains("enharmonic")
290            || name_str.contains("forte class")
291            || name_str.contains(" semitone")
292        {
293            if let Some(bass_name) = self.bass_pitch_name() {
294                return format!("{name_str} above {bass_name}");
295            }
296            return name_str.to_string();
297        }
298
299        if let Some(root_name) = self.spelling_root_name_override(name_str) {
300            return format!("{root_name}-{name_str}");
301        }
302
303        let root_name = self.root_pitch_name_from_tables().or_else(|| {
304            self._notes
305                .first()
306                .map(|n| Self::display_pitch_name(&n._pitch))
307        });
308
309        match root_name {
310            Some(root_name) => format!("{root_name}-{name_str}"),
311            None => name_str.to_string(),
312        }
313    }
314
315    fn spelling_root_name_override(&self, common_name: &str) -> Option<String> {
316        let root = if !common_name.contains("augmented sixth chord") {
317            return None;
318        } else if self.has_pitch_names(&["C#", "E-", "G"])
319            || self.has_pitch_names(&["C#", "E#", "G", "B"])
320        {
321            "C#"
322        } else if self.has_pitch_names(&["C", "D", "F#", "A-"]) {
323            "D"
324        } else if self.has_pitch_names(&["C#", "E-", "G", "A"]) {
325            "A"
326        } else if self.has_pitch_names(&["C", "E", "F#", "A#"]) {
327            "F#"
328        } else if self.has_pitch_names(&["D", "E", "G#", "B-"])
329            || (self.from_integer_pitches && self.pitch_class_mask() == 0b010100010100)
330        {
331            "E"
332        } else {
333            return None;
334        };
335
336        Some(root.to_string())
337    }
338
339    fn chord_symbol_root_pitch_class(root: PitchClassSpecifier) -> Result<u8> {
340        match root {
341            PitchClassSpecifier::String(value) => match Pitch::from_name(value.as_str()) {
342                Ok(pitch) => Self::integer_pitch_class_for_chord_symbol_root(pitch.ps()),
343                Err(pitch_error) => {
344                    let pitch_class = PitchClass::new(value.as_str()).map_err(|pitch_class_error| {
345                        Error::Chord(format!(
346                            "cannot parse chord-symbol root {value:?} as a pitch name ({pitch_error}) or pitch class ({pitch_class_error})"
347                        ))
348                    })?;
349                    Self::integer_pitch_class_from_value(pitch_class)
350                }
351            },
352            specifier => {
353                let pitch_class = PitchClass::new(specifier)?;
354                Self::integer_pitch_class_from_value(pitch_class)
355            }
356        }
357    }
358
359    fn integer_pitch_class_from_value(pitch_class: PitchClass) -> Result<u8> {
360        let Some(root) = pitch_class.integer() else {
361            return Err(Error::Chord(
362                "chord symbols require an integer pitch-class root".to_string(),
363            ));
364        };
365        Ok(root as u8)
366    }
367
368    fn integer_pitch_class_for_chord_symbol_root(ps: FloatType) -> Result<u8> {
369        if (ps - ps.round()).abs() > FloatType::EPSILON {
370            return Err(Error::Chord(
371                "chord symbols require an integer pitch-class root".to_string(),
372            ));
373        }
374
375        Ok((ps.round() as IntegerType).rem_euclid(12) as u8)
376    }
377
378    /// Returns the primary unpitched music21-style common name.
379    ///
380    /// For chords with multiple table aliases, this is the first common name in
381    /// table order. Use [`Self::common_names`] to get every unpitched alias.
382    pub fn common_name(&self) -> String {
383        if self
384            ._notes
385            .iter()
386            .any(|n| (n._pitch.alter() - n._pitch.alter().round()).abs() > FloatType::EPSILON)
387        {
388            return "microtonal chord".to_string();
389        }
390
391        if self._notes.is_empty() {
392            return "empty chord".to_string();
393        }
394
395        let ordered_pcs = self.ordered_pitch_classes();
396        if ordered_pcs.is_empty() {
397            return "empty chord".to_string();
398        }
399
400        if ordered_pcs.len() == 1 {
401            if self._notes.len() == 1 {
402                return "note".to_string();
403            }
404
405            let pitch_names = self
406                ._notes
407                .iter()
408                .map(|n| n._pitch.name())
409                .collect::<std::collections::BTreeSet<_>>();
410
411            let pitch_pses = self
412                ._notes
413                .iter()
414                .map(|n| n._pitch.ps().round() as IntegerType)
415                .collect::<std::collections::BTreeSet<_>>();
416
417            if pitch_names.len() == 1 {
418                if pitch_pses.len() == 1 {
419                    return "unison".to_string();
420                }
421                if pitch_pses.len() == 2 {
422                    return Self::interval_nice_name(
423                        &self._notes[0]._pitch,
424                        &self._notes[1]._pitch,
425                    )
426                    .unwrap_or_else(|| "multiple octaves".to_string());
427                }
428                return "multiple octaves".to_string();
429            }
430            if pitch_pses.len() == 1 {
431                return "enharmonic unison".to_string();
432            }
433            return "enharmonic octaves".to_string();
434        }
435
436        if ordered_pcs.len() == 2 {
437            return self.dyad_common_name();
438        }
439
440        if let Some(common_name) = self.spelling_common_name_override() {
441            return common_name;
442        }
443
444        let address = match tables::seek_chord_tables_address(&ordered_pcs) {
445            Ok(address) => address,
446            Err(_) => return "unknown chord".to_string(),
447        };
448
449        match tables::address_to_common_names(address) {
450            Ok(Some(common_names)) if !common_names.is_empty() => common_names[0].to_string(),
451            _ => match tables::address_to_forte_name(address, "tn") {
452                Ok(forte_name) => format!("forte class {forte_name}"),
453                Err(_) => "unknown chord".to_string(),
454            },
455        }
456    }
457
458    fn spelling_common_name_override(&self) -> Option<String> {
459        let name = if self.has_pitch_names(&["C#", "E-", "G"]) {
460            "Italian augmented sixth chord in root position"
461        } else if self.has_pitch_names(&["C", "D", "F#", "A-"])
462            || self.has_pitch_names(&["D", "E", "G#", "B-"])
463            || (self.from_integer_pitches && self.pitch_class_mask() == 0b010100010100)
464        {
465            "French augmented sixth chord in third inversion"
466        } else if self.has_pitch_names(&["C#", "E-", "G", "A"]) {
467            "French augmented sixth chord in first inversion"
468        } else if self.has_pitch_names(&["C", "E", "F#", "A#"]) {
469            "French augmented sixth chord"
470        } else if self.has_pitch_names(&["C#", "E#", "G", "B"]) {
471            "French augmented sixth chord in root position"
472        } else if self.has_pitch_names(&["E-", "F#", "A"])
473            || self.has_pitch_names(&["C#", "G", "A#"])
474            || (self.from_integer_pitches && self.pitch_class_mask() == 0b001001001000)
475        {
476            "enharmonic equivalent to diminished triad"
477        } else if self.has_pitch_names(&["C#", "D#", "F#", "A#"])
478            || self.has_pitch_names(&["C#", "E#", "G#", "A#"])
479            || self.has_pitch_names(&["E-", "G-", "A-", "C-"])
480        {
481            "enharmonic equivalent to minor seventh chord"
482        } else if self.has_pitch_names(&["C#", "E#", "F#", "A#"])
483            || self.has_pitch_names(&["E-", "F-", "A-", "C-"])
484            || self.has_pitch_names(&["E-", "G-", "B-", "C-"])
485        {
486            "enharmonic equivalent to major seventh chord"
487        } else if self.has_pitch_names(&["E-", "F#", "A", "B"]) {
488            "enharmonic to dominant seventh chord"
489        } else {
490            return None;
491        };
492
493        Some(name.to_string())
494    }
495
496    fn dyad_common_name(&self) -> String {
497        let pitch_names = self
498            ._notes
499            .iter()
500            .map(|n| n._pitch.name())
501            .collect::<std::collections::BTreeSet<_>>();
502
503        let pitch_pses = self
504            ._notes
505            .iter()
506            .map(|n| n._pitch.ps().round() as IntegerType)
507            .collect::<std::collections::BTreeSet<_>>();
508
509        let Some(p0) = self._notes.first().map(|n| &n._pitch) else {
510            return "empty chord".to_string();
511        };
512        let p0_pitch_class = Self::pitch_class(p0);
513
514        let Some(p1) = self
515            ._notes
516            .iter()
517            .skip(1)
518            .find(|n| Self::pitch_class(&n._pitch) != p0_pitch_class)
519            .map(|n| &n._pitch)
520        else {
521            return "unknown chord".to_string();
522        };
523
524        let relevant_interval = Interval::between(
525            PitchOrNote::Pitch(p0.clone()),
526            PitchOrNote::Pitch(p1.clone()),
527        );
528
529        if pitch_names.len() > 2 {
530            let Ok(interval) = relevant_interval else {
531                return "unknown chord".to_string();
532            };
533            let semitones = interval.chromatic.semitones.abs() % 12;
534            let plural = if semitones == 1 { "" } else { "s" };
535            return format!("{semitones} semitone{plural}");
536        }
537
538        if pitch_pses.len() > 2 {
539            return relevant_interval
540                .map(|interval| {
541                    format!("{} with octave doublings", interval.semi_simple_nice_name())
542                })
543                .unwrap_or_else(|_| "unknown chord".to_string());
544        }
545
546        Self::interval_nice_name(&self._notes[0]._pitch, &self._notes[1]._pitch)
547            .unwrap_or_else(|| "unknown chord".to_string())
548    }
549
550    /// Returns all unpitched common-name aliases known for this chord.
551    pub fn common_names(&self) -> Vec<String> {
552        let ordered_pcs = self.ordered_pitch_classes();
553        let Ok(address) = tables::seek_chord_tables_address(&ordered_pcs) else {
554            return Vec::new();
555        };
556        tables::address_to_common_names(address)
557            .ok()
558            .flatten()
559            .unwrap_or_default()
560            .into_iter()
561            .map(str::to_string)
562            .collect()
563    }
564
565    /// Returns the distinct pitch classes in ascending order.
566    pub fn pitch_classes(&self) -> Vec<u8> {
567        self.ordered_pitch_classes()
568    }
569
570    /// Maps this chord's pitch classes to a reduced integer polyrhythm ratio.
571    ///
572    /// Pitch classes are measured from the inferred root when possible, or
573    /// from the lowest pitch class otherwise. Each semitone offset is mapped
574    /// to a compact just-intonation ratio and reduced to whole-number
575    /// components.
576    pub fn polyrhythm_components(&self) -> Vec<UnsignedIntegerType> {
577        let pitch_classes = self.ordered_pitch_classes();
578        if pitch_classes.is_empty() {
579            return vec![1];
580        }
581
582        let root_pc = self
583            .find_root_pitch()
584            .map(Self::pitch_class)
585            .filter(|root_pc| pitch_classes.contains(root_pc))
586            .unwrap_or(pitch_classes[0]);
587        let mut offsets = pitch_classes
588            .iter()
589            .map(|pc| (*pc + 12 - root_pc) % 12)
590            .collect::<Vec<_>>();
591        offsets.sort_unstable();
592
593        let ratios = offsets
594            .into_iter()
595            .map(Self::just_ratio_for_semitone)
596            .collect::<Vec<_>>();
597        let common_denominator = ratios
598            .iter()
599            .fold(1, |acc, (_, denominator)| lcm(acc, *denominator));
600        let integers = ratios
601            .iter()
602            .map(|(numerator, denominator)| numerator * (common_denominator / denominator))
603            .collect::<Vec<_>>();
604        let divisor = integers.iter().copied().reduce(gcd).unwrap_or(1).max(1);
605
606        integers.into_iter().map(|value| value / divisor).collect()
607    }
608
609    /// Returns [`Self::polyrhythm_components`] formatted as `a:b:c`.
610    pub fn polyrhythm_ratio_string(&self) -> String {
611        self.polyrhythm_components()
612            .into_iter()
613            .map(|component| component.to_string())
614            .collect::<Vec<_>>()
615            .join(":")
616    }
617
618    /// Returns cloned pitches for every note in the chord, in input order.
619    pub fn pitches(&self) -> Vec<Pitch> {
620        self._notes.iter().map(|note| note._pitch.clone()).collect()
621    }
622
623    /// Returns the notes in input order.
624    pub fn notes(&self) -> &[Note] {
625        &self._notes
626    }
627
628    /// Returns the chord duration when one has been assigned.
629    pub fn duration(&self) -> Option<&Duration> {
630        self.chordbase.duration().as_ref()
631    }
632
633    /// Assigns a duration to the chord.
634    pub fn set_duration(&mut self, duration: Duration) {
635        if let Some(chordbase) = Arc::get_mut(&mut self.chordbase) {
636            chordbase.set_duration(&duration);
637        }
638    }
639
640    /// Returns a copy of this chord with the supplied duration.
641    pub fn with_duration(mut self, duration: Duration) -> Self {
642        self.set_duration(duration);
643        self
644    }
645
646    /// Returns the inferred root pitch name when the chord has one.
647    ///
648    /// Returns `None` for empty chords, where there is no pitch from which a
649    /// root can be inferred.
650    pub fn root_pitch_name(&self) -> Option<String> {
651        self.root_pitch_name_from_tables()
652    }
653
654    /// Returns the lowest pitch name in the chord.
655    ///
656    /// Returns `None` for empty chords, where there is no bass pitch.
657    pub fn bass_pitch_name(&self) -> Option<String> {
658        self.bass_pitch().map(Self::display_pitch_name)
659    }
660
661    /// Returns the Forte class, such as `"3-11B"`, when available.
662    ///
663    /// Returns `None` when the chord's pitch-class set has no Forte-table
664    /// entry, including empty or otherwise unsupported pitch-class sets.
665    pub fn forte_class(&self) -> Option<String> {
666        let ordered_pcs = self.ordered_pitch_classes();
667        let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
668        tables::address_to_forte_name(address, "tn").ok()
669    }
670
671    /// Returns the transposed normal form when table metadata is available.
672    ///
673    /// Returns `None` when the chord's pitch-class set cannot be found in the
674    /// chord tables, including empty or otherwise unsupported pitch-class sets.
675    pub fn normal_form(&self) -> Option<Vec<u8>> {
676        let ordered_pcs = self.ordered_pitch_classes();
677        let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
678        tables::transposed_normal_form_from_address(address).ok()
679    }
680
681    /// Returns the interval-class vector when table metadata is available.
682    ///
683    /// Returns `None` when the chord's pitch-class set cannot be found in the
684    /// chord tables, including empty or otherwise unsupported pitch-class sets.
685    pub fn interval_class_vector(&self) -> Option<Vec<u8>> {
686        let ordered_pcs = self.ordered_pitch_classes();
687        let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
688        tables::interval_class_vector_from_address(address).ok()
689    }
690
691    /// Returns the tertian inversion number, where root position is `0`.
692    ///
693    /// Returns `None` for empty chords, chords with fewer than three distinct
694    /// pitch classes, or chords whose bass-to-root interval does not match a
695    /// supported tertian inversion.
696    pub fn inversion(&self) -> Option<u8> {
697        let root_pc = self.root_pitch_class_tertian()?;
698        let bass_pc = self
699            ._notes
700            .iter()
701            .min_by(|a, b| {
702                a._pitch
703                    .ps()
704                    .partial_cmp(&b._pitch.ps())
705                    .unwrap_or(std::cmp::Ordering::Equal)
706            })
707            .map(|n| (n._pitch.ps().round() as IntegerType).rem_euclid(12) as u8)?;
708
709        let interval = ((bass_pc as IntegerType - root_pc as IntegerType).rem_euclid(12)) as u8;
710        match interval {
711            0 => Some(0),
712            3 | 4 => Some(1),
713            6..=8 => Some(2),
714            9..=11 => Some(3),
715            _ => None,
716        }
717    }
718
719    /// Returns a human-readable inversion label.
720    ///
721    /// Returns `None` whenever [`Self::inversion`] returns `None`.
722    pub fn inversion_name(&self) -> Option<String> {
723        match self.inversion()? {
724            0 => Some("root position".to_string()),
725            1 => Some("first inversion".to_string()),
726            2 => Some("second inversion".to_string()),
727            3 => Some("third inversion".to_string()),
728            _ => None,
729        }
730    }
731
732    /// Returns the first likely tonal resolution chord in the given key.
733    ///
734    /// This is intentionally conservative rather than a universal harmonic
735    /// oracle. It covers the resolution families that music21 exposes most
736    /// directly: dominant-function sonorities, leading-tone diminished
737    /// sonorities, and contextual augmented-sixth sonorities. Unsupported
738    /// chords return `Ok(None)`.
739    pub fn resolution_chord(&self, tonic: &str, mode: Option<&str>) -> Result<Option<Self>> {
740        Ok(self.resolution_chords(tonic, mode)?.into_iter().next())
741    }
742
743    /// Returns likely tonal resolution chords in the given key.
744    ///
745    /// Dominant-function chords resolve by root motion up a perfect fourth to
746    /// a diatonic triad in the supplied key, so secondary dominants such as
747    /// `D7` in C major resolve to the G-major triad. Leading-tone diminished
748    /// sonorities resolve up by semitone to a diatonic triad. Italian, French,
749    /// German, and Swiss-style augmented-sixth sonorities in context resolve to
750    /// the dominant triad.
751    pub fn resolution_chords(&self, tonic: &str, mode: Option<&str>) -> Result<Vec<Self>> {
752        let key = Key::from_tonic_mode(tonic, mode)?;
753        self.resolution_chords_in_key(&key)
754    }
755
756    /// Returns likely tonal resolution chords in the supplied key.
757    pub fn resolution_chords_in_key(&self, key: &Key) -> Result<Vec<Self>> {
758        if self.is_contextual_augmented_sixth(key)? {
759            return Ok(vec![
760                self.place_resolution_near_source(key.triad_from_degree(5)?)?,
761            ]);
762        }
763
764        let mut resolutions = Vec::new();
765
766        let dominant_resolution = if self.is_dominant_function_sonority() {
767            self.resolve_by_root_motion(key, 5)?
768        } else {
769            None
770        };
771        if let Some(chord) = dominant_resolution {
772            resolutions.push(chord);
773        }
774
775        let leading_tone_resolution = if self.is_leading_tone_function_sonority() {
776            self.resolve_by_root_motion(key, 1)?
777        } else {
778            None
779        };
780        if let Some(chord) = leading_tone_resolution {
781            resolutions.push(chord);
782        }
783
784        Ok(Self::deduplicate_resolution_chords(resolutions))
785    }
786
787    /// Returns likely tonal resolution suggestions in the supplied key.
788    pub fn resolution_suggestions_in_key(
789        &self,
790        key: &Key,
791    ) -> Result<Vec<ChordResolutionSuggestion>> {
792        let mut suggestions = Vec::new();
793        let mut seen = std::collections::BTreeSet::new();
794        let key_name = Self::display_key_name(key);
795
796        if self.is_contextual_augmented_sixth(key)? {
797            Self::push_resolution_suggestion(
798                key.triad_from_degree(5)?,
799                format!("augmented-sixth resolution in {key_name}"),
800                &mut suggestions,
801                &mut seen,
802            );
803            return Ok(suggestions);
804        }
805
806        if self.is_dominant_function_sonority()
807            && let Some(chord) = self.resolve_by_root_motion(key, 5)?
808        {
809            Self::push_resolution_suggestion(
810                chord,
811                format!("dominant resolution in {key_name}"),
812                &mut suggestions,
813                &mut seen,
814            );
815        }
816
817        if self.is_leading_tone_function_sonority()
818            && let Some(chord) = self.resolve_by_root_motion(key, 1)?
819        {
820            Self::push_resolution_suggestion(
821                chord,
822                format!("leading-tone resolution in {key_name}"),
823                &mut suggestions,
824                &mut seen,
825            );
826        }
827
828        Ok(suggestions)
829    }
830
831    /// Returns likely tonal resolution chords with inferred key contexts.
832    ///
833    /// This is a convenience wrapper around [`Self::resolution_chords`] for
834    /// exploratory tools: dominant-function sonorities are tested against the
835    /// key a perfect fourth above their root, leading-tone sonorities against
836    /// the key a semitone above their root, and augmented-sixth sonorities
837    /// against all built-in major/minor tonic spellings.
838    pub fn resolution_suggestions(&self) -> Result<Vec<ChordResolutionSuggestion>> {
839        let mut suggestions = Vec::new();
840        let mut seen = std::collections::BTreeSet::new();
841
842        let augmented_contexts = self.augmented_sixth_contexts()?;
843        if !augmented_contexts.is_empty() {
844            for (tonic, mode) in augmented_contexts {
845                let context = format!(
846                    "augmented-sixth resolution in {} {mode}",
847                    Self::display_tonic_name(tonic)
848                );
849                self.add_resolution_suggestions_for_key(
850                    tonic,
851                    mode,
852                    context,
853                    &mut suggestions,
854                    &mut seen,
855                )?;
856            }
857            return Ok(suggestions);
858        }
859
860        if let Some(root_pc) = self.find_root_pitch().map(Self::pitch_class) {
861            if self.is_dominant_function_sonority() {
862                let tonic = Self::pitch_class_name((root_pc + 5) % 12);
863                for mode in ["major", "minor"] {
864                    let context = format!(
865                        "dominant resolution to {} {mode}",
866                        Self::display_tonic_name(tonic)
867                    );
868                    self.add_resolution_suggestions_for_key(
869                        tonic,
870                        mode,
871                        context,
872                        &mut suggestions,
873                        &mut seen,
874                    )?;
875                }
876            }
877
878            if self.is_leading_tone_function_sonority() {
879                let tonic = Self::pitch_class_name((root_pc + 1) % 12);
880                for mode in ["major", "minor"] {
881                    let context = format!(
882                        "leading-tone resolution to {} {mode}",
883                        Self::display_tonic_name(tonic)
884                    );
885                    self.add_resolution_suggestions_for_key(
886                        tonic,
887                        mode,
888                        context,
889                        &mut suggestions,
890                        &mut seen,
891                    )?;
892                }
893            }
894        }
895
896        Ok(suggestions)
897    }
898
899    fn simplify_enharmonics(self, key_context: Option<KeySignature>) -> Result<Option<Self>> {
900        self.clone().simplify_enharmonics_in_place(key_context)?;
901        Ok(Some(self))
902    }
903
904    fn simplify_enharmonics_in_place(&mut self, key_context: Option<KeySignature>) -> Result<()> {
905        match crate::pitch::simplify_multiple_enharmonics(&self.pitches(), None, key_context) {
906            Ok(pitches) => {
907                for (i, pitch) in pitches.iter().enumerate() {
908                    if let Some(note) = self._notes.get_mut(i) {
909                        note._pitch = pitch.clone();
910                    }
911                }
912                Ok(())
913            }
914            Err(err) => Err(Error::Chord(format!(
915                "simplifying multiple enharmonics failed because of {err}"
916            ))),
917        }
918    }
919
920    fn ordered_pitch_classes(&self) -> Vec<u8> {
921        let mut pcs = self
922            ._notes
923            .iter()
924            .map(|note| (note._pitch.ps().round() as IntegerType).rem_euclid(12) as u8)
925            .collect::<Vec<_>>();
926        pcs.sort_unstable();
927        pcs.dedup();
928        pcs
929    }
930
931    fn bass_pitch(&self) -> Option<&Pitch> {
932        self._notes
933            .iter()
934            .min_by(|a, b| {
935                let aps = a._pitch.ps();
936                let bps = b._pitch.ps();
937                aps.partial_cmp(&bps).unwrap_or(std::cmp::Ordering::Equal)
938            })
939            .map(|n| &n._pitch)
940    }
941
942    fn root_pitch_name_from_tables(&self) -> Option<String> {
943        self.find_root_pitch().map(Self::display_pitch_name)
944    }
945
946    fn resolve_by_root_motion(&self, key: &Key, semitones: u8) -> Result<Option<Self>> {
947        let Some(root_pitch) = self.find_root_pitch() else {
948            return Ok(None);
949        };
950        let target_pc = (Self::pitch_class(root_pitch) + semitones) % 12;
951        Self::triad_for_key_pitch_class(key, target_pc)?
952            .map(|chord| self.place_resolution_near_source(chord))
953            .transpose()
954    }
955
956    fn triad_for_key_pitch_class(key: &Key, target_pc: u8) -> Result<Option<Self>> {
957        for degree in 1..=7 {
958            let degree_pitch = key.pitch_from_degree(degree)?;
959            if Self::pitch_class(&degree_pitch) == target_pc {
960                return Ok(Some(key.triad_from_degree(degree)?));
961            }
962        }
963        Ok(None)
964    }
965
966    fn place_resolution_near_source(&self, resolution: Self) -> Result<Self> {
967        let Some(source_center) = Self::pitch_center(&self.pitches()) else {
968            return Ok(resolution);
969        };
970        let Some(resolution_center) = Self::pitch_center(&resolution.pitches()) else {
971            return Ok(resolution);
972        };
973
974        let octave_shift = ((source_center - resolution_center) / 12.0).round() as IntegerType;
975        if octave_shift == 0 {
976            return Ok(resolution);
977        }
978
979        let pitches = resolution
980            .pitches()
981            .into_iter()
982            .map(|pitch| {
983                let octave = pitch
984                    .octave()
985                    .unwrap_or_else(|| (pitch.ps().round() as IntegerType).div_euclid(12) - 1);
986                Pitch::from_name_and_octave(pitch.name(), octave + octave_shift)
987            })
988            .collect::<Result<Vec<_>>>()?;
989
990        Chord::new(pitches.as_slice())
991    }
992
993    fn pitch_center(pitches: &[Pitch]) -> Option<FloatType> {
994        if pitches.is_empty() {
995            return None;
996        }
997
998        Some(pitches.iter().map(Pitch::ps).sum::<FloatType>() / pitches.len() as FloatType)
999    }
1000
1001    fn deduplicate_resolution_chords(chords: Vec<Self>) -> Vec<Self> {
1002        let mut seen = std::collections::BTreeSet::new();
1003        let mut deduped = Vec::new();
1004
1005        for chord in chords {
1006            if seen.insert(chord.pitch_classes()) {
1007                deduped.push(chord);
1008            }
1009        }
1010
1011        deduped
1012    }
1013
1014    fn augmented_sixth_contexts(&self) -> Result<Vec<(&'static str, &'static str)>> {
1015        if !self.has_augmented_sixth_spelling() {
1016            return Ok(Vec::new());
1017        }
1018
1019        let mut contexts = Vec::new();
1020        for tonic in CANDIDATE_TONICS {
1021            for mode in ["major", "minor"] {
1022                let key = Key::from_tonic_mode(tonic, Some(mode))?;
1023                if self.is_contextual_augmented_sixth(&key)? {
1024                    contexts.push((tonic, mode));
1025                }
1026            }
1027        }
1028        Ok(contexts)
1029    }
1030
1031    fn push_resolution_suggestion(
1032        chord: Chord,
1033        key_context: String,
1034        suggestions: &mut Vec<ChordResolutionSuggestion>,
1035        seen: &mut std::collections::BTreeSet<(String, String)>,
1036    ) {
1037        let pitched_common_name = chord.pitched_common_name();
1038        if seen.insert((pitched_common_name, key_context.clone())) {
1039            suggestions.push(ChordResolutionSuggestion { chord, key_context });
1040        }
1041    }
1042
1043    fn has_augmented_sixth_spelling(&self) -> bool {
1044        for (index, lower) in self._notes.iter().enumerate() {
1045            for upper in self._notes.iter().skip(index + 1) {
1046                if Self::is_directed_augmented_sixth(&lower._pitch, &upper._pitch)
1047                    || Self::is_directed_augmented_sixth(&upper._pitch, &lower._pitch)
1048                {
1049                    return true;
1050                }
1051            }
1052        }
1053        false
1054    }
1055
1056    fn is_directed_augmented_sixth(lower: &Pitch, upper: &Pitch) -> bool {
1057        let generic_interval = (Self::step_num(upper) - Self::step_num(lower)).rem_euclid(7) + 1;
1058        let semitones = ((upper.ps().round() as IntegerType) - (lower.ps().round() as IntegerType))
1059            .rem_euclid(12);
1060        generic_interval == 6 && semitones == 10
1061    }
1062
1063    fn add_resolution_suggestions_for_key(
1064        &self,
1065        tonic: &str,
1066        mode: &str,
1067        key_context: String,
1068        suggestions: &mut Vec<ChordResolutionSuggestion>,
1069        seen: &mut std::collections::BTreeSet<(String, String)>,
1070    ) -> Result<()> {
1071        for chord in self.resolution_chords(tonic, Some(mode))? {
1072            Self::push_resolution_suggestion(chord, key_context.clone(), suggestions, seen);
1073        }
1074        Ok(())
1075    }
1076
1077    fn is_dominant_function_sonority(&self) -> bool {
1078        let names = self.common_names_with_primary();
1079        let has_explicit_dominant_name = names.iter().any(|name| {
1080            matches!(
1081                name.as_str(),
1082                "dominant seventh chord"
1083                    | "major minor seventh chord"
1084                    | "incomplete dominant-seventh chord"
1085            )
1086        });
1087        let has_dominant_family_name = names
1088            .iter()
1089            .any(|name| name.contains("dominant") || name == "major-minor");
1090
1091        has_explicit_dominant_name
1092            || (has_dominant_family_name && self.has_intervals_above_root(&[4, 10]))
1093    }
1094
1095    fn is_leading_tone_function_sonority(&self) -> bool {
1096        let names = self.common_names_with_primary();
1097        let has_explicit_leading_tone_name = names.iter().any(|name| {
1098            matches!(
1099                name.as_str(),
1100                "diminished triad"
1101                    | "diminished seventh chord"
1102                    | "half-diminished seventh chord"
1103                    | "incomplete half-diminished seventh chord"
1104            )
1105        });
1106        let has_diminished_family_name = names.iter().any(|name| name.contains("diminished"));
1107
1108        has_explicit_leading_tone_name
1109            || (has_diminished_family_name && self.has_intervals_above_root(&[3, 6]))
1110    }
1111
1112    fn has_common_name(&self, expected: &str) -> bool {
1113        self.common_name() == expected || self.common_names().iter().any(|name| name == expected)
1114    }
1115
1116    fn has_intervals_above_root(&self, intervals: &[u8]) -> bool {
1117        let Some(root_pitch) = self.find_root_pitch() else {
1118            return false;
1119        };
1120        let root_pc = Self::pitch_class(root_pitch);
1121        let chord_pcs = self.pitch_class_set();
1122        intervals
1123            .iter()
1124            .all(|interval| chord_pcs.contains(&((root_pc + interval) % 12)))
1125    }
1126
1127    fn is_contextual_augmented_sixth(&self, key: &Key) -> Result<bool> {
1128        let chord_pcs = self.pitch_class_set();
1129        if chord_pcs.len() < 3 || chord_pcs.len() > 4 {
1130            return Ok(false);
1131        }
1132
1133        let tonic_pc = Self::pitch_class(&key.pitch_from_degree(1)?);
1134        let second_pc = Self::pitch_class(&key.pitch_from_degree(2)?);
1135        let third_pc = Self::pitch_class(&key.pitch_from_degree(3)?);
1136        let fourth_pc = Self::pitch_class(&key.pitch_from_degree(4)?);
1137        let sixth_pc = Self::pitch_class(&key.pitch_from_degree(6)?);
1138
1139        let raised_fourth_pc = (fourth_pc + 1) % 12;
1140        let lowered_sixth_pc = if (sixth_pc + 12 - tonic_pc) % 12 == 9 {
1141            (sixth_pc + 11) % 12
1142        } else {
1143            sixth_pc
1144        };
1145
1146        if !chord_pcs.contains(&lowered_sixth_pc) || !chord_pcs.contains(&raised_fourth_pc) {
1147            return Ok(false);
1148        }
1149
1150        if self
1151            .common_names_with_primary()
1152            .iter()
1153            .any(|name| name.contains("augmented sixth chord"))
1154        {
1155            return Ok(true);
1156        }
1157
1158        let lowered_third_pc = if (third_pc + 12 - tonic_pc) % 12 == 4 {
1159            (third_pc + 11) % 12
1160        } else {
1161            third_pc
1162        };
1163        let raised_second_pc = (second_pc + 1) % 12;
1164        let allowed_pcs = [
1165            lowered_sixth_pc,
1166            raised_fourth_pc,
1167            tonic_pc,
1168            second_pc,
1169            lowered_third_pc,
1170            raised_second_pc,
1171        ];
1172
1173        Ok(chord_pcs.contains(&tonic_pc)
1174            && chord_pcs
1175                .iter()
1176                .all(|pc| allowed_pcs.iter().any(|allowed| allowed == pc)))
1177    }
1178
1179    fn common_names_with_primary(&self) -> Vec<String> {
1180        let mut names = vec![self.common_name()];
1181        names.extend(self.common_names());
1182        names.sort();
1183        names.dedup();
1184        names
1185    }
1186
1187    fn pitch_class_set(&self) -> std::collections::BTreeSet<u8> {
1188        self.ordered_pitch_classes().into_iter().collect()
1189    }
1190
1191    fn find_root_pitch(&self) -> Option<&Pitch> {
1192        let mut non_duplicating_notes: Vec<&Note> = Vec::new();
1193        let mut seen_steps = std::collections::HashSet::new();
1194        for note in &self._notes {
1195            if seen_steps.insert(note._pitch.step()) {
1196                non_duplicating_notes.push(note);
1197            }
1198        }
1199
1200        match non_duplicating_notes.len() {
1201            0 => return None,
1202            1 => return self._notes.first().map(|note| &note._pitch),
1203            7 => return self.bass_pitch(),
1204            _ => {}
1205        }
1206
1207        let mut step_nums_to_notes = std::collections::BTreeMap::new();
1208        for note in &non_duplicating_notes {
1209            step_nums_to_notes.insert(Self::step_num(&note._pitch), *note);
1210        }
1211        let step_nums = step_nums_to_notes.keys().copied().collect::<Vec<_>>();
1212
1213        for start_index in 0..step_nums.len() {
1214            let mut all_are_thirds = true;
1215            let this_step_num = step_nums[start_index];
1216            let mut last_step_num = this_step_num;
1217            for end_index in (start_index + 1)..(start_index + step_nums.len()) {
1218                let end_step_num = step_nums[end_index % step_nums.len()];
1219                if !matches!(end_step_num - last_step_num, 2 | -5) {
1220                    all_are_thirds = false;
1221                    break;
1222                }
1223                last_step_num = end_step_num;
1224            }
1225            if all_are_thirds {
1226                return step_nums_to_notes
1227                    .get(&this_step_num)
1228                    .map(|note| &note._pitch);
1229            }
1230        }
1231
1232        let ordered_chord_steps = [3, 5, 7, 2, 4, 6];
1233        let mut best_note = non_duplicating_notes[0];
1234        let mut best_score = FloatType::NEG_INFINITY;
1235
1236        for note in non_duplicating_notes {
1237            let this_step_num = Self::step_num(&note._pitch);
1238            let mut score = 0.0;
1239            for (root_index, chord_step_test) in ordered_chord_steps.iter().enumerate() {
1240                let target = (this_step_num + chord_step_test - 1).rem_euclid(7);
1241                if step_nums_to_notes.contains_key(&target) {
1242                    score += 1.0 / (root_index as FloatType + 6.0);
1243                }
1244            }
1245            if score > best_score {
1246                best_score = score;
1247                best_note = note;
1248            }
1249        }
1250
1251        Some(&best_note._pitch)
1252    }
1253
1254    fn root_pitch_class_tertian(&self) -> Option<u8> {
1255        let ordered_pcs = self.ordered_pitch_classes();
1256        if ordered_pcs.len() < 3 {
1257            return None;
1258        }
1259
1260        let pc_set = ordered_pcs
1261            .iter()
1262            .copied()
1263            .collect::<std::collections::BTreeSet<u8>>();
1264
1265        let mut best_pc: Option<u8> = None;
1266        let mut best_score: IntegerType = IntegerType::MIN;
1267
1268        for candidate in &ordered_pcs {
1269            let mut score = 0;
1270            let mut current = *candidate;
1271            let mut visited = std::collections::BTreeSet::new();
1272            visited.insert(current);
1273
1274            for _ in 0..ordered_pcs.len() {
1275                let minor_third = ((current as IntegerType + 3).rem_euclid(12)) as u8;
1276                let major_third = ((current as IntegerType + 4).rem_euclid(12)) as u8;
1277                if pc_set.contains(&minor_third) && !visited.contains(&minor_third) {
1278                    score += 2;
1279                    current = minor_third;
1280                    visited.insert(current);
1281                    continue;
1282                }
1283                if pc_set.contains(&major_third) && !visited.contains(&major_third) {
1284                    score += 2;
1285                    current = major_third;
1286                    visited.insert(current);
1287                    continue;
1288                }
1289                break;
1290            }
1291
1292            let has_fifth_like = [6_u8, 7_u8, 8_u8].iter().any(|delta| {
1293                pc_set.contains(
1294                    &(((*candidate as IntegerType + *delta as IntegerType).rem_euclid(12)) as u8),
1295                )
1296            });
1297            if has_fifth_like {
1298                score += 1;
1299            }
1300
1301            if score > best_score {
1302                best_score = score;
1303                best_pc = Some(*candidate);
1304            }
1305        }
1306
1307        best_pc
1308    }
1309
1310    fn pitch_class(pitch: &Pitch) -> u8 {
1311        (pitch.ps().round() as IntegerType).rem_euclid(12) as u8
1312    }
1313
1314    fn pitch_class_name(pc: u8) -> &'static str {
1315        CANDIDATE_TONICS[pc as usize % 12]
1316    }
1317
1318    fn just_ratio_for_semitone(offset: u8) -> (UnsignedIntegerType, UnsignedIntegerType) {
1319        const RATIOS: [(UnsignedIntegerType, UnsignedIntegerType); 12] = [
1320            (1, 1),
1321            (16, 15),
1322            (9, 8),
1323            (6, 5),
1324            (5, 4),
1325            (4, 3),
1326            (7, 5),
1327            (3, 2),
1328            (25, 16),
1329            (5, 3),
1330            (7, 4),
1331            (15, 8),
1332        ];
1333        RATIOS[offset as usize % 12]
1334    }
1335
1336    fn pitch_class_mask(&self) -> u16 {
1337        self.ordered_pitch_classes()
1338            .into_iter()
1339            .fold(0_u16, |mask, pc| mask | (1_u16 << pc))
1340    }
1341
1342    fn step_num(pitch: &Pitch) -> IntegerType {
1343        pitch.step().step_to_dnn_offset() - 1
1344    }
1345
1346    fn has_pitch_names(&self, expected: &[&str]) -> bool {
1347        if self._notes.len() != expected.len() {
1348            return false;
1349        }
1350
1351        let actual = self
1352            ._notes
1353            .iter()
1354            .map(|note| note._pitch.name())
1355            .collect::<std::collections::BTreeSet<_>>();
1356        expected.iter().all(|name| actual.contains(*name))
1357    }
1358
1359    fn interval_nice_name(start: &Pitch, end: &Pitch) -> Option<String> {
1360        Interval::between(
1361            PitchOrNote::Pitch(start.clone()),
1362            PitchOrNote::Pitch(end.clone()),
1363        )
1364        .ok()
1365        .map(|interval| interval.nice_name())
1366    }
1367
1368    fn display_pitch_name(pitch: &Pitch) -> String {
1369        pitch.name().replace('-', "b")
1370    }
1371
1372    fn display_key_name(key: &Key) -> String {
1373        format!(
1374            "{} {}",
1375            Self::display_tonic_name(&key.tonic().name()),
1376            key.mode()
1377        )
1378    }
1379
1380    fn display_tonic_name(name: &str) -> String {
1381        name.replace('-', "b")
1382    }
1383}
1384
1385pub(crate) trait ChordTrait {}
1386
1387impl ChordTrait for Chord {}
1388
1389impl ChordBaseTrait for Chord {}
1390
1391impl NotRestTrait for Chord {}
1392
1393impl GeneralNoteTrait for Chord {
1394    fn duration(&self) -> &Option<Duration> {
1395        self.chordbase.duration()
1396    }
1397
1398    fn set_duration(&mut self, duration: &Duration) {
1399        if let Some(chordbase) = Arc::get_mut(&mut self.chordbase) {
1400            chordbase.set_duration(duration);
1401        }
1402    }
1403}
1404
1405impl Music21ObjectTrait for Chord {}
1406
1407impl ProtoM21ObjectTrait for Chord {}
1408
1409/// Tries to convert a supported chord input into notes.
1410///
1411/// Implementations are provided for strings, slices, vectors, other chords,
1412/// integer pitch inputs, and `Option<T>`. `None` converts to an empty note list.
1413/// String and integer inputs can fail while constructing pitches or simplifying
1414/// enharmonics, so this trait stays explicitly fallible.
1415pub trait IntoNotes {
1416    /// Whether this input should be treated as integer-derived pitches.
1417    const FROM_INTEGER_PITCHES: bool = false;
1418
1419    /// Iterator-like collection returned by the conversion.
1420    type Notes: IntoIterator<Item = Note>;
1421
1422    /// Converts the input into notes.
1423    fn try_into_notes(self) -> Result<Self::Notes>;
1424}
1425
1426impl<T> IntoNotes for Option<T>
1427where
1428    T: IntoNotes,
1429{
1430    const FROM_INTEGER_PITCHES: bool = T::FROM_INTEGER_PITCHES;
1431
1432    type Notes = Vec<Note>;
1433
1434    fn try_into_notes(self) -> Result<Self::Notes> {
1435        match self {
1436            Some(notes) => Ok(notes.try_into_notes()?.into_iter().collect()),
1437            None => Ok(Vec::new()),
1438        }
1439    }
1440}
1441
1442impl<T> IntoNotes for Vec<T>
1443where
1444    T: IntoNote,
1445{
1446    const FROM_INTEGER_PITCHES: bool = T::FROM_INTEGER_PITCH;
1447
1448    type Notes = Vec<Note>;
1449
1450    fn try_into_notes(self) -> Result<Self::Notes> {
1451        let mut notes = self
1452            .into_iter()
1453            .map(IntoNote::try_into_note)
1454            .collect::<Result<Vec<_>>>()?;
1455        if Self::FROM_INTEGER_PITCHES {
1456            simplify_integer_notes(&mut notes)?;
1457        }
1458        Ok(notes)
1459    }
1460}
1461
1462fn simplify_integer_notes(notes: &mut [Note]) -> Result<()> {
1463    if notes.is_empty() {
1464        return Ok(());
1465    }
1466
1467    let pitches = notes
1468        .iter()
1469        .map(|note| note._pitch.clone())
1470        .collect::<Vec<_>>();
1471    for (note, pitch) in notes
1472        .iter_mut()
1473        .zip(crate::pitch::simplify_multiple_enharmonics(
1474            &pitches, None, None,
1475        )?)
1476    {
1477        note._pitch = pitch;
1478    }
1479
1480    Ok(())
1481}
1482
1483impl IntoNotes for &[Pitch] {
1484    type Notes = Vec<Note>;
1485
1486    fn try_into_notes(self) -> Result<Self::Notes> {
1487        self.iter()
1488            .map(|pitch| Note::new(Some(pitch.clone()), None, None, None))
1489            .collect::<Result<Vec<_>>>()
1490    }
1491}
1492
1493impl IntoNotes for &[Note] {
1494    type Notes = Vec<Note>;
1495
1496    fn try_into_notes(self) -> Result<Self::Notes> {
1497        Ok(self.to_vec())
1498    }
1499}
1500
1501impl IntoNotes for &[Chord] {
1502    type Notes = Vec<Note>;
1503
1504    fn try_into_notes(self) -> Result<Self::Notes> {
1505        Ok(self.iter().flat_map(|chord| chord._notes.clone()).collect())
1506    }
1507}
1508
1509impl IntoNotes for &[String] {
1510    type Notes = Vec<Note>;
1511
1512    fn try_into_notes(self) -> Result<Self::Notes> {
1513        self.iter()
1514            .map(|s| Note::new(Some(s.to_string()), None, None, None))
1515            .collect::<Result<Vec<_>>>()
1516    }
1517}
1518
1519impl IntoNotes for String {
1520    type Notes = Vec<Note>;
1521
1522    fn try_into_notes(self) -> Result<Self::Notes> {
1523        if self.trim().is_empty() {
1524            Ok(Vec::new())
1525        } else if self.contains(char::is_whitespace) {
1526            self.split_whitespace()
1527                .collect::<Vec<&str>>()
1528                .as_slice()
1529                .try_into_notes()
1530        } else {
1531            Ok(vec![Note::new(Some(self), None, None, None)?])
1532        }
1533    }
1534}
1535
1536impl IntoNotes for &[&str] {
1537    type Notes = Vec<Note>;
1538
1539    fn try_into_notes(self) -> Result<Self::Notes> {
1540        let mut vec = vec![];
1541        for str in self {
1542            vec.append(&mut str.try_into_notes()?);
1543        }
1544        Ok(vec)
1545    }
1546}
1547
1548impl IntoNotes for &str {
1549    type Notes = Vec<Note>;
1550
1551    fn try_into_notes(self) -> Result<Self::Notes> {
1552        if self.trim().is_empty() {
1553            Ok(Vec::new())
1554        } else if self.contains(char::is_whitespace) {
1555            self.split_whitespace()
1556                .collect::<Vec<&str>>()
1557                .try_into_notes()
1558        } else {
1559            Ok(vec![Note::new(Some(self), None, None, None)?])
1560        }
1561    }
1562}
1563
1564impl IntoNotes for &[IntegerType] {
1565    const FROM_INTEGER_PITCHES: bool = true;
1566
1567    type Notes = Vec<Note>;
1568
1569    fn try_into_notes(self) -> Result<Self::Notes> {
1570        let mut notes = self
1571            .iter()
1572            .map(|i| Note::new(Some(*i), None, None, None))
1573            .collect::<Result<Vec<_>>>()?;
1574        simplify_integer_notes(&mut notes)?;
1575        Ok(notes)
1576    }
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581    use crate::{GuitarTuning, Key, Pitch, chord::Chord};
1582
1583    #[cfg(feature = "python")]
1584    mod utils {
1585        include!(concat!(env!("CARGO_MANIFEST_DIR"), "/shared.rs"));
1586    }
1587
1588    #[cfg(feature = "python")]
1589    use pyo3::{Bound, PyAny, PyErr, PyResult, Python, prelude::PyModule, types::PyAnyMethods};
1590    #[cfg(feature = "python")]
1591    use utils::{init_py, init_py_with_dummies, prepare};
1592
1593    #[cfg(feature = "python")]
1594    fn import_music21_chord_without_package_init(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
1595        let sys = py.import("sys")?;
1596        let modules = sys.getattr("modules")?;
1597        modules.call_method1("pop", ("music21.chord", py.None()))?;
1598        modules.call_method1("pop", ("music21", py.None()))?;
1599
1600        let music21_src = format!("{}/music21/music21", env!("CARGO_MANIFEST_DIR"));
1601        let types = py.import("types")?;
1602        let music21_pkg = types.getattr("ModuleType")?.call1(("music21",))?;
1603        music21_pkg.setattr("__path__", vec![music21_src])?;
1604        modules.call_method1("__setitem__", ("music21", music21_pkg))?;
1605
1606        py.import("music21.chord")
1607    }
1608
1609    #[test]
1610    fn c_e_g_pitchedcommonname() {
1611        let chord = Chord::new("C E G");
1612
1613        assert!(chord.is_ok());
1614
1615        assert_eq!(chord.unwrap().pitched_common_name(), "C-major triad");
1616    }
1617
1618    #[test]
1619    fn new_accepts_empty_inputs() {
1620        assert_eq!(Chord::new("").unwrap().pitched_common_name(), "empty chord");
1621        assert_eq!(
1622            Chord::new(Vec::<Pitch>::new())
1623                .unwrap()
1624                .pitched_common_name(),
1625            "empty chord"
1626        );
1627        assert_eq!(
1628            Chord::new(Option::<&str>::None)
1629                .unwrap()
1630                .pitched_common_name(),
1631            "empty chord"
1632        );
1633    }
1634
1635    #[test]
1636    fn pitched_common_names_returns_aliases() {
1637        let chord = Chord::new("C E G#").unwrap();
1638        assert_eq!(
1639            chord.pitched_common_names(),
1640            vec![
1641                "C-augmented triad".to_string(),
1642                "C-equal 3-part octave division".to_string()
1643            ]
1644        );
1645    }
1646
1647    #[test]
1648    fn chord_symbols_return_symbol_names() {
1649        let major_seventh = Chord::new("C E G B").unwrap();
1650        let petrushka = Chord::new("C4 D4 Eb4 F#4 Ab4 A4").unwrap();
1651        let slash_chord = Chord::new("F4 C5 D5 E-5").unwrap();
1652
1653        assert_eq!(major_seventh.chord_symbol().as_deref(), Some("Cmaj7"));
1654        assert_eq!(
1655            petrushka.chord_symbol().as_deref(),
1656            Some("Ddom7dim5/CaddA,E-")
1657        );
1658        assert_eq!(slash_chord.chord_symbol().as_deref(), None);
1659    }
1660
1661    #[test]
1662    fn chord_symbols_with_root_accept_pitch_names() {
1663        let chord = Chord::new("G3 C4 E4").unwrap();
1664
1665        assert_eq!(
1666            chord.chord_symbol_with_root("C").unwrap().as_deref(),
1667            Some("C/G")
1668        );
1669        assert_eq!(
1670            chord.chord_symbol_with_root(0).unwrap().as_deref(),
1671            Some("C/G")
1672        );
1673    }
1674
1675    #[test]
1676    fn guitar_fingering_covers_common_chord_tones() {
1677        let chord = Chord::new("C E G").unwrap();
1678        let fingering = chord.guitar_fingering().unwrap();
1679
1680        assert_eq!(fingering.strings.len(), 6);
1681        assert_eq!(fingering.covered_pitch_spaces, vec![60, 64, 67]);
1682        assert_eq!(fingering.covered_pitch_classes, vec![0, 4, 7]);
1683        assert!(fingering.omitted_pitch_spaces.is_empty());
1684        assert!(fingering.omitted_pitch_classes.is_empty());
1685        assert!(
1686            fingering
1687                .strings
1688                .iter()
1689                .filter(|string| string.fret.is_some_and(|fret| fret > 0))
1690                .all(|string| string
1691                    .finger
1692                    .is_some_and(|finger| (1..=4).contains(&finger)))
1693        );
1694    }
1695
1696    #[test]
1697    fn guitar_fingering_still_returns_large_pitch_sets() {
1698        let chord = Chord::new("C D E F G A B").unwrap();
1699        let fingering = chord.guitar_fingering().unwrap();
1700
1701        assert_eq!(fingering.strings.len(), 6);
1702        assert!(!fingering.covered_pitch_classes.is_empty());
1703        assert!(!fingering.omitted_pitch_classes.is_empty());
1704    }
1705
1706    #[test]
1707    fn guitar_fingering_uses_supplied_tuning_and_octaves() {
1708        let chord = Chord::new("D3 A3 D4").unwrap();
1709        let tuning = GuitarTuning::new(["D2", "A2", "D3", "G3", "A3", "D4"]).unwrap();
1710        let fingering = chord.guitar_fingering_with_tuning(&tuning).unwrap();
1711
1712        assert_eq!(fingering.strings.len(), 6);
1713        assert_eq!(fingering.strings[0].string_name, "D2");
1714        assert_eq!(fingering.covered_pitch_spaces, vec![50, 57, 62]);
1715        assert!(fingering.omitted_pitch_spaces.is_empty());
1716    }
1717
1718    #[test]
1719    fn guitar_tuning_rejects_empty_tunings() {
1720        assert!(GuitarTuning::new(Vec::<&str>::new()).is_err());
1721    }
1722
1723    #[test]
1724    fn dyad_names_follow_music21_interval_rules() {
1725        let pcs = [0, 1];
1726        let integer_chord = Chord::new(pcs.as_slice()).unwrap();
1727        assert_eq!(integer_chord.common_name(), "Minor Second");
1728        assert_eq!(integer_chord.pitched_common_name(), "Minor Second above C");
1729
1730        let spelled_chord = Chord::new("C C#").unwrap();
1731        assert_eq!(spelled_chord.common_name(), "Augmented Unison");
1732        assert_eq!(
1733            spelled_chord.pitched_common_name(),
1734            "Augmented Unison above C"
1735        );
1736
1737        let octave = Chord::new("D3 D4").unwrap();
1738        assert_eq!(octave.common_name(), "Perfect Octave");
1739        assert_eq!(octave.pitched_common_name(), "Perfect Octave above D");
1740
1741        let compound = Chord::new("E-3 C5 C6").unwrap();
1742        assert_eq!(compound.common_name(), "Major Sixth with octave doublings");
1743        assert_eq!(
1744            compound.pitched_common_name(),
1745            "Major Sixth with octave doublings above Eb"
1746        );
1747    }
1748
1749    #[test]
1750    fn chord_metadata_methods_have_forte_and_inversion() {
1751        let chord = Chord::new("C E G").unwrap();
1752        assert_eq!(chord.root_pitch_name().as_deref(), Some("C"));
1753        assert_eq!(chord.bass_pitch_name().as_deref(), Some("C"));
1754        assert_eq!(chord.inversion(), Some(0));
1755        assert_eq!(chord.inversion_name().as_deref(), Some("root position"));
1756        assert_eq!(chord.forte_class().as_deref(), Some("3-11B"));
1757        assert!(
1758            chord
1759                .common_names()
1760                .iter()
1761                .any(|name| name == "major triad")
1762        );
1763    }
1764
1765    #[test]
1766    fn chord_maps_to_reduced_polyrhythm_components() {
1767        let major = Chord::new("C E G").unwrap();
1768        assert_eq!(major.polyrhythm_components(), vec![4, 5, 6]);
1769        assert_eq!(major.polyrhythm_ratio_string(), "4:5:6");
1770
1771        let empty = Chord::empty().unwrap();
1772        assert_eq!(empty.polyrhythm_ratio_string(), "1");
1773    }
1774
1775    #[test]
1776    fn new_rejects_invalid_pitch_inputs() {
1777        assert!(Chord::new("C nope G").is_err());
1778    }
1779
1780    #[test]
1781    fn chord_supports_rust_conversion_traits() {
1782        let parsed: Chord = "C E G".parse().unwrap();
1783        assert_eq!(parsed.to_string(), "C-major triad");
1784        assert_eq!(parsed.notes().len(), 3);
1785
1786        let from_str = Chord::try_from("C E G").unwrap();
1787        assert_eq!(from_str.pitched_common_name(), "C-major triad");
1788
1789        let midi = [60, 64, 67];
1790        let from_slice = Chord::try_from(midi.as_slice()).unwrap();
1791        assert_eq!(from_slice.pitched_common_name(), "C-major triad");
1792    }
1793
1794    #[test]
1795    fn known_chord_types_include_music21_table_names() {
1796        let known = Chord::known_chord_types();
1797        assert_eq!(known.len(), 351);
1798        assert!(
1799            known
1800                .iter()
1801                .any(|entry| entry.common_names.iter().any(|name| name == "major triad"))
1802        );
1803        assert!(known.iter().any(|entry| {
1804            entry
1805                .common_names
1806                .iter()
1807                .any(|name| name == "dominant seventh chord")
1808        }));
1809    }
1810
1811    #[test]
1812    fn chord_first_inversion_detected() {
1813        let chord = Chord::new("E3 G3 C4").unwrap();
1814        assert_eq!(chord.inversion(), Some(1));
1815        assert_eq!(chord.inversion_name().as_deref(), Some("first inversion"));
1816    }
1817
1818    #[test]
1819    fn dominant_seventh_resolves_to_tonic() {
1820        let chord = Chord::new("G3 B3 D4 F4").unwrap();
1821        let resolution = chord.resolution_chord("C", Some("major")).unwrap().unwrap();
1822
1823        assert_eq!(resolution.pitched_common_name(), "C-major triad");
1824    }
1825
1826    #[test]
1827    fn resolution_chords_stay_near_source_register() {
1828        let chord = Chord::new("G2 B2 D3 F3").unwrap();
1829        let resolution = chord.resolution_chord("C", Some("major")).unwrap().unwrap();
1830        let names = resolution
1831            .pitches()
1832            .into_iter()
1833            .map(|pitch| pitch.name_with_octave())
1834            .collect::<Vec<_>>();
1835
1836        assert_eq!(names, vec!["C3", "E3", "G3"]);
1837    }
1838
1839    #[test]
1840    fn resolution_suggestions_infer_contexts() {
1841        let chord = Chord::new("G3 B3 D4 F4").unwrap();
1842        let suggestions = chord.resolution_suggestions().unwrap();
1843
1844        assert!(suggestions.iter().any(|suggestion| {
1845            suggestion.key_context == "dominant resolution to C major"
1846                && suggestion.chord.pitched_common_name() == "C-major triad"
1847        }));
1848        assert!(suggestions.iter().any(|suggestion| {
1849            suggestion.key_context == "dominant resolution to C minor"
1850                && suggestion.chord.pitched_common_name() == "C-minor triad"
1851        }));
1852    }
1853
1854    #[test]
1855    fn resolution_suggestions_stay_near_source_register() {
1856        let chord = Chord::new("G2 B2 D3 F3").unwrap();
1857        let suggestions = chord.resolution_suggestions().unwrap();
1858        let c_major = suggestions
1859            .iter()
1860            .find(|suggestion| suggestion.key_context == "dominant resolution to C major")
1861            .unwrap();
1862        let names = c_major
1863            .chord
1864            .pitches()
1865            .into_iter()
1866            .map(|pitch| pitch.name_with_octave())
1867            .collect::<Vec<_>>();
1868
1869        assert_eq!(names, vec!["C3", "E3", "G3"]);
1870    }
1871
1872    #[test]
1873    fn resolution_suggestions_can_use_explicit_key_context() {
1874        let secondary_dominant = Chord::new("D3 F#3 A3 C4").unwrap();
1875        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
1876        let suggestions = secondary_dominant
1877            .resolution_suggestions_in_key(&c_major)
1878            .unwrap();
1879
1880        assert_eq!(suggestions.len(), 1);
1881        assert_eq!(suggestions[0].key_context, "dominant resolution in C major");
1882        assert_eq!(suggestions[0].chord.pitched_common_name(), "G-major triad");
1883    }
1884
1885    #[test]
1886    fn dominant_seventh_resolves_to_minor_tonic() {
1887        let chord = Chord::new("G3 B3 D4 F4").unwrap();
1888        let resolution = chord.resolution_chord("C", Some("minor")).unwrap().unwrap();
1889
1890        assert_eq!(resolution.pitched_common_name(), "C-minor triad");
1891    }
1892
1893    #[test]
1894    fn secondary_dominant_resolves_to_diatonic_target() {
1895        let chord = Chord::new("D3 F#3 A3 C4").unwrap();
1896        let resolution = chord.resolution_chord("C", Some("major")).unwrap().unwrap();
1897
1898        assert_eq!(resolution.pitched_common_name(), "G-major triad");
1899    }
1900
1901    #[test]
1902    fn dominant_extensions_resolve_to_tonic() {
1903        let dominant_ninth = Chord::new("G2 B2 D3 F3 A3").unwrap();
1904        let dominant_eleventh = Chord::new("G2 B2 D3 F3 A3 C4").unwrap();
1905        let dominant_thirteenth = Chord::new("G2 B2 D3 F3 A3 C4 E4").unwrap();
1906
1907        for chord in [dominant_ninth, dominant_eleventh, dominant_thirteenth] {
1908            let resolution = chord.resolution_chord("C", Some("major")).unwrap().unwrap();
1909            assert_eq!(resolution.pitched_common_name(), "C-major triad");
1910        }
1911    }
1912
1913    #[test]
1914    fn leading_tone_sevenths_resolve_by_semitone() {
1915        let fully_diminished = Chord::new("B3 D4 F4 A-4").unwrap();
1916        let half_diminished = Chord::new("B3 D4 F4 A4").unwrap();
1917
1918        assert_eq!(
1919            fully_diminished
1920                .resolution_chord("C", Some("major"))
1921                .unwrap()
1922                .unwrap()
1923                .pitched_common_name(),
1924            "C-major triad"
1925        );
1926        assert_eq!(
1927            half_diminished
1928                .resolution_chord("C", Some("major"))
1929                .unwrap()
1930                .unwrap()
1931                .pitched_common_name(),
1932            "C-major triad"
1933        );
1934    }
1935
1936    #[test]
1937    fn leading_tone_diminished_triad_resolves_by_semitone() {
1938        let chord = Chord::new("B3 D4 F4").unwrap();
1939        let resolution = chord.resolution_chord("C", Some("major")).unwrap().unwrap();
1940
1941        assert_eq!(resolution.pitched_common_name(), "C-major triad");
1942    }
1943
1944    #[test]
1945    fn contextual_augmented_sixth_resolves_to_dominant() {
1946        let german_augmented_sixth = Chord::new("A-3 C4 E-4 F#4").unwrap();
1947        let resolution = german_augmented_sixth
1948            .resolution_chord("C", Some("major"))
1949            .unwrap()
1950            .unwrap();
1951
1952        assert_eq!(resolution.pitched_common_name(), "G-major triad");
1953    }
1954
1955    #[test]
1956    fn unsupported_resolution_returns_none() {
1957        let tonic = Chord::new("C E G").unwrap();
1958        assert!(
1959            tonic
1960                .resolution_chord("C", Some("major"))
1961                .unwrap()
1962                .is_none()
1963        );
1964    }
1965
1966    #[test]
1967    #[cfg(feature = "python")]
1968    fn compare_chords_python() {
1969        let x = "C E G";
1970        let y = "C C# D D# E F F# G G# A A# B";
1971
1972        prepare().unwrap();
1973
1974        Python::attach(|py| -> PyResult<()> {
1975            init_py(py)?;
1976            init_py_with_dummies(py)?;
1977
1978            let chord: Bound<'_, PyModule> = match import_music21_chord_without_package_init(py) {
1979                Ok(module) => module,
1980                Err(_) => {
1981                    // In constrained environments we may only have the dummy
1982                    // shim module available; skip Python parity here.
1983                    return Ok(());
1984                }
1985            };
1986
1987            let chord_class = match chord.getattr("Chord") {
1988                Ok(value) => value,
1989                Err(_) => {
1990                    return Ok(());
1991                }
1992            };
1993
1994            compare_chord(x, &chord_class)?;
1995            compare_chord(y, &chord_class)?;
1996
1997            Ok(())
1998        })
1999        .unwrap();
2000    }
2001
2002    #[test]
2003    #[cfg(feature = "python")]
2004    fn compare_all_pitch_class_subsets_python() {
2005        prepare().unwrap();
2006
2007        Python::attach(|py| -> PyResult<()> {
2008            init_py(py)?;
2009            init_py_with_dummies(py)?;
2010
2011            let chord: Bound<'_, PyModule> = match import_music21_chord_without_package_init(py) {
2012                Ok(module) => module,
2013                Err(_) => return Ok(()),
2014            };
2015            let chord_class = match chord.getattr("Chord") {
2016                Ok(value) => value,
2017                Err(_) => return Ok(()),
2018            };
2019
2020            for mask in 0_u16..(1_u16 << 12) {
2021                let pcs = (0..12)
2022                    .filter(|pc| mask & (1 << pc) != 0)
2023                    .collect::<Vec<_>>();
2024                let chord_instance = chord_class.call1((pcs.clone(),))?;
2025
2026                let python_common_name: String = chord_instance.getattr("commonName")?.extract()?;
2027                let python_pitched_common_name: String =
2028                    chord_instance.getattr("pitchedCommonName")?.extract()?;
2029
2030                let rust_chord = Chord::new(pcs.as_slice()).unwrap();
2031                let rust_common_name = rust_chord.common_name();
2032                let rust_pitched_common_name = rust_chord.pitched_common_name();
2033                assert_eq!(
2034                    rust_common_name, python_common_name,
2035                    "commonName mismatch for mask {mask:012b} pcs {pcs:?}"
2036                );
2037                assert_eq!(
2038                    rust_pitched_common_name, python_pitched_common_name,
2039                    "pitchedCommonName mismatch for mask {mask:012b} pcs {pcs:?}"
2040                );
2041            }
2042
2043            Ok(())
2044        })
2045        .unwrap();
2046    }
2047
2048    #[cfg(feature = "python")]
2049    fn compare_chord(x: &str, chord_class: &Bound<'_, PyAny>) -> Result<(), PyErr> {
2050        let chord_instance = chord_class.call1((x,))?;
2051
2052        let chord = Chord::new(x).unwrap();
2053
2054        let pitched_common_name: String = chord_instance.getattr("pitchedCommonName")?.extract()?;
2055        assert_eq!(chord.pitched_common_name(), pitched_common_name);
2056
2057        let common_name: String = chord_instance.getattr("commonName")?.extract()?;
2058        assert_eq!(chord.common_name(), common_name);
2059        Ok(())
2060    }
2061}