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