1use crate::{
19 chord::Chord,
20 error::{Error, Result},
21 interval::Interval,
22 pitch::Pitch,
23};
24
25use super::enharmonics::{EnharmonicRules, best_spelling};
26
27const TRIAD_FORTE_CLASS: &str = "3-11";
29
30const HEXATONIC_CYCLE: [Transform; 6] = [
32 Transform::P,
33 Transform::L,
34 Transform::P,
35 Transform::L,
36 Transform::P,
37 Transform::L,
38];
39
40const SLIDE: [Transform; 3] = [Transform::L, Transform::P, Transform::R];
42
43const NEBENVERWANDT: [Transform; 3] = [Transform::R, Transform::L, Transform::P];
45
46#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum Transform {
50 L,
53 P,
55 R,
58}
59
60#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub enum ChainOrder {
64 #[default]
66 LeftToRight,
67 RightToLeft,
70}
71
72#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub enum Respell {
76 #[default]
78 Keep,
79 Simplify,
81}
82
83#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum Mediant {
87 UpperFlat,
89 UpperSharp,
91 LowerFlat,
93 LowerSharp,
95}
96
97#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub enum MediantSide {
102 Upper,
104 Lower,
106}
107
108#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum HexatonicSystem {
113 Northern,
115 Eastern,
117 Southern,
119 Western,
121}
122
123impl Transform {
124 pub fn from_symbol(symbol: char) -> Result<Self> {
130 match symbol {
131 'L' => Ok(Self::L),
132 'P' => Ok(Self::P),
133 'R' => Ok(Self::R),
134 other => Err(Error::Analysis(format!(
135 "{other} is not a NeoRiemannian transformation (L, R, or P)"
136 ))),
137 }
138 }
139
140 pub fn symbol(self) -> char {
142 match self {
143 Self::L => 'L',
144 Self::P => 'P',
145 Self::R => 'R',
146 }
147 }
148
149 pub fn parse_chain(symbols: &str) -> Result<Vec<Self>> {
155 symbols.chars().map(Self::from_symbol).collect()
156 }
157
158 pub fn apply(self, chord: &Chord) -> Result<Chord> {
175 let (interval, changing) = if chord.is_major_triad() {
176 match self {
177 Self::L => ("-m2", chord.root()),
178 Self::P => ("-A1", chord.third()),
179 Self::R => ("M2", chord.fifth()),
180 }
181 } else if chord.is_minor_triad() {
182 match self {
183 Self::L => ("m2", chord.fifth()),
184 Self::P => ("A1", chord.third()),
185 Self::R => ("-M2", chord.root()),
186 }
187 } else {
188 return Err(self.not_a_triad());
189 };
190
191 let changing = changing.ok_or_else(|| self.not_a_triad())?;
192 single_pitch_transform(chord, &Interval::from_name(interval)?, changing)
193 }
194
195 fn not_a_triad(self) -> Error {
197 let modes = match self {
198 Self::L => "major or minor",
199 Self::P | Self::R => "Major or Minor",
200 };
201 Error::Analysis(format!(
202 "Cannot perform {} on this chord: not a {modes} triad",
203 self.symbol()
204 ))
205 }
206}
207
208impl Mediant {
209 pub const ALL: [Self; 4] = [
211 Self::UpperFlat,
212 Self::UpperSharp,
213 Self::LowerFlat,
214 Self::LowerSharp,
215 ];
216
217 pub fn from_code(code: &str) -> Result<Self> {
223 Self::ALL
224 .into_iter()
225 .find(|mediant| mediant.code() == code)
226 .ok_or_else(|| {
227 Error::Value(
228 "Transformation must be one of ['UFM', 'USM', 'LFM', 'LSM']".to_string(),
229 )
230 })
231 }
232
233 pub fn code(self) -> &'static str {
235 match self {
236 Self::UpperFlat => "UFM",
237 Self::UpperSharp => "USM",
238 Self::LowerFlat => "LFM",
239 Self::LowerSharp => "LSM",
240 }
241 }
242
243 fn chain(self) -> [Transform; 2] {
246 match self {
247 Self::UpperFlat => [Transform::P, Transform::R],
248 Self::UpperSharp => [Transform::L, Transform::P],
249 Self::LowerFlat => [Transform::P, Transform::L],
250 Self::LowerSharp => [Transform::R, Transform::P],
251 }
252 }
253}
254
255impl HexatonicSystem {
256 pub fn name(self) -> &'static str {
259 match self {
260 Self::Northern => "northern",
261 Self::Eastern => "eastern",
262 Self::Southern => "southern",
263 Self::Western => "western",
264 }
265 }
266}
267
268fn single_pitch_transform(chord: &Chord, interval: &Interval, changing: &Pitch) -> Result<Chord> {
271 let target = changing.name();
272 let mut pitches = chord.pitches();
273 for pitch in &mut pitches {
274 pitch.set_spelling_is_inferred(false);
275 if pitch.name() != target {
276 continue;
277 }
278 *pitch = pitch.transpose(interval)?;
279 }
280
281 with_duration_of(chord, Chord::new(pitches.as_slice())?)
282}
283
284fn with_duration_of(source: &Chord, mut made: Chord) -> Result<Chord> {
286 if let Some(duration) = source.duration() {
287 made.set_duration(duration.clone());
288 }
289 Ok(made)
290}
291
292fn names_with_octave(chord: &Chord) -> Vec<String> {
294 chord
295 .pitches()
296 .iter()
297 .map(Pitch::name_with_octave)
298 .collect()
299}
300
301fn not_a_triad_chain(chord: &Chord) -> Error {
303 Error::Analysis(format!(
304 "Cannot perform transformations on chord {}: not a major or minor triad",
305 names_with_octave(chord).join(" ")
306 ))
307}
308
309pub fn simpler_enharmonics(chord: &Chord) -> Result<Chord> {
316 let best = best_spelling(&chord.pitches(), EnharmonicRules::MELODIC)?;
317 with_duration_of(chord, Chord::new(best.as_slice())?)
318}
319
320pub fn lrp_chain(
329 chord: &Chord,
330 transforms: &[Transform],
331 order: ChainOrder,
332 respell: Respell,
333) -> Result<Vec<Chord>> {
334 if !chord.is_major_triad() && !chord.is_minor_triad() {
335 return Err(not_a_triad_chain(chord));
336 }
337
338 let mut transforms = transforms.to_vec();
339 if order == ChainOrder::RightToLeft {
340 transforms.reverse();
341 }
342
343 let mut chain: Vec<Chord> = Vec::with_capacity(transforms.len());
344 for transform in transforms {
345 let last = chain.last().unwrap_or(chord);
346 chain.push(transform.apply(last)?);
347 }
348
349 if respell == Respell::Keep {
350 return Ok(chain);
351 }
352 chain.iter().map(simpler_enharmonics).collect()
353}
354
355pub fn lrp_combination(
372 chord: &Chord,
373 transforms: &[Transform],
374 order: ChainOrder,
375 respell: Respell,
376) -> Result<Chord> {
377 let chain = lrp_chain(chord, transforms, order, Respell::Keep)?;
378 let last = chain.last().unwrap_or(chord);
379
380 match respell {
381 Respell::Keep => Ok(last.clone()),
382 Respell::Simplify => simpler_enharmonics(last),
383 }
384}
385
386pub fn complete_hexatonic(chord: &Chord, respell: Respell) -> Result<Vec<Chord>> {
394 if chord.forte_class_tni().as_deref() != Some(TRIAD_FORTE_CLASS) {
395 return Err(Error::Analysis(
396 "Cannot perform transformations on this chord: not a major or minor triad".to_string(),
397 ));
398 }
399
400 let mut cycle: Vec<Chord> = Vec::with_capacity(HEXATONIC_CYCLE.len());
401 for transform in HEXATONIC_CYCLE {
402 let last = cycle.last().unwrap_or(chord);
403 let mut next = transform.apply(last)?;
404 if respell == Respell::Simplify {
405 next = simpler_enharmonics(&next)?;
406 }
407 cycle.push(next);
408 }
409 Ok(cycle)
410}
411
412pub fn hexatonic_system(chord: &Chord) -> Result<HexatonicSystem> {
420 const SYSTEMS: [HexatonicSystem; 4] = [
421 HexatonicSystem::Northern,
422 HexatonicSystem::Eastern,
423 HexatonicSystem::Southern,
424 HexatonicSystem::Western,
425 ];
426
427 let root = chord
428 .root()
429 .ok_or_else(|| Error::Analysis("no pitches in chord".to_string()))?;
430
431 let pitch_class = root.pitch_class().number() as usize;
434 Ok(SYSTEMS[pitch_class % SYSTEMS.len()])
435}
436
437pub fn chromatic_mediant(chord: &Chord, mediant: Mediant) -> Result<Chord> {
443 let order = if chord.is_minor_triad() {
444 ChainOrder::RightToLeft
445 } else if chord.is_major_triad() {
446 ChainOrder::LeftToRight
447 } else {
448 return Err(Error::Value("Chord must be major or minor".to_string()));
449 };
450
451 lrp_combination(chord, &mediant.chain(), order, Respell::Simplify)
452}
453
454pub fn disjunct_mediant(chord: &Chord, side: MediantSide) -> Result<Chord> {
461 const PRP: [Transform; 3] = [Transform::P, Transform::R, Transform::P];
462 const PLP: [Transform; 3] = [Transform::P, Transform::L, Transform::P];
463
464 let chain = match (chord.is_major_triad(), chord.is_minor_triad(), side) {
466 (true, _, MediantSide::Upper) | (_, true, MediantSide::Lower) => PRP,
467 (true, _, MediantSide::Lower) | (_, true, MediantSide::Upper) => PLP,
468 _ => return Err(Error::Value("Chord must be major or minor".to_string())),
469 };
470
471 lrp_combination(chord, &chain, ChainOrder::LeftToRight, Respell::Simplify)
472}
473
474pub fn slide(chord: &Chord) -> Result<Chord> {
481 lrp_combination(chord, &SLIDE, ChainOrder::LeftToRight, Respell::Keep)
482}
483
484pub fn nebenverwandt(chord: &Chord) -> Result<Chord> {
491 lrp_combination(
492 chord,
493 &NEBENVERWANDT,
494 ChainOrder::LeftToRight,
495 Respell::Keep,
496 )
497}
498
499pub fn is_neo_r(from: &Chord, to: &Chord, transforms: &[Transform]) -> Result<Option<Transform>> {
506 let target = to.normal_order();
507 for transform in transforms {
508 if transform.apply(from)?.normal_order() == target {
509 return Ok(Some(*transform));
510 }
511 }
512 Ok(None)
513}
514
515pub fn is_chromatic_mediant(from: &Chord, to: &Chord) -> Result<Option<Mediant>> {
522 let target = to.normal_order();
523 for mediant in Mediant::ALL {
524 if chromatic_mediant(from, mediant)?.normal_order() == target {
525 return Ok(Some(mediant));
526 }
527 }
528 Ok(None)
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::duration::Duration;
535
536 fn chord(names: &str) -> Chord {
537 Chord::new(names).unwrap()
538 }
539
540 fn named(chord: &Chord) -> Vec<String> {
541 names_with_octave(chord)
542 }
543
544 fn all_named(chords: &[Chord]) -> Vec<Vec<String>> {
545 chords.iter().map(named).collect()
546 }
547
548 fn chain(symbols: &str) -> Vec<Transform> {
549 Transform::parse_chain(symbols).unwrap()
550 }
551
552 #[test]
553 fn each_transform_moves_one_pitch() {
554 let c_major = chord("C4 E4 G4");
555 assert_eq!(
556 named(&Transform::L.apply(&c_major).unwrap()),
557 ["B3", "E4", "G4"]
558 );
559 assert_eq!(
560 named(&Transform::P.apply(&c_major).unwrap()),
561 ["C4", "Eb4", "G4"]
562 );
563 assert_eq!(
564 named(&Transform::R.apply(&c_major).unwrap()),
565 ["C4", "E4", "A4"]
566 );
567 }
568
569 #[test]
570 fn integer_chords_have_their_spelling_fixed() {
571 let c_minor = Chord::new([0, 3, 7].as_slice()).unwrap();
572 let l = Transform::L.apply(&c_minor).unwrap();
573 assert_eq!(named(&l), ["C", "Eb", "Ab"]);
574 assert!(l.pitches().iter().all(|p| !p.spelling_is_inferred()));
575 }
576
577 #[test]
578 fn a_non_triad_is_refused() {
579 let error = Transform::L.apply(&chord("C4 D4 E4")).unwrap_err();
580 assert!(error.to_string().contains("Cannot perform L on this chord"));
581 assert!(Transform::from_symbol('X').is_err());
582 }
583
584 #[test]
585 fn chains_run_in_either_order() {
586 let c_major = chord("C4 E4 G4");
587 let lp = chain("LP");
588 let forward = lrp_combination(&c_major, &lp, ChainOrder::LeftToRight, Respell::Keep);
589 let backward = lrp_combination(&c_major, &lp, ChainOrder::RightToLeft, Respell::Keep);
590 assert_eq!(named(&forward.unwrap()), ["B3", "E4", "G#4"]);
591 assert_eq!(named(&backward.unwrap()), ["C4", "Eb4", "Ab4"]);
592
593 let doubled = chord("C4 E4 G4 C5 E5");
594 let rlp = lrp_combination(
595 &doubled,
596 &chain("RLP"),
597 ChainOrder::LeftToRight,
598 Respell::Keep,
599 );
600 assert_eq!(named(&rlp.unwrap()), ["C4", "F4", "Ab4", "C5", "F5"]);
601 }
602
603 #[test]
604 fn a_hexatonic_chain_comes_back_respelled() {
605 let hexatonic = chain("LPLPLP");
606 let b_major = chord("B4 D#5 F#5");
607 let back = lrp_combination(
608 &b_major,
609 &hexatonic,
610 ChainOrder::RightToLeft,
611 Respell::Simplify,
612 );
613 assert_eq!(named(&back.unwrap()), ["B4", "D#5", "F#5"]);
614
615 let c_major = chord("C4 E4 G4");
616 let kept = lrp_combination(&c_major, &hexatonic, ChainOrder::RightToLeft, Respell::Keep);
617 assert_eq!(named(&kept.unwrap()), ["Dbb4", "Fb4", "Abb4"]);
618
619 let a_flat = chord("A-4 C4 E-5");
620 let sharp = lrp_combination(&a_flat, &hexatonic, ChainOrder::LeftToRight, Respell::Keep);
621 assert_eq!(named(&sharp.unwrap()), ["G#4", "B#3", "D#5"]);
622 }
623
624 #[test]
625 fn every_step_of_a_chain_can_be_kept() {
626 let steps = lrp_chain(
627 &chord("C4 E4 G4"),
628 &chain("LPLPLP"),
629 ChainOrder::LeftToRight,
630 Respell::Simplify,
631 );
632 assert_eq!(
633 all_named(&steps.unwrap()),
634 [
635 ["B3", "E4", "G4"],
636 ["B3", "E4", "G#4"],
637 ["B3", "D#4", "G#4"],
638 ["C4", "Eb4", "Ab4"],
639 ["C4", "Eb4", "G4"],
640 ["C4", "E4", "G4"],
641 ]
642 );
643 }
644
645 #[test]
646 fn the_hexatonic_cycle_is_music21s() {
647 let c_major = chord("C4 E4 G4");
648 assert_eq!(
649 all_named(&complete_hexatonic(&c_major, Respell::Keep).unwrap()),
650 [
651 ["C4", "Eb4", "G4"],
652 ["C4", "Eb4", "Ab4"],
653 ["Cb4", "Eb4", "Ab4"],
654 ["Cb4", "Fb4", "Ab4"],
655 ["Cb4", "Fb4", "Abb4"],
656 ["Dbb4", "Fb4", "Abb4"],
657 ]
658 );
659 assert_eq!(
660 all_named(&complete_hexatonic(&c_major, Respell::Simplify).unwrap()),
661 [
662 ["C4", "Eb4", "G4"],
663 ["C4", "Eb4", "Ab4"],
664 ["B3", "D#4", "G#4"],
665 ["B3", "E4", "G#4"],
666 ["B3", "E4", "G4"],
667 ["C4", "E4", "G4"],
668 ]
669 );
670 assert!(complete_hexatonic(&chord("C4 D4 E4"), Respell::Keep).is_err());
671 }
672
673 #[test]
674 fn hexatonic_systems_follow_the_root() {
675 assert_eq!(
676 hexatonic_system(&chord("C E G")).unwrap(),
677 HexatonicSystem::Northern
678 );
679 assert_eq!(
680 hexatonic_system(&chord("G B- D")).unwrap(),
681 HexatonicSystem::Western
682 );
683 assert_eq!(
684 hexatonic_system(&chord("F#4 D5 A5 C6")).unwrap(),
685 HexatonicSystem::Southern
686 );
687 assert!(hexatonic_system(&Chord::empty()).is_err());
688 }
689
690 #[test]
691 fn mediants_are_music21s() {
692 let c_major = chord("C5 E5 G5");
693 let normal = |mediant| chromatic_mediant(&c_major, mediant).unwrap().normal_order();
694 assert_eq!(normal(Mediant::UpperFlat), [3, 7, 10]);
695 assert_eq!(normal(Mediant::UpperSharp), [4, 8, 11]);
696 assert_eq!(
697 named(&chromatic_mediant(&c_major, Mediant::LowerFlat).unwrap()),
698 ["C5", "Eb5", "Ab5"]
699 );
700 assert_eq!(
701 named(&chromatic_mediant(&c_major, Mediant::LowerSharp).unwrap()),
702 ["C#5", "E5", "A5"]
703 );
704 assert_eq!(
705 named(&chromatic_mediant(&c_major, Mediant::UpperSharp).unwrap()),
706 ["B4", "E5", "G#5"]
707 );
708
709 let names = |c: Chord| -> Vec<String> { c.pitches().iter().map(Pitch::name).collect() };
710 assert_eq!(
711 names(disjunct_mediant(&c_major, MediantSide::Upper).unwrap()),
712 ["Bb", "Eb", "Gb"]
713 );
714 assert_eq!(
715 names(disjunct_mediant(&c_major, MediantSide::Lower).unwrap()),
716 ["B", "D#", "G#"]
717 );
718 assert_eq!(names(slide(&c_major).unwrap()), ["C#", "E", "G#"]);
719 assert_eq!(names(slide(&chord("A4 C5 E5")).unwrap()), ["Ab", "C", "Eb"]);
720 assert_eq!(names(nebenverwandt(&c_major).unwrap()), ["C", "F", "Ab"]);
721 assert_eq!(
722 names(nebenverwandt(&chord("A4 C5 E5")).unwrap()),
723 ["G#", "B", "E"]
724 );
725
726 assert_eq!(Mediant::from_code("LFM").unwrap(), Mediant::LowerFlat);
727 assert!(Mediant::from_code("XYZ").is_err());
728 }
729
730 #[test]
731 fn relations_are_recognised() {
732 let c_major = chord("C4 E4 G4");
733 let lrp = chain("LRP");
734 assert_eq!(
735 is_neo_r(&c_major, &chord("B3 E4 G4"), &lrp).unwrap(),
736 Some(Transform::L)
737 );
738 assert_eq!(
739 is_neo_r(&c_major, &chord("C4 E-4 G4"), &lrp).unwrap(),
740 Some(Transform::P)
741 );
742 assert_eq!(
743 is_neo_r(&c_major, &chord("C4 E4 A4"), &lrp).unwrap(),
744 Some(Transform::R)
745 );
746 assert_eq!(
747 is_neo_r(&c_major, &chord("C-4 E-4 A-4"), &lrp).unwrap(),
748 None
749 );
750 assert_eq!(
751 is_neo_r(&c_major, &chord("C4 E-4 G4"), &chain("LR")).unwrap(),
752 None
753 );
754
755 assert_eq!(
756 is_chromatic_mediant(&c_major, &chord("A-3 C4 E-4")).unwrap(),
757 Some(Mediant::LowerFlat)
758 );
759 assert_eq!(
760 is_chromatic_mediant(&c_major, &chord("C-4 E-4 A-4")).unwrap(),
761 None
762 );
763 }
764
765 #[test]
766 fn minor_triads_take_their_mediants_as_music21_does() {
767 let a_minor = chord("A4 C5 E5");
768 let mediant = |mediant| named(&chromatic_mediant(&a_minor, mediant).unwrap());
769 assert_eq!(mediant(Mediant::UpperFlat), ["G4", "C5", "Eb5"]);
770 assert_eq!(mediant(Mediant::UpperSharp), ["G#4", "C#5", "E5"]);
771 assert_eq!(mediant(Mediant::LowerFlat), ["Ab4", "C5", "F5"]);
772 assert_eq!(mediant(Mediant::LowerSharp), ["A4", "C#5", "F#5"]);
773
774 let side = |side| named(&disjunct_mediant(&a_minor, side).unwrap());
775 assert_eq!(side(MediantSide::Upper), ["Ab4", "Db5", "F5"]);
776 assert_eq!(side(MediantSide::Lower), ["A#4", "C#5", "F#5"]);
777 }
778
779 #[test]
780 fn only_triads_take_mediants_or_chains() {
781 let cluster = chord("C4 D4 E4");
782 assert!(matches!(
783 chromatic_mediant(&cluster, Mediant::UpperFlat),
784 Err(Error::Value(_))
785 ));
786 assert!(matches!(
787 disjunct_mediant(&cluster, MediantSide::Upper),
788 Err(Error::Value(_))
789 ));
790
791 let chain = lrp_chain(
792 &cluster,
793 &[Transform::L],
794 ChainOrder::LeftToRight,
795 Respell::Keep,
796 );
797 let message = chain.unwrap_err().to_string();
798 assert!(message.contains("Cannot perform transformations on chord C4 D4 E4"));
799 }
800
801 #[test]
802 fn p_and_r_refuse_in_music21s_capitals() {
803 let cluster = chord("C4 D4 E4");
804 for transform in [Transform::P, Transform::R] {
805 let message = transform.apply(&cluster).unwrap_err().to_string();
806 let expected = format!(
807 "Cannot perform {} on this chord: not a Major or Minor triad",
808 transform.symbol()
809 );
810 assert!(message.contains(&expected), "{message}");
811 }
812 }
813
814 #[test]
815 fn a_chain_parses_back_to_its_letters() {
816 let symbols: String = chain("LPR").into_iter().map(Transform::symbol).collect();
817 assert_eq!(symbols, "LPR");
818 }
819
820 #[test]
821 fn a_result_lasts_as_long_as_its_source() {
822 let mut c_major = chord("C4 E4 G4");
823 c_major.set_duration(Duration::half());
824 let lasts = |chord: &Chord| chord.duration().map(Duration::quarter_length);
825
826 assert_eq!(lasts(&Transform::L.apply(&c_major).unwrap()), Some(2.0));
827 let cycle = complete_hexatonic(&c_major, Respell::Simplify).unwrap();
828 assert_eq!(lasts(&cycle[5]), Some(2.0));
829 }
830
831 #[test]
832 fn hexatonic_systems_have_music21s_names() {
833 let names: Vec<&str> = [
834 HexatonicSystem::Northern,
835 HexatonicSystem::Eastern,
836 HexatonicSystem::Southern,
837 HexatonicSystem::Western,
838 ]
839 .into_iter()
840 .map(HexatonicSystem::name)
841 .collect();
842 assert_eq!(names, ["northern", "eastern", "southern", "western"]);
843 }
844
845 #[test]
846 fn simpler_enharmonics_respell() {
847 assert_eq!(
848 named(&simpler_enharmonics(&chord("B# F- G")).unwrap()),
849 ["C", "E", "G"]
850 );
851 }
852}