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_name) = self.bass_pitch_name() {
109 return format!("{name_str} above {bass_name}");
110 }
111 return name_str.to_string();
112 }
113
114 if let Some(root_name) = self.spelling_root_name_override(name_str) {
115 return format!("{root_name}-{name_str}");
116 }
117
118 let root_name = self.root_pitch_name_from_tables().or_else(|| {
119 self.notes
120 .first()
121 .map(|n| Self::display_pitch_name(&n.pitch))
122 });
123
124 match root_name {
125 Some(root_name) => format!("{root_name}-{name_str}"),
126 None => name_str.to_string(),
127 }
128 }
129
130 pub(super) fn spelling_root_name_override(&self, common_name: &str) -> Option<String> {
131 if !common_name.contains("augmented sixth chord") {
132 return None;
133 }
134 let names = self.unique_pitch_names();
135 let root = if self.names_are(&names, &["C#", "E-", "G"])
136 || self.names_are(&names, &["C#", "E#", "G", "B"])
137 {
138 "C#"
139 } else if self.names_are(&names, &["C", "D", "F#", "A-"]) {
140 "D"
141 } else if self.names_are(&names, &["C#", "E-", "G", "A"]) {
142 "A"
143 } else if self.names_are(&names, &["C", "E", "F#", "A#"]) {
144 "F#"
145 } else if self.names_are(&names, &["D", "E", "G#", "B-"])
146 || (self.from_integer_pitches && self.pitch_class_mask() == 0b010100010100)
147 {
148 "E"
149 } else {
150 return None;
151 };
152
153 Some(root.to_string())
154 }
155
156 pub(super) fn chord_symbol_root_pitch_class(root: PitchClassSpecifier) -> Result<u8> {
157 match root {
158 PitchClassSpecifier::String(value) => match Pitch::from_name(value.as_str()) {
159 Ok(pitch) => Self::integer_pitch_class_for_chord_symbol_root(pitch.ps()),
160 Err(pitch_error) => {
161 let pitch_class = PitchClass::new(value.as_str()).map_err(|pitch_class_error| {
162 Error::Chord(format!(
163 "cannot parse chord-symbol root {value:?} as a pitch name ({pitch_error}) or pitch class ({pitch_class_error})"
164 ))
165 })?;
166 Self::integer_pitch_class_from_value(pitch_class)
167 }
168 },
169 specifier => {
170 let pitch_class = PitchClass::new(specifier)?;
171 Self::integer_pitch_class_from_value(pitch_class)
172 }
173 }
174 }
175
176 pub(super) fn integer_pitch_class_from_value(pitch_class: PitchClass) -> Result<u8> {
177 let Some(root) = pitch_class.integer() else {
178 return Err(Error::Chord(
179 "chord symbols require an integer pitch-class root".to_string(),
180 ));
181 };
182 Ok(root as u8)
183 }
184
185 pub(super) fn integer_pitch_class_for_chord_symbol_root(ps: FloatType) -> Result<u8> {
186 if (ps - ps.round()).abs() > FloatType::EPSILON {
187 return Err(Error::Chord(
188 "chord symbols require an integer pitch-class root".to_string(),
189 ));
190 }
191
192 Ok((ps.round() as IntegerType).rem_euclid(12) as u8)
193 }
194
195 pub fn common_name(&self) -> String {
200 if self
201 .notes
202 .iter()
203 .any(|n| (n.pitch.alter() - n.pitch.alter().round()).abs() > FloatType::EPSILON)
204 {
205 return "microtonal chord".to_string();
206 }
207
208 if self.notes.is_empty() {
209 return "empty chord".to_string();
210 }
211
212 let ordered_pcs = self.ordered_pitch_classes();
213 if ordered_pcs.is_empty() {
214 return "empty chord".to_string();
215 }
216
217 if ordered_pcs.len() == 1 {
218 if self.notes.len() == 1 {
219 return "note".to_string();
220 }
221
222 let pitch_names = self
223 .notes
224 .iter()
225 .map(|n| n.pitch.name())
226 .collect::<std::collections::BTreeSet<_>>();
227
228 let pitch_pses = self
229 .notes
230 .iter()
231 .map(|n| n.pitch.ps().round() as IntegerType)
232 .collect::<std::collections::BTreeSet<_>>();
233
234 if pitch_names.len() == 1 {
235 if pitch_pses.len() == 1 {
236 return "unison".to_string();
237 }
238 if pitch_pses.len() == 2 {
239 return Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
240 .unwrap_or_else(|| "multiple octaves".to_string());
241 }
242 return "multiple octaves".to_string();
243 }
244 if pitch_pses.len() == 1 {
245 return "enharmonic unison".to_string();
246 }
247 return "enharmonic octaves".to_string();
248 }
249
250 if ordered_pcs.len() == 2 {
251 return self.dyad_common_name();
252 }
253
254 if let Some(common_name) = self.spelling_common_name_override() {
255 return common_name;
256 }
257
258 let address = match tables::seek_chord_tables_address(&ordered_pcs) {
259 Ok(address) => address,
260 Err(_) => return "unknown chord".to_string(),
261 };
262
263 let common_names: Vec<String> = match tables::address_to_common_names(address) {
264 Ok(Some(names)) => names.iter().map(|name| name.to_string()).collect(),
265 _ => Vec::new(),
266 };
267 let forte_name = tables::address_to_forte_name(address, "tn").ok();
268
269 if let Some(forte_name) = forte_name.as_deref() {
270 if let Some(name) = self.augmented_sixth_common_name(forte_name, &common_names) {
271 return name;
272 }
273 if let Some(first) = common_names.first() {
274 if matches!(forte_name, "4-20" | "4-26") {
275 return if self.is_seventh_with_perfect_fifths_above_root_and_third() {
276 first.clone()
277 } else {
278 format!("enharmonic equivalent to {first}")
279 };
280 }
281 if let Some(spelled) = self.spelled_as_named(forte_name) {
282 return if spelled {
283 first.clone()
284 } else {
285 format!("enharmonic equivalent to {first}")
286 };
287 }
288 }
289 }
290
291 match common_names.first() {
292 Some(name) => name.clone(),
293 None => match forte_name {
294 Some(forte_name) => format!("forte class {forte_name}"),
295 None => "unknown chord".to_string(),
296 },
297 }
298 }
299
300 pub(super) fn augmented_sixth_common_name(
304 &self,
305 forte_name: &str,
306 common_names: &[String],
307 ) -> Option<String> {
308 let named = |index: usize| common_names.get(index).cloned();
309 let in_inversion = |index: usize| {
310 named(index).map(|name| format!("{name} in {}", self.inversion_text().to_lowercase()))
311 };
312 match forte_name {
313 "4-27B" => {
314 if self.is_dominant_seventh() {
315 named(0)
316 } else if self.is_german_augmented_sixth(false) {
317 named(2)
318 } else if self.is_german_augmented_sixth(true) {
319 in_inversion(2)
320 } else if self.is_swiss_augmented_sixth(false) {
321 named(3)
322 } else if self.is_swiss_augmented_sixth(true) {
323 in_inversion(3)
324 } else {
325 named(0).map(|name| format!("enharmonic to {name}"))
326 }
327 }
328 "4-25" => {
329 if self.is_french_augmented_sixth(false) {
330 named(1)
331 } else if self.is_french_augmented_sixth(true) {
332 in_inversion(1)
333 } else {
334 named(0)
335 }
336 }
337 "3-8A" => {
338 if self.is_italian_augmented_sixth(false, false) {
339 named(1)
340 } else if self.is_italian_augmented_sixth(true, false) {
341 in_inversion(1)
342 } else {
343 named(0)
344 }
345 }
346 _ => None,
347 }
348 }
349
350 pub(super) fn spelled_as_named(&self, forte_name: &str) -> Option<bool> {
354 match forte_name {
355 "3-11A" => Some(self.is_minor_triad()),
356 "3-11B" => Some(self.is_major_triad()),
357 "3-10" => Some(self.is_diminished_triad()),
358 "3-12" => Some(self.is_augmented_triad()),
359 "4-27A" => Some(self.is_half_diminished_seventh()),
360 "4-28" => Some(self.is_diminished_seventh()),
361 "5-27A" | "5-27B" | "5-34" => Some(self.is_ninth()),
362 _ => None,
363 }
364 }
365
366 pub(super) fn is_seventh_with_perfect_fifths_above_root_and_third(&self) -> bool {
370 if !self.is_seventh() {
371 return false;
372 }
373 let names = self.pitch_names();
374 let has_fifth_above = |pitch: &Pitch| {
375 PERFECT_FIFTH
376 .transpose_pitch(pitch)
377 .is_ok_and(|above| names.contains(&above.name()))
378 };
379 let (Some(root), Some(third)) = (self.root(), self.third()) else {
380 return false;
381 };
382 has_fifth_above(root) && has_fifth_above(third)
383 }
384
385 pub(super) fn spelling_common_name_override(&self) -> Option<String> {
386 let names = self.unique_pitch_names();
387 let name = if self.names_are(&names, &["C#", "E-", "G"]) {
388 "Italian augmented sixth chord in root position"
389 } else if self.names_are(&names, &["C", "D", "F#", "A-"])
390 || self.names_are(&names, &["D", "E", "G#", "B-"])
391 || (self.from_integer_pitches && self.pitch_class_mask() == 0b010100010100)
392 {
393 "French augmented sixth chord in third inversion"
394 } else if self.names_are(&names, &["C#", "E-", "G", "A"]) {
395 "French augmented sixth chord in first inversion"
396 } else if self.names_are(&names, &["C", "E", "F#", "A#"]) {
397 "French augmented sixth chord"
398 } else if self.names_are(&names, &["C#", "E#", "G", "B"]) {
399 "French augmented sixth chord in root position"
400 } else if self.names_are(&names, &["E-", "F#", "A"])
401 || self.names_are(&names, &["C#", "G", "A#"])
402 || (self.from_integer_pitches && self.pitch_class_mask() == 0b001001001000)
403 {
404 "enharmonic equivalent to diminished triad"
405 } else if self.from_integer_pitches
406 && (self.names_are(&names, &["C#", "D#", "F#", "A#"])
407 || self.names_are(&names, &["C#", "E#", "G#", "A#"])
408 || self.names_are(&names, &["E-", "G-", "A-", "C-"]))
409 {
410 "enharmonic equivalent to minor seventh chord"
414 } else if self.from_integer_pitches
415 && (self.names_are(&names, &["C#", "E#", "F#", "A#"])
416 || self.names_are(&names, &["E-", "F-", "A-", "C-"])
417 || self.names_are(&names, &["E-", "G-", "B-", "C-"]))
418 {
419 "enharmonic equivalent to major seventh chord"
420 } else if self.names_are(&names, &["E-", "F#", "A", "B"]) {
421 "enharmonic to dominant seventh chord"
422 } else {
423 return None;
424 };
425
426 Some(name.to_string())
427 }
428
429 pub(super) fn dyad_common_name(&self) -> String {
430 let pitch_names = self
431 .notes
432 .iter()
433 .map(|n| n.pitch.name())
434 .collect::<std::collections::BTreeSet<_>>();
435
436 let pitch_pses = self
437 .notes
438 .iter()
439 .map(|n| n.pitch.ps().round() as IntegerType)
440 .collect::<std::collections::BTreeSet<_>>();
441
442 let Some(p0) = self.notes.first().map(|n| &n.pitch) else {
443 return "empty chord".to_string();
444 };
445 let p0_pitch_class = root::pitch_class(p0);
446
447 let Some(p1) = self
448 .notes
449 .iter()
450 .skip(1)
451 .find(|n| root::pitch_class(&n.pitch) != p0_pitch_class)
452 .map(|n| &n.pitch)
453 else {
454 return "unknown chord".to_string();
455 };
456
457 let relevant_interval = Interval::between(
458 PitchOrNote::Pitch(p0.clone()),
459 PitchOrNote::Pitch(p1.clone()),
460 );
461
462 if pitch_names.len() > 2 {
463 let Ok(interval) = relevant_interval else {
464 return "unknown chord".to_string();
465 };
466 let semitones = interval.chromatic.simple_undirected();
467 let plural = if semitones == 1 { "" } else { "s" };
468 return format!("{semitones} semitone{plural}");
469 }
470
471 if pitch_pses.len() > 2 {
472 return relevant_interval
473 .map(|interval| {
474 format!("{} with octave doublings", interval.semi_simple_nice_name())
475 })
476 .unwrap_or_else(|_| "unknown chord".to_string());
477 }
478
479 Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
480 .unwrap_or_else(|| "unknown chord".to_string())
481 }
482
483 pub fn common_names(&self) -> Vec<String> {
485 let ordered_pcs = self.ordered_pitch_classes();
486 let Ok(address) = tables::seek_chord_tables_address(&ordered_pcs) else {
487 return Vec::new();
488 };
489 tables::address_to_common_names(address)
490 .ok()
491 .flatten()
492 .unwrap_or_default()
493 .into_iter()
494 .map(str::to_string)
495 .collect()
496 }
497
498 pub fn root_pitch_name(&self) -> Option<String> {
503 self.root_pitch_name_from_tables()
504 }
505
506 pub fn bass_pitch_name(&self) -> Option<String> {
510 self.bass_pitch().map(Self::display_pitch_name)
511 }
512
513 pub(super) fn root_pitch_name_from_tables(&self) -> Option<String> {
514 self.find_root_pitch().map(Self::display_pitch_name)
515 }
516
517 pub(super) fn common_names_with_primary(&self) -> Vec<String> {
518 let mut names = vec![self.common_name()];
519 names.extend(self.common_names());
520 names.sort();
521 names.dedup();
522 names
523 }
524
525 pub(super) fn pitch_class_name(pc: u8) -> &'static str {
526 CANDIDATE_TONICS[pc as usize % 12]
527 }
528
529 pub(super) fn names_are(
536 &self,
537 names: &std::collections::BTreeSet<String>,
538 expected: &[&str],
539 ) -> bool {
540 self.notes.len() == expected.len() && expected.iter().all(|name| names.contains(*name))
541 }
542
543 pub(super) fn interval_nice_name(start: &Pitch, end: &Pitch) -> Option<String> {
544 Interval::between(
545 PitchOrNote::Pitch(start.clone()),
546 PitchOrNote::Pitch(end.clone()),
547 )
548 .ok()
549 .map(|interval| interval.nice_name())
550 }
551
552 pub(super) fn display_pitch_name(pitch: &Pitch) -> String {
553 pitch.name().replace('-', "b")
554 }
555
556 pub(super) fn display_key_name(key: &Key) -> String {
557 format!(
558 "{} {}",
559 Self::display_tonic_name(&key.tonic().name()),
560 key.mode()
561 )
562 }
563
564 pub(super) fn display_tonic_name(name: &str) -> String {
565 name.replace('-', "b")
566 }
567
568 pub fn full_name(&self) -> String {
572 let pitches = self
573 .notes
574 .iter()
575 .map(|note| note.pitch.full_name())
576 .collect::<Vec<_>>()
577 .join(" | ");
578 let duration = self
579 .duration
580 .clone()
581 .unwrap_or_else(Duration::quarter)
582 .full_name();
583 format!("Chord {{{pitches}}} {duration}")
584 }
585}
586
587#[derive(Debug, Clone, PartialEq)]
588#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
589pub struct KnownChordType {
591 pub cardinality: u8,
593 pub common_names: Vec<String>,
595 pub forte_class: String,
597 pub normal_form: Vec<u8>,
599 pub interval_class_vector: Vec<u8>,
601}