1use crate::{
9 defaults::{FloatType, IntegerType},
10 error::{Error, Result},
11 interval::{Interval, IntervalDirection},
12 key::Key,
13 pitch::Pitch,
14 scale::{Scale, ScaleType},
15 stream::{Stream, StreamElement, StreamKind},
16};
17
18use crate::interval::constants::{
19 PERFECT_FIFTH_UP as PERFECT_FIFTH, PERFECT_OCTAVE, PERFECT_UNISON,
20};
21
22#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum MotionType {
26 AntiParallel,
29 Contrary,
31 NoMotion,
33 Oblique,
35 Parallel,
37 Similar,
39}
40
41impl MotionType {
42 pub fn as_str(self) -> &'static str {
44 match self {
45 Self::AntiParallel => "Anti-Parallel",
46 Self::Contrary => "Contrary",
47 Self::NoMotion => "No Motion",
48 Self::Oblique => "Oblique",
49 Self::Parallel => "Parallel",
50 Self::Similar => "Similar",
51 }
52 }
53}
54
55impl std::fmt::Display for MotionType {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str(self.as_str())
58 }
59}
60
61#[derive(Clone, Debug, PartialEq)]
67pub enum ParallelRequirement {
68 Wide(IntegerType),
70 Named(Box<Interval>),
72}
73
74impl From<Interval> for ParallelRequirement {
75 fn from(interval: Interval) -> Self {
76 Self::Named(Box::new(interval))
77 }
78}
79
80impl From<IntegerType> for ParallelRequirement {
81 fn from(steps: IntegerType) -> Self {
82 Self::Wide(steps)
83 }
84}
85
86#[derive(Clone, Debug)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[must_use]
90pub struct VoiceLeadingQuartet {
91 v1n1: Pitch,
92 v1n2: Pitch,
93 v2n1: Pitch,
94 v2n2: Pitch,
95 vertical: [Interval; 2],
96 horizontal: [Interval; 2],
97 key: Option<Key>,
98}
99
100impl VoiceLeadingQuartet {
101 pub fn new(v1n1: Pitch, v1n2: Pitch, v2n1: Pitch, v2n2: Pitch) -> Result<Self> {
104 let vertical = [
105 Interval::between_pitches(&v1n1, &v2n1)?,
106 Interval::between_pitches(&v1n2, &v2n2)?,
107 ];
108 let horizontal = [
109 Interval::between_pitches(&v1n1, &v1n2)?,
110 Interval::between_pitches(&v2n1, &v2n2)?,
111 ];
112 Ok(Self {
113 v1n1,
114 v1n2,
115 v2n1,
116 v2n2,
117 vertical,
118 horizontal,
119 key: None,
120 })
121 }
122
123 pub fn from_names(v1n1: &str, v1n2: &str, v2n1: &str, v2n2: &str) -> Result<Self> {
125 Self::new(
126 Pitch::from_name(v1n1)?,
127 Pitch::from_name(v1n2)?,
128 Pitch::from_name(v2n1)?,
129 Pitch::from_name(v2n2)?,
130 )
131 }
132
133 pub fn with_key(mut self, key: Key) -> Self {
136 self.key = Some(key);
137 self
138 }
139
140 pub fn key(&self) -> Option<&Key> {
142 self.key.as_ref()
143 }
144
145 pub fn set_key(&mut self, key: Option<Key>) {
147 self.key = key;
148 }
149
150 pub fn is_proper_resolution(&self) -> Result<bool> {
157 if self.no_motion() {
158 return Ok(true);
159 }
160 let (lower_degree_before, lower_degree_after) = match &self.key {
161 Some(key) => {
162 let scale = key.scale();
163 let mut before = scale.degree_of(&self.v2n1)?;
164 let after = scale.degree_of(&self.v2n2)?;
165 if key.mode() == "minor" && before.is_none() {
166 before =
167 Scale::new(ScaleType::MelodicMinor, key.tonic()).degree_of(&self.v2n1)?;
168 }
169 (before, after)
170 }
171 None => (None, None),
172 };
173 let keyed = self.key.is_some();
174 let first = self.vertical[0].simple_name();
175 let second = self.vertical[1].generic().simple_undirected();
176 Ok(match first.as_str() {
177 "P4" => self.v1n1.ps() >= self.v1n2.ps(),
178 "A4" => {
179 if keyed && lower_degree_before != Some(4) {
180 true
181 } else if keyed && lower_degree_after != Some(3) {
182 false
183 } else {
184 self.outward_contrary_motion() && second == 6
185 }
186 }
187 "d5" => {
188 if keyed && lower_degree_before != Some(7) {
189 true
190 } else if keyed && lower_degree_after != Some(1) {
191 false
192 } else {
193 self.inward_contrary_motion() && second == 3
194 }
195 }
196 "m7" => {
197 if keyed && lower_degree_before != Some(5) {
198 true
199 } else if keyed && lower_degree_after != Some(1) {
200 false
201 } else {
202 second == 3
203 }
204 }
205 _ => true,
206 })
207 }
208
209 pub fn leap_not_set_with_step(&self) -> bool {
213 if self.no_motion() {
214 return false;
215 }
216 let [upper, lower] = &self.horizontal;
217 let steps_or_holds =
218 |interval: &Interval| interval.is_diatonic_step() || interval.is_unison();
219 if upper.generic().undirected() == 3
220 && lower.generic().undirected() == 3
221 && self.contrary_motion()
222 {
223 return false;
224 }
225 if upper.is_skip() {
226 !steps_or_holds(lower)
227 } else if lower.is_skip() {
228 !steps_or_holds(upper)
229 } else {
230 false
231 }
232 }
233
234 pub fn modal_opening(&self) -> Result<bool> {
243 let Some(key) = &self.key else {
244 return Err(Error::Analysis(
245 "modalOpening requires a key to be set on the VoiceLeadingQuartet".to_string(),
246 ));
247 };
248 let opening = ["P1", "P5"];
249 let sounds_open = opening.contains(&self.vertical[0].simple_name().as_str())
250 || opening.contains(&self.vertical[1].simple_name().as_str());
251 let function_of = |first: &Pitch, second: &Pitch| -> Result<Option<bool>> {
252 let chord = crate::chord::Chord::new([first.clone(), second.clone()].as_slice())?;
253 Ok(
254 crate::roman::identify_as_tonic_or_dominant(&chord, key)?.map(|figure| {
255 figure
256 .chars()
257 .next()
258 .is_some_and(|numeral| matches!(numeral.to_ascii_uppercase(), 'I' | 'V'))
259 }),
260 )
261 };
262 let established = match function_of(&self.v1n1, &self.v2n1)? {
263 Some(established) => established,
264 None => function_of(&self.v1n2, &self.v2n2)?.unwrap_or(false),
265 };
266 Ok(sounds_open && established)
267 }
268
269 pub fn opens_incorrectly(&self) -> Result<bool> {
271 self.modal_opening().map(|opens| !opens)
272 }
273
274 pub fn closes_incorrectly(&self) -> Result<bool> {
276 self.clausula_vera().map(|closes| !closes)
277 }
278
279 pub fn clausula_vera(&self) -> Result<bool> {
283 let Some(key) = &self.key else {
284 return Err(Error::Analysis(
285 "clausulaVera requires a key to be set on the VoiceLeadingQuartet".to_string(),
286 ));
287 };
288 let tonic = key.tonic().name();
289 let mut horizontal = [
290 self.horizontal[0].short_name(),
291 self.horizontal[1].short_name(),
292 ];
293 horizontal.sort_unstable();
294 Ok(horizontal == ["M2", "m2"]
295 && self.contrary_motion()
296 && matches!(self.vertical[1].short_name().as_str(), "P1" | "P8")
297 && self.v1n2.name() == tonic
298 && self.v2n2.name() == tonic)
299 }
300
301 pub fn v1n1(&self) -> &Pitch {
303 &self.v1n1
304 }
305
306 pub fn v1n2(&self) -> &Pitch {
308 &self.v1n2
309 }
310
311 pub fn v2n1(&self) -> &Pitch {
313 &self.v2n1
314 }
315
316 pub fn v2n2(&self) -> &Pitch {
318 &self.v2n2
319 }
320
321 pub fn vertical_intervals(&self) -> &[Interval; 2] {
324 &self.vertical
325 }
326
327 pub fn horizontal_intervals(&self) -> &[Interval; 2] {
329 &self.horizontal
330 }
331
332 pub fn motion_type(&self, allow_anti_parallel: bool) -> MotionType {
335 if self.oblique_motion() {
336 MotionType::Oblique
337 } else if self.parallel_motion(None, false) {
338 MotionType::Parallel
339 } else if self.similar_motion() {
340 MotionType::Similar
341 } else if allow_anti_parallel && self.anti_parallel_motion(None) {
342 MotionType::AntiParallel
343 } else if self.contrary_motion() {
344 MotionType::Contrary
345 } else {
346 MotionType::NoMotion
347 }
348 }
349
350 pub fn no_motion(&self) -> bool {
352 self.horizontal.iter().all(Interval::is_perfect_unison)
353 }
354
355 pub fn oblique_motion(&self) -> bool {
357 !self.no_motion() && self.horizontal.iter().any(Interval::is_perfect_unison)
358 }
359
360 pub fn similar_motion(&self) -> bool {
362 !self.no_motion() && self.horizontal[0].direction() == self.horizontal[1].direction()
363 }
364
365 pub fn parallel_motion(
369 &self,
370 required: Option<&ParallelRequirement>,
371 allow_octave_displacement: bool,
372 ) -> bool {
373 let [first, second] = &self.vertical;
374 if !self.similar_motion() {
375 return false;
376 }
377 if first.generic().directed() != second.generic().directed() && !allow_octave_displacement {
378 return false;
379 }
380 if first.generic().semi_simple_undirected() != second.generic().semi_simple_undirected() {
381 return false;
382 }
383 match required {
384 None => true,
385 Some(ParallelRequirement::Wide(steps)) => {
386 first.generic().semi_simple_undirected() == *steps
387 }
388 Some(ParallelRequirement::Named(required)) => {
389 first.semi_simple_key() == required.semi_simple_key()
390 && second.semi_simple_key() == required.semi_simple_key()
391 }
392 }
393 }
394
395 pub fn contrary_motion(&self) -> bool {
397 !self.no_motion()
398 && !self.oblique_motion()
399 && self.horizontal[0].direction() != self.horizontal[1].direction()
400 }
401
402 pub fn outward_contrary_motion(&self) -> bool {
404 self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Ascending
405 }
406
407 pub fn inward_contrary_motion(&self) -> bool {
409 self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Descending
410 }
411
412 pub fn anti_parallel_motion(&self, required: Option<&Interval>) -> bool {
416 let [first, second] = &self.vertical;
417 self.contrary_motion()
418 && first.simple_key() == second.simple_key()
419 && required.is_none_or(|required| first.simple_key() == required.simple_key())
420 }
421
422 pub fn parallel_interval(&self, interval: &Interval) -> bool {
425 let required = ParallelRequirement::Named(Box::new(interval.clone()));
426 self.parallel_motion(Some(&required), true) || self.anti_parallel_motion(Some(interval))
427 }
428
429 pub fn parallel_fifth(&self) -> bool {
431 self.parallel_interval(&PERFECT_FIFTH)
432 }
433
434 pub fn parallel_octave(&self) -> bool {
436 self.parallel_interval(&PERFECT_OCTAVE)
437 }
438
439 pub fn parallel_unison(&self) -> bool {
441 self.parallel_interval(&PERFECT_UNISON)
442 }
443
444 pub fn parallel_unison_or_octave(&self) -> bool {
446 self.parallel_unison() || self.parallel_octave()
447 }
448
449 pub fn hidden_interval(&self, interval: &Interval) -> bool {
452 if self.parallel_motion(None, true) || !self.similar_motion() {
453 return false;
454 }
455 self.vertical[1].simple_key() == interval.simple_key()
456 }
457
458 pub fn hidden_fifth(&self) -> bool {
460 self.hidden_interval(&PERFECT_FIFTH)
461 }
462
463 pub fn hidden_octave(&self) -> bool {
465 self.hidden_interval(&PERFECT_OCTAVE)
466 }
467
468 pub fn voice_overlap(&self) -> bool {
470 self.v1n2.ps() < self.v2n1.ps() || self.v2n2.ps() > self.v1n1.ps()
471 }
472
473 pub fn voice_crossing(&self) -> bool {
476 self.v1n1.ps() < self.v2n1.ps() || self.v1n2.ps() < self.v2n2.ps()
477 }
478}
479
480#[derive(Clone, Copy, Debug, Eq, PartialEq)]
483#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
484pub struct QuartetOptions {
485 pub include_rests: bool,
488 pub include_oblique: bool,
490 pub include_no_motion: bool,
492}
493
494impl Default for QuartetOptions {
495 fn default() -> Self {
496 Self {
497 include_rests: true,
498 include_oblique: true,
499 include_no_motion: false,
500 }
501 }
502}
503
504struct Span<'a> {
506 offset: FloatType,
507 end: FloatType,
508 part: usize,
509 position: usize,
511 element: &'a StreamElement,
512}
513
514impl Span<'_> {
515 fn is_pitched(&self) -> bool {
516 self.element.is_note_or_chord()
517 }
518
519 fn class_sort_order(&self) -> i8 {
522 match self.element {
523 StreamElement::TextExpression(_) => -30,
524 StreamElement::Instrument(_) => -25,
525 StreamElement::Clef(_) => 0,
526 StreamElement::MetronomeMark(_) | StreamElement::TempoText(_) => 1,
527 StreamElement::KeySignature(_) | StreamElement::Key(_) => 2,
528 StreamElement::TimeSignature(_) => 4,
529 StreamElement::Dynamic(_) => 10,
530 StreamElement::ChordSymbol(_) => 19,
531 _ => 20,
532 }
533 }
534}
535
536fn parts_by_position(stream: &Stream) -> Vec<Vec<(usize, FloatType, &StreamElement)>> {
541 let tops: Vec<usize> = stream
542 .events()
543 .iter()
544 .enumerate()
545 .filter(|(_, event)| {
546 event
547 .element()
548 .as_stream()
549 .is_some_and(|part| matches!(part.kind(), StreamKind::Part | StreamKind::PartStaff))
550 })
551 .map(|(top, _)| top)
552 .collect();
553 let leaves = stream.leaves_under_top();
554 let mut parts: Vec<Vec<(usize, FloatType, &StreamElement)>> = if tops.is_empty() {
555 vec![
556 leaves
557 .iter()
558 .enumerate()
559 .map(|(position, &(_, offset, element))| (position, offset, element))
560 .collect(),
561 ]
562 } else {
563 tops.iter()
564 .map(|&top| {
565 leaves
566 .iter()
567 .enumerate()
568 .filter(|(_, (under, _, _))| *under == top)
569 .map(|(position, &(_, offset, element))| (position, offset, element))
570 .collect()
571 })
572 .collect()
573 };
574 for part in &mut parts {
576 part.sort_by(|left, right| left.1.total_cmp(&right.1));
577 }
578 parts
579}
580
581pub fn iterate_all_voice_leading_quartets(
600 stream: &Stream,
601 options: QuartetOptions,
602) -> Result<Vec<VoiceLeadingQuartet>> {
603 let leaves = stream.leaves();
604 let pitch = |position: usize| match leaves[position].1 {
605 StreamElement::Note(note) => note.pitch().clone(),
606 _ => unreachable!("a quartet is made of notes alone"),
607 };
608 voice_leading_quartet_positions(stream, options)
609 .into_iter()
610 .map(|(_, [v1n1, v1n2, v2n1, v2n2])| {
611 VoiceLeadingQuartet::new(pitch(v1n1), pitch(v1n2), pitch(v2n1), pitch(v2n2))
612 })
613 .collect()
614}
615
616pub fn voice_leading_quartet_positions(
640 stream: &Stream,
641 options: QuartetOptions,
642) -> Vec<(FloatType, [usize; 4])> {
643 let parts = parts_by_position(stream);
644 let mut spans: Vec<Span<'_>> = Vec::new();
645 for (part, flat) in parts.iter().enumerate() {
646 for &(position, offset, element) in flat {
647 spans.push(Span {
648 offset,
649 end: offset + element.quarter_length(),
650 part,
651 position,
652 element,
653 });
654 }
655 }
656 spans.sort_by(|left, right| {
659 left.offset
660 .total_cmp(&right.offset)
661 .then(left.class_sort_order().cmp(&right.class_sort_order()))
662 });
663
664 let mut quartets = Vec::new();
665 let mut start = 0;
666 while start < spans.len() {
667 let offset = spans[start].offset;
668 let stop = start
669 + spans[start..]
670 .iter()
671 .take_while(|span| span.offset == offset)
672 .count();
673
674 let mut motions: Vec<(&Span<'_>, &Span<'_>)> = Vec::new();
675 for starting in spans[start..stop].iter().filter(|span| span.is_pitched()) {
676 let Some(nearest) = spans[..start]
679 .iter()
680 .rev()
681 .find(|span| span.part == starting.part)
682 .map(|span| span.offset)
683 else {
684 continue;
685 };
686 let previous = spans[..start]
687 .iter()
688 .find(|span| span.part == starting.part && span.offset == nearest);
689 let Some(previous) = previous.filter(|span| span.is_pitched()) else {
690 continue;
691 };
692 if !options.include_rests && (previous.end - offset).abs() > 1e-9 {
695 continue;
696 }
697 if !options.include_oblique && previous.element.pitches() == starting.element.pitches()
698 {
699 continue;
700 }
701 motions.push((previous, starting));
702 }
703 if options.include_oblique {
704 for held in spans[..start]
705 .iter()
706 .filter(|span| span.is_pitched() && span.end - offset > 1e-9)
707 {
708 motions.push((held, held));
709 }
710 }
711
712 for (index, first) in motions.iter().enumerate() {
713 for second in &motions[index + 1..] {
714 if !options.include_no_motion
715 && first.0.element.pitches() == first.1.element.pitches()
716 && second.0.element.pitches() == second.1.element.pitches()
717 {
718 continue;
719 }
720 let four = [first.0, first.1, second.0, second.1];
721 if four
722 .iter()
723 .all(|span| matches!(span.element, StreamElement::Note(_)))
724 {
725 quartets.push((offset, four.map(|span| span.position)));
726 }
727 }
728 }
729 start = stop;
730 }
731 quartets
732}
733
734pub fn verticality_at(stream: &Stream, offset: FloatType) -> Vec<(usize, Vec<StreamElement>)> {
742 let leaves = stream.leaves();
743 verticality_positions_at(stream, offset)
744 .into_iter()
745 .map(|(part, positions)| {
746 (
747 part,
748 positions
749 .into_iter()
750 .map(|position| leaves[position].1.clone())
751 .collect(),
752 )
753 })
754 .collect()
755}
756
757pub fn verticality_positions_at(stream: &Stream, offset: FloatType) -> Vec<(usize, Vec<usize>)> {
772 parts_by_position(stream)
773 .iter()
774 .enumerate()
775 .filter_map(|(part, flat)| {
776 let positions: Vec<usize> = flat
777 .iter()
778 .filter(|(_, start, element)| {
779 *start == offset
780 || (*start < offset && offset < *start + element.quarter_length())
781 })
782 .map(|(position, _, _)| *position)
783 .collect();
784 (!positions.is_empty()).then_some((part, positions))
785 })
786 .collect()
787}
788
789#[cfg(test)]
790mod tests {
791
792 #[test]
793 fn a_quartet_hands_back_its_pitches_intervals_and_key() {
794 use super::ParallelRequirement;
795 use crate::Interval;
796
797 let mut quartet = VoiceLeadingQuartet::from_names("C5", "D5", "C4", "D4").unwrap();
798 assert_eq!(quartet.v1n1().name_with_octave(), "C5");
799 assert_eq!(quartet.v1n2().name_with_octave(), "D5");
800 assert_eq!(quartet.v2n1().name_with_octave(), "C4");
801 assert_eq!(quartet.v2n2().name_with_octave(), "D4");
802 let vertical: Vec<String> = quartet
803 .vertical_intervals()
804 .iter()
805 .map(Interval::short_name)
806 .collect();
807 assert_eq!(vertical, ["P8", "P8"]);
808 let horizontal: Vec<String> = quartet
809 .horizontal_intervals()
810 .iter()
811 .map(Interval::short_name)
812 .collect();
813 assert_eq!(horizontal, ["M2", "M2"]);
814 assert!(quartet.parallel_unison_or_octave());
815 assert!(quartet.key().is_none());
816 quartet.set_key(Some(Key::from_tonic("C").unwrap()));
817 assert!(quartet.key().is_some());
818 assert!(matches!(
819 ParallelRequirement::from(8),
820 ParallelRequirement::Wide(8)
821 ));
822 assert!(matches!(
823 ParallelRequirement::from(Interval::from_name("P5").unwrap()),
824 ParallelRequirement::Named(_)
825 ));
826 }
827
828 #[test]
829 fn opening_and_closing_incorrectly_are_the_opposites_of_the_rules() {
830 let quartet = |v1n1, v1n2, v2n1, v2n2, key: &str| {
831 VoiceLeadingQuartet::from_names(v1n1, v1n2, v2n1, v2n2)
832 .unwrap()
833 .with_key(Key::from_tonic(key).unwrap())
834 };
835 let opens = quartet("D", "D", "D", "F#", "D");
836 assert!(opens.modal_opening().unwrap());
837 assert!(!opens.opens_incorrectly().unwrap());
838 let closes = quartet("B4", "C5", "D4", "C4", "C");
839 assert_eq!(
840 closes.closes_incorrectly().unwrap(),
841 !closes.clausula_vera().unwrap()
842 );
843 let unkeyed = VoiceLeadingQuartet::from_names("D", "D", "D", "F#").unwrap();
844 assert!(unkeyed.opens_incorrectly().is_err());
845 assert!(unkeyed.closes_incorrectly().is_err());
846 }
847
848 #[test]
849 fn a_modal_opening_needs_a_perfect_interval_and_a_tonic_or_dominant() {
850 let opening = |v1n1, v1n2, v2n1, v2n2, key: &str| {
852 VoiceLeadingQuartet::from_names(v1n1, v1n2, v2n1, v2n2)
853 .unwrap()
854 .with_key(Key::from_tonic(key).unwrap())
855 .modal_opening()
856 .unwrap()
857 };
858 assert!(opening("D", "D", "D", "F#", "D"));
859 assert!(opening("B", "A", "G#", "A", "A"));
860 assert!(opening("A", "A", "F#", "D", "A"));
861 assert!(!opening("C#", "C#", "D", "E", "A"));
862 assert!(!opening("B", "B", "A", "A", "C"));
863
864 assert!(
866 VoiceLeadingQuartet::from_names("D", "D", "D", "F#")
867 .unwrap()
868 .modal_opening()
869 .is_err()
870 );
871 }
872
873 #[test]
874 #[allow(clippy::type_complexity)]
875 fn proper_resolution_leaps_and_clausula_vera_match_music21() {
876 let quartet = |a: &str, b: &str, c: &str, d: &str, key: Option<&str>| {
877 let built = VoiceLeadingQuartet::from_names(a, b, c, d).unwrap();
878 match key {
879 Some(key) => built.with_key(Key::from_tonic(key).unwrap()),
880 None => built,
881 }
882 };
883 let cases: [(
884 &str,
885 &str,
886 &str,
887 &str,
888 Option<&str>,
889 bool,
890 bool,
891 Option<bool>,
892 ); 20] = [
893 ("C4", "B3", "F4", "E4", None, true, false, None),
894 ("C4", "B3", "F4", "E4", Some("C"), true, false, Some(false)),
895 ("B3", "C4", "F4", "E4", Some("C"), true, false, Some(false)),
896 ("B3", "C4", "F4", "E4", None, false, false, None),
897 ("F4", "E4", "B3", "C4", Some("C"), true, false, Some(false)),
898 ("F4", "E4", "B3", "C4", None, true, false, None),
899 ("G3", "C4", "F4", "E4", Some("C"), true, false, Some(false)),
900 ("G3", "C4", "F4", "E4", Some("G"), true, false, Some(false)),
901 ("C4", "C4", "E4", "E4", Some("C"), true, false, Some(false)),
902 ("D4", "F4", "F4", "A4", None, true, true, None),
903 ("C4", "F4", "E4", "F4", None, true, false, None),
904 ("C4", "E4", "E4", "G4", None, true, true, None),
905 ("C4", "E4", "E4", "C4", None, true, false, None),
906 ("B3", "C4", "D4", "C4", Some("C"), true, false, Some(true)),
907 ("B3", "C4", "D4", "C4", Some("G"), true, false, Some(false)),
908 ("B3", "C4", "D4", "C5", Some("C"), true, false, Some(false)),
909 ("F4", "E4", "G3", "C4", Some("C"), true, false, Some(false)),
910 ("F4", "E4", "B3", "C4", Some("F"), true, false, Some(false)),
911 ("D4", "C4", "B3", "C4", Some("C"), true, false, Some(true)),
912 ("C4", "D4", "G4", "F4", None, true, false, None),
913 ];
914 for (a, b, c, d, key, proper, leap, clausula) in cases {
915 let vlq = quartet(a, b, c, d, key);
916 assert_eq!(
917 vlq.is_proper_resolution().unwrap(),
918 proper,
919 "{a} {b} {c} {d} {key:?}"
920 );
921 assert_eq!(
922 vlq.leap_not_set_with_step(),
923 leap,
924 "{a} {b} {c} {d} {key:?}"
925 );
926 match clausula {
927 Some(expected) => assert_eq!(
928 vlq.clausula_vera().unwrap(),
929 expected,
930 "{a} {b} {c} {d} {key:?}"
931 ),
932 None => assert!(vlq.clausula_vera().is_err(), "{a} {b} {c} {d}"),
933 }
934 assert_eq!(vlq.key().is_some(), key.is_some());
935 }
936 }
937 use super::*;
938
939 struct Expected {
940 motion: MotionType,
941 with_anti_parallel: MotionType,
942 flags: [bool; 15],
943 }
944
945 #[test]
946 fn quartets_match_music21() {
947 use MotionType::*;
948 let t = true;
949 let f = false;
950 let cases = [
951 (
952 ("C4", "D4", "C3", "D3"),
953 Expected {
954 motion: Parallel,
955 with_anti_parallel: Parallel,
956 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
957 },
958 ),
959 (
960 ("C4", "D4", "E3", "F3"),
961 Expected {
962 motion: Parallel,
963 with_anti_parallel: Parallel,
964 flags: [f, f, t, t, f, f, f, f, f, f, f, f, f, f, f],
965 },
966 ),
967 (
968 ("C4", "G4", "C3", "C3"),
969 Expected {
970 motion: Oblique,
971 with_anti_parallel: Oblique,
972 flags: [f, t, f, f, f, f, f, f, f, f, f, f, f, f, f],
973 },
974 ),
975 (
976 ("C4", "C4", "C3", "C3"),
977 Expected {
978 motion: NoMotion,
979 with_anti_parallel: NoMotion,
980 flags: [t, f, f, f, f, f, f, f, f, f, f, f, f, f, f],
981 },
982 ),
983 (
984 ("C4", "D4", "G3", "F3"),
985 Expected {
986 motion: Contrary,
987 with_anti_parallel: Contrary,
988 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
989 },
990 ),
991 (
992 ("C4", "C5", "F3", "F4"),
993 Expected {
994 motion: Parallel,
995 with_anti_parallel: Parallel,
996 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, t, f],
997 },
998 ),
999 (
1000 ("C5", "D5", "C4", "D4"),
1001 Expected {
1002 motion: Parallel,
1003 with_anti_parallel: Parallel,
1004 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
1005 },
1006 ),
1007 (
1008 ("C4", "D4", "G4", "A4"),
1009 Expected {
1010 motion: Parallel,
1011 with_anti_parallel: Parallel,
1012 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, t, t],
1013 },
1014 ),
1015 (
1016 ("C5", "D5", "G4", "E5"),
1017 Expected {
1018 motion: Similar,
1019 with_anti_parallel: Similar,
1020 flags: [f, f, t, f, f, f, f, f, f, f, f, f, f, t, t],
1021 },
1022 ),
1023 (
1024 ("E4", "F4", "C4", "A3"),
1025 Expected {
1026 motion: Contrary,
1027 with_anti_parallel: Contrary,
1028 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
1029 },
1030 ),
1031 (
1032 ("G4", "F4", "C4", "D4"),
1033 Expected {
1034 motion: Contrary,
1035 with_anti_parallel: Contrary,
1036 flags: [f, f, f, f, t, f, t, f, f, f, f, f, f, f, f],
1037 },
1038 ),
1039 (
1040 ("C4", "C#4", "C3", "C#3"),
1041 Expected {
1042 motion: Parallel,
1043 with_anti_parallel: Parallel,
1044 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
1045 },
1046 ),
1047 (
1048 ("C4", "B3", "F3", "G3"),
1049 Expected {
1050 motion: Contrary,
1051 with_anti_parallel: Contrary,
1052 flags: [f, f, f, f, t, f, t, f, f, f, f, f, f, f, f],
1053 },
1054 ),
1055 (
1056 ("D5", "A5", "G3", "D4"),
1057 Expected {
1058 motion: Parallel,
1059 with_anti_parallel: Parallel,
1060 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, f, f],
1061 },
1062 ),
1063 (
1064 ("A4", "B4", "F4", "E4"),
1065 Expected {
1066 motion: Contrary,
1067 with_anti_parallel: Contrary,
1068 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
1069 },
1070 ),
1071 (
1072 ("C5", "C5", "E4", "F4"),
1073 Expected {
1074 motion: Oblique,
1075 with_anti_parallel: Oblique,
1076 flags: [f, t, f, f, f, f, f, f, f, f, f, f, f, f, f],
1077 },
1078 ),
1079 ];
1080 for ((a, b, c, d), expected) in cases {
1081 let quartet = VoiceLeadingQuartet::from_names(a, b, c, d).unwrap();
1082 let label = format!("{a} {b} / {c} {d}");
1083 assert_eq!(quartet.motion_type(false), expected.motion, "{label}");
1084 assert_eq!(
1085 quartet.motion_type(true),
1086 expected.with_anti_parallel,
1087 "{label}"
1088 );
1089 let actual = [
1090 quartet.no_motion(),
1091 quartet.oblique_motion(),
1092 quartet.similar_motion(),
1093 quartet.parallel_motion(None, false),
1094 quartet.contrary_motion(),
1095 quartet.outward_contrary_motion(),
1096 quartet.inward_contrary_motion(),
1097 quartet.anti_parallel_motion(None),
1098 quartet.parallel_fifth(),
1099 quartet.parallel_octave(),
1100 quartet.parallel_unison(),
1101 quartet.hidden_fifth(),
1102 quartet.hidden_octave(),
1103 quartet.voice_overlap(),
1104 quartet.voice_crossing(),
1105 ];
1106 assert_eq!(actual, expected.flags, "{label}");
1107 }
1108 }
1109
1110 #[test]
1111 fn anti_parallel_fifths_are_contrary_unless_asked_for() {
1112 let quartet = VoiceLeadingQuartet::from_names("G4", "D5", "C4", "G3").unwrap();
1113 assert_eq!(quartet.motion_type(false), MotionType::Contrary);
1114 assert_eq!(quartet.motion_type(true), MotionType::AntiParallel);
1115 assert!(quartet.parallel_fifth());
1116 assert_eq!(MotionType::AntiParallel.to_string(), "Anti-Parallel");
1117 }
1118
1119 #[test]
1120 fn hidden_intervals_need_similar_motion_into_a_perfect_interval() {
1121 let quartet = VoiceLeadingQuartet::from_names("E4", "G4", "C4", "C3").unwrap();
1122 assert!(!quartet.hidden_fifth());
1123 let quartet = VoiceLeadingQuartet::from_names("E4", "D5", "C4", "G4").unwrap();
1124 assert!(quartet.hidden_fifth());
1125 assert!(!quartet.hidden_octave());
1126 let quartet = VoiceLeadingQuartet::from_names("E4", "C5", "C4", "C4").unwrap();
1127 assert!(!quartet.hidden_octave());
1128 }
1129
1130 #[test]
1133 fn a_score_is_walked_into_its_quartets() {
1134 use crate::{Duration, Note, Rest};
1135
1136 let line = |notes: &[(&str, FloatType)]| {
1137 let mut part = Stream::with_kind(StreamKind::Part);
1138 for (name, length) in notes {
1139 let duration = Duration::new(*length).unwrap();
1140 if name.is_empty() {
1141 part.push(Rest::new(duration));
1142 } else {
1143 part.push(Note::from_name(*name).unwrap().with_duration(duration));
1144 }
1145 }
1146 part
1147 };
1148 let mut score = Stream::with_kind(StreamKind::Score);
1149 score.insert(
1150 0.0,
1151 line(&[("C5", 1.0), ("D5", 1.0), ("", 1.0), ("E5", 1.0)]),
1152 );
1153 score.insert(0.0, line(&[("C4", 2.0), ("G3", 2.0)]));
1154
1155 let names = |options| -> Vec<String> {
1156 iterate_all_voice_leading_quartets(&score, options)
1157 .unwrap()
1158 .iter()
1159 .map(|quartet| {
1160 [
1161 quartet.v1n1(),
1162 quartet.v1n2(),
1163 quartet.v2n1(),
1164 quartet.v2n2(),
1165 ]
1166 .map(Pitch::name_with_octave)
1167 .join(" ")
1168 })
1169 .collect()
1170 };
1171 assert_eq!(names(QuartetOptions::default()), ["C5 D5 C4 C4"]);
1175 let strict = QuartetOptions {
1176 include_oblique: false,
1177 ..QuartetOptions::default()
1178 };
1179 assert!(names(strict).is_empty());
1180
1181 let sounding = verticality_at(&score, 1.5);
1182 assert_eq!(sounding.len(), 2);
1183 assert_eq!(sounding[0].1[0].pitches()[0].name_with_octave(), "D5");
1184 assert_eq!(sounding[1].1[0].pitches()[0].name_with_octave(), "C4");
1185 assert!(verticality_at(&score, 9.0).is_empty());
1186 }
1187}