1use std::str::FromStr;
2
3use crate::{
4 chord::Chord,
5 defaults::{FloatType, IntegerType},
6 error::{Error, Result},
7 interval::Interval,
8 pitch::Pitch,
9};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum ChordQuality {
16 Major,
18 Minor,
20 Dominant,
22 Diminished,
24 Augmented,
26 HalfDiminished,
28 Suspended2,
30 Suspended4,
32 Power,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct ChordAlteration {
40 degree: u8,
41 semitones: IntegerType,
42}
43
44impl ChordAlteration {
45 pub fn new(degree: u8, semitones: IntegerType) -> Self {
47 Self { degree, semitones }
48 }
49
50 pub fn degree(&self) -> u8 {
52 self.degree
53 }
54
55 pub fn semitones(&self) -> IntegerType {
57 self.semitones
58 }
59}
60
61#[derive(Clone, Debug, PartialEq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct ChordSymbol {
65 figure: String,
66 root: Pitch,
67 bass: Option<Pitch>,
68 quality: ChordQuality,
69 extensions: Vec<u8>,
70 alterations: Vec<ChordAlteration>,
71 #[cfg_attr(feature = "serde", serde(default))]
72 omissions: Vec<u8>,
73 #[cfg_attr(feature = "serde", serde(default))]
74 additions: Vec<ChordAlteration>,
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78struct Music21ChordType {
79 kind: &'static str,
80 notation: &'static str,
81 abbreviation: &'static str,
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85struct Music21Degree {
86 degree: u8,
87 semitone: u8,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91struct Music21FigureMatch {
92 kind: &'static str,
93 notation: &'static str,
94 abbreviation: &'static str,
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98struct Music21ChordAnalysis {
99 d3: Option<u8>,
100 d5: Option<u8>,
101 d7: Option<u8>,
102 d9: Option<u8>,
103 d11: Option<u8>,
104 d13: Option<u8>,
105 is_triad: bool,
106 is_seventh: bool,
107}
108
109const MUSIC21_CHORD_TYPES: &[Music21ChordType] = &[
110 Music21ChordType {
111 kind: "major",
112 notation: "1,3,5",
113 abbreviation: "",
114 },
115 Music21ChordType {
116 kind: "minor",
117 notation: "1,-3,5",
118 abbreviation: "m",
119 },
120 Music21ChordType {
121 kind: "augmented",
122 notation: "1,3,#5",
123 abbreviation: "+",
124 },
125 Music21ChordType {
126 kind: "diminished",
127 notation: "1,-3,-5",
128 abbreviation: "dim",
129 },
130 Music21ChordType {
131 kind: "dominant-seventh",
132 notation: "1,3,5,-7",
133 abbreviation: "7",
134 },
135 Music21ChordType {
136 kind: "major-seventh",
137 notation: "1,3,5,7",
138 abbreviation: "maj7",
139 },
140 Music21ChordType {
141 kind: "minor-major-seventh",
142 notation: "1,-3,5,7",
143 abbreviation: "mM7",
144 },
145 Music21ChordType {
146 kind: "minor-seventh",
147 notation: "1,-3,5,-7",
148 abbreviation: "m7",
149 },
150 Music21ChordType {
151 kind: "augmented-major-seventh",
152 notation: "1,3,#5,7",
153 abbreviation: "+M7",
154 },
155 Music21ChordType {
156 kind: "augmented-seventh",
157 notation: "1,3,#5,-7",
158 abbreviation: "7+",
159 },
160 Music21ChordType {
161 kind: "half-diminished-seventh",
162 notation: "1,-3,-5,-7",
163 abbreviation: "\u{00f8}7",
164 },
165 Music21ChordType {
166 kind: "diminished-seventh",
167 notation: "1,-3,-5,--7",
168 abbreviation: "o7",
169 },
170 Music21ChordType {
171 kind: "seventh-flat-five",
172 notation: "1,3,-5,-7",
173 abbreviation: "dom7dim5",
174 },
175 Music21ChordType {
176 kind: "major-sixth",
177 notation: "1,3,5,6",
178 abbreviation: "6",
179 },
180 Music21ChordType {
181 kind: "minor-sixth",
182 notation: "1,-3,5,6",
183 abbreviation: "m6",
184 },
185 Music21ChordType {
186 kind: "major-ninth",
187 notation: "1,3,5,7,9",
188 abbreviation: "M9",
189 },
190 Music21ChordType {
191 kind: "dominant-ninth",
192 notation: "1,3,5,-7,9",
193 abbreviation: "9",
194 },
195 Music21ChordType {
196 kind: "minor-major-ninth",
197 notation: "1,-3,5,7,9",
198 abbreviation: "mM9",
199 },
200 Music21ChordType {
201 kind: "minor-ninth",
202 notation: "1,-3,5,-7,9",
203 abbreviation: "m9",
204 },
205 Music21ChordType {
206 kind: "augmented-major-ninth",
207 notation: "1,3,#5,7,9",
208 abbreviation: "+M9",
209 },
210 Music21ChordType {
211 kind: "augmented-dominant-ninth",
212 notation: "1,3,#5,-7,9",
213 abbreviation: "9#5",
214 },
215 Music21ChordType {
216 kind: "half-diminished-ninth",
217 notation: "1,-3,-5,-7,9",
218 abbreviation: "\u{00f8}9",
219 },
220 Music21ChordType {
221 kind: "half-diminished-minor-ninth",
222 notation: "1,-3,-5,-7,-9",
223 abbreviation: "\u{00f8}b9",
224 },
225 Music21ChordType {
226 kind: "diminished-ninth",
227 notation: "1,-3,-5,--7,9",
228 abbreviation: "o9",
229 },
230 Music21ChordType {
231 kind: "diminished-minor-ninth",
232 notation: "1,-3,-5,--7,-9",
233 abbreviation: "ob9",
234 },
235 Music21ChordType {
236 kind: "dominant-11th",
237 notation: "1,3,5,-7,9,11",
238 abbreviation: "11",
239 },
240 Music21ChordType {
241 kind: "major-11th",
242 notation: "1,3,5,7,9,11",
243 abbreviation: "M11",
244 },
245 Music21ChordType {
246 kind: "minor-major-11th",
247 notation: "1,-3,5,7,9,11",
248 abbreviation: "mM11",
249 },
250 Music21ChordType {
251 kind: "minor-11th",
252 notation: "1,-3,5,-7,9,11",
253 abbreviation: "m11",
254 },
255 Music21ChordType {
256 kind: "augmented-major-11th",
257 notation: "1,3,#5,7,9,11",
258 abbreviation: "+M11",
259 },
260 Music21ChordType {
261 kind: "augmented-11th",
262 notation: "1,3,#5,-7,9,11",
263 abbreviation: "+11",
264 },
265 Music21ChordType {
266 kind: "half-diminished-11th",
267 notation: "1,-3,-5,-7,9,11",
268 abbreviation: "\u{00f8}11",
269 },
270 Music21ChordType {
271 kind: "diminished-11th",
272 notation: "1,-3,-5,--7,9,11",
273 abbreviation: "o11",
274 },
275 Music21ChordType {
276 kind: "major-13th",
277 notation: "1,3,5,7,9,11,13",
278 abbreviation: "M13",
279 },
280 Music21ChordType {
281 kind: "dominant-13th",
282 notation: "1,3,5,-7,9,11,13",
283 abbreviation: "13",
284 },
285 Music21ChordType {
286 kind: "minor-major-13th",
287 notation: "1,-3,5,7,9,11,13",
288 abbreviation: "mM13",
289 },
290 Music21ChordType {
291 kind: "minor-13th",
292 notation: "1,-3,5,-7,9,11,13",
293 abbreviation: "m13",
294 },
295 Music21ChordType {
296 kind: "augmented-major-13th",
297 notation: "1,3,#5,7,9,11,13",
298 abbreviation: "+M13",
299 },
300 Music21ChordType {
301 kind: "augmented-dominant-13th",
302 notation: "1,3,#5,-7,9,11,13",
303 abbreviation: "+13",
304 },
305 Music21ChordType {
306 kind: "half-diminished-13th",
307 notation: "1,-3,-5,-7,9,11,13",
308 abbreviation: "\u{00f8}13",
309 },
310 Music21ChordType {
311 kind: "suspended-second",
312 notation: "1,2,5",
313 abbreviation: "sus2",
314 },
315 Music21ChordType {
316 kind: "suspended-fourth",
317 notation: "1,4,5",
318 abbreviation: "sus",
319 },
320 Music21ChordType {
321 kind: "suspended-fourth-seventh",
322 notation: "1,4,5,-7",
323 abbreviation: "7sus",
324 },
325 Music21ChordType {
326 kind: "Neapolitan",
327 notation: "1,-2,3,-5",
328 abbreviation: "N6",
329 },
330 Music21ChordType {
331 kind: "Italian",
332 notation: "1,#4,-6",
333 abbreviation: "It+6",
334 },
335 Music21ChordType {
336 kind: "French",
337 notation: "1,2,#4,-6",
338 abbreviation: "Fr+6",
339 },
340 Music21ChordType {
341 kind: "German",
342 notation: "1,-3,#4,-6",
343 abbreviation: "Gr+6",
344 },
345 Music21ChordType {
346 kind: "pedal",
347 notation: "1",
348 abbreviation: "pedal",
349 },
350 Music21ChordType {
351 kind: "power",
352 notation: "1,5",
353 abbreviation: "power",
354 },
355 Music21ChordType {
356 kind: "Tristan",
357 notation: "1,#4,#6,#9",
358 abbreviation: "tristan",
359 },
360];
361
362impl ChordSymbol {
363 pub fn parse(figure: impl Into<String>) -> Result<Self> {
365 let figure = figure.into();
366 let trimmed = figure.trim();
367 if trimmed.is_empty() {
368 return Err(Error::Chord("chord symbol cannot be empty".to_string()));
369 }
370
371 let (body, bass_segment) = match trimmed.split_once('/') {
372 Some((body, bass)) => (body, Some(bass)),
373 None => (trimmed, None),
374 };
375 let body_parts = split_music21_pitch_modifiers(body);
376 let bass_parts = bass_segment.map(split_music21_pitch_modifiers);
377 let bass = bass_parts
378 .as_ref()
379 .map(|parts| parse_pitch_only(&parts.base))
380 .transpose()?;
381
382 let (root_name, suffix) = parse_pitch_prefix(&body_parts.base)?;
383 let root = Pitch::from_name(root_name)?;
384 let suffix_without_additions = strip_addition_groups(suffix);
385 let mut additions = parse_additions(suffix);
386 let mut omissions = parse_omissions(suffix);
387 for pitch_name in body_parts
388 .additions
389 .iter()
390 .chain(bass_parts.iter().flat_map(|parts| parts.additions.iter()))
391 {
392 if let Some(addition) = pitch_name_addition(&root, pitch_name) {
393 additions.push(addition);
394 }
395 }
396 for pitch_name in body_parts
397 .omissions
398 .iter()
399 .chain(bass_parts.iter().flat_map(|parts| parts.omissions.iter()))
400 {
401 if let Some(omission) = pitch_name_degree(&root, pitch_name)
402 && !omissions.contains(&omission)
403 {
404 omissions.push(omission);
405 }
406 }
407
408 let mut alterations = parse_alterations(&suffix_without_additions);
409 add_implicit_music21_alterations(&suffix_without_additions, &mut alterations);
410 let extensions = parse_extensions(&suffix_without_additions, &alterations);
411 let quality = parse_quality(&suffix_without_additions, &alterations);
412
413 Ok(Self {
414 figure: trimmed.to_string(),
415 root,
416 bass,
417 quality,
418 extensions,
419 alterations,
420 omissions,
421 additions,
422 })
423 }
424
425 pub fn figure(&self) -> &str {
427 &self.figure
428 }
429
430 pub fn root(&self) -> &Pitch {
432 &self.root
433 }
434
435 pub fn bass(&self) -> Option<&Pitch> {
437 self.bass.as_ref()
438 }
439
440 pub fn quality(&self) -> ChordQuality {
442 self.quality
443 }
444
445 pub fn extensions(&self) -> &[u8] {
447 &self.extensions
448 }
449
450 pub fn alterations(&self) -> &[ChordAlteration] {
452 &self.alterations
453 }
454
455 pub fn omissions(&self) -> &[u8] {
457 &self.omissions
458 }
459
460 pub fn additions(&self) -> &[ChordAlteration] {
462 &self.additions
463 }
464
465 pub fn to_chord(&self) -> Result<Chord> {
467 let mut interval_names = self.base_intervals();
468
469 for extension in [6, 9, 11, 13] {
470 if self.extensions.contains(&extension)
471 && !self.alterations.iter().any(|alt| alt.degree == extension)
472 {
473 interval_names.push(default_extension_interval(extension));
474 }
475 }
476
477 for alteration in &self.alterations {
478 if alteration.degree == 5 && matches!(self.quality, ChordQuality::HalfDiminished) {
479 continue;
480 }
481 if alteration.degree == 5 {
482 continue;
483 }
484 interval_names.push(altered_interval(alteration)?);
485 }
486
487 for addition in &self.additions {
488 interval_names.push(added_interval(addition)?);
489 }
490
491 interval_names.sort_unstable_by_key(|name| interval_sort_key(name));
492 interval_names.dedup();
493
494 let mut pitches = interval_names
495 .into_iter()
496 .map(|name| Interval::from_name(name)?.transpose_pitch(&self.root))
497 .collect::<Result<Vec<_>>>()?;
498
499 if let Some(bass) = &self.bass {
500 if let Some(index) = pitches.iter().position(|pitch| pitch.name() == bass.name()) {
501 let bass = pitches.remove(index);
502 pitches.insert(0, bass);
503 } else {
504 pitches.insert(0, bass.clone());
505 }
506 }
507
508 Chord::new(pitches.as_slice())
509 }
510
511 fn base_intervals(&self) -> Vec<&'static str> {
512 let altered_fifth = self
513 .alterations
514 .iter()
515 .find(|alteration| alteration.degree == 5)
516 .and_then(|alteration| match alteration.semitones {
517 -1 => Some("d5"),
518 1 => Some("a5"),
519 _ => None,
520 });
521
522 let fifth = altered_fifth.unwrap_or("P5");
523 let has_seventh = self
524 .extensions
525 .iter()
526 .any(|degree| matches!(degree, 7 | 9 | 11 | 13));
527
528 let intervals = match self.quality {
529 ChordQuality::Major => {
530 if has_seventh {
531 vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "M7")]
532 } else {
533 vec![(1, "P1"), (3, "M3"), (5, fifth)]
534 }
535 }
536 ChordQuality::Minor => {
537 if has_seventh {
538 vec![(1, "P1"), (3, "m3"), (5, fifth), (7, "m7")]
539 } else {
540 vec![(1, "P1"), (3, "m3"), (5, fifth)]
541 }
542 }
543 ChordQuality::Dominant => vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "m7")],
544 ChordQuality::Diminished => {
545 if has_seventh {
546 vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "d7")]
547 } else {
548 vec![(1, "P1"), (3, "m3"), (5, "d5")]
549 }
550 }
551 ChordQuality::Augmented => vec![(1, "P1"), (3, "M3"), (5, "a5")],
552 ChordQuality::HalfDiminished => vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "m7")],
553 ChordQuality::Suspended2 => vec![(1, "P1"), (2, "M2"), (5, fifth)],
554 ChordQuality::Suspended4 => vec![(1, "P1"), (4, "P4"), (5, fifth)],
555 ChordQuality::Power => vec![(1, "P1"), (5, fifth)],
556 };
557
558 intervals
559 .into_iter()
560 .filter_map(|(degree, interval)| {
561 (!self.omissions.contains(°ree)).then_some(interval)
562 })
563 .collect()
564 }
565}
566
567pub(crate) fn chord_symbol_spellings(chord: &Chord) -> Vec<String> {
573 chord_symbol_spellings_for_root(chord, None)
574}
575
576pub(crate) fn chord_symbol_spellings_with_root(chord: &Chord, root: u8) -> Vec<String> {
577 chord_symbol_spellings_for_root(chord, Some(root % 12))
578}
579
580fn chord_symbol_spellings_for_root(chord: &Chord, explicit_root: Option<u8>) -> Vec<String> {
581 music21_chord_symbol_figure(chord, explicit_root)
582 .into_iter()
583 .collect()
584}
585
586fn music21_chord_symbol_figure(chord: &Chord, explicit_root: Option<u8>) -> Option<String> {
587 let pitches = chord.pitches();
588 if pitches.iter().any(|pitch| {
589 let ps = pitch.ps();
590 (ps - ps.round()).abs() > FloatType::EPSILON
591 }) {
592 return None;
593 }
594
595 if pitches.is_empty() {
596 return None;
597 }
598
599 let mut root_pitch = if let Some(root) = explicit_root {
600 pitches
601 .iter()
602 .find(|pitch| pitch_class(pitch) == root)
603 .cloned()?
604 } else {
605 find_root_pitch(&pitches).cloned()?
606 };
607
608 if pitches.len() == 1 {
609 return Some(format!("{}pedal", root_pitch.name()));
610 }
611
612 let analysis = Music21ChordAnalysis::new(&pitches, &root_pitch);
613 let matched = identify_music21_chord_type(&analysis)?;
614 let bass_pitch = bass_pitch(&pitches)?;
615 let mut notation = matched.notation;
616 let mut abbreviation = matched.abbreviation;
617
618 if pitch_class(bass_pitch) != pitch_class(&root_pitch)
619 && matched.kind == "suspended-second"
620 && matched.abbreviation == "sus2"
621 {
622 root_pitch = bass_pitch.clone();
623 notation = "1,4,5";
624 abbreviation = "sus";
625 }
626
627 let mut figure = format!("{}{}", root_pitch.name(), abbreviation);
628 if pitch_class(bass_pitch) != pitch_class(&root_pitch) {
629 figure.push('/');
630 figure.push_str(&bass_pitch.name());
631 }
632
633 let perfect = perfect_pitch_names(&root_pitch, notation)?;
634 let in_pitches = pitches
635 .iter()
636 .map(Pitch::name)
637 .collect::<BTreeSet<String>>();
638
639 if !perfect.is_superset(&in_pitches) {
640 let additions = in_pitches.difference(&perfect).cloned().collect::<Vec<_>>();
641 let subtractions = perfect.difference(&in_pitches).cloned().collect::<Vec<_>>();
642
643 if !additions.is_empty() {
644 figure.push_str("add");
645 figure.push_str(&additions.join(","));
646 }
647 if !subtractions.is_empty() {
648 figure.push_str("omit");
649 figure.push_str(&subtractions.join(","));
650 }
651 }
652
653 Some(figure)
654}
655
656impl Music21ChordAnalysis {
657 fn new(pitches: &[Pitch], root_pitch: &Pitch) -> Self {
658 let d3 = semitones_from_chord_step(pitches, root_pitch, 3);
659 let d5 = semitones_from_chord_step(pitches, root_pitch, 5);
660 let d7 = semitones_from_chord_step(pitches, root_pitch, 7);
661 let d9 = semitones_from_chord_step(pitches, root_pitch, 2);
662 let d11 = semitones_from_chord_step(pitches, root_pitch, 4);
663 let d13 = semitones_from_chord_step(pitches, root_pitch, 6);
664 let unique_pitch_names = pitches
665 .iter()
666 .map(Pitch::name)
667 .collect::<BTreeSet<String>>();
668
669 Self {
670 d3,
671 d5,
672 d7,
673 d9,
674 d11,
675 d13,
676 is_triad: unique_pitch_names.len() == 3 && d3.is_some() && d5.is_some(),
677 is_seventh: unique_pitch_names.len() == 4
678 && d3.is_some()
679 && d5.is_some()
680 && d7.is_some(),
681 }
682 }
683}
684
685fn identify_music21_chord_type(analysis: &Music21ChordAnalysis) -> Option<Music21FigureMatch> {
686 let mut matched = None;
687
688 for chord_type in MUSIC21_CHORD_TYPES {
689 let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
690 let is_match = match chord_degrees.len() {
691 2 if analysis.is_triad => {
692 compare_music21_degrees(&[analysis.d3, analysis.d5], &chord_degrees, &[])
693 }
694 3 if analysis.is_seventh => compare_music21_degrees(
695 &[analysis.d3, analysis.d5, analysis.d7],
696 &chord_degrees,
697 &[],
698 ),
699 4 if music21_truthy(analysis.d9)
700 && !music21_truthy(analysis.d11)
701 && !music21_truthy(analysis.d13) =>
702 {
703 compare_music21_degrees(
704 &[analysis.d3, analysis.d5, analysis.d7, analysis.d9],
705 &chord_degrees,
706 &[5],
707 )
708 }
709 5 if music21_truthy(analysis.d11) && !music21_truthy(analysis.d13) => {
710 compare_music21_degrees(
711 &[
712 analysis.d3,
713 analysis.d5,
714 analysis.d7,
715 analysis.d9,
716 analysis.d11,
717 ],
718 &chord_degrees,
719 &[3, 5],
720 )
721 }
722 6 if music21_truthy(analysis.d13) => compare_music21_degrees(
723 &[
724 analysis.d3,
725 analysis.d5,
726 analysis.d7,
727 analysis.d9,
728 analysis.d11,
729 analysis.d13,
730 ],
731 &chord_degrees,
732 &[5, 11, 9],
733 ),
734 _ => false,
735 };
736
737 if is_match {
738 matched = Some(Music21FigureMatch {
739 kind: chord_type.kind,
740 notation: chord_type.notation,
741 abbreviation: chord_type.abbreviation,
742 });
743 }
744 }
745
746 if matched.is_some() {
747 return matched;
748 }
749
750 let mut number_of_matched_degrees = 0;
751 for chord_type in MUSIC21_CHORD_TYPES {
752 let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
753 let mut degrees = degree_numbers_for_notation(chord_type.notation)?;
754 degrees.sort_unstable();
755 let to_compare = degrees
756 .into_iter()
757 .filter(|degree| *degree != 1)
758 .map(|degree| analysis_value_for_degree(analysis, degree))
759 .collect::<Vec<_>>();
760
761 if compare_music21_degrees(&to_compare, &chord_degrees, &[])
762 && number_of_matched_degrees < chord_degrees.len()
763 {
764 number_of_matched_degrees = chord_degrees.len();
765 matched = Some(Music21FigureMatch {
766 kind: chord_type.kind,
767 notation: chord_type.notation,
768 abbreviation: chord_type.abbreviation,
769 });
770 }
771 }
772
773 matched
774}
775
776fn compare_music21_degrees(
777 in_chord_nums: &[Option<u8>],
778 given_chord_nums: &[u8],
779 permitted_omissions: &[u8],
780) -> bool {
781 if given_chord_nums.len() > in_chord_nums.len() {
782 return false;
783 }
784
785 for (index, expected) in given_chord_nums.iter().enumerate() {
786 if in_chord_nums[index] == Some(*expected) {
787 continue;
788 }
789
790 let (degree, natural) = match index {
791 0 => (3, 4),
792 1 => (5, 7),
793 2 => (7, 11),
794 3 => (9, 2),
795 4 => (11, 5),
796 5 => (13, 9),
797 _ => return false,
798 };
799
800 if !(permitted_omissions.contains(°ree)
801 && *expected == natural
802 && in_chord_nums[index].is_none())
803 {
804 return false;
805 }
806 }
807
808 true
809}
810
811fn music21_truthy(value: Option<u8>) -> bool {
812 value.is_some_and(|value| value != 0)
813}
814
815fn chord_degrees_for_notation(notation: &str) -> Option<Vec<u8>> {
816 notation
817 .split(',')
818 .filter(|token| *token != "1")
819 .map(|token| parse_music21_degree(token).map(|degree| degree.semitone))
820 .collect()
821}
822
823fn degree_numbers_for_notation(notation: &str) -> Option<Vec<u8>> {
824 notation
825 .split(',')
826 .map(|token| parse_music21_degree(token).map(|degree| degree.degree))
827 .collect()
828}
829
830fn parse_music21_degree(token: &str) -> Option<Music21Degree> {
831 let alteration = token.chars().fold(0_i32, |sum, ch| match ch {
832 '#' => sum + 1,
833 '-' => sum - 1,
834 _ => sum,
835 });
836 let degree = token
837 .chars()
838 .filter(char::is_ascii_digit)
839 .collect::<String>()
840 .parse::<u8>()
841 .ok()?;
842 let semitone = (base_semitone_for_degree(degree)? + alteration).rem_euclid(12) as u8;
843
844 Some(Music21Degree { degree, semitone })
845}
846
847fn base_semitone_for_degree(degree: u8) -> Option<IntegerType> {
848 match degree {
849 1 => Some(0),
850 2 | 9 => Some(2),
851 3 => Some(4),
852 4 | 11 => Some(5),
853 5 => Some(7),
854 6 | 13 => Some(9),
855 7 => Some(11),
856 _ => None,
857 }
858}
859
860fn analysis_value_for_degree(analysis: &Music21ChordAnalysis, degree: u8) -> Option<u8> {
861 match degree {
862 2 | 9 => analysis.d9,
863 3 => analysis.d3,
864 4 | 11 => analysis.d11,
865 5 => analysis.d5,
866 6 | 13 => analysis.d13,
867 7 => analysis.d7,
868 _ => None,
869 }
870}
871
872fn semitones_from_chord_step(pitches: &[Pitch], root_pitch: &Pitch, chord_step: u8) -> Option<u8> {
873 let root_step = step_num(root_pitch);
874 let root_pc = pitch_class(root_pitch);
875
876 pitches.iter().find_map(|pitch| {
877 let generic_interval = (step_num(pitch) - root_step).rem_euclid(7) + 1;
878 if generic_interval == chord_step as IntegerType {
879 Some((pitch_class(pitch) + 12 - root_pc) % 12)
880 } else {
881 None
882 }
883 })
884}
885
886fn perfect_pitch_names(root_pitch: &Pitch, notation: &str) -> Option<BTreeSet<String>> {
887 let mut pitch_names = BTreeSet::new();
888 pitch_names.insert(root_pitch.name());
889 for token in notation.split(',').filter(|token| *token != "1") {
890 let degree = parse_music21_degree(token)?;
891 pitch_names.insert(pitch_name_for_music21_degree(
892 root_pitch,
893 degree.degree,
894 degree.semitone,
895 )?);
896 }
897 Some(pitch_names)
898}
899
900fn pitch_name_for_music21_degree(root_pitch: &Pitch, degree: u8, semitone: u8) -> Option<String> {
901 const LETTERS: [char; 7] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
902 const NATURAL_PCS: [IntegerType; 7] = [0, 2, 4, 5, 7, 9, 11];
903
904 let root_letter = root_pitch.name().chars().next()?.to_ascii_uppercase();
905 let root_index = LETTERS.iter().position(|letter| *letter == root_letter)?;
906 let target_index = (root_index + (degree.saturating_sub(1) as usize % 7)) % 7;
907 let desired_pc =
908 ((pitch_class(root_pitch) as IntegerType) + semitone as IntegerType).rem_euclid(12);
909 let mut accidental = desired_pc - NATURAL_PCS[target_index];
910 while accidental > 6 {
911 accidental -= 12;
912 }
913 while accidental < -6 {
914 accidental += 12;
915 }
916
917 let mut name = LETTERS[target_index].to_string();
918 if accidental > 0 {
919 name.push_str(&"#".repeat(accidental as usize));
920 } else if accidental < 0 {
921 name.push_str(&"-".repeat((-accidental) as usize));
922 }
923 Some(name)
924}
925
926fn find_root_pitch(pitches: &[Pitch]) -> Option<&Pitch> {
927 let mut non_duplicating_pitches = Vec::new();
928 let mut seen_steps = BTreeSet::new();
929 for pitch in pitches {
930 if seen_steps.insert(step_num(pitch)) {
931 non_duplicating_pitches.push(pitch);
932 }
933 }
934
935 match non_duplicating_pitches.len() {
936 0 => return None,
937 1 => return pitches.first(),
938 7 => return bass_pitch(pitches),
939 _ => {}
940 }
941
942 let mut step_nums_to_pitches = BTreeMap::new();
943 for pitch in &non_duplicating_pitches {
944 step_nums_to_pitches.insert(step_num(pitch), *pitch);
945 }
946 let step_nums = step_nums_to_pitches.keys().copied().collect::<Vec<_>>();
947
948 for start_index in 0..step_nums.len() {
949 let mut all_are_thirds = true;
950 let this_step_num = step_nums[start_index];
951 let mut last_step_num = this_step_num;
952 for end_index in (start_index + 1)..(start_index + step_nums.len()) {
953 let end_step_num = step_nums[end_index % step_nums.len()];
954 if !matches!(end_step_num - last_step_num, 2 | -5) {
955 all_are_thirds = false;
956 break;
957 }
958 last_step_num = end_step_num;
959 }
960 if all_are_thirds {
961 return step_nums_to_pitches.get(&this_step_num).copied();
962 }
963 }
964
965 let ordered_chord_steps = [3, 5, 7, 2, 4, 6];
966 let mut best_pitch = non_duplicating_pitches[0];
967 let mut best_score = FloatType::NEG_INFINITY;
968
969 for pitch in non_duplicating_pitches {
970 let this_step_num = step_num(pitch);
971 let mut score = 0.0;
972 for (root_index, chord_step_test) in ordered_chord_steps.iter().enumerate() {
973 let target = (this_step_num + chord_step_test - 1).rem_euclid(7);
974 if step_nums_to_pitches.contains_key(&target) {
975 score += 1.0 / (root_index as FloatType + 6.0);
976 }
977 }
978 if score > best_score {
979 best_score = score;
980 best_pitch = pitch;
981 }
982 }
983
984 Some(best_pitch)
985}
986
987fn bass_pitch(pitches: &[Pitch]) -> Option<&Pitch> {
988 pitches.iter().min_by(|left, right| {
989 left.ps()
990 .partial_cmp(&right.ps())
991 .unwrap_or(std::cmp::Ordering::Equal)
992 })
993}
994
995fn step_num(pitch: &Pitch) -> IntegerType {
996 pitch.step().step_to_dnn_offset() - 1
997}
998
999fn pitch_class(pitch: &Pitch) -> u8 {
1000 (pitch.ps().round() as IntegerType).rem_euclid(12) as u8
1001}
1002
1003impl FromStr for ChordSymbol {
1004 type Err = Error;
1005
1006 fn from_str(value: &str) -> Result<Self> {
1007 Self::parse(value)
1008 }
1009}
1010
1011impl TryFrom<&str> for ChordSymbol {
1012 type Error = Error;
1013
1014 fn try_from(value: &str) -> Result<Self> {
1015 Self::parse(value)
1016 }
1017}
1018
1019impl TryFrom<String> for ChordSymbol {
1020 type Error = Error;
1021
1022 fn try_from(value: String) -> Result<Self> {
1023 Self::parse(value)
1024 }
1025}
1026
1027#[derive(Clone, Debug, Default, Eq, PartialEq)]
1028struct Music21PitchModifiers {
1029 base: String,
1030 additions: Vec<String>,
1031 omissions: Vec<String>,
1032}
1033
1034fn split_music21_pitch_modifiers(value: &str) -> Music21PitchModifiers {
1035 let Some(start) = find_music21_modifier_start(value) else {
1036 return Music21PitchModifiers {
1037 base: value.to_string(),
1038 ..Music21PitchModifiers::default()
1039 };
1040 };
1041
1042 let mut parts = Music21PitchModifiers {
1043 base: value[..start].trim_end().to_string(),
1044 ..Music21PitchModifiers::default()
1045 };
1046 let mut cursor = start;
1047 while cursor < value.len() {
1048 let Some(marker) = music21_modifier_at(value, cursor) else {
1049 cursor += value[cursor..]
1050 .chars()
1051 .next()
1052 .map(char::len_utf8)
1053 .unwrap_or(1);
1054 continue;
1055 };
1056 let content_start = cursor + marker.len();
1057 let content_end = find_music21_modifier_start(&value[content_start..])
1058 .map(|relative| content_start + relative)
1059 .unwrap_or(value.len());
1060 let tokens = value[content_start..content_end]
1061 .split(|ch: char| ch == ',' || ch.is_whitespace())
1062 .filter(|token| !token.trim().is_empty())
1063 .map(|token| token.trim().to_string());
1064
1065 match marker {
1066 "add" => parts.additions.extend(tokens),
1067 "omit" => parts.omissions.extend(tokens),
1068 _ => {}
1069 }
1070 cursor = content_end;
1071 }
1072
1073 parts
1074}
1075
1076fn find_music21_modifier_start(value: &str) -> Option<usize> {
1077 value
1078 .char_indices()
1079 .find_map(|(idx, _)| music21_modifier_at(value, idx).map(|_| idx))
1080}
1081
1082fn music21_modifier_at(value: &str, idx: usize) -> Option<&'static str> {
1083 let rest = value.get(idx..)?;
1084 let lower = rest.to_ascii_lowercase();
1085 if lower.starts_with("add") && !matches!(rest.as_bytes().get(3), Some(b'(')) {
1086 Some("add")
1087 } else if lower.starts_with("omit") && !matches!(rest.as_bytes().get(4), Some(b'(')) {
1088 Some("omit")
1089 } else {
1090 None
1091 }
1092}
1093
1094fn pitch_name_addition(root: &Pitch, pitch_name: &str) -> Option<ChordAlteration> {
1095 let pitch = Pitch::from_name(pitch_name).ok()?;
1096 let degree = pitch_name_degree(root, pitch_name)?;
1097 let actual = ((pitch_class(&pitch) + 12 - pitch_class(root)) % 12) as IntegerType;
1098 let base = base_semitone_for_degree(degree)?.rem_euclid(12);
1099 let mut semitones = actual - base;
1100 while semitones > 6 {
1101 semitones -= 12;
1102 }
1103 while semitones < -6 {
1104 semitones += 12;
1105 }
1106
1107 Some(ChordAlteration::new(degree, semitones))
1108}
1109
1110fn pitch_name_degree(root: &Pitch, pitch_name: &str) -> Option<u8> {
1111 let pitch = Pitch::from_name(pitch_name).ok()?;
1112 let generic = (step_num(&pitch) - step_num(root)).rem_euclid(7) + 1;
1113 Some(match generic as u8 {
1114 2 => 9,
1115 4 => 11,
1116 6 => 13,
1117 degree => degree,
1118 })
1119}
1120
1121fn add_implicit_music21_alterations(suffix: &str, alterations: &mut Vec<ChordAlteration>) {
1122 let lower = suffix.to_ascii_lowercase();
1123 if lower.contains("dim5")
1124 && !alterations
1125 .iter()
1126 .any(|alteration| alteration.degree == 5 && alteration.semitones == -1)
1127 {
1128 alterations.push(ChordAlteration::new(5, -1));
1129 }
1130 if lower.ends_with("7+")
1131 && !alterations
1132 .iter()
1133 .any(|alteration| alteration.degree == 5 && alteration.semitones == 1)
1134 {
1135 alterations.push(ChordAlteration::new(5, 1));
1136 }
1137}
1138
1139fn parse_quality(suffix: &str, alterations: &[ChordAlteration]) -> ChordQuality {
1140 let lower = suffix.to_ascii_lowercase();
1141 let has_flat_five = alterations
1142 .iter()
1143 .any(|alteration| alteration.degree == 5 && alteration.semitones == -1);
1144
1145 if suffix.starts_with('\u{00f8}') {
1146 ChordQuality::HalfDiminished
1147 } else if lower.contains("sus2") {
1148 ChordQuality::Suspended2
1149 } else if lower.contains("sus") {
1150 ChordQuality::Suspended4
1151 } else if lower.starts_with("maj") || suffix.starts_with('M') {
1152 ChordQuality::Major
1153 } else if lower.starts_with("min") || lower.starts_with('m') {
1154 if has_flat_five && lower.contains('7') {
1155 ChordQuality::HalfDiminished
1156 } else {
1157 ChordQuality::Minor
1158 }
1159 } else if lower.starts_with("dim") || lower.starts_with('o') {
1160 ChordQuality::Diminished
1161 } else if lower.starts_with("aug") || lower.starts_with('+') {
1162 ChordQuality::Augmented
1163 } else if lower.starts_with('5') {
1164 ChordQuality::Power
1165 } else if lower.starts_with("dom")
1166 || lower.starts_with('7')
1167 || lower.starts_with('9')
1168 || lower.starts_with("11")
1169 || lower.starts_with("13")
1170 {
1171 ChordQuality::Dominant
1172 } else {
1173 ChordQuality::Major
1174 }
1175}
1176
1177fn parse_extensions(suffix: &str, alterations: &[ChordAlteration]) -> Vec<u8> {
1178 let mut extensions = Vec::new();
1179 let bytes = suffix.as_bytes();
1180 let mut idx = 0;
1181 while idx < bytes.len() {
1182 let byte = bytes[idx];
1183 if byte.is_ascii_digit()
1184 && idx
1185 .checked_sub(1)
1186 .is_none_or(|prev| !matches!(bytes[prev] as char, '#' | 'b' | '-'))
1187 {
1188 let start = idx;
1189 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
1190 idx += 1;
1191 }
1192 if let Ok(degree) = suffix[start..idx].parse::<u8>()
1193 && matches!(degree, 6 | 7 | 9 | 11 | 13)
1194 && !extensions.contains(°ree)
1195 {
1196 extensions.push(degree);
1197 }
1198 } else {
1199 idx += 1;
1200 }
1201 }
1202
1203 for alteration in alterations {
1204 if alteration.degree > 5 && !extensions.contains(&alteration.degree) {
1205 extensions.push(alteration.degree);
1206 }
1207 }
1208
1209 extensions.sort_unstable();
1210 extensions
1211}
1212
1213fn strip_addition_groups(suffix: &str) -> String {
1214 let lower = suffix.to_ascii_lowercase();
1215 let mut stripped = String::with_capacity(suffix.len());
1216 let mut cursor = 0;
1217
1218 while let Some(relative_start) = lower[cursor..].find("add(") {
1219 let start = cursor + relative_start;
1220 let content_start = start + "add(".len();
1221 let Some(relative_end) = suffix[content_start..].find(')') else {
1222 break;
1223 };
1224
1225 stripped.push_str(&suffix[cursor..start]);
1226 cursor = content_start + relative_end + 1;
1227 }
1228
1229 stripped.push_str(&suffix[cursor..]);
1230 stripped
1231}
1232
1233fn parse_additions(suffix: &str) -> Vec<ChordAlteration> {
1234 let lower = suffix.to_ascii_lowercase();
1235 let mut additions = Vec::new();
1236 let mut cursor = 0;
1237
1238 while let Some(relative_start) = lower[cursor..].find("add(") {
1239 let content_start = cursor + relative_start + "add(".len();
1240 let Some(relative_end) = suffix[content_start..].find(')') else {
1241 break;
1242 };
1243 let content_end = content_start + relative_end;
1244
1245 for token in
1246 suffix[content_start..content_end].split(|ch: char| ch == ',' || ch.is_whitespace())
1247 {
1248 if let Some(addition) = parse_addition_token(token) {
1249 additions.push(addition);
1250 }
1251 }
1252
1253 cursor = content_end + 1;
1254 }
1255
1256 additions
1257}
1258
1259fn parse_omissions(suffix: &str) -> Vec<u8> {
1260 let lower = suffix.to_ascii_lowercase();
1261 let bytes = lower.as_bytes();
1262 let mut omissions = Vec::new();
1263 let mut cursor = 0;
1264
1265 while cursor < bytes.len() {
1266 let marker_len = if bytes[cursor..].starts_with(b"omit") {
1267 4
1268 } else if bytes[cursor..].starts_with(b"no") {
1269 2
1270 } else {
1271 cursor += 1;
1272 continue;
1273 };
1274
1275 cursor += marker_len;
1276 while cursor < bytes.len() && (bytes[cursor].is_ascii_whitespace() || bytes[cursor] == b'(')
1277 {
1278 cursor += 1;
1279 }
1280
1281 let degree_start = cursor;
1282 while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
1283 cursor += 1;
1284 }
1285 if degree_start == cursor {
1286 continue;
1287 }
1288
1289 if let Ok(degree) = std::str::from_utf8(&bytes[degree_start..cursor])
1290 .unwrap_or_default()
1291 .parse::<u8>()
1292 && !omissions.contains(°ree)
1293 {
1294 omissions.push(degree);
1295 }
1296 }
1297
1298 omissions
1299}
1300
1301fn parse_addition_token(token: &str) -> Option<ChordAlteration> {
1302 let token = token.trim();
1303 if token.is_empty() {
1304 return None;
1305 }
1306
1307 let (semitones, degree) = match token.as_bytes()[0] as char {
1308 '#' => (1, &token[1..]),
1309 'b' | '-' => (-1, &token[1..]),
1310 _ => (0, token),
1311 };
1312
1313 degree
1314 .parse::<u8>()
1315 .ok()
1316 .map(|degree| ChordAlteration::new(degree, semitones))
1317}
1318
1319fn parse_alterations(suffix: &str) -> Vec<ChordAlteration> {
1320 let bytes = suffix.as_bytes();
1321 let mut alterations = Vec::new();
1322 let mut idx = 0;
1323 while idx < bytes.len() {
1324 let semitones = match bytes[idx] as char {
1325 '#' => 1,
1326 'b' | '-' => -1,
1327 _ => {
1328 idx += 1;
1329 continue;
1330 }
1331 };
1332 idx += 1;
1333 let start = idx;
1334 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
1335 idx += 1;
1336 }
1337 if start == idx {
1338 continue;
1339 }
1340 if let Ok(degree) = suffix[start..idx].parse::<u8>() {
1341 alterations.push(ChordAlteration::new(degree, semitones));
1342 }
1343 }
1344 alterations
1345}
1346
1347fn parse_pitch_only(value: &str) -> Result<Pitch> {
1348 let (name, rest) = parse_pitch_prefix(value)?;
1349 if !rest.is_empty() {
1350 return Err(Error::Chord(format!("invalid slash bass {value:?}")));
1351 }
1352 Pitch::from_name(name)
1353}
1354
1355fn parse_pitch_prefix(value: &str) -> Result<(String, &str)> {
1356 let mut chars = value.char_indices();
1357 let Some((_, first)) = chars.next() else {
1358 return Err(Error::Chord("missing pitch name".to_string()));
1359 };
1360
1361 if !matches!(first.to_ascii_uppercase(), 'A'..='G') {
1362 return Err(Error::Chord(format!("invalid pitch name in {value:?}")));
1363 }
1364
1365 let mut end = first.len_utf8();
1366 let mut name = first.to_ascii_uppercase().to_string();
1367 for (idx, ch) in chars {
1368 match ch {
1369 '#' => {
1370 name.push('#');
1371 end = idx + ch.len_utf8();
1372 }
1373 'b' | '-' => {
1374 name.push('-');
1375 end = idx + ch.len_utf8();
1376 }
1377 _ => break,
1378 }
1379 }
1380
1381 Ok((name, &value[end..]))
1382}
1383
1384fn default_extension_interval(degree: u8) -> &'static str {
1385 match degree {
1386 6 => "M6",
1387 9 => "M9",
1388 11 => "P11",
1389 13 => "M13",
1390 _ => "P1",
1391 }
1392}
1393
1394fn altered_interval(alteration: &ChordAlteration) -> Result<&'static str> {
1395 match (alteration.degree, alteration.semitones) {
1396 (5, -1) => Ok("d5"),
1397 (5, 1) => Ok("a5"),
1398 (9, -1) => Ok("m9"),
1399 (9, 1) => Ok("a9"),
1400 (11, 1) => Ok("a11"),
1401 (13, -1) => Ok("m13"),
1402 (13, 1) => Ok("a13"),
1403 _ => Err(Error::Chord(format!(
1404 "unsupported chord-symbol alteration {alteration:?}"
1405 ))),
1406 }
1407}
1408
1409fn added_interval(addition: &ChordAlteration) -> Result<&'static str> {
1410 let degree = match addition.degree {
1411 2 => 9,
1412 4 => 11,
1413 6 => 13,
1414 degree => degree,
1415 };
1416
1417 match (degree, addition.semitones) {
1418 (3, -1) => Ok("m3"),
1419 (3, 0) => Ok("M3"),
1420 (3, 1) => Ok("a3"),
1421 (5, -1) => Ok("d5"),
1422 (5, 0) => Ok("P5"),
1423 (5, 1) => Ok("a5"),
1424 (7, -1) => Ok("m7"),
1425 (7, 0) => Ok("M7"),
1426 (9, -1) => Ok("m9"),
1427 (9, 0) => Ok("M9"),
1428 (9, 1) => Ok("a9"),
1429 (11, -1) => Ok("d11"),
1430 (11, 0) => Ok("P11"),
1431 (11, 1) => Ok("a11"),
1432 (13, -1) => Ok("m13"),
1433 (13, 0) => Ok("M13"),
1434 (13, 1) => Ok("a13"),
1435 _ => Err(Error::Chord(format!(
1436 "unsupported chord-symbol added tone {addition:?}"
1437 ))),
1438 }
1439}
1440
1441fn interval_sort_key(name: &str) -> u8 {
1442 name.chars()
1443 .filter(|ch| ch.is_ascii_digit())
1444 .collect::<String>()
1445 .parse::<u8>()
1446 .unwrap_or(1)
1447}
1448
1449#[cfg(test)]
1450mod tests {
1451 use super::*;
1452
1453 #[test]
1454 fn parses_major_seventh_symbol() {
1455 let symbol: ChordSymbol = "Cmaj7".parse().unwrap();
1456 assert_eq!(symbol.root().name(), "C");
1457 assert_eq!(symbol.quality(), ChordQuality::Major);
1458 assert_eq!(symbol.extensions(), &[7]);
1459 assert_eq!(
1460 symbol.to_chord().unwrap().pitched_common_name(),
1461 "C-major seventh chord"
1462 );
1463 }
1464
1465 #[test]
1466 fn parses_half_diminished_symbol() {
1467 let symbol = ChordSymbol::parse("F#m7b5").unwrap();
1468 assert_eq!(symbol.root().name(), "F#");
1469 assert_eq!(symbol.quality(), ChordQuality::HalfDiminished);
1470 assert_eq!(
1471 symbol.to_chord().unwrap().pitched_common_name(),
1472 "F#-half-diminished seventh chord"
1473 );
1474 }
1475
1476 #[test]
1477 fn parses_dominant_altered_symbol() {
1478 let symbol = ChordSymbol::parse("Bb7#11").unwrap();
1479 assert_eq!(symbol.root().name(), "B-");
1480 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1481 assert_eq!(symbol.extensions(), &[7, 11]);
1482 assert_eq!(symbol.alterations()[0], ChordAlteration::new(11, 1));
1483 let names = symbol
1484 .to_chord()
1485 .unwrap()
1486 .pitches()
1487 .iter()
1488 .map(Pitch::name)
1489 .collect::<Vec<_>>();
1490 assert_eq!(names, vec!["B-", "D", "F", "A-", "E"]);
1491 }
1492
1493 #[test]
1494 fn parses_added_tones_without_changing_the_base_chord() {
1495 let symbol = ChordSymbol::parse("Cdim9 add(#5)").unwrap();
1496 assert_eq!(symbol.extensions(), &[9]);
1497 assert_eq!(symbol.additions(), &[ChordAlteration::new(5, 1)]);
1498 assert_eq!(
1499 symbol.to_chord().unwrap().pitch_classes(),
1500 vec![0, 2, 3, 6, 8, 9]
1501 );
1502 }
1503
1504 #[test]
1505 fn parses_altered_dominant_with_slash_bass() {
1506 let symbol = ChordSymbol::parse("D7b9#11/C").unwrap();
1507 assert_eq!(symbol.root().name(), "D");
1508 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1509 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1510 assert_eq!(symbol.extensions(), &[7, 9, 11]);
1511 assert_eq!(
1512 symbol.alterations(),
1513 &[ChordAlteration::new(9, -1), ChordAlteration::new(11, 1)]
1514 );
1515 assert_eq!(
1516 symbol.to_chord().unwrap().pitch_classes(),
1517 vec![0, 2, 3, 6, 8, 9]
1518 );
1519 }
1520
1521 #[test]
1522 fn parses_music21_pitch_name_additions() {
1523 let symbol = ChordSymbol::parse("Ddom7dim5/CaddA,E-").unwrap();
1524
1525 assert_eq!(symbol.root().name(), "D");
1526 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1527 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1528 assert_eq!(symbol.alterations(), &[ChordAlteration::new(5, -1)]);
1529 assert_eq!(
1530 symbol.additions(),
1531 &[ChordAlteration::new(5, 0), ChordAlteration::new(9, -1)]
1532 );
1533 assert_eq!(
1534 symbol.to_chord().unwrap().pitch_classes(),
1535 vec![0, 2, 3, 6, 8, 9]
1536 );
1537 }
1538
1539 #[test]
1540 fn generates_petrushka_chord_symbol_name() {
1541 let chord = Chord::new("C4 D4 Eb4 F#4 Ab4 A4").unwrap();
1542 let names = chord_symbol_spellings(&chord);
1543
1544 assert_eq!(
1545 names.first().map(String::as_str),
1546 Some("Ddom7dim5/CaddA,E-")
1547 );
1548 assert!(names.iter().any(|name| name == "Ddom7dim5/CaddA,E-"));
1549 }
1550
1551 #[test]
1552 fn generates_common_chord_symbols() {
1553 let major_seventh = Chord::new("C E G B").unwrap();
1554 let dominant_ninth = Chord::new("C E G B- D").unwrap();
1555
1556 assert_eq!(
1557 chord_symbol_spellings(&major_seventh)
1558 .first()
1559 .map(String::as_str),
1560 Some("Cmaj7")
1561 );
1562 assert_eq!(
1563 chord_symbol_spellings(&dominant_ninth)
1564 .first()
1565 .map(String::as_str),
1566 Some("C9")
1567 );
1568 }
1569
1570 #[test]
1571 fn split_third_triads_do_not_spell_lower_third_as_sharp_nine() {
1572 let split_third = Chord::new("D4 A4 F#4 F4").unwrap();
1573 let names = chord_symbol_spellings(&split_third);
1574
1575 assert_eq!(names.first().map(String::as_str), Some("DaddF"));
1576 assert!(!names.iter().any(|name| name == "D add(#9)"));
1577 }
1578
1579 #[test]
1580 fn altered_dominants_use_music21_pitch_name_additions() {
1581 let altered_dominant = Chord::new("C4 E4 G4 Bb4 Eb5").unwrap();
1582 let names = chord_symbol_spellings(&altered_dominant);
1583
1584 assert_eq!(names.first().map(String::as_str), Some("C7addE-"));
1585 }
1586
1587 #[test]
1588 fn unrecognized_music21_figures_return_no_symbol() {
1589 let chord = Chord::new("F4 C5 D5 E-5").unwrap();
1590 let names = chord_symbol_spellings(&chord);
1591
1592 assert!(names.is_empty());
1593 }
1594
1595 #[test]
1596 fn generates_music21_figures_with_explicit_root() {
1597 let major_triad = Chord::new("G3 C4 E4").unwrap();
1598 let dominant_seventh = Chord::new("G3 B-3 C4 E4").unwrap();
1599 let power_chord = Chord::new("C4 G4").unwrap();
1600 let unsupported_dyad = Chord::new("C4 A4").unwrap();
1601
1602 assert_eq!(
1603 chord_symbol_spellings_with_root(&major_triad, 0)
1604 .first()
1605 .map(String::as_str),
1606 Some("C/G")
1607 );
1608 assert_eq!(
1609 chord_symbol_spellings_with_root(&dominant_seventh, 0)
1610 .first()
1611 .map(String::as_str),
1612 Some("C7/G")
1613 );
1614 assert_eq!(
1615 chord_symbol_spellings_with_root(&power_chord, 0)
1616 .first()
1617 .map(String::as_str),
1618 Some("Cpower")
1619 );
1620 assert!(chord_symbol_spellings_with_root(&unsupported_dyad, 0).is_empty());
1621 }
1622
1623 #[test]
1624 fn dense_sets_follow_music21_fallback_matching() {
1625 let chord = Chord::new("C4 D-4 E-4 E4 F#4 G4 A-4 A4").unwrap();
1626
1627 assert_eq!(
1628 chord_symbol_spellings(&chord).first().map(String::as_str),
1629 Some("CsusaddA,A-,D-,E,E-,F#omitF")
1630 );
1631 }
1632}