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 let found = Some((address.0, address.1, address.2));
235 if let Some(name) = self.augmented_sixth_common_name(forte_name, found, &common_names) {
236 return name;
237 }
238 if let Some(first) = common_names.first() {
239 if matches!(forte_name, "4-20" | "4-26") {
240 return if self.is_seventh_with_perfect_fifths_above_root_and_third() {
241 (*first).to_string()
242 } else {
243 format!("enharmonic equivalent to {first}")
244 };
245 }
246 if let Some(spelled) = self.spelled_as_named(forte_name) {
247 return if spelled {
248 (*first).to_string()
249 } else {
250 format!("enharmonic equivalent to {first}")
251 };
252 }
253 }
254 }
255
256 match common_names.first() {
257 Some(name) => (*name).to_string(),
258 None => match forte_name {
259 Some(forte_name) => format!("forte class {forte_name}"),
260 None => "unknown chord".to_string(),
261 },
262 }
263 }
264
265 pub(super) fn augmented_sixth_common_name(
269 &self,
270 forte_name: &str,
271 found: Option<(u8, u8, i8)>,
272 common_names: &[&str],
273 ) -> Option<String> {
274 let named = |index: usize| common_names.get(index).map(|name| (*name).to_string());
275 let in_inversion = |index: usize| {
276 named(index).map(|name| format!("{name} in {}", self.inversion_text().to_lowercase()))
277 };
278 match forte_name {
279 "4-27B" => {
280 if self.is_dominant_seventh() {
281 named(0)
282 } else if self.is_german_from(found, false) {
283 named(2)
284 } else if self.is_german_from(found, true) {
285 in_inversion(2)
286 } else if self.is_swiss_from(found, false) {
287 named(3)
288 } else if self.is_swiss_from(found, true) {
289 in_inversion(3)
290 } else {
291 named(0).map(|name| format!("enharmonic to {name}"))
292 }
293 }
294 "4-25" => {
295 if self.is_french_from(found, false) {
296 named(1)
297 } else if self.is_french_from(found, true) {
298 in_inversion(1)
299 } else {
300 named(0)
301 }
302 }
303 "3-8A" => {
304 if self.is_italian_augmented_sixth(false, false) {
305 named(1)
306 } else if self.is_italian_augmented_sixth(true, false) {
307 in_inversion(1)
308 } else {
309 named(0)
310 }
311 }
312 _ => None,
313 }
314 }
315
316 pub(super) fn spelled_as_named(&self, forte_name: &str) -> Option<bool> {
320 match forte_name {
321 "3-11A" => Some(self.is_minor_triad()),
322 "3-11B" => Some(self.is_major_triad()),
323 "3-10" => Some(self.is_diminished_triad()),
324 "3-12" => Some(self.is_augmented_triad()),
325 "4-27A" => Some(self.is_half_diminished_seventh()),
326 "4-28" => Some(self.is_diminished_seventh()),
327 "5-27A" | "5-27B" | "5-34" => Some(self.is_ninth()),
328 _ => None,
329 }
330 }
331
332 pub(super) fn is_seventh_with_perfect_fifths_above_root_and_third(&self) -> bool {
336 let Some(root) = self.root() else {
338 return false;
339 };
340 if !self.is_seventh_from(root) {
341 return false;
342 }
343 let names = self.pitch_names();
344 let has_fifth_above = |pitch: &Pitch| {
348 pitch
349 .transpose(&PERFECT_FIFTH)
350 .is_ok_and(|above| names.contains(&above.name()))
351 };
352 let Some(third) = self.chord_step_from(3, root) else {
353 return false;
354 };
355 has_fifth_above(root) && has_fifth_above(third)
356 }
357
358 fn distinct_names_and_spaces(
361 &self,
362 ) -> (
363 std::collections::BTreeSet<String>,
364 std::collections::BTreeSet<IntegerType>,
365 ) {
366 let names = self.notes.iter().map(|n| n.pitch.name()).collect();
367 let spaces = self
368 .notes
369 .iter()
370 .map(|n| n.pitch.ps().round() as IntegerType)
371 .collect();
372 (names, spaces)
373 }
374
375 pub(super) fn dyad_common_name(&self) -> String {
376 let (pitch_names, pitch_pses) = self.distinct_names_and_spaces();
377
378 let Some(p0) = self.notes.first().map(|n| &n.pitch) else {
379 return "empty chord".to_string();
380 };
381 let p0_pitch_class = root::pitch_class(p0);
382
383 let Some(p1) = self
384 .notes
385 .iter()
386 .skip(1)
387 .find(|n| root::pitch_class(&n.pitch) != p0_pitch_class)
388 .map(|n| &n.pitch)
389 else {
390 return "unknown chord".to_string();
391 };
392
393 let relevant_interval = Interval::between(p0.clone(), p1.clone());
394
395 if pitch_names.len() > 2 {
396 let Ok(interval) = relevant_interval else {
397 return "unknown chord".to_string();
398 };
399 let semitones = interval.chromatic.simple_undirected();
400 let plural = if semitones == 1 { "" } else { "s" };
401 return format!("{semitones} semitone{plural}");
402 }
403
404 if pitch_pses.len() > 2 {
405 return relevant_interval
406 .map(|interval| {
407 format!("{} with octave doublings", interval.semi_simple_nice_name())
408 })
409 .unwrap_or_else(|_| "unknown chord".to_string());
410 }
411
412 Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
413 .unwrap_or_else(|| "unknown chord".to_string())
414 }
415
416 pub fn common_names(&self) -> Vec<String> {
418 let ordered_pcs = self.ordered_pitch_classes();
419 let Ok(address) = tables::seek_chord_tables_address(&ordered_pcs) else {
420 return Vec::new();
421 };
422 tables::address_to_common_names(address)
423 .ok()
424 .flatten()
425 .unwrap_or_default()
426 .into_iter()
427 .map(str::to_string)
428 .collect()
429 }
430
431 pub fn root_pitch_name(&self) -> Option<String> {
436 self.root_pitch_name_from_tables()
437 }
438
439 pub fn bass_pitch_name(&self) -> Option<String> {
443 self.bass_pitch().map(Pitch::name)
444 }
445
446 pub(super) fn root_pitch_name_from_tables(&self) -> Option<String> {
447 self.find_root_pitch().map(Pitch::name)
448 }
449
450 pub(super) fn common_names_with_primary(&self) -> Vec<String> {
451 let mut names = vec![self.common_name()];
452 names.extend(self.common_names());
453 names.sort();
454 names.dedup();
455 names
456 }
457
458 pub(super) fn interval_nice_name(start: &Pitch, end: &Pitch) -> Option<String> {
459 Interval::between(start.clone(), end.clone())
460 .ok()
461 .map(|interval| interval.nice_name())
462 }
463
464 pub(super) fn display_key_name(key: &Key) -> String {
465 format!("{} {}", key.tonic().name(), key.mode())
466 }
467
468 pub fn full_name(&self) -> String {
472 let pitches = self
473 .notes
474 .iter()
475 .map(|note| note.pitch.full_name())
476 .collect::<Vec<_>>()
477 .join(" | ");
478 let duration = self
479 .duration
480 .clone()
481 .unwrap_or_else(Duration::quarter)
482 .full_name();
483 format!("Chord {{{pitches}}} {duration}")
484 }
485}
486
487#[derive(Debug, Clone, PartialEq)]
488#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
489pub struct KnownChordType {
491 pub cardinality: u8,
493 pub common_names: Vec<String>,
495 pub forte_class: String,
497 pub normal_form: Vec<u8>,
499 pub interval_class_vector: Vec<u8>,
501}
502
503#[cfg(test)]
504mod augmented_sixth_voicings {
505 use crate::{Chord, defaults::IntegerType};
506
507 const EXPECTED: &str = "\
511A#3 C#4 D#4 F#4 minor seventh chord
512A#3 C#4 E#4 F#4 major seventh chord
513A#3 C#4 E#4 G#4 minor seventh chord
514A#3 C#4 G4 enharmonic equivalent to diminished triad
515A#3 C4 E4 F#4 French augmented sixth chord in first inversion
516A- C D F# French augmented sixth chord in third inversion
517A- C E- F# German augmented sixth chord in second inversion
518A- C F# Italian augmented sixth chord in second inversion
519A-3 C-4 E-4 F-4 major seventh chord
520A-3 C-4 E-4 G-4 minor seventh chord
521A-3 C4 D4 F#4 French augmented sixth chord
522A-3 C4 E-4 F#4 German augmented sixth chord
523A-3 C4 F#4 Italian augmented sixth chord
524A3 B3 E-4 F#4 enharmonic to dominant seventh chord
525A3 C#4 E-4 G4 French augmented sixth chord in root position
526A3 E-4 F#4 enharmonic equivalent to diminished triad
527B-3 C-4 E-4 G-4 major seventh chord
528B-3 D4 E4 G#4 French augmented sixth chord
529B3 C#4 E#4 G4 French augmented sixth chord in third inversion
530B3 E-4 F#4 A4 enharmonic to dominant seventh chord
531C D F# A- French augmented sixth chord in third inversion
532C E F# A# French augmented sixth chord
533C# D# F# A# minor seventh chord
534C# E# F# A# major seventh chord
535C# E# G B French augmented sixth chord in root position
536C# E# G# A# minor seventh chord
537C# E- G Italian augmented sixth chord in root position
538C# E- G A French augmented sixth chord in first inversion
539C# G A# enharmonic equivalent to diminished triad
540C#3 D#3 F#3 A#3 minor seventh chord
541C#3 E#3 F#3 A#3 major seventh chord
542C#3 E#3 G#3 A#3 minor seventh chord
543C#3 E#3 G3 B3 French augmented sixth chord in root position
544C#3 E-3 G3 Italian augmented sixth chord in root position
545C#3 E-3 G3 A3 French augmented sixth chord in first inversion
546C#3 G3 A#3 enharmonic equivalent to diminished triad
547C-3 E-3 F-3 A-3 major seventh chord
548C-3 E-3 G-3 A-3 minor seventh chord
549C-3 E-3 G-3 B-3 major seventh chord
550C3 D3 F#3 A-3 French augmented sixth chord in third inversion
551C3 E-3 F#3 A-3 German augmented sixth chord in second inversion
552C3 E3 F#3 A#3 French augmented sixth chord
553C3 F#3 A-3 Italian augmented sixth chord in second inversion
554D E G# B- French augmented sixth chord in third inversion
555D#3 F#3 A#3 C#4 minor seventh chord
556D3 E3 G#3 B-3 French augmented sixth chord in third inversion
557D3 F#3 A-3 C4 French augmented sixth chord in root position
558E#3 F#3 A#3 C#4 major seventh chord
559E#3 G#3 A#3 C#4 minor seventh chord
560E#3 G3 B3 C#4 French augmented sixth chord in first inversion
561E- F# A enharmonic equivalent to diminished triad
562E- F# A B enharmonic to dominant seventh chord
563E- F- A- C- major seventh chord
564E- G- A- C- minor seventh chord
565E- G- B- C- major seventh chord
566E-3 F#3 A-3 C4 German augmented sixth chord in third inversion
567E-3 F#3 A3 enharmonic equivalent to diminished triad
568E-3 F#3 A3 B3 enharmonic to dominant seventh chord
569E-3 F-3 A-3 C-4 major seventh chord
570E-3 G-3 A-3 C-4 minor seventh chord
571E-3 G-3 B-3 C-4 major seventh chord
572E-3 G3 A3 C#4 French augmented sixth chord
573E-3 G3 C#4 Italian augmented sixth chord
574E3 F#3 A#3 C4 French augmented sixth chord in third inversion
575E3 G#3 B-3 D4 French augmented sixth chord in root position
576F#3 A#3 C#4 D#4 minor seventh chord
577F#3 A#3 C#4 E#4 major seventh chord
578F#3 A#3 C4 E4 French augmented sixth chord in root position
579F#3 A-3 C4 Italian augmented sixth chord in root position
580F#3 A-3 C4 D4 French augmented sixth chord in first inversion
581F#3 A-3 C4 E-4 German augmented sixth chord in root position
582F#3 A3 B3 E-4 enharmonic to dominant seventh chord
583F#3 A3 E-4 enharmonic equivalent to diminished triad
584F-3 A-3 C-4 E-4 major seventh chord
585G#3 A#3 C#4 E#4 minor seventh chord
586G#3 B-3 D4 E4 French augmented sixth chord in first inversion
587G-3 A-3 C-4 E-4 minor seventh chord
588G-3 B-3 C-4 E-4 major seventh chord
589G3 A#3 C#4 enharmonic equivalent to diminished triad
590G3 A3 C#4 E-4 French augmented sixth chord in third inversion
591G3 B3 C#4 E#4 French augmented sixth chord
592G3 C#4 E-4 Italian augmented sixth chord in second inversion
593[0, 2, 6, 8] French augmented sixth chord in third inversion
594[1, 3, 6, 10] enharmonic equivalent to minor seventh chord
595[1, 3, 7] Italian augmented sixth chord in root position
596[1, 5, 6, 10] enharmonic equivalent to major seventh chord
597[1, 5, 8, 10] enharmonic equivalent to minor seventh chord
598[1, 7, 10] enharmonic equivalent to diminished triad
599[3, 4, 8, 11] enharmonic equivalent to major seventh chord
600[3, 6, 10, 11] enharmonic equivalent to major seventh chord
601[3, 6, 8, 11] enharmonic equivalent to minor seventh chord
602[3, 6, 9] enharmonic equivalent to diminished triad
603";
604
605 #[test]
609 fn pitched_names_read_the_root_music21_reads() {
610 let cases = [
611 (
612 "C# E- G",
613 "C#-Italian augmented sixth chord in root position",
614 ),
615 (
616 "G4 C#5 E-5",
617 "C#-Italian augmented sixth chord in second inversion",
618 ),
619 ("E-4 G4 C#5", "C#-Italian augmented sixth chord"),
620 (
621 "C D F# A-",
622 "D-French augmented sixth chord in third inversion",
623 ),
624 ("A-4 C5 D5 F#5", "D-French augmented sixth chord"),
625 (
626 "F#4 A-4 C5 D5",
627 "D-French augmented sixth chord in first inversion",
628 ),
629 (
630 "D4 F#4 A-4 C5",
631 "D-French augmented sixth chord in root position",
632 ),
633 (
634 "C# E- G A",
635 "A-French augmented sixth chord in first inversion",
636 ),
637 (
638 "A4 C#5 E-5 G5",
639 "A-French augmented sixth chord in root position",
640 ),
641 ("C E F# A#", "F#-French augmented sixth chord"),
642 (
643 "A#3 C4 E4 F#4",
644 "F#-French augmented sixth chord in first inversion",
645 ),
646 (
647 "C# E# G B",
648 "C#-French augmented sixth chord in root position",
649 ),
650 (
651 "B4 C#5 E#5 G5",
652 "C#-French augmented sixth chord in third inversion",
653 ),
654 (
655 "D E G# B-",
656 "E-French augmented sixth chord in third inversion",
657 ),
658 ];
659 for (notes, expected) in cases {
660 assert_eq!(
661 Chord::new(notes).unwrap().pitched_common_name(),
662 expected,
663 "{notes}"
664 );
665 }
666
667 let mut rooted = Chord::new("F# A C# E#").unwrap();
668 rooted.set_root(Some("A".parse().unwrap()));
669 assert_eq!(rooted.pitched_common_name(), "A-minor-augmented tetrachord");
670
671 let mut added = Chord::new("C E G B#").unwrap();
672 added.set_bass(Some("E".parse().unwrap()));
673 assert_eq!(
674 added.pitched_common_name(),
675 "enharmonic equivalent to major triad above E"
676 );
677 }
678
679 #[test]
680 fn every_voicing_is_named_as_music21_names_it() {
681 let mut wrong = Vec::new();
682 for line in EXPECTED.lines() {
683 let (notes, expected) = line.split_once('\t').unwrap();
684 let chord = if let Some(numbers) = notes.strip_prefix('[') {
685 let classes: Vec<IntegerType> = numbers
686 .trim_end_matches(']')
687 .split(", ")
688 .map(|number| number.parse().unwrap())
689 .collect();
690 Chord::new(classes.as_slice()).unwrap()
691 } else {
692 Chord::new(notes).unwrap()
693 };
694 let named = chord.common_name();
695 if named != expected {
696 wrong.push(format!("{notes}: {named}, music21 {expected}"));
697 }
698 }
699 assert!(
700 wrong.is_empty(),
701 "{} of {}:\n{}",
702 wrong.len(),
703 EXPECTED.lines().count(),
704 wrong.join("\n")
705 );
706 }
707}