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