1use crate::{
9 defaults::IntegerType,
10 error::{Error, Result},
11 interval::{Interval, IntervalDirection},
12 key::Key,
13 pitch::Pitch,
14 scale::{Scale, ScaleType},
15};
16
17use crate::interval::constants::{
18 PERFECT_FIFTH_UP as PERFECT_FIFTH, PERFECT_OCTAVE, PERFECT_UNISON,
19};
20
21#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum MotionType {
25 AntiParallel,
28 Contrary,
30 NoMotion,
32 Oblique,
34 Parallel,
36 Similar,
38}
39
40impl MotionType {
41 pub fn as_str(self) -> &'static str {
43 match self {
44 Self::AntiParallel => "Anti-Parallel",
45 Self::Contrary => "Contrary",
46 Self::NoMotion => "No Motion",
47 Self::Oblique => "Oblique",
48 Self::Parallel => "Parallel",
49 Self::Similar => "Similar",
50 }
51 }
52}
53
54impl std::fmt::Display for MotionType {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str(self.as_str())
57 }
58}
59
60#[derive(Clone, Debug, PartialEq)]
66pub enum ParallelRequirement {
67 Wide(IntegerType),
69 Named(Box<Interval>),
71}
72
73impl From<Interval> for ParallelRequirement {
74 fn from(interval: Interval) -> Self {
75 Self::Named(Box::new(interval))
76 }
77}
78
79impl From<IntegerType> for ParallelRequirement {
80 fn from(steps: IntegerType) -> Self {
81 Self::Wide(steps)
82 }
83}
84
85#[derive(Clone, Debug)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88#[must_use]
89pub struct VoiceLeadingQuartet {
90 v1n1: Pitch,
91 v1n2: Pitch,
92 v2n1: Pitch,
93 v2n2: Pitch,
94 vertical: [Interval; 2],
95 horizontal: [Interval; 2],
96 key: Option<Key>,
97}
98
99impl VoiceLeadingQuartet {
100 pub fn new(v1n1: Pitch, v1n2: Pitch, v2n1: Pitch, v2n2: Pitch) -> Result<Self> {
103 let vertical = [
104 Interval::between_pitches(&v1n1, &v2n1)?,
105 Interval::between_pitches(&v1n2, &v2n2)?,
106 ];
107 let horizontal = [
108 Interval::between_pitches(&v1n1, &v1n2)?,
109 Interval::between_pitches(&v2n1, &v2n2)?,
110 ];
111 Ok(Self {
112 v1n1,
113 v1n2,
114 v2n1,
115 v2n2,
116 vertical,
117 horizontal,
118 key: None,
119 })
120 }
121
122 pub fn from_names(v1n1: &str, v1n2: &str, v2n1: &str, v2n2: &str) -> Result<Self> {
124 Self::new(
125 Pitch::from_name(v1n1)?,
126 Pitch::from_name(v1n2)?,
127 Pitch::from_name(v2n1)?,
128 Pitch::from_name(v2n2)?,
129 )
130 }
131
132 pub fn with_key(mut self, key: Key) -> Self {
135 self.key = Some(key);
136 self
137 }
138
139 pub fn key(&self) -> Option<&Key> {
141 self.key.as_ref()
142 }
143
144 pub fn set_key(&mut self, key: Option<Key>) {
146 self.key = key;
147 }
148
149 pub fn is_proper_resolution(&self) -> Result<bool> {
156 if self.no_motion() {
157 return Ok(true);
158 }
159 let (lower_degree_before, lower_degree_after) = match &self.key {
160 Some(key) => {
161 let scale = key.as_scale()?;
162 let mut before = scale.degree_of(&self.v2n1)?;
163 let after = scale.degree_of(&self.v2n2)?;
164 if key.mode() == "minor" && before.is_none() {
165 before =
166 Scale::new(ScaleType::MelodicMinor, key.tonic()).degree_of(&self.v2n1)?;
167 }
168 (before, after)
169 }
170 None => (None, None),
171 };
172 let keyed = self.key.is_some();
173 let first = self.vertical[0].simple_name();
174 let second = self.vertical[1].generic().simple_undirected();
175 Ok(match first.as_str() {
176 "P4" => self.v1n1.ps() >= self.v1n2.ps(),
177 "A4" => {
178 if keyed && lower_degree_before != Some(4) {
179 true
180 } else if keyed && lower_degree_after != Some(3) {
181 false
182 } else {
183 self.outward_contrary_motion() && second == 6
184 }
185 }
186 "d5" => {
187 if keyed && lower_degree_before != Some(7) {
188 true
189 } else if keyed && lower_degree_after != Some(1) {
190 false
191 } else {
192 self.inward_contrary_motion() && second == 3
193 }
194 }
195 "m7" => {
196 if keyed && lower_degree_before != Some(5) {
197 true
198 } else if keyed && lower_degree_after != Some(1) {
199 false
200 } else {
201 second == 3
202 }
203 }
204 _ => true,
205 })
206 }
207
208 pub fn leap_not_set_with_step(&self) -> bool {
212 if self.no_motion() {
213 return false;
214 }
215 let [upper, lower] = &self.horizontal;
216 let steps_or_holds =
217 |interval: &Interval| interval.is_diatonic_step() || interval.is_unison();
218 if upper.generic().undirected() == 3
219 && lower.generic().undirected() == 3
220 && self.contrary_motion()
221 {
222 return false;
223 }
224 if upper.is_skip() {
225 !steps_or_holds(lower)
226 } else if lower.is_skip() {
227 !steps_or_holds(upper)
228 } else {
229 false
230 }
231 }
232
233 pub fn modal_opening(&self) -> Result<bool> {
242 let Some(key) = &self.key else {
243 return Err(Error::Analysis(
244 "modalOpening requires a key to be set on the VoiceLeadingQuartet".to_string(),
245 ));
246 };
247 let opening = ["P1", "P5"];
248 let sounds_open = opening.contains(&self.vertical[0].simple_name().as_str())
249 || opening.contains(&self.vertical[1].simple_name().as_str());
250 let function_of = |first: &Pitch, second: &Pitch| -> Result<Option<bool>> {
251 let chord = crate::chord::Chord::new([first.clone(), second.clone()].as_slice())?;
252 Ok(
253 crate::roman::identify_as_tonic_or_dominant(&chord, key)?.map(|figure| {
254 figure
255 .chars()
256 .next()
257 .is_some_and(|numeral| matches!(numeral.to_ascii_uppercase(), 'I' | 'V'))
258 }),
259 )
260 };
261 let established = match function_of(&self.v1n1, &self.v2n1)? {
262 Some(established) => established,
263 None => function_of(&self.v1n2, &self.v2n2)?.unwrap_or(false),
264 };
265 Ok(sounds_open && established)
266 }
267
268 pub fn opens_incorrectly(&self) -> Result<bool> {
270 self.modal_opening().map(|opens| !opens)
271 }
272
273 pub fn closes_incorrectly(&self) -> Result<bool> {
275 self.clausula_vera().map(|closes| !closes)
276 }
277
278 pub fn clausula_vera(&self) -> Result<bool> {
282 let Some(key) = &self.key else {
283 return Err(Error::Analysis(
284 "clausulaVera requires a key to be set on the VoiceLeadingQuartet".to_string(),
285 ));
286 };
287 let tonic = key.tonic().name();
288 let mut horizontal = [
289 self.horizontal[0].short_name(),
290 self.horizontal[1].short_name(),
291 ];
292 horizontal.sort_unstable();
293 Ok(horizontal == ["M2", "m2"]
294 && self.contrary_motion()
295 && matches!(self.vertical[1].short_name().as_str(), "P1" | "P8")
296 && self.v1n2.name() == tonic
297 && self.v2n2.name() == tonic)
298 }
299
300 pub fn v1n1(&self) -> &Pitch {
302 &self.v1n1
303 }
304
305 pub fn v1n2(&self) -> &Pitch {
307 &self.v1n2
308 }
309
310 pub fn v2n1(&self) -> &Pitch {
312 &self.v2n1
313 }
314
315 pub fn v2n2(&self) -> &Pitch {
317 &self.v2n2
318 }
319
320 pub fn vertical_intervals(&self) -> &[Interval; 2] {
323 &self.vertical
324 }
325
326 pub fn horizontal_intervals(&self) -> &[Interval; 2] {
328 &self.horizontal
329 }
330
331 pub fn motion_type(&self, allow_anti_parallel: bool) -> MotionType {
334 if self.oblique_motion() {
335 MotionType::Oblique
336 } else if self.parallel_motion(None, false) {
337 MotionType::Parallel
338 } else if self.similar_motion() {
339 MotionType::Similar
340 } else if allow_anti_parallel && self.anti_parallel_motion(None) {
341 MotionType::AntiParallel
342 } else if self.contrary_motion() {
343 MotionType::Contrary
344 } else {
345 MotionType::NoMotion
346 }
347 }
348
349 pub fn no_motion(&self) -> bool {
351 self.horizontal.iter().all(Interval::is_perfect_unison)
352 }
353
354 pub fn oblique_motion(&self) -> bool {
356 !self.no_motion() && self.horizontal.iter().any(Interval::is_perfect_unison)
357 }
358
359 pub fn similar_motion(&self) -> bool {
361 !self.no_motion() && self.horizontal[0].direction() == self.horizontal[1].direction()
362 }
363
364 pub fn parallel_motion(
368 &self,
369 required: Option<&ParallelRequirement>,
370 allow_octave_displacement: bool,
371 ) -> bool {
372 let [first, second] = &self.vertical;
373 if !self.similar_motion() {
374 return false;
375 }
376 if first.generic().directed() != second.generic().directed() && !allow_octave_displacement {
377 return false;
378 }
379 if first.generic().semi_simple_undirected() != second.generic().semi_simple_undirected() {
380 return false;
381 }
382 match required {
383 None => true,
384 Some(ParallelRequirement::Wide(steps)) => {
385 first.generic().semi_simple_undirected() == *steps
386 }
387 Some(ParallelRequirement::Named(required)) => {
388 first.semi_simple_key() == required.semi_simple_key()
389 && second.semi_simple_key() == required.semi_simple_key()
390 }
391 }
392 }
393
394 pub fn contrary_motion(&self) -> bool {
396 !self.no_motion()
397 && !self.oblique_motion()
398 && self.horizontal[0].direction() != self.horizontal[1].direction()
399 }
400
401 pub fn outward_contrary_motion(&self) -> bool {
403 self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Ascending
404 }
405
406 pub fn inward_contrary_motion(&self) -> bool {
408 self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Descending
409 }
410
411 pub fn anti_parallel_motion(&self, required: Option<&Interval>) -> bool {
415 let [first, second] = &self.vertical;
416 self.contrary_motion()
417 && first.simple_key() == second.simple_key()
418 && required.is_none_or(|required| first.simple_key() == required.simple_key())
419 }
420
421 pub fn parallel_interval(&self, interval: &Interval) -> bool {
424 let required = ParallelRequirement::Named(Box::new(interval.clone()));
425 self.parallel_motion(Some(&required), true) || self.anti_parallel_motion(Some(interval))
426 }
427
428 pub fn parallel_fifth(&self) -> bool {
430 self.parallel_interval(&PERFECT_FIFTH)
431 }
432
433 pub fn parallel_octave(&self) -> bool {
435 self.parallel_interval(&PERFECT_OCTAVE)
436 }
437
438 pub fn parallel_unison(&self) -> bool {
440 self.parallel_interval(&PERFECT_UNISON)
441 }
442
443 pub fn parallel_unison_or_octave(&self) -> bool {
445 self.parallel_unison() || self.parallel_octave()
446 }
447
448 pub fn hidden_interval(&self, interval: &Interval) -> bool {
451 if self.parallel_motion(None, true) || !self.similar_motion() {
452 return false;
453 }
454 self.vertical[1].simple_key() == interval.simple_key()
455 }
456
457 pub fn hidden_fifth(&self) -> bool {
459 self.hidden_interval(&PERFECT_FIFTH)
460 }
461
462 pub fn hidden_octave(&self) -> bool {
464 self.hidden_interval(&PERFECT_OCTAVE)
465 }
466
467 pub fn voice_overlap(&self) -> bool {
469 self.v1n2.ps() < self.v2n1.ps() || self.v2n2.ps() > self.v1n1.ps()
470 }
471
472 pub fn voice_crossing(&self) -> bool {
475 self.v1n1.ps() < self.v2n1.ps() || self.v1n2.ps() < self.v2n2.ps()
476 }
477}
478
479#[cfg(test)]
480mod tests {
481
482 #[test]
483 fn a_quartet_hands_back_its_pitches_intervals_and_key() {
484 use super::ParallelRequirement;
485 use crate::Interval;
486
487 let mut quartet = VoiceLeadingQuartet::from_names("C5", "D5", "C4", "D4").unwrap();
488 assert_eq!(quartet.v1n1().name_with_octave(), "C5");
489 assert_eq!(quartet.v1n2().name_with_octave(), "D5");
490 assert_eq!(quartet.v2n1().name_with_octave(), "C4");
491 assert_eq!(quartet.v2n2().name_with_octave(), "D4");
492 let vertical: Vec<String> = quartet
493 .vertical_intervals()
494 .iter()
495 .map(Interval::short_name)
496 .collect();
497 assert_eq!(vertical, ["P8", "P8"]);
498 let horizontal: Vec<String> = quartet
499 .horizontal_intervals()
500 .iter()
501 .map(Interval::short_name)
502 .collect();
503 assert_eq!(horizontal, ["M2", "M2"]);
504 assert!(quartet.parallel_unison_or_octave());
505 assert!(quartet.key().is_none());
506 quartet.set_key(Some(Key::from_tonic("C").unwrap()));
507 assert!(quartet.key().is_some());
508 assert!(matches!(
509 ParallelRequirement::from(8),
510 ParallelRequirement::Wide(8)
511 ));
512 assert!(matches!(
513 ParallelRequirement::from(Interval::from_name("P5").unwrap()),
514 ParallelRequirement::Named(_)
515 ));
516 }
517
518 #[test]
519 fn opening_and_closing_incorrectly_are_the_opposites_of_the_rules() {
520 let quartet = |v1n1, v1n2, v2n1, v2n2, key: &str| {
521 VoiceLeadingQuartet::from_names(v1n1, v1n2, v2n1, v2n2)
522 .unwrap()
523 .with_key(Key::from_tonic(key).unwrap())
524 };
525 let opens = quartet("D", "D", "D", "F#", "D");
526 assert!(opens.modal_opening().unwrap());
527 assert!(!opens.opens_incorrectly().unwrap());
528 let closes = quartet("B4", "C5", "D4", "C4", "C");
529 assert_eq!(
530 closes.closes_incorrectly().unwrap(),
531 !closes.clausula_vera().unwrap()
532 );
533 let unkeyed = VoiceLeadingQuartet::from_names("D", "D", "D", "F#").unwrap();
534 assert!(unkeyed.opens_incorrectly().is_err());
535 assert!(unkeyed.closes_incorrectly().is_err());
536 }
537
538 #[test]
539 fn a_modal_opening_needs_a_perfect_interval_and_a_tonic_or_dominant() {
540 let opening = |v1n1, v1n2, v2n1, v2n2, key: &str| {
542 VoiceLeadingQuartet::from_names(v1n1, v1n2, v2n1, v2n2)
543 .unwrap()
544 .with_key(Key::from_tonic(key).unwrap())
545 .modal_opening()
546 .unwrap()
547 };
548 assert!(opening("D", "D", "D", "F#", "D"));
549 assert!(opening("B", "A", "G#", "A", "A"));
550 assert!(opening("A", "A", "F#", "D", "A"));
551 assert!(!opening("C#", "C#", "D", "E", "A"));
552 assert!(!opening("B", "B", "A", "A", "C"));
553
554 assert!(
556 VoiceLeadingQuartet::from_names("D", "D", "D", "F#")
557 .unwrap()
558 .modal_opening()
559 .is_err()
560 );
561 }
562
563 #[test]
564 #[allow(clippy::type_complexity)]
565 fn proper_resolution_leaps_and_clausula_vera_match_music21() {
566 let quartet = |a: &str, b: &str, c: &str, d: &str, key: Option<&str>| {
567 let built = VoiceLeadingQuartet::from_names(a, b, c, d).unwrap();
568 match key {
569 Some(key) => built.with_key(Key::from_tonic(key).unwrap()),
570 None => built,
571 }
572 };
573 let cases: [(
574 &str,
575 &str,
576 &str,
577 &str,
578 Option<&str>,
579 bool,
580 bool,
581 Option<bool>,
582 ); 20] = [
583 ("C4", "B3", "F4", "E4", None, true, false, None),
584 ("C4", "B3", "F4", "E4", Some("C"), true, false, Some(false)),
585 ("B3", "C4", "F4", "E4", Some("C"), true, false, Some(false)),
586 ("B3", "C4", "F4", "E4", None, false, false, None),
587 ("F4", "E4", "B3", "C4", Some("C"), true, false, Some(false)),
588 ("F4", "E4", "B3", "C4", None, true, false, None),
589 ("G3", "C4", "F4", "E4", Some("C"), true, false, Some(false)),
590 ("G3", "C4", "F4", "E4", Some("G"), true, false, Some(false)),
591 ("C4", "C4", "E4", "E4", Some("C"), true, false, Some(false)),
592 ("D4", "F4", "F4", "A4", None, true, true, None),
593 ("C4", "F4", "E4", "F4", None, true, false, None),
594 ("C4", "E4", "E4", "G4", None, true, true, None),
595 ("C4", "E4", "E4", "C4", None, true, false, None),
596 ("B3", "C4", "D4", "C4", Some("C"), true, false, Some(true)),
597 ("B3", "C4", "D4", "C4", Some("G"), true, false, Some(false)),
598 ("B3", "C4", "D4", "C5", Some("C"), true, false, Some(false)),
599 ("F4", "E4", "G3", "C4", Some("C"), true, false, Some(false)),
600 ("F4", "E4", "B3", "C4", Some("F"), true, false, Some(false)),
601 ("D4", "C4", "B3", "C4", Some("C"), true, false, Some(true)),
602 ("C4", "D4", "G4", "F4", None, true, false, None),
603 ];
604 for (a, b, c, d, key, proper, leap, clausula) in cases {
605 let vlq = quartet(a, b, c, d, key);
606 assert_eq!(
607 vlq.is_proper_resolution().unwrap(),
608 proper,
609 "{a} {b} {c} {d} {key:?}"
610 );
611 assert_eq!(
612 vlq.leap_not_set_with_step(),
613 leap,
614 "{a} {b} {c} {d} {key:?}"
615 );
616 match clausula {
617 Some(expected) => assert_eq!(
618 vlq.clausula_vera().unwrap(),
619 expected,
620 "{a} {b} {c} {d} {key:?}"
621 ),
622 None => assert!(vlq.clausula_vera().is_err(), "{a} {b} {c} {d}"),
623 }
624 assert_eq!(vlq.key().is_some(), key.is_some());
625 }
626 }
627 use super::*;
628
629 struct Expected {
630 motion: MotionType,
631 with_anti_parallel: MotionType,
632 flags: [bool; 15],
633 }
634
635 #[test]
636 fn quartets_match_music21() {
637 use MotionType::*;
638 let t = true;
639 let f = false;
640 let cases = [
641 (
642 ("C4", "D4", "C3", "D3"),
643 Expected {
644 motion: Parallel,
645 with_anti_parallel: Parallel,
646 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
647 },
648 ),
649 (
650 ("C4", "D4", "E3", "F3"),
651 Expected {
652 motion: Parallel,
653 with_anti_parallel: Parallel,
654 flags: [f, f, t, t, f, f, f, f, f, f, f, f, f, f, f],
655 },
656 ),
657 (
658 ("C4", "G4", "C3", "C3"),
659 Expected {
660 motion: Oblique,
661 with_anti_parallel: Oblique,
662 flags: [f, t, f, f, f, f, f, f, f, f, f, f, f, f, f],
663 },
664 ),
665 (
666 ("C4", "C4", "C3", "C3"),
667 Expected {
668 motion: NoMotion,
669 with_anti_parallel: NoMotion,
670 flags: [t, f, f, f, f, f, f, f, f, f, f, f, f, f, f],
671 },
672 ),
673 (
674 ("C4", "D4", "G3", "F3"),
675 Expected {
676 motion: Contrary,
677 with_anti_parallel: Contrary,
678 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
679 },
680 ),
681 (
682 ("C4", "C5", "F3", "F4"),
683 Expected {
684 motion: Parallel,
685 with_anti_parallel: Parallel,
686 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, t, f],
687 },
688 ),
689 (
690 ("C5", "D5", "C4", "D4"),
691 Expected {
692 motion: Parallel,
693 with_anti_parallel: Parallel,
694 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
695 },
696 ),
697 (
698 ("C4", "D4", "G4", "A4"),
699 Expected {
700 motion: Parallel,
701 with_anti_parallel: Parallel,
702 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, t, t],
703 },
704 ),
705 (
706 ("C5", "D5", "G4", "E5"),
707 Expected {
708 motion: Similar,
709 with_anti_parallel: Similar,
710 flags: [f, f, t, f, f, f, f, f, f, f, f, f, f, t, t],
711 },
712 ),
713 (
714 ("E4", "F4", "C4", "A3"),
715 Expected {
716 motion: Contrary,
717 with_anti_parallel: Contrary,
718 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
719 },
720 ),
721 (
722 ("G4", "F4", "C4", "D4"),
723 Expected {
724 motion: Contrary,
725 with_anti_parallel: Contrary,
726 flags: [f, f, f, f, t, f, t, f, f, f, f, f, f, f, f],
727 },
728 ),
729 (
730 ("C4", "C#4", "C3", "C#3"),
731 Expected {
732 motion: Parallel,
733 with_anti_parallel: Parallel,
734 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
735 },
736 ),
737 (
738 ("C4", "B3", "F3", "G3"),
739 Expected {
740 motion: Contrary,
741 with_anti_parallel: Contrary,
742 flags: [f, f, f, f, t, f, t, f, f, f, f, f, f, f, f],
743 },
744 ),
745 (
746 ("D5", "A5", "G3", "D4"),
747 Expected {
748 motion: Parallel,
749 with_anti_parallel: Parallel,
750 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, f, f],
751 },
752 ),
753 (
754 ("A4", "B4", "F4", "E4"),
755 Expected {
756 motion: Contrary,
757 with_anti_parallel: Contrary,
758 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
759 },
760 ),
761 (
762 ("C5", "C5", "E4", "F4"),
763 Expected {
764 motion: Oblique,
765 with_anti_parallel: Oblique,
766 flags: [f, t, f, f, f, f, f, f, f, f, f, f, f, f, f],
767 },
768 ),
769 ];
770 for ((a, b, c, d), expected) in cases {
771 let quartet = VoiceLeadingQuartet::from_names(a, b, c, d).unwrap();
772 let label = format!("{a} {b} / {c} {d}");
773 assert_eq!(quartet.motion_type(false), expected.motion, "{label}");
774 assert_eq!(
775 quartet.motion_type(true),
776 expected.with_anti_parallel,
777 "{label}"
778 );
779 let actual = [
780 quartet.no_motion(),
781 quartet.oblique_motion(),
782 quartet.similar_motion(),
783 quartet.parallel_motion(None, false),
784 quartet.contrary_motion(),
785 quartet.outward_contrary_motion(),
786 quartet.inward_contrary_motion(),
787 quartet.anti_parallel_motion(None),
788 quartet.parallel_fifth(),
789 quartet.parallel_octave(),
790 quartet.parallel_unison(),
791 quartet.hidden_fifth(),
792 quartet.hidden_octave(),
793 quartet.voice_overlap(),
794 quartet.voice_crossing(),
795 ];
796 assert_eq!(actual, expected.flags, "{label}");
797 }
798 }
799
800 #[test]
801 fn anti_parallel_fifths_are_contrary_unless_asked_for() {
802 let quartet = VoiceLeadingQuartet::from_names("G4", "D5", "C4", "G3").unwrap();
803 assert_eq!(quartet.motion_type(false), MotionType::Contrary);
804 assert_eq!(quartet.motion_type(true), MotionType::AntiParallel);
805 assert!(quartet.parallel_fifth());
806 assert_eq!(MotionType::AntiParallel.to_string(), "Anti-Parallel");
807 }
808
809 #[test]
810 fn hidden_intervals_need_similar_motion_into_a_perfect_interval() {
811 let quartet = VoiceLeadingQuartet::from_names("E4", "G4", "C4", "C3").unwrap();
812 assert!(!quartet.hidden_fifth());
813 let quartet = VoiceLeadingQuartet::from_names("E4", "D5", "C4", "G4").unwrap();
814 assert!(quartet.hidden_fifth());
815 assert!(!quartet.hidden_octave());
816 let quartet = VoiceLeadingQuartet::from_names("E4", "C5", "C4", "C4").unwrap();
817 assert!(!quartet.hidden_octave());
818 }
819}