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