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