Skip to main content

music21_rs/
voiceleading.rs

1//! Two-voice voice-leading checks, a port of music21's `VoiceLeadingQuartet`.
2//!
3//! A quartet is two consecutive notes in an upper voice and two in a lower
4//! voice. It classifies how the voices move between them and finds the
5//! parallel and hidden perfect intervals that common-practice counterpoint
6//! forbids.
7
8use 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/// How two voices move relative to each other.
22#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum MotionType {
25    /// Contrary motion between two spellings of the same simple interval,
26    /// such as a fifth opening out to a twelfth. Only reported when asked for.
27    AntiParallel,
28    /// The voices move in opposite directions.
29    Contrary,
30    /// Neither voice moves.
31    NoMotion,
32    /// One voice holds while the other moves.
33    Oblique,
34    /// The voices move the same way and keep the same generic interval.
35    Parallel,
36    /// The voices move the same way through different intervals.
37    Similar,
38}
39
40impl MotionType {
41    /// Returns music21's label for the motion.
42    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/// What a caller may ask of a parallel motion.
61///
62/// music21 takes either: a number is how wide the interval must be, whatever
63/// it is spelled — `3` for parallel thirds of any quality — and an interval
64/// is the interval itself, spelling and all.
65#[derive(Clone, Debug, PartialEq)]
66pub enum ParallelRequirement {
67    /// However many steps wide, counted inclusively.
68    Wide(IntegerType),
69    /// This interval exactly.
70    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/// Two consecutive notes in each of two voices. Voice one is the upper voice.
86#[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    /// Builds a quartet from the first and second pitch of the upper voice,
101    /// then the first and second pitch of the lower voice.
102    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    /// Builds a quartet from pitch names, in the same order as [`Self::new`].
123    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    /// Attaches the key the progression is heard in, which
133    /// [`Self::is_proper_resolution`] and [`Self::clausula_vera`] consult.
134    pub fn with_key(mut self, key: Key) -> Self {
135        self.key = Some(key);
136        self
137    }
138
139    /// The key the progression is heard in, if one was given.
140    pub fn key(&self) -> Option<&Key> {
141        self.key.as_ref()
142    }
143
144    /// Attaches a key, or takes one away.
145    pub fn set_key(&mut self, key: Option<Key>) {
146        self.key = key;
147    }
148
149    /// Whether a dissonant first interval resolves the way voice-leading
150    /// rules expect: music21's `isProperResolution`. A fourth wants the upper
151    /// voice to fall or stay; a tritone or a minor seventh wants the
152    /// leading tone up, the seventh down and the right contrary motion — and
153    /// with a key set, only when the notes really are those scale degrees.
154    /// Anything else, and no motion at all, is proper.
155    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    /// Whether one voice leaps while the other neither steps nor holds:
209    /// music21's `leapNotSetWithStep`. Two thirds in contrary motion are let
210    /// through.
211    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    /// Whether the two voices open the way sixteenth-century counterpoint
234    /// opens: music21's `modalOpening`. Errors without a key.
235    ///
236    /// One of the two harmonic intervals must be a unison or a fifth — the
237    /// second may be, to allow for an anacrusis — and the pair must establish
238    /// the tonic or the dominant. Which of the two says so is whichever can
239    /// be read at all: music21 asks the first, and only falls to the second
240    /// when the first says nothing.
241    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    /// The opposite of [`Self::modal_opening`]: music21's `opensIncorrectly`.
269    pub fn opens_incorrectly(&self) -> Result<bool> {
270        self.modal_opening().map(|opens| !opens)
271    }
272
273    /// The opposite of [`Self::clausula_vera`]: music21's `closesIncorrectly`.
274    pub fn closes_incorrectly(&self) -> Result<bool> {
275        self.clausula_vera().map(|closes| !closes)
276    }
277
278    /// Whether the two voices close a clausula vera: stepwise contrary
279    /// motion, one voice by a semitone and the other by a tone, onto a unison
280    /// or octave on the tonic. Errors without a key.
281    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    /// The upper voice's first pitch.
301    pub fn v1n1(&self) -> &Pitch {
302        &self.v1n1
303    }
304
305    /// The upper voice's second pitch.
306    pub fn v1n2(&self) -> &Pitch {
307        &self.v1n2
308    }
309
310    /// The lower voice's first pitch.
311    pub fn v2n1(&self) -> &Pitch {
312        &self.v2n1
313    }
314
315    /// The lower voice's second pitch.
316    pub fn v2n2(&self) -> &Pitch {
317        &self.v2n2
318    }
319
320    /// The harmonic intervals from the upper voice to the lower voice, at the
321    /// first and second moment.
322    pub fn vertical_intervals(&self) -> &[Interval; 2] {
323        &self.vertical
324    }
325
326    /// The melodic intervals each voice moves through, upper voice first.
327    pub fn horizontal_intervals(&self) -> &[Interval; 2] {
328        &self.horizontal
329    }
330
331    /// Classifies the motion. Anti-parallel motion is reported as contrary
332    /// unless `allow_anti_parallel` is set.
333    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    /// Returns whether neither voice moves.
350    pub fn no_motion(&self) -> bool {
351        self.horizontal.iter().all(Interval::is_perfect_unison)
352    }
353
354    /// Returns whether exactly one voice holds its pitch.
355    pub fn oblique_motion(&self) -> bool {
356        !self.no_motion() && self.horizontal.iter().any(Interval::is_perfect_unison)
357    }
358
359    /// Returns whether both voices move in the same direction.
360    pub fn similar_motion(&self) -> bool {
361        !self.no_motion() && self.horizontal[0].direction() == self.horizontal[1].direction()
362    }
363
364    /// Returns whether the voices move in the same direction keeping the same
365    /// generic interval. With `required`, the interval must also be that one;
366    /// `allow_octave_displacement` accepts a fifth answered by a twelfth.
367    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    /// Returns whether the voices move in opposite directions.
395    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    /// Returns whether the voices move apart.
402    pub fn outward_contrary_motion(&self) -> bool {
403        self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Ascending
404    }
405
406    /// Returns whether the voices move towards each other.
407    pub fn inward_contrary_motion(&self) -> bool {
408        self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Descending
409    }
410
411    /// Returns whether contrary motion lands on the same simple interval it
412    /// left, such as a fifth opening out to a twelfth. With `required`, that
413    /// interval must also be the given one.
414    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    /// Returns whether the voices move in parallel or anti-parallel through
422    /// the given interval, in any octave.
423    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    /// Returns whether the voices move in parallel fifths.
429    pub fn parallel_fifth(&self) -> bool {
430        self.parallel_interval(&PERFECT_FIFTH)
431    }
432
433    /// Returns whether the voices move in parallel octaves.
434    pub fn parallel_octave(&self) -> bool {
435        self.parallel_interval(&PERFECT_OCTAVE)
436    }
437
438    /// Returns whether the voices move in parallel unisons.
439    pub fn parallel_unison(&self) -> bool {
440        self.parallel_interval(&PERFECT_UNISON)
441    }
442
443    /// Returns whether the voices move in parallel unisons or octaves.
444    pub fn parallel_unison_or_octave(&self) -> bool {
445        self.parallel_unison() || self.parallel_octave()
446    }
447
448    /// Returns whether similar motion arrives at the given interval without
449    /// having started from it.
450    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    /// Returns whether similar motion arrives at a perfect fifth.
458    pub fn hidden_fifth(&self) -> bool {
459        self.hidden_interval(&PERFECT_FIFTH)
460    }
461
462    /// Returns whether similar motion arrives at a perfect octave.
463    pub fn hidden_octave(&self) -> bool {
464        self.hidden_interval(&PERFECT_OCTAVE)
465    }
466
467    /// Returns whether a voice moves past where the other voice just was.
468    pub fn voice_overlap(&self) -> bool {
469        self.v1n2.ps() < self.v2n1.ps() || self.v2n2.ps() > self.v1n1.ps()
470    }
471
472    /// Returns whether the lower voice is above the upper voice at either
473    /// moment.
474    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        // music21's own examples.
541        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        // Without a key there is nothing to be the tonic of.
555        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}