1use std::str::FromStr;
2
3use crate::{
4 chord::Chord,
5 chord::root::{pitch_class, step_num},
6 defaults::{FloatType, IntegerType},
7 duration::{Duration, DurationType},
8 error::{Error, Result},
9 interval::Interval,
10 pitch::Pitch,
11 stream::{Stream, StreamElement},
12};
13use std::collections::BTreeSet;
14
15mod figure;
16mod parse;
17mod realize;
18mod tables;
19
20pub use figure::{
21 ChordSymbolFigure, alter_to_chord_symbol_string, chord_symbol_figure_from_chord,
22 chord_symbol_from_chord, chord_symbol_kind_from_chord,
23};
24pub(crate) use figure::{chord_symbol_spellings, chord_symbol_spellings_with_root};
25pub use tables::{
26 CHORD_KIND_ALIASES, Music21ChordType, abbreviations_for_kind, current_abbreviation_for_kind,
27 known_chord_symbol_types, notation_for_kind, resolve_kind_alias,
28};
29
30use parse::*;
31pub use realize::{
32 ChordStepModification, ChordStepModificationType, ChordVoicing, inversion_is_valid_for_kind,
33 sound_chord_kind, sound_chord_notation, voice_chord_notation,
34};
35use tables::*;
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum ChordQuality {
41 Major,
43 Minor,
45 Dominant,
47 Diminished,
49 Augmented,
51 HalfDiminished,
53 Suspended2,
55 Suspended4,
57 Power,
59 Pedal,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct ChordAlteration {
67 degree: u8,
68 semitones: IntegerType,
69}
70
71impl ChordAlteration {
72 pub fn new(degree: u8, semitones: IntegerType) -> Self {
74 Self { degree, semitones }
75 }
76
77 pub fn degree(&self) -> u8 {
79 self.degree
80 }
81
82 pub fn semitones(&self) -> IntegerType {
84 self.semitones
85 }
86}
87
88#[derive(Clone, Debug, PartialEq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91#[must_use]
92pub struct ChordSymbol {
93 figure: String,
94 root: Pitch,
95 bass: Option<Pitch>,
96 quality: ChordQuality,
97 extensions: Vec<u8>,
98 alterations: Vec<ChordAlteration>,
99 #[cfg_attr(feature = "serde", serde(default))]
100 omissions: Vec<u8>,
101 #[cfg_attr(feature = "serde", serde(default))]
102 additions: Vec<ChordAlteration>,
103 #[cfg_attr(feature = "serde", serde(default))]
105 kind: Option<String>,
106 #[cfg_attr(feature = "serde", serde(default))]
109 modifications: Vec<ChordStepModification>,
110 #[cfg_attr(feature = "serde", serde(default = "no_time"))]
113 duration: Duration,
114 #[cfg_attr(feature = "serde", serde(default))]
117 kind_text: crate::display::Drawn<Option<String>>,
118 #[cfg_attr(feature = "serde", serde(default))]
119 placement: crate::display::Drawn<Option<crate::notation::Placement>>,
120 #[cfg_attr(feature = "serde", serde(default))]
123 lyrics: Vec<crate::notation::Lyric>,
124 #[cfg_attr(feature = "serde", serde(default))]
127 transposed: Option<Vec<Pitch>>,
128}
129
130fn no_time() -> Duration {
132 Duration::from_type(DurationType::Zero)
133}
134
135fn music21_kind(shorthand: &str) -> Option<(&'static Music21ChordType, usize)> {
140 let mut cut = shorthand.len();
141 for marker in ["add", "alter", "omit", "subtract"] {
142 if let Some(at) = shorthand.find(marker) {
143 cut = cut.min(at);
144 }
145 }
146 let mut head = &shorthand[..cut];
147 if let Some(at) = head.find('#')
148 && head[at + 1..].starts_with(|c: char| c.is_ascii_digit())
149 {
150 head = &head[..at];
151 }
152 if let Some(at) = head.find('b')
153 && at < head.len() - 1
154 && head[at + 1..].starts_with(|c: char| c.is_ascii_digit())
155 && !head.contains("ob9")
156 && !head.contains("øb9")
157 {
158 head = &head[..at];
159 }
160 MUSIC21_CHORD_TYPES
161 .iter()
162 .find(|chord_type| chord_type.abbreviations.contains(&head))
163 .map(|chord_type| (chord_type, head.len()))
164}
165
166fn python_int(text: &str) -> Option<i64> {
173 let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
174 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
175 return None;
176 }
177 text.parse().ok()
178}
179
180fn music21_modifications(remaining: &str) -> Option<Vec<ChordStepModification>> {
181 const MARKERS: [(&str, ChordStepModificationType); 4] = [
182 ("add", ChordStepModificationType::Add),
183 ("alter", ChordStepModificationType::Alter),
184 ("omit", ChordStepModificationType::Subtract),
185 ("subtract", ChordStepModificationType::Subtract),
186 ];
187 let mut out = Vec::new();
188 let first_marker = MARKERS
189 .iter()
190 .filter_map(|(marker, _)| remaining.find(marker))
191 .min();
192 if let Some(start) = first_marker {
193 let mut text = &remaining[start..];
194 while let Some((at, marker, kind)) = MARKERS
195 .iter()
196 .filter_map(|(marker, kind)| text.find(marker).map(|at| (at, *marker, *kind)))
197 .min_by_key(|(at, _, _)| *at)
198 {
199 let mut rest = &text[at + marker.len()..];
200 let mut alter = 0;
201 while let Some(sign) = rest.chars().next().filter(|c| matches!(c, 'b' | '-' | '#')) {
202 alter += if sign == '#' { 1 } else { -1 };
203 rest = &rest[1..];
204 }
205 let digits = rest.chars().take_while(char::is_ascii_digit).count().min(2);
206 let degree_text = &rest[..digits];
207 if let Ok(degree) = degree_text.parse::<u8>() {
208 out.push(ChordStepModification::new(kind, degree, alter).ok()?);
209 }
210 text = &rest[digits..];
211 }
212 }
213
214 let before: String = remaining[..first_marker.unwrap_or(remaining.len())].replace(',', "");
217 let mut items: Vec<String> = Vec::new();
218 if before.contains(['b', '#']) {
219 let chars: Vec<char> = before.chars().collect();
220 let mut current = String::new();
221 let mut index = 0;
222 while index < chars.len() {
223 if matches!(chars[index], 'b' | '#') {
224 if !current.is_empty() {
225 items.push(std::mem::take(&mut current));
226 }
227 let mut group = String::new();
228 while index < chars.len() && matches!(chars[index], 'b' | '#') {
229 group.push(chars[index]);
230 index += 1;
231 }
232 while index < chars.len() && !matches!(chars[index], 'b' | '#') {
233 group.push(chars[index]);
234 index += 1;
235 }
236 items.push(group);
237 } else {
238 current.push(chars[index]);
239 index += 1;
240 }
241 }
242 if !current.is_empty() {
243 items.push(current);
244 }
245 } else if !before.is_empty() {
246 items.push(before);
247 }
248
249 let mut tokens: Vec<String> = Vec::new();
253 for item in items {
254 let unsigned: String = item.chars().filter(|c| !matches!(c, 'b' | '#')).collect();
255 let number = python_int(&unsigned)?;
256 if number > 20 {
257 let chars: Vec<char> = item.chars().collect();
260 let mut prefix = String::new();
261 let mut skip = false;
262 for (index, &ch) in chars.iter().enumerate() {
263 if skip {
264 skip = false;
265 continue;
266 }
267 if ch == '1' {
268 let mut token = String::from(ch);
269 if let Some(next) = chars.get(index + 1) {
270 token.push(*next);
271 }
272 tokens.push(token);
273 skip = true;
274 } else if matches!(ch, 'b' | '#') {
275 prefix.push(ch);
276 } else {
277 prefix.push(ch);
278 tokens.push(std::mem::take(&mut prefix));
279 }
280 }
281 } else {
282 tokens.push(item);
283 }
284 }
285 for token in tokens {
286 let alter = if token.contains('b') {
287 -(token.matches('b').count() as IntegerType)
288 } else {
289 token.matches('#').count() as IntegerType
290 };
291 let nonzero = |c: &char| matches!(c, '1'..='9');
294 let digits: String = token
295 .chars()
296 .skip_while(|c| !nonzero(c))
297 .take_while(nonzero)
298 .collect();
299 if let Ok(degree) = digits.parse::<u8>() {
300 out.push(
301 ChordStepModification::new(ChordStepModificationType::Add, degree, alter).ok()?,
302 );
303 }
304 }
305 Some(out)
306}
307
308impl ChordSymbol {
309 pub fn parse(figure: impl Into<String>) -> Result<Self> {
314 Self::parse_with(figure.into(), false)
315 }
316
317 pub fn parse_music21(figure: impl Into<String>) -> Result<Self> {
331 let figure = figure.into();
332 let compact: String = figure.chars().filter(|c| !c.is_whitespace()).collect();
333 let refused_root = || {
334 Error::Chord(format!(
335 "Chord {compact} does not begin with a valid root note."
336 ))
337 };
338 let (root_text, rest) = match compact.find(',') {
339 Some(at) => {
340 let root = compact[..at].to_string();
341 let rest = compact.replace(',', "").replace(root.as_str(), "");
342 (root, rest)
343 }
344 None => {
345 let mut letters = compact.char_indices();
346 match letters.next() {
347 Some((_, first)) if "ABCDEFGabcdefg".contains(first) => {}
348 _ => return Err(refused_root()),
349 }
350 let end = letters
351 .find(|(_, c)| !matches!(c, '#' | '-'))
352 .map_or(compact.len(), |(at, _)| at);
353 let root = compact[..end].to_string();
354 let rest = compact.replacen(root.as_str(), "", 1);
355 (root, rest)
356 }
357 };
358 let root = Pitch::from_name(&root_text)?;
361 let mut bass = None;
364 let mut remaining = rest.clone();
365 if let Some(slash) = compact.find('/') {
366 let after = &compact[slash + 1..];
367 let mut letters = after.char_indices();
368 if let Some((_, first)) = letters.next()
369 && "ABCDEFGabcdefg".contains(first)
370 {
371 let end = letters
372 .find(|(_, c)| !matches!(c, '#' | '-'))
373 .map_or(after.len(), |(at, _)| at);
374 let written = &after[..end];
375 bass = Some(Pitch::from_name(written)?);
376 remaining = rest.replace(&format!("/{written}"), "");
377 }
378 }
379 let (kind, taken) = match music21_kind(&remaining) {
380 Some((chord_type, taken)) => (chord_type.kind.to_string(), taken),
381 None => (String::new(), 0),
382 };
383 let refused = || {
384 let mut said = remaining[taken..].replace(',', "");
385 for marker in ["add", "alter", "omit", "subtract"] {
386 if let Some(at) = said.find(marker) {
387 said.truncate(at);
388 }
389 }
390 Error::Chord(format!(
391 "Invalid chord abbreviation '{said}'; see music21.harmony.CHORD_TYPES for valid abbreviations or specify all alterations."
392 ))
393 };
394 let modifications = music21_modifications(&remaining[taken..]).ok_or_else(refused)?;
395 let own = Self::parse_with(format!("C{remaining}"), false).ok();
399 Ok(Self {
400 figure: figure.trim().to_string(),
401 root,
402 bass,
403 quality: own.as_ref().map_or(ChordQuality::Major, |own| own.quality),
404 extensions: own
405 .as_ref()
406 .map(|own| own.extensions.clone())
407 .unwrap_or_default(),
408 alterations: own.map(|own| own.alterations).unwrap_or_default(),
409 omissions: Vec::new(),
410 additions: Vec::new(),
411 kind: Some(kind),
412 modifications,
413 duration: no_time(),
414 kind_text: crate::display::Drawn(None),
415 placement: crate::display::Drawn(None),
416 lyrics: Vec::new(),
417 transposed: None,
418 })
419 }
420
421 fn parse_with(figure: String, music21: bool) -> Result<Self> {
422 let trimmed = figure.trim();
423 if trimmed.is_empty() {
424 return Err(Error::Chord("chord symbol cannot be empty".to_string()));
425 }
426 let compact: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
429
430 let (body, bass_segment) = match compact.split_once('/') {
431 Some((body, bass)) => (body, Some(bass)),
432 None => (compact.as_str(), None),
433 };
434 let body_parts = split_music21_pitch_modifiers(body);
435 let bass_parts = bass_segment.map(split_music21_pitch_modifiers);
436 let bass = bass_parts
437 .as_ref()
438 .map(|parts| parse_pitch_only(&parts.base, music21))
439 .transpose()?;
440
441 let (root_name, suffix) = parse_pitch_prefix(&body_parts.base, music21)?;
442 let root = Pitch::from_name(root_name)?;
443 let suffix_without_additions = strip_addition_groups(suffix);
444 let mut additions = parse_additions(suffix);
445 let mut omissions = parse_omissions(suffix);
446 for pitch_name in body_parts
447 .additions
448 .iter()
449 .chain(bass_parts.iter().flat_map(|parts| parts.additions.iter()))
450 {
451 if let Some(addition) = pitch_name_addition(&root, pitch_name) {
452 additions.push(addition);
453 }
454 }
455 for pitch_name in body_parts
456 .omissions
457 .iter()
458 .chain(bass_parts.iter().flat_map(|parts| parts.omissions.iter()))
459 {
460 if let Some(omission) = pitch_name_degree(&root, pitch_name)
461 && !omissions.contains(&omission)
462 {
463 omissions.push(omission);
464 }
465 }
466
467 let mut alterations = parse_alterations(&suffix_without_additions);
468 add_implicit_music21_alterations(&suffix_without_additions, &mut alterations);
469 let extensions = parse_extensions(&suffix_without_additions, &alterations);
470 let quality = parse_quality(&suffix_without_additions, &alterations);
471 let full_suffix = &body[body_parts.base.len() - suffix.len()..];
478 let (kind, modifications) = match music21_kind(full_suffix) {
479 Some((chord_type, taken)) => match music21_modifications(&full_suffix[taken..]) {
480 Some(modifications) => (Some(chord_type.kind.to_string()), modifications),
481 None => (None, Vec::new()),
482 },
483 None => (None, Vec::new()),
484 };
485
486 Ok(Self {
487 figure: trimmed.to_string(),
488 root,
489 bass,
490 quality,
491 extensions,
492 alterations,
493 omissions,
494 additions,
495 kind,
496 modifications,
497 duration: no_time(),
498 kind_text: crate::display::Drawn(None),
499 placement: crate::display::Drawn(None),
500 lyrics: Vec::new(),
501 transposed: None,
502 })
503 }
504
505 pub fn no_chord(text: Option<String>) -> Self {
509 Self {
510 figure: "N.C.".to_string(),
511 root: Pitch::from_name("C").expect("C is a pitch name"),
512 bass: None,
513 quality: ChordQuality::Major,
514 extensions: Vec::new(),
515 alterations: Vec::new(),
516 omissions: Vec::new(),
517 additions: Vec::new(),
518 kind: Some("none".to_string()),
519 modifications: Vec::new(),
520 duration: no_time(),
521 kind_text: crate::display::Drawn(Some(text.unwrap_or_else(|| "N.C.".to_string()))),
522 placement: crate::display::Drawn(None),
523 lyrics: Vec::new(),
524 transposed: None,
525 }
526 }
527
528 pub fn lyrics(&self) -> &[crate::notation::Lyric] {
530 &self.lyrics
531 }
532
533 pub fn lyrics_mut(&mut self) -> &mut Vec<crate::notation::Lyric> {
535 &mut self.lyrics
536 }
537
538 pub fn add_lyric(&mut self, text: &str, number: Option<IntegerType>, apply_raw: bool) {
541 crate::notation::verses::add(&mut self.lyrics, text, number, apply_raw);
542 }
543
544 pub fn is_no_chord(&self) -> bool {
546 self.kind.as_deref() == Some("none")
547 }
548
549 pub fn kind_text(&self) -> Option<&str> {
552 self.kind_text.0.as_deref()
553 }
554
555 pub fn set_kind_text(&mut self, text: Option<String>) {
557 self.kind_text.0 = text.filter(|text| !text.is_empty());
558 }
559
560 pub fn placement(&self) -> Option<crate::notation::Placement> {
562 self.placement.0
563 }
564
565 pub fn set_placement(&mut self, placement: Option<crate::notation::Placement>) {
567 self.placement.0 = placement;
568 }
569
570 pub fn duration(&self) -> &Duration {
574 &self.duration
575 }
576
577 pub fn set_duration(&mut self, duration: Duration) {
579 self.duration = duration;
580 }
581
582 pub fn with_duration(mut self, duration: Duration) -> Self {
584 self.duration = duration;
585 self
586 }
587
588 pub fn kind(&self) -> Option<&str> {
592 self.kind.as_deref()
593 }
594
595 pub fn figure(&self) -> &str {
597 &self.figure
598 }
599
600 pub fn root(&self) -> &Pitch {
602 &self.root
603 }
604
605 pub fn bass(&self) -> Option<&Pitch> {
607 self.bass.as_ref()
608 }
609
610 pub fn quality(&self) -> ChordQuality {
612 self.quality
613 }
614
615 pub fn extensions(&self) -> &[u8] {
617 &self.extensions
618 }
619
620 pub fn alterations(&self) -> &[ChordAlteration] {
622 &self.alterations
623 }
624
625 pub fn omissions(&self) -> &[u8] {
627 &self.omissions
628 }
629
630 pub fn additions(&self) -> &[ChordAlteration] {
632 &self.additions
633 }
634
635 pub fn find_figure(&self) -> String {
640 let mut figure = self.root.name();
641 figure.push_str(&self.quality_and_extension());
642 for alteration in &self.alterations {
643 if alteration.degree == 5 && self.fifth_is_implied() {
644 continue;
645 }
646 figure.push_str(&alteration_text(alteration));
647 }
648 for addition in &self.additions {
649 figure.push_str(&format!("add({})", alteration_text(addition)));
650 }
651 for omission in &self.omissions {
652 figure.push_str(&format!("no{omission}"));
653 }
654 if let Some(bass) = &self.bass
655 && bass.name() != self.root.name()
656 {
657 figure.push('/');
658 figure.push_str(&bass.name());
659 }
660 figure
661 }
662
663 pub fn transpose(&self, interval: &Interval) -> Result<ChordSymbol> {
667 let mut transposed = self.clone();
668 if !self.is_no_chord() {
671 let moved = self
672 .pitches()?
673 .iter()
674 .map(|pitch| interval.transpose_pitch(pitch))
675 .collect::<Result<Vec<_>>>()?;
676 transposed.transposed = Some(moved);
677 }
678 transposed.root = self.root.transpose(interval)?;
679 transposed.bass = self
680 .bass
681 .as_ref()
682 .map(|bass| bass.transpose(interval))
683 .transpose()?;
684 transposed.figure = transposed.find_figure();
685 Ok(transposed)
686 }
687
688 pub fn inversion_is_valid(&self, inversion: u8) -> bool {
696 if let Some(kind) = &self.kind {
697 return realize::inversion_is_valid_for_kind(kind, inversion);
698 }
699 let highest = self
700 .extensions
701 .iter()
702 .copied()
703 .filter(|degree| matches!(degree, 7 | 9 | 11 | 13))
704 .max()
705 .unwrap_or(5);
706 match inversion {
707 1 | 2 => true,
708 3 => highest >= 7,
709 4 => highest >= 9,
710 5 => highest >= 11,
711 _ => false,
712 }
713 }
714
715 pub fn chord_step_modifications(&self) -> &[ChordStepModification] {
718 &self.modifications
719 }
720
721 pub fn add_chord_step_modification(&mut self, modification: ChordStepModification) {
725 if !self.modifications.contains(&modification) {
726 self.modifications.push(modification);
727 }
728 self.transposed = None;
729 }
730
731 pub fn set_chord_step_modifications(&mut self, modifications: Vec<ChordStepModification>) {
733 self.modifications = modifications;
734 self.transposed = None;
735 }
736
737 pub fn from_kind(root: Pitch, kind: &str, bass: Option<Pitch>) -> Result<Self> {
741 let abbreviation = current_abbreviation_for_kind(kind)
742 .ok_or_else(|| Error::Chord(format!("no such chord kind: {kind}")))?;
743 let mut figure = format!("{}{abbreviation}", root.name());
744 if let Some(bass) = bass.as_ref().filter(|bass| bass.name() != root.name()) {
745 figure.push('/');
746 figure.push_str(&bass.name());
747 }
748 let mut symbol = Self::parse(&figure)?;
749 symbol.kind = Some(kind.to_string());
750 symbol.modifications.clear();
751 symbol.root = root;
752 if bass.is_some() {
753 symbol.bass = bass;
754 }
755 Ok(symbol)
756 }
757
758 pub fn with_kind(mut self, kind: &str) -> Self {
762 self.kind = Some(kind.to_string());
763 self.modifications.clear();
764 self.transposed = None;
765 self
766 }
767
768 pub fn set_root(&mut self, root: Pitch) {
770 self.root = root;
771 self.transposed = None;
772 }
773
774 pub fn set_bass(&mut self, bass: Option<Pitch>) {
776 self.bass = bass;
777 self.transposed = None;
778 }
779
780 fn fifth_is_implied(&self) -> bool {
781 matches!(
782 self.quality,
783 ChordQuality::Diminished | ChordQuality::Augmented | ChordQuality::HalfDiminished
784 )
785 }
786
787 fn quality_and_extension(&self) -> String {
790 let seventh_family = self
791 .extensions
792 .iter()
793 .copied()
794 .filter(|degree| matches!(degree, 7 | 9 | 11 | 13))
795 .filter(|degree| {
796 !self
797 .alterations
798 .iter()
799 .any(|alteration| alteration.degree == *degree)
800 })
801 .max()
802 .or_else(|| {
803 self.extensions
804 .iter()
805 .any(|degree| matches!(degree, 7 | 9 | 11 | 13))
806 .then_some(7)
807 });
808 let sixth = self.extensions.contains(&6);
809 let extension = |written: &str| -> String {
810 match seventh_family {
811 Some(degree) => format!("{written}{degree}"),
812 None if sixth => format!("{written}6"),
813 None => written.to_string(),
814 }
815 };
816 match self.quality {
817 ChordQuality::Major => match seventh_family {
818 Some(degree) => format!("maj{degree}"),
819 None if sixth => "6".to_string(),
820 None => String::new(),
821 },
822 ChordQuality::Minor => extension("m"),
823 ChordQuality::Dominant => seventh_family.unwrap_or(7).to_string(),
824 ChordQuality::Diminished => extension("dim"),
825 ChordQuality::Augmented => extension("aug"),
826 ChordQuality::HalfDiminished => format!("m{}b5", seventh_family.unwrap_or(7)),
827 ChordQuality::Suspended2 => match seventh_family {
828 Some(degree) => format!("{degree}sus2"),
829 None => "sus2".to_string(),
830 },
831 ChordQuality::Suspended4 => match seventh_family {
832 Some(degree) => format!("{degree}sus4"),
833 None => "sus4".to_string(),
834 },
835 ChordQuality::Power => "5".to_string(),
836 ChordQuality::Pedal => "pedal".to_string(),
837 }
838 }
839
840 pub fn to_chord(&self) -> Result<Chord> {
843 Chord::new(self.pitches()?.as_slice())
844 }
845
846 pub fn transposed_pitches(&self) -> Option<&[Pitch]> {
850 self.transposed.as_deref()
851 }
852
853 pub fn pitches(&self) -> Result<Vec<Pitch>> {
868 if self.is_no_chord() {
869 return Ok(Vec::new());
870 }
871 if let Some(moved) = &self.transposed {
872 return Ok(moved.clone());
873 }
874 if self.kind.as_deref() == Some("")
878 || (self.kind.is_some() && self.kind_notation().is_none())
879 {
880 return realize::sound_chord_kind(
881 &self.root,
882 self.kind.as_deref().unwrap_or(""),
883 self.bass.as_ref(),
884 &self.modifications,
885 );
886 }
887 match self.kind_notation() {
888 Some(notation) => {
889 let mut realized = realize::realize(
890 &self.root,
891 self.bass.as_ref(),
892 self.kind.as_deref(),
893 Some(notation),
894 None,
895 &self.modifications,
896 )?;
897 self.apply_own_reading(&mut realized.pitches, notation)?;
898 Ok(realized.pitches)
899 }
900 None => {
901 let mut intervals = self.spelled_intervals()?;
902 intervals.sort_unstable_by_key(|(degree, _)| *degree);
903 intervals.dedup();
904 let names = intervals
905 .into_iter()
906 .map(|(_, name)| {
907 Ok(Interval::from_name(&name)?
908 .transpose_pitch(&self.root)?
909 .name())
910 })
911 .collect::<Result<Vec<_>>>()?;
912 Ok(
913 realize::realize(&self.root, self.bass.as_ref(), None, None, Some(names), &[])?
914 .pitches,
915 )
916 }
917 }
918 }
919
920 fn apply_own_reading(&self, pitches: &mut Vec<Pitch>, notation: &str) -> Result<()> {
924 if !self.omissions.is_empty() {
925 for (degree, name) in notation_intervals(notation)? {
926 if !self.omissions.contains(°ree) {
927 continue;
928 }
929 let omitted = Interval::from_name(&name)?
930 .transpose_pitch(&self.root)?
931 .name();
932 pitches.retain(|pitch| pitch.name() != omitted);
933 }
934 }
935 for addition in &self.additions {
936 let (_, name) = added_interval(addition)?;
937 let pitch = Interval::from_name(name)?.transpose_pitch(&self.root)?;
938 if pitches
939 .iter()
940 .any(|existing| existing.name() == pitch.name())
941 {
942 continue;
943 }
944 let mut placed = pitch;
946 let top = pitches.last().map_or(0.0, Pitch::ps);
947 placed.set_octave(pitches.last().and_then(Pitch::octave));
948 while placed.ps() <= top {
949 let octave = placed.octave().unwrap_or(3);
950 placed.set_octave(Some(octave + 1));
951 }
952 pitches.push(placed);
953 }
954 Ok(())
955 }
956
957 fn kind_notation(&self) -> Option<&'static str> {
961 notation_for_kind(self.kind.as_deref()?)
962 }
963
964 fn spelled_intervals(&self) -> Result<Vec<(u8, String)>> {
967 let mut intervals = self.base_intervals();
968
969 let highest = self
970 .extensions
971 .iter()
972 .copied()
973 .filter(|degree| !self.alterations.iter().any(|alt| alt.degree == *degree))
974 .max()
975 .unwrap_or(0);
976 for extension in [6, 9, 11, 13] {
977 let implied = match extension {
978 9 => highest >= 11,
979 11 => highest == 13,
980 _ => false,
981 };
982 if (self.extensions.contains(&extension) || implied)
983 && !self.alterations.iter().any(|alt| alt.degree == extension)
984 && !self.omissions.contains(&extension)
985 {
986 intervals.push((extension, default_extension_interval(extension)));
987 }
988 }
989
990 for alteration in &self.alterations {
991 if alteration.degree == 5 {
992 continue;
993 }
994 let altered = altered_interval(alteration)?;
995 intervals.retain(|(degree, _)| *degree != altered.0);
999 intervals.push(altered);
1000 }
1001
1002 for addition in &self.additions {
1003 intervals.push(added_interval(addition)?);
1004 }
1005
1006 Ok(intervals
1007 .into_iter()
1008 .map(|(degree, name)| (degree, name.to_string()))
1009 .collect())
1010 }
1011
1012 fn base_intervals(&self) -> Vec<(u8, &'static str)> {
1013 let altered_fifth = self
1014 .alterations
1015 .iter()
1016 .find(|alteration| alteration.degree == 5)
1017 .and_then(|alteration| match alteration.semitones {
1018 -1 => Some("d5"),
1019 1 => Some("a5"),
1020 _ => None,
1021 });
1022
1023 let fifth = altered_fifth.unwrap_or("P5");
1024 let has_seventh = self
1025 .extensions
1026 .iter()
1027 .any(|degree| matches!(degree, 7 | 9 | 11 | 13));
1028
1029 let intervals = match self.quality {
1030 ChordQuality::Major => {
1031 if has_seventh {
1032 vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "M7")]
1033 } else {
1034 vec![(1, "P1"), (3, "M3"), (5, fifth)]
1035 }
1036 }
1037 ChordQuality::Minor => {
1038 if has_seventh {
1039 vec![(1, "P1"), (3, "m3"), (5, fifth), (7, "m7")]
1040 } else {
1041 vec![(1, "P1"), (3, "m3"), (5, fifth)]
1042 }
1043 }
1044 ChordQuality::Dominant => vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "m7")],
1045 ChordQuality::Diminished => {
1046 if has_seventh {
1047 vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "d7")]
1048 } else {
1049 vec![(1, "P1"), (3, "m3"), (5, "d5")]
1050 }
1051 }
1052 ChordQuality::Augmented => vec![(1, "P1"), (3, "M3"), (5, "a5")],
1053 ChordQuality::HalfDiminished => vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "m7")],
1054 ChordQuality::Suspended2 => vec![(1, "P1"), (2, "M2"), (5, fifth)],
1055 ChordQuality::Suspended4 => vec![(1, "P1"), (4, "P4"), (5, fifth)],
1056 ChordQuality::Power => vec![(1, "P1"), (5, fifth)],
1057 ChordQuality::Pedal => vec![(1, "P1")],
1058 };
1059
1060 intervals
1061 .into_iter()
1062 .filter(|(degree, _)| !self.omissions.contains(degree))
1063 .collect()
1064 }
1065}
1066
1067pub fn realize_chord_symbol_durations(stream: &mut Stream) {
1099 let lengths = chord_symbol_durations(stream);
1100 let mut position = 0;
1101 stream.for_each_mut(&mut |_, element| {
1102 if let StreamElement::ChordSymbol(symbol) = element
1103 && let Some((_, length)) = lengths.iter().find(|(at, _)| *at == position)
1104 && let Ok(duration) = Duration::new(*length)
1105 {
1106 symbol.set_duration(duration);
1107 }
1108 position += 1;
1109 });
1110}
1111
1112pub fn chord_symbol_durations(stream: &Stream) -> Vec<(usize, FloatType)> {
1130 let end = stream.end_offset();
1131 let mut symbols: Vec<(FloatType, usize)> = stream
1132 .leaves()
1133 .iter()
1134 .enumerate()
1135 .filter(|(_, (_, element))| matches!(element, StreamElement::ChordSymbol(_)))
1136 .map(|(position, (offset, _))| (*offset, position))
1137 .collect();
1138 symbols.sort_by(|left, right| left.0.total_cmp(&right.0));
1140 symbols
1141 .iter()
1142 .enumerate()
1143 .map(|(index, &(offset, position))| {
1144 let until = symbols.get(index + 1).map_or(end, |next| next.0);
1145 (position, (until - offset).max(0.0))
1146 })
1147 .collect()
1148}
1149
1150impl FromStr for ChordSymbol {
1151 type Err = Error;
1152
1153 fn from_str(value: &str) -> Result<Self> {
1154 Self::parse(value)
1155 }
1156}
1157
1158impl TryFrom<&str> for ChordSymbol {
1159 type Error = Error;
1160
1161 fn try_from(value: &str) -> Result<Self> {
1162 Self::parse(value)
1163 }
1164}
1165
1166impl TryFrom<String> for ChordSymbol {
1167 type Error = Error;
1168
1169 fn try_from(value: String) -> Result<Self> {
1170 Self::parse(value)
1171 }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176
1177 #[test]
1181 fn a_transposed_symbol_moves_each_pitch() {
1182 let symbol = ChordSymbol::parse_music21("B").unwrap();
1183 let interval = crate::interval::Interval::from_semitones(-12).unwrap();
1184 let moved = symbol.transpose(&interval).unwrap();
1185 let names: Vec<String> = moved
1186 .pitches()
1187 .unwrap()
1188 .iter()
1189 .map(Pitch::name_with_octave)
1190 .collect();
1191 assert_eq!(names, ["B1", "Eb2", "F#2"]);
1192 }
1193
1194 #[test]
1199 fn the_voicing_keeps_music21s_root_and_bass() {
1200 let voiced = |figure: &str| {
1201 let symbol = ChordSymbol::parse_music21(figure).unwrap();
1202 let kind = symbol.kind().unwrap_or_default();
1203 voice_chord_notation(
1204 symbol.root(),
1205 kind,
1206 notation_for_kind(resolve_kind_alias(kind)),
1207 symbol.bass(),
1208 symbol.chord_step_modifications(),
1209 )
1210 .unwrap()
1211 };
1212 let a = voiced("A10/C");
1213 assert_eq!(a.root().name_with_octave(), "A3");
1214 assert_eq!(a.bass().name_with_octave(), "C2");
1215 let names: Vec<String> = a.pitches().iter().map(Pitch::name_with_octave).collect();
1216 assert_eq!(names, ["C2", "A2", "A3"]);
1217 assert_eq!(voiced("Ab10/F#").root().name_with_octave(), "A3");
1218
1219 let flat_one = voiced("Bb10/B-");
1222 assert_eq!(flat_one.bass().name_with_octave(), "Bb1");
1223 assert_eq!(voiced("Ab10/A-").bass().name_with_octave(), "Ab1");
1224 }
1225
1226 #[test]
1227 fn a_slash_bass_is_voiced_as_music21_voices_it() {
1228 let cases: [(&str, &[&str]); 20] = [
1229 ("B-5/E", &["E2", "E2", "Bb3", "F4"]),
1230 ("B-69/E", &["E2", "E2", "Bb3", "G4", "C5"]),
1231 ("C5/G", &["G3", "G3", "C4"]),
1232 ("C5/D", &["D2", "D2", "C3", "G3"]),
1233 ("C69/D", &["D2", "D2", "C3", "A3", "D4"]),
1234 ("C5/C#", &["C#2", "C#2", "C3", "G3"]),
1235 ("F5/E", &["E2", "E2", "F3", "C4"]),
1236 ("A-10/E", &["E3", "Ab3", "Ab3"]),
1237 ("A#10/E", &["E3", "A#3", "A#3"]),
1238 ("C10/G", &["C3", "G3", "C4"]),
1239 ("C35/B", &["B2", "B2", "C3", "E3", "G3"]),
1240 ("C/B-", &["Bb2", "C3", "E3", "G3"]),
1241 ("C7/D", &["D2", "C3", "E3", "G3", "Bb3"]),
1242 ("Cm/E-", &["Eb3", "G3", "C4"]),
1243 ("C9/E", &["E3", "G3", "Bb3", "C4", "D4"]),
1244 ("C13/B-", &["Bb2", "C3", "D3", "E3", "F3", "G3", "A3"]),
1245 ("Cadd9/D", &["D2", "C3", "E3", "G3", "D4"]),
1246 ("C6/D", &["D2", "C3", "E3", "G3", "A3"]),
1247 ("C69/E", &["E3", "A3", "C4", "D4"]),
1248 ("C5/E", &["E3", "G3", "C4"]),
1249 ];
1250 for (figure, voiced) in cases {
1251 let symbol = ChordSymbol::parse_music21(figure).unwrap();
1252 let names: Vec<String> = symbol
1253 .pitches()
1254 .unwrap()
1255 .iter()
1256 .map(Pitch::name_with_octave)
1257 .collect();
1258 assert_eq!(names, voiced, "{figure}");
1259 }
1260 }
1261
1262 #[test]
1266 fn degrees_are_read_as_music21_reads_them() {
1267 type Adds = &'static [(u8, IntegerType)];
1268 let cases: [(&str, &str, Adds); 8] = [
1269 ("Cb-7", "", &[(7, -1)]),
1270 ("Cb+7", "", &[(7, -1)]),
1271 ("C7b-9", "", &[(7, 0), (9, -1)]),
1272 ("C9b-13", "", &[(9, 0), (13, -1)]),
1273 ("C10", "", &[(1, 0)]),
1274 ("C#10", "", &[(1, 0)]),
1275 ("Cb10", "major", &[(1, -1)]),
1276 ("Abb7", "", &[(7, -2)]),
1277 ];
1278 for (figure, kind, adds) in cases {
1279 let symbol = ChordSymbol::parse_music21(figure).unwrap();
1280 assert_eq!(symbol.kind(), Some(kind), "{figure}");
1281 let read: Vec<(u8, IntegerType)> = symbol
1282 .chord_step_modifications()
1283 .iter()
1284 .map(|modification| {
1285 let semitones = modification.interval().semitones() as IntegerType;
1286 (modification.degree(), semitones)
1287 })
1288 .collect();
1289 assert_eq!(read, adds, "{figure}");
1290 }
1291 }
1292
1293 #[test]
1296 fn a_modification_takes_every_accidental_before_its_degree() {
1297 for (figure, semitones) in [("G7addb9", -1), ("G7add-9", -1), ("G7addbb9", -2)] {
1298 let symbol = ChordSymbol::parse_music21(figure).unwrap();
1299 let modification = &symbol.chord_step_modifications()[0];
1300 assert_eq!(modification.degree(), 9, "{figure}");
1301 assert_eq!(
1302 modification.interval().semitones() as IntegerType,
1303 semitones,
1304 "{figure}"
1305 );
1306 }
1307 }
1308
1309 #[test]
1312 fn additions_and_omissions_are_written_as_degrees_that_read_back() {
1313 use super::chord_symbol_figure_from_chord;
1314 for (notes, expected) in [
1315 ("G3 B3 D4 F4 A-4 C5 E-5", "G7addb9add11addb13"),
1316 ("E2 B2 G#3 D4 F##4", "E7add#9"),
1317 ("B-2 F3 A3 D4 E4", "Bbmaj7add#11"),
1318 ("A-2 E-3 G-3 C4 D4 B-4", "Ab9add#11"),
1319 ("A2 E3 A3 B3 C4 E4", "Amadd9"),
1320 ("D4 E4 F#4 A4", "Dadd9"),
1321 ("D3 A3 F#4 E5", "Dadd9"),
1322 ("G B D F A C# E", "C#øb9addb11addb13"),
1323 ("C E G B- D- F#", "C7addb9add#11"),
1324 ("C E G A D", "Am7add11/C"),
1325 ("C# F# B D-", "C#susaddb7addbb9omit5"),
1326 ("D-3 F#3 A-3", "Dbsusadd#3omit4"),
1327 ("F#2 C#3 E-3 A3", "F#m6addbb7omit6"),
1328 ] {
1329 let chord = Chord::new(notes).unwrap();
1330 let figure = chord_symbol_figure_from_chord(&chord).unwrap().unwrap();
1331 assert_eq!(figure, expected, "{notes}");
1332 let respell = |name: &str| {
1335 let mut chars = name.chars();
1336 let Some(first) = chars.next() else {
1337 return String::new();
1338 };
1339 let rest = chars.as_str();
1340 let flats = rest.chars().take_while(|c| *c == 'b').count();
1341 format!("{first}{}{}", "-".repeat(flats), &rest[flats..])
1342 };
1343 let spelled = match figure.split_once('/') {
1344 Some((root, bass)) => format!("{}/{}", respell(root), respell(bass)),
1345 None => respell(&figure),
1346 };
1347 let sounded: BTreeSet<String> = ChordSymbol::parse_music21(spelled)
1348 .unwrap()
1349 .pitches()
1350 .unwrap()
1351 .iter()
1352 .map(Pitch::name)
1353 .collect();
1354 let written: BTreeSet<String> = chord.pitches().iter().map(Pitch::name).collect();
1355 assert_eq!(sounded, written, "{notes}");
1356 }
1357 assert_eq!(super::alter_to_chord_symbol_string(-1), "b");
1358 assert_eq!(super::alter_to_chord_symbol_string(2), "##");
1359 assert_eq!(super::alter_to_chord_symbol_string(0), "");
1360 }
1361
1362 #[test]
1365 fn a_root_with_an_unspellable_accidental_is_an_accidental_error() {
1366 for figure in ["C#-7", "C##-", "F#-7/E"] {
1367 let refused = ChordSymbol::parse_music21(figure).unwrap_err();
1368 assert!(
1369 matches!(refused, Error::Accidental(_)),
1370 "{figure}: {refused:?}"
1371 );
1372 }
1373 assert!(ChordSymbol::parse_music21("H7").is_err());
1374 }
1375 #[test]
1378 fn every_music21_kind_realizes_its_own_notation() {
1379 use super::{ChordSymbol, MUSIC21_CHORD_TYPES};
1380 use crate::pitch::Accidental;
1381 use crate::{Key, Pitch};
1382 use std::collections::BTreeSet;
1383
1384 let major = Key::from_tonic("C").unwrap();
1385 for chord_type in MUSIC21_CHORD_TYPES {
1386 let expected: BTreeSet<String> = chord_type
1387 .notation
1388 .split(',')
1389 .map(|token| {
1390 let degree: usize = token.trim_matches(['#', '-']).parse().unwrap();
1391 let alter =
1392 token.matches('#').count() as f64 - token.matches('-').count() as f64;
1393 let mut pitch = major.pitch_from_degree((degree - 1) % 7 + 1).unwrap();
1394 if alter != 0.0 {
1395 pitch.set_accidental(Accidental::new(alter).unwrap());
1396 }
1397 pitch.name()
1398 })
1399 .collect();
1400 for abbreviation in chord_type.abbreviations {
1401 if music21_kind(abbreviation).is_none_or(|(read, _)| read.kind != chord_type.kind) {
1406 continue;
1407 }
1408 let symbol = ChordSymbol::parse(format!("C{abbreviation}")).unwrap();
1409 assert_eq!(symbol.kind(), Some(chord_type.kind), "C{abbreviation}");
1410 let names: BTreeSet<String> = symbol
1411 .to_chord()
1412 .unwrap()
1413 .pitch_names()
1414 .into_iter()
1415 .collect();
1416 assert_eq!(names, expected, "C{abbreviation} ({})", chord_type.kind);
1417 }
1418 }
1419 assert_eq!(
1422 ChordSymbol::parse("C7#11").unwrap().kind(),
1423 Some("dominant-seventh")
1424 );
1425 assert_eq!(
1426 ChordSymbol::parse("CN6/E")
1427 .unwrap()
1428 .to_chord()
1429 .unwrap()
1430 .pitch_names(),
1431 ["E", "Gb", "C", "Db"]
1432 );
1433 let _ = Pitch::from_name("C").unwrap();
1434 }
1435
1436 #[test]
1439 fn figures_are_written_as_music21_writes_them() {
1440 use super::{
1441 ChordSymbolFigure, chord_symbol_figure_from_chord, chord_symbol_kind_from_chord,
1442 };
1443 use crate::{Chord, Pitch};
1444
1445 let figure =
1446 |notes: &str| chord_symbol_figure_from_chord(&Chord::new(notes).unwrap()).unwrap();
1447 let kind = |notes: &str| chord_symbol_kind_from_chord(&Chord::new(notes).unwrap());
1448 assert_eq!(figure("F3 A3 C#4 E-4 G4 B-4").as_deref(), Some("F+11"));
1449 assert_eq!(kind("F3 A3 C#4 E-4 G4 B-4"), Some("augmented-11th"));
1450 assert_eq!(figure("C3 E3 B3 D4").as_deref(), Some("CM9"));
1451 assert_eq!(figure("C3 D-3 E3 G-3").as_deref(), Some("CN6"));
1452 assert_eq!(kind("C3 D-3 E3 G-3"), Some("Neapolitan"));
1453 assert_eq!(figure("C3 D3 G3").as_deref(), Some("Csus2"));
1454 assert_eq!(figure("C3 E3 G3 D-4").as_deref(), Some("Caddb9"));
1455 assert_eq!(figure("C3").as_deref(), Some("Cpedal"));
1456 assert_eq!(figure("").as_deref(), Some(""));
1457
1458 let inverted = Chord::new("C3 F3 G3").unwrap();
1460 let parts = ChordSymbolFigure::from_chord(&inverted).unwrap();
1461 assert_eq!(parts.root, "C");
1462 assert_eq!(
1463 (parts.kind, parts.abbreviation, parts.bass.as_deref()),
1464 ("suspended-fourth", "sus", None)
1465 );
1466 assert_eq!(parts.to_string(), "Csus");
1467 assert_eq!(parts.written_with("sus4"), "Csus4");
1468
1469 let with_root = |notes: &str, root: &str| {
1471 ChordSymbolFigure::from_chord_with_root(
1472 &Chord::new(notes).unwrap(),
1473 &Pitch::from_name(root).unwrap(),
1474 )
1475 .unwrap()
1476 };
1477 assert_eq!(with_root("C3 F#3 A-3", "C3").to_string(), "CIt+6");
1478 assert_eq!(with_root("C3 D3 F#3 A-3", "C3").to_string(), "CFr+6");
1479 assert_eq!(with_root("C3 E-3 F#3 A-3", "C3").to_string(), "CGr+6");
1480 assert_eq!(with_root("F2 B2 D#3 G#3", "F2").to_string(), "Ftristan");
1481 assert_eq!(with_root("F2 B2 D#3 G#3", "F2").kind, "Tristan");
1482
1483 let parts = ChordSymbolFigure::from_chord(&Chord::new("E3 G3 C4 B-4").unwrap()).unwrap();
1486 assert_eq!(parts.to_string(), "C7/E");
1487 assert_eq!(figure("D3 C4 E-4 G4").as_deref(), Some("Cm/D"));
1489 assert!(ChordSymbolFigure::from_chord(&Chord::new("C4 C~4").unwrap()).is_none());
1490 assert!(ChordSymbolFigure::from_chord(&Chord::new("").unwrap()).is_none());
1491 }
1492
1493 #[test]
1494 fn a_symbol_is_parsed_through_try_from_and_reports_its_alterations() {
1495 use super::{ChordSymbol, chord_symbol_kind_from_chord};
1496 use crate::chord::Chord;
1497
1498 let symbol = ChordSymbol::try_from("C7#11").unwrap();
1499 assert_eq!(symbol.alterations().len(), 1);
1500 assert_eq!(symbol.alterations()[0].degree(), 11);
1501 assert_eq!(symbol.alterations()[0].semitones(), 1);
1502 assert!(symbol.omissions().is_empty());
1503 let omitting = ChordSymbol::try_from("C[no3]".to_string()).unwrap();
1504 assert_eq!(omitting.omissions(), [3]);
1505 assert!(ChordSymbol::try_from("").is_err());
1506
1507 assert_eq!(
1508 chord_symbol_kind_from_chord(&Chord::new("C").unwrap()),
1509 Some("pedal")
1510 );
1511 assert_eq!(
1512 chord_symbol_kind_from_chord(&Chord::new("C G").unwrap()),
1513 Some("power")
1514 );
1515 assert_eq!(
1516 chord_symbol_kind_from_chord(&Chord::new("C E G").unwrap()),
1517 Some("major")
1518 );
1519 assert_eq!(chord_symbol_kind_from_chord(&Chord::new("").unwrap()), None);
1520 }
1521
1522 #[test]
1523 fn chord_symbol_figures_match_music21() {
1524 let cases: [(&str, Option<&str>); 26] = [
1525 ("C4 E4 G4", Some("C")),
1526 ("C4 E-4 G4", Some("Cm")),
1527 ("C4 E4 G#4", Some("C+")),
1528 ("C4 E-4 G-4", Some("Cdim")),
1529 ("C4 E4 G4 B-4", Some("C7")),
1530 ("C4 E4 G4 B4", Some("Cmaj7")),
1531 ("C4 E-4 G4 B-4", Some("Cm7")),
1532 ("C4 E-4 G-4 B--4", Some("Co7")),
1533 ("C4 E-4 G-4 B-4", Some("C\u{00f8}7")),
1534 ("E4 G4 C5", Some("C/E")),
1535 ("G3 C4 E4", Some("C/G")),
1536 ("E4 G4 B-4 C5", Some("C7/E")),
1537 ("C4 E4 G4 B-4 D5", Some("C9")),
1538 ("C4 E4 G4 B4 D5", Some("CM9")),
1539 ("C4 D4 G4", Some("Csus2")),
1540 ("C4 F4 G4", Some("Csus")),
1541 ("C4", Some("Cpedal")),
1542 ("C4 G4", Some("Cpower")),
1543 ("C4 E4 G4 B-4 D5 F5", Some("C11")),
1544 ("C4 E4 G4 B-4 D5 F5 A5", Some("C13")),
1545 ("C4 E4 G4 A4", Some("Am7/C")),
1546 ("C4 E-4 G4 A4", Some("A\u{00f8}7/C")),
1547 ("C4 E4 G4 D5", Some("Cadd9")),
1548 ("F4 A4 C5 D5", Some("Dm7/F")),
1549 ("B3 D4 F4", Some("Bdim")),
1550 ("C4 D-4 E4", None),
1551 ];
1552 for (notes, expected) in cases {
1553 let chord = Chord::new(notes).unwrap();
1554 assert_eq!(
1555 chord_symbol_figure_from_chord(&chord).unwrap().as_deref(),
1556 expected,
1557 "{notes}"
1558 );
1559 }
1560 assert_eq!(
1561 chord_symbol_figure_from_chord(&Chord::empty())
1562 .unwrap()
1563 .as_deref(),
1564 Some("")
1565 );
1566
1567 let symbol = chord_symbol_from_chord(&Chord::new("E4 G4 B-4 C5").unwrap())
1568 .unwrap()
1569 .unwrap();
1570 assert_eq!(symbol.figure(), "C7/E");
1571 assert_eq!(symbol.root().name(), "C");
1572 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("E"));
1573 assert!(
1574 chord_symbol_from_chord(&Chord::new("C4 D-4 E4").unwrap())
1575 .unwrap()
1576 .is_none()
1577 );
1578 assert!(chord_symbol_from_chord(&Chord::empty()).unwrap().is_none());
1579 }
1580
1581 #[test]
1582 fn kind_lookups_match_music21() {
1583 assert_eq!(
1584 abbreviations_for_kind("dominant-seventh"),
1585 Some(&["7", "dom7"][..])
1586 );
1587 assert_eq!(notation_for_kind("dominant-seventh"), Some("1,3,5,-7"));
1588 assert_eq!(current_abbreviation_for_kind("dominant-seventh"), Some("7"));
1589 assert_eq!(abbreviations_for_kind("major"), Some(&["", "M", "maj"][..]));
1590 assert_eq!(current_abbreviation_for_kind("major"), Some(""));
1591 assert_eq!(abbreviations_for_kind("nonsense"), None);
1592 for chord_type in known_chord_symbol_types() {
1593 assert_eq!(
1594 chord_type.abbreviations.first(),
1595 Some(&chord_type.abbreviation)
1596 );
1597 }
1598 }
1599
1600 #[test]
1601 fn find_figure_writes_one_canonical_spelling_that_parses_back() {
1602 let cases = [
1603 ("C", "C", "D"),
1604 ("Cm7", "Cm7", "Dm7"),
1605 ("F#dim", "F#dim", "G#dim"),
1606 ("Bb/D", "Bb/D", "C/E"),
1607 ("G7/B", "G7/B", "A7/C#"),
1608 ("Am/C", "Am/C", "Bm/D"),
1609 ("Cmaj7", "Cmaj7", "Dmaj7"),
1610 ("Dm7b5", "Dm7b5", "Em7b5"),
1611 ("C\u{00f8}7", "Cm7b5", "Dm7b5"),
1612 ("E7#9", "E7#9", "F#7#9"),
1613 ("Csus4", "Csus4", "Dsus4"),
1614 ("G7sus4", "G7sus4", "A7sus4"),
1615 ("Cadd(9)", "Cadd(9)", "Dadd(9)"),
1616 ("C6", "C6", "D6"),
1617 ("Cm6", "Cm6", "Dm6"),
1618 ("Cdim7", "Cdim7", "Ddim7"),
1619 ("Caug", "Caug", "Daug"),
1620 ("C+", "Caug", "Daug"),
1621 ("C9", "C9", "D9"),
1622 ("Cmaj7#11", "Cmaj7#11", "Dmaj7#11"),
1623 ("C5", "C5", "D5"),
1624 ("Cno3", "Cno3", "Dno3"),
1625 ];
1626 let whole_tone = Interval::from_name("M2").unwrap();
1627 for (typed, canonical, transposed) in cases {
1628 let symbol = ChordSymbol::parse(typed).unwrap();
1629 assert_eq!(symbol.find_figure(), canonical, "{typed}");
1630 let reparsed = ChordSymbol::parse(symbol.find_figure()).unwrap();
1631 assert_eq!(reparsed.quality(), symbol.quality(), "{typed}");
1632 assert_eq!(
1633 reparsed.to_chord().unwrap().pitch_names(),
1634 symbol.to_chord().unwrap().pitch_names(),
1635 "{typed}"
1636 );
1637 let up = symbol.transpose(&whole_tone).unwrap();
1638 assert_eq!(up.figure(), transposed, "{typed}");
1639 assert_eq!(up.find_figure(), transposed, "{typed}");
1640 }
1641 }
1642
1643 #[test]
1644 fn inversion_validity_follows_the_chord_size() {
1645 let valid = |figure: &str| -> Vec<u8> {
1646 let symbol = ChordSymbol::parse(figure).unwrap();
1647 (0..=6).filter(|n| symbol.inversion_is_valid(*n)).collect()
1648 };
1649 assert_eq!(valid("C"), [1, 2]);
1650 assert_eq!(valid("C6"), [1, 2]);
1651 assert_eq!(valid("Cm7"), [1, 2, 3]);
1652 assert_eq!(valid("C9"), [1, 2, 3, 4]);
1653 assert_eq!(valid("C13"), [1, 2, 3, 4, 5]);
1654 assert_eq!(valid("E7#9"), [1, 2, 3]);
1657 assert_eq!(valid("Cmaj11"), [1, 2, 3, 4, 5]);
1660 }
1661 use super::*;
1662
1663 #[test]
1664 fn parses_major_seventh_symbol() {
1665 let symbol: ChordSymbol = "Cmaj7".parse().unwrap();
1666 assert_eq!(symbol.root().name(), "C");
1667 assert_eq!(symbol.quality(), ChordQuality::Major);
1668 assert_eq!(symbol.extensions(), &[7]);
1669 assert_eq!(
1670 symbol.to_chord().unwrap().pitched_common_name(),
1671 "C-major seventh chord"
1672 );
1673 }
1674
1675 #[test]
1676 fn parses_half_diminished_symbol() {
1677 let symbol = ChordSymbol::parse("F#m7b5").unwrap();
1678 assert_eq!(symbol.root().name(), "F#");
1679 assert_eq!(symbol.quality(), ChordQuality::HalfDiminished);
1680 assert_eq!(
1681 symbol.to_chord().unwrap().pitched_common_name(),
1682 "F#-half-diminished seventh chord"
1683 );
1684 }
1685
1686 #[test]
1687 fn parses_dominant_altered_symbol() {
1688 let symbol = ChordSymbol::parse("Bb7#11").unwrap();
1689 assert_eq!(symbol.root().name(), "Bb");
1690 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1691 assert_eq!(symbol.extensions(), &[7, 11]);
1692 assert_eq!(symbol.alterations()[0], ChordAlteration::new(11, 1));
1693 let names = symbol
1694 .to_chord()
1695 .unwrap()
1696 .pitches()
1697 .iter()
1698 .map(Pitch::name)
1699 .collect::<Vec<_>>();
1700 assert_eq!(names, vec!["Bb", "D", "F", "Ab", "E"]);
1701 }
1702
1703 #[test]
1704 fn parses_added_tones_without_changing_the_base_chord() {
1705 let symbol = ChordSymbol::parse("Cdim9 add(#5)").unwrap();
1706 assert_eq!(symbol.extensions(), &[9]);
1707 assert_eq!(symbol.additions(), &[ChordAlteration::new(5, 1)]);
1708 assert_eq!(
1709 symbol.to_chord().unwrap().pitch_classes(),
1710 vec![0, 2, 3, 6, 8, 9]
1711 );
1712 }
1713
1714 #[test]
1715 fn parses_altered_dominant_with_slash_bass() {
1716 let symbol = ChordSymbol::parse("D7b9#11/C").unwrap();
1717 assert_eq!(symbol.root().name(), "D");
1718 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1719 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1720 assert_eq!(symbol.extensions(), &[7, 9, 11]);
1721 assert_eq!(
1722 symbol.alterations(),
1723 &[ChordAlteration::new(9, -1), ChordAlteration::new(11, 1)]
1724 );
1725 assert_eq!(
1726 symbol.to_chord().unwrap().pitch_classes(),
1727 vec![0, 2, 3, 6, 8, 9]
1728 );
1729 }
1730
1731 #[test]
1732 fn parses_music21_pitch_name_additions() {
1733 let symbol = ChordSymbol::parse("Ddom7dim5/CaddA,E-").unwrap();
1734
1735 assert_eq!(symbol.root().name(), "D");
1736 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1737 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1738 assert_eq!(symbol.alterations(), &[ChordAlteration::new(5, -1)]);
1739 assert_eq!(
1740 symbol.additions(),
1741 &[ChordAlteration::new(5, 0), ChordAlteration::new(9, -1)]
1742 );
1743 assert_eq!(
1744 symbol.to_chord().unwrap().pitch_classes(),
1745 vec![0, 2, 3, 6, 8, 9]
1746 );
1747 }
1748
1749 #[test]
1750 fn generates_petrushka_chord_symbol_name() {
1751 let chord = Chord::new("C4 D4 Eb4 F#4 Ab4 A4").unwrap();
1752 let names = chord_symbol_spellings(&chord);
1753
1754 assert_eq!(
1755 names.first().map(String::as_str),
1756 Some("Ddom7dim5add5addb9/C")
1757 );
1758 }
1759
1760 #[test]
1761 fn generates_common_chord_symbols() {
1762 let major_seventh = Chord::new("C E G B").unwrap();
1763 let dominant_ninth = Chord::new("C E G B- D").unwrap();
1764
1765 assert_eq!(
1766 chord_symbol_spellings(&major_seventh)
1767 .first()
1768 .map(String::as_str),
1769 Some("Cmaj7")
1770 );
1771 assert_eq!(
1772 chord_symbol_spellings(&dominant_ninth)
1773 .first()
1774 .map(String::as_str),
1775 Some("C9")
1776 );
1777 }
1778
1779 #[test]
1780 fn split_third_triads_do_not_spell_lower_third_as_sharp_nine() {
1781 let split_third = Chord::new("D4 A4 F#4 F4").unwrap();
1782 let names = chord_symbol_spellings(&split_third);
1783
1784 assert_eq!(names.first().map(String::as_str), Some("Daddb3"));
1785 assert!(!names.iter().any(|name| name == "D add(#9)"));
1786 }
1787
1788 #[test]
1789 fn altered_dominants_write_additions_as_degrees() {
1790 let altered_dominant = Chord::new("C4 E4 G4 Bb4 Eb5").unwrap();
1791 let names = chord_symbol_spellings(&altered_dominant);
1792
1793 assert_eq!(names.first().map(String::as_str), Some("C7addb3"));
1794 }
1795
1796 #[test]
1797 fn unrecognized_music21_figures_return_no_symbol() {
1798 let chord = Chord::new("F4 C5 D5 E-5").unwrap();
1799 let names = chord_symbol_spellings(&chord);
1800
1801 assert!(names.is_empty());
1802 }
1803
1804 #[test]
1805 fn generates_music21_figures_with_explicit_root() {
1806 let major_triad = Chord::new("G3 C4 E4").unwrap();
1807 let dominant_seventh = Chord::new("G3 B-3 C4 E4").unwrap();
1808 let power_chord = Chord::new("C4 G4").unwrap();
1809 let unsupported_dyad = Chord::new("C4 A4").unwrap();
1810
1811 assert_eq!(
1812 chord_symbol_spellings_with_root(&major_triad, 0)
1813 .first()
1814 .map(String::as_str),
1815 Some("C/G")
1816 );
1817 assert_eq!(
1818 chord_symbol_spellings_with_root(&dominant_seventh, 0)
1819 .first()
1820 .map(String::as_str),
1821 Some("C7/G")
1822 );
1823 assert_eq!(
1824 chord_symbol_spellings_with_root(&power_chord, 0)
1825 .first()
1826 .map(String::as_str),
1827 Some("Cpower")
1828 );
1829 assert!(chord_symbol_spellings_with_root(&unsupported_dyad, 0).is_empty());
1830 }
1831
1832 #[test]
1833 fn dense_sets_follow_music21_fallback_matching() {
1834 let chord = Chord::new("C4 D-4 E-4 E4 F#4 G4 A-4 A4").unwrap();
1835
1836 assert_eq!(
1837 chord_symbol_spellings(&chord).first().map(String::as_str),
1838 Some("Csusaddb3add3add#4addb9addb13add13omit4")
1839 );
1840 }
1841
1842 #[test]
1845 fn chord_symbols_hold_until_the_next_one_across_bars() {
1846 use crate::{Note, Stream, StreamElement, StreamKind};
1847
1848 let bar = |figure: &str, at: FloatType| {
1849 let mut measure = Stream::with_kind(StreamKind::Measure);
1850 measure.insert(at, ChordSymbol::parse(figure).unwrap());
1851 for _ in 0..4 {
1854 measure.push(Note::from_name("C4").unwrap());
1855 }
1856 measure
1857 };
1858 let held = |stream: &Stream| -> Vec<FloatType> {
1859 stream
1860 .flatten()
1861 .events()
1862 .iter()
1863 .filter(|event| matches!(event.element(), StreamElement::ChordSymbol(_)))
1864 .map(|event| event.element().quarter_length())
1865 .collect()
1866 };
1867
1868 let mut part = Stream::with_kind(StreamKind::Part);
1869 part.insert(0.0, bar("C", 0.0));
1870 part.insert(4.0, bar("G7", 1.5));
1871 part.insert(8.0, bar("Am", 2.0));
1872 assert_eq!(held(&part), [0.0, 0.0, 0.0]);
1873 realize_chord_symbol_durations(&mut part);
1874 assert_eq!(held(&part), [5.5, 4.5, 4.0]);
1875
1876 let mut stream = Stream::new();
1878 stream.insert(1.0, ChordSymbol::parse("C").unwrap());
1879 for beat in 1..7 {
1880 stream.insert(FloatType::from(beat), Note::from_name("C4").unwrap());
1881 }
1882 realize_chord_symbol_durations(&mut stream);
1883 assert_eq!(held(&stream), [6.0]);
1884
1885 let mut bare = Stream::new();
1887 bare.push(Note::from_name("C4").unwrap());
1888 realize_chord_symbol_durations(&mut bare);
1889 assert_eq!(bare.len(), 1);
1890 }
1891}