1use super::*;
5
6impl Chord {
7 pub fn known_chord_types() -> Vec<KnownChordType> {
9 tables::known_chord_table_entries()
10 .into_iter()
11 .map(|entry| KnownChordType {
12 cardinality: entry.cardinality,
13 common_names: entry.common_names.into_iter().map(str::to_string).collect(),
14 forte_class: entry.forte_class,
15 normal_form: entry.normal_form,
16 interval_class_vector: entry.interval_class_vector,
17 })
18 .collect()
19 }
20
21 pub fn pitched_common_name(&self) -> String {
23 self.pitched_name_for_common_name(&self.common_name())
24 }
25
26 pub fn pitched_common_names(&self) -> Vec<String> {
31 let common_names = self.common_names();
32 if common_names.is_empty() {
33 return vec![self.pitched_common_name()];
34 }
35
36 common_names
37 .iter()
38 .map(|name| self.pitched_name_for_common_name(name))
39 .collect()
40 }
41
42 pub fn chord_symbol(&self) -> Option<String> {
48 self.chord_symbols().into_iter().next()
49 }
50
51 pub fn chord_symbols(&self) -> Vec<String> {
56 crate::chordsymbol::chord_symbol_spellings(self)
57 }
58
59 pub fn chord_symbol_with_root(
67 &self,
68 root: impl Into<PitchClassSpecifier>,
69 ) -> Result<Option<String>> {
70 Ok(self.chord_symbols_with_root(root)?.into_iter().next())
71 }
72
73 pub fn chord_symbols_with_root(
79 &self,
80 root: impl Into<PitchClassSpecifier>,
81 ) -> Result<Vec<String>> {
82 let root = Self::chord_symbol_root_pitch_class(root.into())?;
83
84 Ok(crate::chordsymbol::chord_symbol_spellings_with_root(
85 self, root,
86 ))
87 }
88
89 pub(super) fn pitched_name_for_common_name(&self, name_str: &str) -> String {
90 if name_str == "empty chord" {
91 return name_str.to_string();
92 }
93
94 if matches!(name_str, "note" | "unison") {
95 return self
96 .notes
97 .first()
98 .map(|n| n.pitch.name())
99 .unwrap_or_else(|| name_str.to_string());
100 }
101
102 let pitch_class_cardinality = self.ordered_pitch_classes().len();
103 if pitch_class_cardinality <= 2
104 || name_str.contains("enharmonic")
105 || name_str.contains("forte class")
106 || name_str.contains(" semitone")
107 {
108 if let Some(bass) = self.bass() {
110 return format!("{name_str} above {}", bass.name());
111 }
112 return name_str.to_string();
113 }
114
115 let root_name = self
118 .root()
119 .or_else(|| self.notes.first().map(|n| &n.pitch))
120 .map(Pitch::name);
121
122 match root_name {
123 Some(root_name) => format!("{root_name}-{name_str}"),
124 None => name_str.to_string(),
125 }
126 }
127
128 pub(super) fn chord_symbol_root_pitch_class(root: PitchClassSpecifier) -> Result<u8> {
129 match root {
130 PitchClassSpecifier::String(value) => match Pitch::from_name(value.as_str()) {
131 Ok(pitch) => Self::integer_pitch_class_for_chord_symbol_root(pitch.ps()),
132 Err(pitch_error) => {
133 let pitch_class = PitchClass::new(value.as_str()).map_err(|pitch_class_error| {
134 Error::Chord(format!(
135 "cannot parse chord-symbol root {value:?} as a pitch name ({pitch_error}) or pitch class ({pitch_class_error})"
136 ))
137 })?;
138 Self::integer_pitch_class_from_value(pitch_class)
139 }
140 },
141 specifier => {
142 let pitch_class = PitchClass::new(specifier)?;
143 Self::integer_pitch_class_from_value(pitch_class)
144 }
145 }
146 }
147
148 pub(super) fn integer_pitch_class_from_value(pitch_class: PitchClass) -> Result<u8> {
149 let Some(root) = pitch_class.integer() else {
150 return Err(Error::Chord(
151 "chord symbols require an integer pitch-class root".to_string(),
152 ));
153 };
154 Ok(root as u8)
155 }
156
157 pub(super) fn integer_pitch_class_for_chord_symbol_root(ps: FloatType) -> Result<u8> {
158 if (ps - ps.round()).abs() > FloatType::EPSILON {
159 return Err(Error::Chord(
160 "chord symbols require an integer pitch-class root".to_string(),
161 ));
162 }
163
164 Ok((ps.round() as IntegerType).rem_euclid(12) as u8)
165 }
166
167 pub fn common_name(&self) -> String {
172 if self
173 .notes
174 .iter()
175 .any(|n| (n.pitch.alter() - n.pitch.alter().round()).abs() > FloatType::EPSILON)
176 {
177 return "microtonal chord".to_string();
178 }
179
180 if self.notes.is_empty() {
181 return "empty chord".to_string();
182 }
183
184 let ordered_pcs = self.ordered_pitch_classes();
185 if ordered_pcs.is_empty() {
186 return "empty chord".to_string();
187 }
188
189 if ordered_pcs.len() == 1 {
190 if self.notes.len() == 1 {
191 return "note".to_string();
192 }
193
194 let (pitch_names, pitch_pses) = self.distinct_names_and_spaces();
195
196 if pitch_names.len() == 1 {
197 if pitch_pses.len() == 1 {
198 return "unison".to_string();
199 }
200 if pitch_pses.len() == 2 {
201 return Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
202 .unwrap_or_else(|| "multiple octaves".to_string());
203 }
204 return "multiple octaves".to_string();
205 }
206 if pitch_pses.len() == 1 {
207 return "enharmonic unison".to_string();
208 }
209 return "enharmonic octaves".to_string();
210 }
211
212 if ordered_pcs.len() == 2 {
213 return self.dyad_common_name();
214 }
215
216 let address = match tables::seek_chord_tables_address(&ordered_pcs) {
217 Ok(address) => address,
218 Err(_) => return "unknown chord".to_string(),
219 };
220
221 let common_names: Vec<&'static str> = match tables::address_to_common_names(address) {
226 Ok(Some(names)) => names,
227 _ => Vec::new(),
228 };
229 let forte_name = tables::address_to_forte_name(address, "tn").ok();
230
231 if let Some(forte_name) = forte_name.as_deref() {
232 if let Some(name) = self.augmented_sixth_common_name(forte_name, &common_names) {
233 return name;
234 }
235 if let Some(first) = common_names.first() {
236 if matches!(forte_name, "4-20" | "4-26") {
237 return if self.is_seventh_with_perfect_fifths_above_root_and_third() {
238 (*first).to_string()
239 } else {
240 format!("enharmonic equivalent to {first}")
241 };
242 }
243 if let Some(spelled) = self.spelled_as_named(forte_name) {
244 return if spelled {
245 (*first).to_string()
246 } else {
247 format!("enharmonic equivalent to {first}")
248 };
249 }
250 }
251 }
252
253 match common_names.first() {
254 Some(name) => (*name).to_string(),
255 None => match forte_name {
256 Some(forte_name) => format!("forte class {forte_name}"),
257 None => "unknown chord".to_string(),
258 },
259 }
260 }
261
262 pub(super) fn augmented_sixth_common_name(
266 &self,
267 forte_name: &str,
268 common_names: &[&str],
269 ) -> Option<String> {
270 let named = |index: usize| common_names.get(index).map(|name| (*name).to_string());
271 let in_inversion = |index: usize| {
272 named(index).map(|name| format!("{name} in {}", self.inversion_text().to_lowercase()))
273 };
274 match forte_name {
275 "4-27B" => {
276 if self.is_dominant_seventh() {
277 named(0)
278 } else if self.is_german_augmented_sixth(false) {
279 named(2)
280 } else if self.is_german_augmented_sixth(true) {
281 in_inversion(2)
282 } else if self.is_swiss_augmented_sixth(false) {
283 named(3)
284 } else if self.is_swiss_augmented_sixth(true) {
285 in_inversion(3)
286 } else {
287 named(0).map(|name| format!("enharmonic to {name}"))
288 }
289 }
290 "4-25" => {
291 if self.is_french_augmented_sixth(false) {
292 named(1)
293 } else if self.is_french_augmented_sixth(true) {
294 in_inversion(1)
295 } else {
296 named(0)
297 }
298 }
299 "3-8A" => {
300 if self.is_italian_augmented_sixth(false, false) {
301 named(1)
302 } else if self.is_italian_augmented_sixth(true, false) {
303 in_inversion(1)
304 } else {
305 named(0)
306 }
307 }
308 _ => None,
309 }
310 }
311
312 pub(super) fn spelled_as_named(&self, forte_name: &str) -> Option<bool> {
316 match forte_name {
317 "3-11A" => Some(self.is_minor_triad()),
318 "3-11B" => Some(self.is_major_triad()),
319 "3-10" => Some(self.is_diminished_triad()),
320 "3-12" => Some(self.is_augmented_triad()),
321 "4-27A" => Some(self.is_half_diminished_seventh()),
322 "4-28" => Some(self.is_diminished_seventh()),
323 "5-27A" | "5-27B" | "5-34" => Some(self.is_ninth()),
324 _ => None,
325 }
326 }
327
328 pub(super) fn is_seventh_with_perfect_fifths_above_root_and_third(&self) -> bool {
332 if !self.is_seventh() {
333 return false;
334 }
335 let names = self.pitch_names();
336 let has_fifth_above = |pitch: &Pitch| {
340 pitch
341 .transpose(&PERFECT_FIFTH)
342 .is_ok_and(|above| names.contains(&above.name()))
343 };
344 let (Some(root), Some(third)) = (self.root(), self.third()) else {
345 return false;
346 };
347 has_fifth_above(root) && has_fifth_above(third)
348 }
349
350 fn distinct_names_and_spaces(
353 &self,
354 ) -> (
355 std::collections::BTreeSet<String>,
356 std::collections::BTreeSet<IntegerType>,
357 ) {
358 let names = self.notes.iter().map(|n| n.pitch.name()).collect();
359 let spaces = self
360 .notes
361 .iter()
362 .map(|n| n.pitch.ps().round() as IntegerType)
363 .collect();
364 (names, spaces)
365 }
366
367 pub(super) fn dyad_common_name(&self) -> String {
368 let (pitch_names, pitch_pses) = self.distinct_names_and_spaces();
369
370 let Some(p0) = self.notes.first().map(|n| &n.pitch) else {
371 return "empty chord".to_string();
372 };
373 let p0_pitch_class = root::pitch_class(p0);
374
375 let Some(p1) = self
376 .notes
377 .iter()
378 .skip(1)
379 .find(|n| root::pitch_class(&n.pitch) != p0_pitch_class)
380 .map(|n| &n.pitch)
381 else {
382 return "unknown chord".to_string();
383 };
384
385 let relevant_interval = Interval::between(p0.clone(), p1.clone());
386
387 if pitch_names.len() > 2 {
388 let Ok(interval) = relevant_interval else {
389 return "unknown chord".to_string();
390 };
391 let semitones = interval.chromatic.simple_undirected();
392 let plural = if semitones == 1 { "" } else { "s" };
393 return format!("{semitones} semitone{plural}");
394 }
395
396 if pitch_pses.len() > 2 {
397 return relevant_interval
398 .map(|interval| {
399 format!("{} with octave doublings", interval.semi_simple_nice_name())
400 })
401 .unwrap_or_else(|_| "unknown chord".to_string());
402 }
403
404 Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
405 .unwrap_or_else(|| "unknown chord".to_string())
406 }
407
408 pub fn common_names(&self) -> Vec<String> {
410 let ordered_pcs = self.ordered_pitch_classes();
411 let Ok(address) = tables::seek_chord_tables_address(&ordered_pcs) else {
412 return Vec::new();
413 };
414 tables::address_to_common_names(address)
415 .ok()
416 .flatten()
417 .unwrap_or_default()
418 .into_iter()
419 .map(str::to_string)
420 .collect()
421 }
422
423 pub fn root_pitch_name(&self) -> Option<String> {
428 self.root_pitch_name_from_tables()
429 }
430
431 pub fn bass_pitch_name(&self) -> Option<String> {
435 self.bass_pitch().map(Pitch::name)
436 }
437
438 pub(super) fn root_pitch_name_from_tables(&self) -> Option<String> {
439 self.find_root_pitch().map(Pitch::name)
440 }
441
442 pub(super) fn common_names_with_primary(&self) -> Vec<String> {
443 let mut names = vec![self.common_name()];
444 names.extend(self.common_names());
445 names.sort();
446 names.dedup();
447 names
448 }
449
450 pub(super) fn interval_nice_name(start: &Pitch, end: &Pitch) -> Option<String> {
451 Interval::between(start.clone(), end.clone())
452 .ok()
453 .map(|interval| interval.nice_name())
454 }
455
456 pub(super) fn display_key_name(key: &Key) -> String {
457 format!("{} {}", key.tonic().name(), key.mode())
458 }
459
460 pub fn full_name(&self) -> String {
464 let pitches = self
465 .notes
466 .iter()
467 .map(|note| note.pitch.full_name())
468 .collect::<Vec<_>>()
469 .join(" | ");
470 let duration = self
471 .duration
472 .clone()
473 .unwrap_or_else(Duration::quarter)
474 .full_name();
475 format!("Chord {{{pitches}}} {duration}")
476 }
477}
478
479#[derive(Debug, Clone, PartialEq)]
480#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
481pub struct KnownChordType {
483 pub cardinality: u8,
485 pub common_names: Vec<String>,
487 pub forte_class: String,
489 pub normal_form: Vec<u8>,
491 pub interval_class_vector: Vec<u8>,
493}
494
495#[cfg(test)]
496mod augmented_sixth_voicings {
497 use crate::{Chord, defaults::IntegerType};
498
499 const EXPECTED: &str = "\
503A#3 C#4 D#4 F#4 minor seventh chord
504A#3 C#4 E#4 F#4 major seventh chord
505A#3 C#4 E#4 G#4 minor seventh chord
506A#3 C#4 G4 enharmonic equivalent to diminished triad
507A#3 C4 E4 F#4 French augmented sixth chord in first inversion
508A- C D F# French augmented sixth chord in third inversion
509A- C E- F# German augmented sixth chord in second inversion
510A- C F# Italian augmented sixth chord in second inversion
511A-3 C-4 E-4 F-4 major seventh chord
512A-3 C-4 E-4 G-4 minor seventh chord
513A-3 C4 D4 F#4 French augmented sixth chord
514A-3 C4 E-4 F#4 German augmented sixth chord
515A-3 C4 F#4 Italian augmented sixth chord
516A3 B3 E-4 F#4 enharmonic to dominant seventh chord
517A3 C#4 E-4 G4 French augmented sixth chord in root position
518A3 E-4 F#4 enharmonic equivalent to diminished triad
519B-3 C-4 E-4 G-4 major seventh chord
520B-3 D4 E4 G#4 French augmented sixth chord
521B3 C#4 E#4 G4 French augmented sixth chord in third inversion
522B3 E-4 F#4 A4 enharmonic to dominant seventh chord
523C D F# A- French augmented sixth chord in third inversion
524C E F# A# French augmented sixth chord
525C# D# F# A# minor seventh chord
526C# E# F# A# major seventh chord
527C# E# G B French augmented sixth chord in root position
528C# E# G# A# minor seventh chord
529C# E- G Italian augmented sixth chord in root position
530C# E- G A French augmented sixth chord in first inversion
531C# G A# enharmonic equivalent to diminished triad
532C#3 D#3 F#3 A#3 minor seventh chord
533C#3 E#3 F#3 A#3 major seventh chord
534C#3 E#3 G#3 A#3 minor seventh chord
535C#3 E#3 G3 B3 French augmented sixth chord in root position
536C#3 E-3 G3 Italian augmented sixth chord in root position
537C#3 E-3 G3 A3 French augmented sixth chord in first inversion
538C#3 G3 A#3 enharmonic equivalent to diminished triad
539C-3 E-3 F-3 A-3 major seventh chord
540C-3 E-3 G-3 A-3 minor seventh chord
541C-3 E-3 G-3 B-3 major seventh chord
542C3 D3 F#3 A-3 French augmented sixth chord in third inversion
543C3 E-3 F#3 A-3 German augmented sixth chord in second inversion
544C3 E3 F#3 A#3 French augmented sixth chord
545C3 F#3 A-3 Italian augmented sixth chord in second inversion
546D E G# B- French augmented sixth chord in third inversion
547D#3 F#3 A#3 C#4 minor seventh chord
548D3 E3 G#3 B-3 French augmented sixth chord in third inversion
549D3 F#3 A-3 C4 French augmented sixth chord in root position
550E#3 F#3 A#3 C#4 major seventh chord
551E#3 G#3 A#3 C#4 minor seventh chord
552E#3 G3 B3 C#4 French augmented sixth chord in first inversion
553E- F# A enharmonic equivalent to diminished triad
554E- F# A B enharmonic to dominant seventh chord
555E- F- A- C- major seventh chord
556E- G- A- C- minor seventh chord
557E- G- B- C- major seventh chord
558E-3 F#3 A-3 C4 German augmented sixth chord in third inversion
559E-3 F#3 A3 enharmonic equivalent to diminished triad
560E-3 F#3 A3 B3 enharmonic to dominant seventh chord
561E-3 F-3 A-3 C-4 major seventh chord
562E-3 G-3 A-3 C-4 minor seventh chord
563E-3 G-3 B-3 C-4 major seventh chord
564E-3 G3 A3 C#4 French augmented sixth chord
565E-3 G3 C#4 Italian augmented sixth chord
566E3 F#3 A#3 C4 French augmented sixth chord in third inversion
567E3 G#3 B-3 D4 French augmented sixth chord in root position
568F#3 A#3 C#4 D#4 minor seventh chord
569F#3 A#3 C#4 E#4 major seventh chord
570F#3 A#3 C4 E4 French augmented sixth chord in root position
571F#3 A-3 C4 Italian augmented sixth chord in root position
572F#3 A-3 C4 D4 French augmented sixth chord in first inversion
573F#3 A-3 C4 E-4 German augmented sixth chord in root position
574F#3 A3 B3 E-4 enharmonic to dominant seventh chord
575F#3 A3 E-4 enharmonic equivalent to diminished triad
576F-3 A-3 C-4 E-4 major seventh chord
577G#3 A#3 C#4 E#4 minor seventh chord
578G#3 B-3 D4 E4 French augmented sixth chord in first inversion
579G-3 A-3 C-4 E-4 minor seventh chord
580G-3 B-3 C-4 E-4 major seventh chord
581G3 A#3 C#4 enharmonic equivalent to diminished triad
582G3 A3 C#4 E-4 French augmented sixth chord in third inversion
583G3 B3 C#4 E#4 French augmented sixth chord
584G3 C#4 E-4 Italian augmented sixth chord in second inversion
585[0, 2, 6, 8] French augmented sixth chord in third inversion
586[1, 3, 6, 10] enharmonic equivalent to minor seventh chord
587[1, 3, 7] Italian augmented sixth chord in root position
588[1, 5, 6, 10] enharmonic equivalent to major seventh chord
589[1, 5, 8, 10] enharmonic equivalent to minor seventh chord
590[1, 7, 10] enharmonic equivalent to diminished triad
591[3, 4, 8, 11] enharmonic equivalent to major seventh chord
592[3, 6, 10, 11] enharmonic equivalent to major seventh chord
593[3, 6, 8, 11] enharmonic equivalent to minor seventh chord
594[3, 6, 9] enharmonic equivalent to diminished triad
595";
596
597 #[test]
601 fn pitched_names_read_the_root_music21_reads() {
602 let cases = [
603 (
604 "C# E- G",
605 "C#-Italian augmented sixth chord in root position",
606 ),
607 (
608 "G4 C#5 E-5",
609 "C#-Italian augmented sixth chord in second inversion",
610 ),
611 ("E-4 G4 C#5", "C#-Italian augmented sixth chord"),
612 (
613 "C D F# A-",
614 "D-French augmented sixth chord in third inversion",
615 ),
616 ("A-4 C5 D5 F#5", "D-French augmented sixth chord"),
617 (
618 "F#4 A-4 C5 D5",
619 "D-French augmented sixth chord in first inversion",
620 ),
621 (
622 "D4 F#4 A-4 C5",
623 "D-French augmented sixth chord in root position",
624 ),
625 (
626 "C# E- G A",
627 "A-French augmented sixth chord in first inversion",
628 ),
629 (
630 "A4 C#5 E-5 G5",
631 "A-French augmented sixth chord in root position",
632 ),
633 ("C E F# A#", "F#-French augmented sixth chord"),
634 (
635 "A#3 C4 E4 F#4",
636 "F#-French augmented sixth chord in first inversion",
637 ),
638 (
639 "C# E# G B",
640 "C#-French augmented sixth chord in root position",
641 ),
642 (
643 "B4 C#5 E#5 G5",
644 "C#-French augmented sixth chord in third inversion",
645 ),
646 (
647 "D E G# B-",
648 "E-French augmented sixth chord in third inversion",
649 ),
650 ];
651 for (notes, expected) in cases {
652 assert_eq!(
653 Chord::new(notes).unwrap().pitched_common_name(),
654 expected,
655 "{notes}"
656 );
657 }
658
659 let mut rooted = Chord::new("F# A C# E#").unwrap();
660 rooted.set_root(Some("A".parse().unwrap()));
661 assert_eq!(rooted.pitched_common_name(), "A-minor-augmented tetrachord");
662
663 let mut added = Chord::new("C E G B#").unwrap();
664 added.set_bass(Some("E".parse().unwrap()));
665 assert_eq!(
666 added.pitched_common_name(),
667 "enharmonic equivalent to major triad above E"
668 );
669 }
670
671 #[test]
672 fn every_voicing_is_named_as_music21_names_it() {
673 let mut wrong = Vec::new();
674 for line in EXPECTED.lines() {
675 let (notes, expected) = line.split_once('\t').unwrap();
676 let chord = if let Some(numbers) = notes.strip_prefix('[') {
677 let classes: Vec<IntegerType> = numbers
678 .trim_end_matches(']')
679 .split(", ")
680 .map(|number| number.parse().unwrap())
681 .collect();
682 Chord::new(classes.as_slice()).unwrap()
683 } else {
684 Chord::new(notes).unwrap()
685 };
686 let named = chord.common_name();
687 if named != expected {
688 wrong.push(format!("{notes}: {named}, music21 {expected}"));
689 }
690 }
691 assert!(
692 wrong.is_empty(),
693 "{} of {}:\n{}",
694 wrong.len(),
695 EXPECTED.lines().count(),
696 wrong.join("\n")
697 );
698 }
699}