Skip to main content

music21_rs/chord/
mod.rs

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