1pub(crate) mod chordbase;
2pub 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))]
27pub 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))]
42pub struct KnownChordType {
44 pub cardinality: u8,
46 pub common_names: Vec<String>,
48 pub forte_class: String,
50 pub normal_form: Vec<u8>,
52 pub interval_class_vector: Vec<u8>,
54}
55
56#[derive(Debug, Clone)]
57pub struct ChordResolutionSuggestion {
59 pub chord: Chord,
61 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 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 Ok(chord)
161 }
162
163 pub fn empty() -> Result<Self> {
165 Self::new(Option::<&str>::None)
166 }
167
168 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 pub fn pitched_common_name(&self) -> String {
184 self.pitched_name_for_common_name(&self.common_name())
185 }
186
187 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 pub fn chord_symbol(&self) -> Option<String> {
209 self.chord_symbols().into_iter().next()
210 }
211
212 pub fn chord_symbols(&self) -> Vec<String> {
217 crate::chordsymbol::chord_symbol_spellings(self)
218 }
219
220 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 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 pub fn guitar_fingering(&self) -> Option<GuitarFingering> {
258 guitar::suggested_guitar_fingering(self)
259 }
260
261 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 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 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 pub fn pitch_classes(&self) -> Vec<u8> {
563 self.ordered_pitch_classes()
564 }
565
566 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 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 pub fn pitches(&self) -> Vec<Pitch> {
616 self._notes.iter().map(|note| note._pitch.clone()).collect()
617 }
618
619 pub fn notes(&self) -> &[Note] {
621 &self._notes
622 }
623
624 pub fn duration(&self) -> Option<&Duration> {
626 self.chordbase.duration().as_ref()
627 }
628
629 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 pub fn with_duration(mut self, duration: Duration) -> Self {
638 self.set_duration(duration);
639 self
640 }
641
642 pub fn root_pitch_name(&self) -> Option<String> {
647 self.root_pitch_name_from_tables()
648 }
649
650 pub fn bass_pitch_name(&self) -> Option<String> {
654 self.bass_pitch().map(Self::display_pitch_name)
655 }
656
657 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 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 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 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 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 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 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 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 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 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 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 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 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 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(°ree_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| ¬e._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(¬e._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| ¬e._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(¬e._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
1416pub trait IntoNotes {
1423 const FROM_INTEGER_PITCHES: bool = false;
1425
1426 type Notes: IntoIterator<Item = Note>;
1428
1429 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}