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 set_pitches(&mut self, v1n1: Pitch, v1n2: Pitch, v2n1: Pitch, v2n2: Pitch) {
155 self.v1n1 = v1n1;
156 self.v1n2 = v1n2;
157 self.v2n1 = v2n1;
158 self.v2n2 = v2n2;
159 }
160
161 pub fn is_proper_resolution(&self) -> Result<bool> {
168 if self.no_motion() {
169 return Ok(true);
170 }
171 let (lower_degree_before, lower_degree_after) = match &self.key {
172 Some(key) => {
173 let scale = match key.mode() {
178 "major" | "minor" => key.key_signature().scale(key.mode())?,
179 _ => key.scale(),
180 };
181 let mut before = scale.degree_of(&self.v2n1)?;
182 let after = scale.degree_of(&self.v2n2)?;
183 if key.mode() == "minor" && before.is_none() {
184 before =
185 Scale::new(ScaleType::MelodicMinor, key.tonic()).degree_of(&self.v2n1)?;
186 }
187 (before, after)
188 }
189 None => (None, None),
190 };
191 let keyed = self.key.is_some();
192 let first = self.vertical[0].simple_name();
193 let second = self.vertical[1].generic().simple_undirected();
194 Ok(match first.as_str() {
195 "P4" => self.v1n1.ps() >= self.v1n2.ps(),
196 "A4" => {
197 if keyed && lower_degree_before != Some(4) {
198 true
199 } else if keyed && lower_degree_after != Some(3) {
200 false
201 } else {
202 self.outward_contrary_motion() && second == 6
203 }
204 }
205 "d5" => {
206 if keyed && lower_degree_before != Some(7) {
207 true
208 } else if keyed && lower_degree_after != Some(1) {
209 false
210 } else {
211 self.inward_contrary_motion() && second == 3
212 }
213 }
214 "m7" => {
215 if keyed && lower_degree_before != Some(5) {
216 true
217 } else if keyed && lower_degree_after != Some(1) {
218 false
219 } else {
220 second == 3
221 }
222 }
223 _ => true,
224 })
225 }
226
227 pub fn leap_not_set_with_step(&self) -> bool {
231 if self.no_motion() {
232 return false;
233 }
234 let [upper, lower] = &self.horizontal;
235 let steps_or_holds =
236 |interval: &Interval| interval.is_diatonic_step() || interval.is_unison();
237 if upper.generic().undirected() == 3
238 && lower.generic().undirected() == 3
239 && self.contrary_motion()
240 {
241 return false;
242 }
243 if upper.is_skip() {
244 !steps_or_holds(lower)
245 } else if lower.is_skip() {
246 !steps_or_holds(upper)
247 } else {
248 false
249 }
250 }
251
252 pub fn modal_opening(&self) -> Result<bool> {
261 let Some(key) = &self.key else {
262 return Err(Error::Analysis(
263 "modalOpening requires a key to be set on the VoiceLeadingQuartet".to_string(),
264 ));
265 };
266 let opening = ["P1", "P5"];
267 let sounds_open = opening.contains(&self.vertical[0].simple_name().as_str())
268 || opening.contains(&self.vertical[1].simple_name().as_str());
269 let function_of = |first: &Pitch, second: &Pitch| -> Result<Option<bool>> {
270 let chord = crate::chord::Chord::new([first.clone(), second.clone()].as_slice())?;
271 Ok(
272 crate::roman::identify_as_tonic_or_dominant(&chord, key)?.map(|figure| {
273 figure
274 .chars()
275 .next()
276 .is_some_and(|numeral| matches!(numeral.to_ascii_uppercase(), 'I' | 'V'))
277 }),
278 )
279 };
280 let established = match function_of(&self.v1n1, &self.v2n1)? {
281 Some(established) => established,
282 None => function_of(&self.v1n2, &self.v2n2)?.unwrap_or(false),
283 };
284 Ok(sounds_open && established)
285 }
286
287 pub fn opens_incorrectly(&self) -> Result<bool> {
289 self.modal_opening().map(|opens| !opens)
290 }
291
292 pub fn closes_incorrectly(&self) -> Result<bool> {
294 self.clausula_vera().map(|closes| !closes)
295 }
296
297 pub fn clausula_vera(&self) -> Result<bool> {
301 let Some(key) = &self.key else {
302 return Err(Error::Analysis(
303 "clausulaVera requires a key to be set on the VoiceLeadingQuartet".to_string(),
304 ));
305 };
306 let tonic = key.tonic().name();
307 let mut horizontal = [
308 self.horizontal[0].short_name(),
309 self.horizontal[1].short_name(),
310 ];
311 horizontal.sort_unstable();
312 Ok(horizontal == ["M2", "m2"]
313 && self.contrary_motion()
314 && matches!(self.vertical[1].short_name().as_str(), "P1" | "P8")
315 && self.v1n2.name() == tonic
316 && self.v2n2.name() == tonic)
317 }
318
319 pub fn v1n1(&self) -> &Pitch {
321 &self.v1n1
322 }
323
324 pub fn v1n2(&self) -> &Pitch {
326 &self.v1n2
327 }
328
329 pub fn v2n1(&self) -> &Pitch {
331 &self.v2n1
332 }
333
334 pub fn v2n2(&self) -> &Pitch {
336 &self.v2n2
337 }
338
339 pub fn vertical_intervals(&self) -> &[Interval; 2] {
342 &self.vertical
343 }
344
345 pub fn horizontal_intervals(&self) -> &[Interval; 2] {
347 &self.horizontal
348 }
349
350 pub fn motion_type(&self, allow_anti_parallel: bool) -> MotionType {
353 if self.oblique_motion() {
354 MotionType::Oblique
355 } else if self.parallel_motion(None, false) {
356 MotionType::Parallel
357 } else if self.similar_motion() {
358 MotionType::Similar
359 } else if allow_anti_parallel && self.anti_parallel_motion(None) {
360 MotionType::AntiParallel
361 } else if self.contrary_motion() {
362 MotionType::Contrary
363 } else {
364 MotionType::NoMotion
365 }
366 }
367
368 pub fn no_motion(&self) -> bool {
370 self.horizontal.iter().all(Interval::is_perfect_unison)
371 }
372
373 pub fn oblique_motion(&self) -> bool {
375 !self.no_motion() && self.horizontal.iter().any(Interval::is_perfect_unison)
376 }
377
378 pub fn similar_motion(&self) -> bool {
380 !self.no_motion() && self.horizontal[0].direction() == self.horizontal[1].direction()
381 }
382
383 pub fn parallel_motion(
387 &self,
388 required: Option<&ParallelRequirement>,
389 allow_octave_displacement: bool,
390 ) -> bool {
391 let [first, second] = &self.vertical;
392 if !self.similar_motion() {
393 return false;
394 }
395 if first.generic().directed() != second.generic().directed() && !allow_octave_displacement {
396 return false;
397 }
398 if first.generic().semi_simple_undirected() != second.generic().semi_simple_undirected() {
399 return false;
400 }
401 match required {
402 None => true,
403 Some(ParallelRequirement::Wide(steps)) => {
404 first.generic().semi_simple_undirected() == *steps
405 }
406 Some(ParallelRequirement::Named(required)) => {
407 first.semi_simple_key() == required.semi_simple_key()
408 && second.semi_simple_key() == required.semi_simple_key()
409 }
410 }
411 }
412
413 pub fn contrary_motion(&self) -> bool {
415 !self.no_motion()
416 && !self.oblique_motion()
417 && self.horizontal[0].direction() != self.horizontal[1].direction()
418 }
419
420 pub fn outward_contrary_motion(&self) -> bool {
422 self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Ascending
423 }
424
425 pub fn inward_contrary_motion(&self) -> bool {
427 self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Descending
428 }
429
430 pub fn anti_parallel_motion(&self, required: Option<&Interval>) -> bool {
434 let [first, second] = &self.vertical;
435 self.contrary_motion()
436 && first.simple_key() == second.simple_key()
437 && required.is_none_or(|required| first.simple_key() == required.simple_key())
438 }
439
440 pub fn parallel_interval(&self, interval: &Interval) -> bool {
443 let required = ParallelRequirement::Named(Box::new(interval.clone()));
444 self.parallel_motion(Some(&required), true) || self.anti_parallel_motion(Some(interval))
445 }
446
447 pub fn parallel_fifth(&self) -> bool {
449 self.parallel_interval(&PERFECT_FIFTH)
450 }
451
452 pub fn parallel_octave(&self) -> bool {
454 self.parallel_interval(&PERFECT_OCTAVE)
455 }
456
457 pub fn parallel_unison(&self) -> bool {
459 self.parallel_interval(&PERFECT_UNISON)
460 }
461
462 pub fn parallel_unison_or_octave(&self) -> bool {
464 self.parallel_unison() || self.parallel_octave()
465 }
466
467 pub fn hidden_interval(&self, interval: &Interval) -> bool {
470 if self.parallel_motion(None, true) || !self.similar_motion() {
471 return false;
472 }
473 self.vertical[1].simple_key() == interval.simple_key()
474 }
475
476 pub fn hidden_fifth(&self) -> bool {
478 self.hidden_interval(&PERFECT_FIFTH)
479 }
480
481 pub fn hidden_octave(&self) -> bool {
483 self.hidden_interval(&PERFECT_OCTAVE)
484 }
485
486 pub fn voice_overlap(&self) -> bool {
488 self.v1n2.ps() < self.v2n1.ps() || self.v2n2.ps() > self.v1n1.ps()
489 }
490
491 pub fn voice_crossing(&self) -> bool {
494 self.v1n1.ps() < self.v2n1.ps() || self.v1n2.ps() < self.v2n2.ps()
495 }
496}
497
498#[derive(Clone, Copy, Debug, Eq, PartialEq)]
501#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
502#[non_exhaustive]
503pub struct QuartetOptions {
504 pub include_rests: bool,
507 pub include_oblique: bool,
509 pub include_no_motion: bool,
511}
512
513impl Default for QuartetOptions {
514 fn default() -> Self {
515 Self {
516 include_rests: true,
517 include_oblique: true,
518 include_no_motion: false,
519 }
520 }
521}
522
523struct Span<'a> {
525 offset: FloatType,
526 end: FloatType,
527 part: usize,
528 position: usize,
530 element: &'a StreamElement,
531}
532
533impl Span<'_> {
534 fn is_pitched(&self) -> bool {
535 self.element.is_note_or_chord()
536 }
537
538 fn class_sort_order(&self) -> i8 {
541 match self.element {
542 StreamElement::TextExpression(_) => -30,
543 StreamElement::Instrument(_) => -25,
544 StreamElement::Clef(_) => 0,
545 StreamElement::MetronomeMark(_) | StreamElement::TempoText(_) => 1,
546 StreamElement::KeySignature(_) | StreamElement::Key(_) => 2,
547 StreamElement::TimeSignature(_) => 4,
548 StreamElement::Dynamic(_) => 10,
549 StreamElement::ChordSymbol(_) => 19,
550 _ => 20,
551 }
552 }
553}
554
555fn parts_by_position(stream: &Stream) -> Vec<Vec<(usize, FloatType, &StreamElement)>> {
560 let tops: Vec<usize> = stream
561 .events()
562 .iter()
563 .enumerate()
564 .filter(|(_, event)| {
565 event
566 .element()
567 .as_stream()
568 .is_some_and(|part| matches!(part.kind(), StreamKind::Part | StreamKind::PartStaff))
569 })
570 .map(|(top, _)| top)
571 .collect();
572 let leaves = stream.leaves_under_top();
573 let mut parts: Vec<Vec<(usize, FloatType, &StreamElement)>> = if tops.is_empty() {
574 vec![
575 leaves
576 .iter()
577 .enumerate()
578 .map(|(position, &(_, offset, element))| (position, offset, element))
579 .collect(),
580 ]
581 } else {
582 tops.iter()
583 .map(|&top| {
584 leaves
585 .iter()
586 .enumerate()
587 .filter(|(_, (under, _, _))| *under == top)
588 .map(|(position, &(_, offset, element))| (position, offset, element))
589 .collect()
590 })
591 .collect()
592 };
593 for part in &mut parts {
595 part.sort_by(|left, right| left.1.total_cmp(&right.1));
596 }
597 parts
598}
599
600pub fn iterate_all_voice_leading_quartets(
619 stream: &Stream,
620 options: QuartetOptions,
621) -> Result<Vec<VoiceLeadingQuartet>> {
622 let leaves = stream.leaves();
623 let pitch = |position: usize| match leaves[position].1 {
624 StreamElement::Note(note) => note.pitch().clone(),
625 _ => unreachable!("a quartet is made of notes alone"),
626 };
627 voice_leading_quartet_positions(stream, options)
628 .into_iter()
629 .map(|(_, [v1n1, v1n2, v2n1, v2n2])| {
630 VoiceLeadingQuartet::new(pitch(v1n1), pitch(v1n2), pitch(v2n1), pitch(v2n2))
631 })
632 .collect()
633}
634
635pub fn voice_leading_quartet_positions(
659 stream: &Stream,
660 options: QuartetOptions,
661) -> Vec<(FloatType, [usize; 4])> {
662 let parts = parts_by_position(stream);
663 let mut spans: Vec<Span<'_>> = Vec::new();
664 for (part, flat) in parts.iter().enumerate() {
665 for &(position, offset, element) in flat {
666 spans.push(Span {
667 offset,
668 end: offset + element.quarter_length(),
669 part,
670 position,
671 element,
672 });
673 }
674 }
675 spans.sort_by(|left, right| {
678 left.offset
679 .total_cmp(&right.offset)
680 .then(left.class_sort_order().cmp(&right.class_sort_order()))
681 });
682
683 let mut quartets = Vec::new();
684 let mut start = 0;
685 while start < spans.len() {
686 let offset = spans[start].offset;
687 let stop = start
688 + spans[start..]
689 .iter()
690 .take_while(|span| span.offset == offset)
691 .count();
692
693 let mut motions: Vec<(&Span<'_>, &Span<'_>)> = Vec::new();
694 for starting in spans[start..stop].iter().filter(|span| span.is_pitched()) {
695 let Some(nearest) = spans[..start]
698 .iter()
699 .rev()
700 .find(|span| span.part == starting.part)
701 .map(|span| span.offset)
702 else {
703 continue;
704 };
705 let previous = spans[..start]
706 .iter()
707 .find(|span| span.part == starting.part && span.offset == nearest);
708 let Some(previous) = previous.filter(|span| span.is_pitched()) else {
709 continue;
710 };
711 if !options.include_rests && (previous.end - offset).abs() > 1e-9 {
714 continue;
715 }
716 if !options.include_oblique && previous.element.pitches() == starting.element.pitches()
717 {
718 continue;
719 }
720 motions.push((previous, starting));
721 }
722 if options.include_oblique {
723 for held in spans[..start]
724 .iter()
725 .filter(|span| span.is_pitched() && span.end - offset > 1e-9)
726 {
727 motions.push((held, held));
728 }
729 }
730
731 for (index, first) in motions.iter().enumerate() {
732 for second in &motions[index + 1..] {
733 if !options.include_no_motion
734 && first.0.element.pitches() == first.1.element.pitches()
735 && second.0.element.pitches() == second.1.element.pitches()
736 {
737 continue;
738 }
739 let four = [first.0, first.1, second.0, second.1];
740 if four
741 .iter()
742 .all(|span| matches!(span.element, StreamElement::Note(_)))
743 {
744 quartets.push((offset, four.map(|span| span.position)));
745 }
746 }
747 }
748 start = stop;
749 }
750 quartets
751}
752
753pub fn verticality_at(stream: &Stream, offset: FloatType) -> Vec<(usize, Vec<StreamElement>)> {
761 let leaves = stream.leaves();
762 verticality_positions_at(stream, offset)
763 .into_iter()
764 .map(|(part, positions)| {
765 (
766 part,
767 positions
768 .into_iter()
769 .map(|position| leaves[position].1.clone())
770 .collect(),
771 )
772 })
773 .collect()
774}
775
776pub fn verticality_positions_at(stream: &Stream, offset: FloatType) -> Vec<(usize, Vec<usize>)> {
791 parts_by_position(stream)
792 .iter()
793 .enumerate()
794 .filter_map(|(part, flat)| {
795 let positions: Vec<usize> = flat
796 .iter()
797 .filter(|(_, start, element)| {
798 *start == offset
799 || (*start < offset && offset < *start + element.quarter_length())
800 })
801 .map(|(position, _, _)| *position)
802 .collect();
803 (!positions.is_empty()).then_some((part, positions))
804 })
805 .collect()
806}
807
808#[cfg(test)]
809mod tests {
810
811 #[test]
812 fn a_resolution_is_counted_in_the_scale_the_signature_implies() {
813 let quartet = VoiceLeadingQuartet::from_names("F4", "E4", "B3", "A3").unwrap();
818 for key in [
819 Key::from_tonic("C").unwrap(),
820 Key::from_tonic("F#").unwrap().with_sharps(0),
821 ] {
822 assert!(
823 !quartet
824 .clone()
825 .with_key(key)
826 .is_proper_resolution()
827 .unwrap()
828 );
829 }
830 let fresh = quartet.with_key(Key::from_tonic("F#").unwrap());
832 assert!(fresh.is_proper_resolution().unwrap());
833 }
834
835 #[test]
836 fn a_quartet_hands_back_its_pitches_intervals_and_key() {
837 use super::ParallelRequirement;
838 use crate::Interval;
839
840 let mut quartet = VoiceLeadingQuartet::from_names("C5", "D5", "C4", "D4").unwrap();
841 assert_eq!(quartet.v1n1().name_with_octave(), "C5");
842 assert_eq!(quartet.v1n2().name_with_octave(), "D5");
843 assert_eq!(quartet.v2n1().name_with_octave(), "C4");
844 assert_eq!(quartet.v2n2().name_with_octave(), "D4");
845 let vertical: Vec<String> = quartet
846 .vertical_intervals()
847 .iter()
848 .map(Interval::short_name)
849 .collect();
850 assert_eq!(vertical, ["P8", "P8"]);
851 let horizontal: Vec<String> = quartet
852 .horizontal_intervals()
853 .iter()
854 .map(Interval::short_name)
855 .collect();
856 assert_eq!(horizontal, ["M2", "M2"]);
857 assert!(quartet.parallel_unison_or_octave());
858 assert!(quartet.key().is_none());
859 quartet.set_key(Some(Key::from_tonic("C").unwrap()));
860 assert!(quartet.key().is_some());
861 assert!(matches!(
862 ParallelRequirement::from(8),
863 ParallelRequirement::Wide(8)
864 ));
865 assert!(matches!(
866 ParallelRequirement::from(Interval::from_name("P5").unwrap()),
867 ParallelRequirement::Named(_)
868 ));
869 }
870
871 #[test]
872 fn opening_and_closing_incorrectly_are_the_opposites_of_the_rules() {
873 let quartet = |v1n1, v1n2, v2n1, v2n2, key: &str| {
874 VoiceLeadingQuartet::from_names(v1n1, v1n2, v2n1, v2n2)
875 .unwrap()
876 .with_key(Key::from_tonic(key).unwrap())
877 };
878 let opens = quartet("D", "D", "D", "F#", "D");
879 assert!(opens.modal_opening().unwrap());
880 assert!(!opens.opens_incorrectly().unwrap());
881 let closes = quartet("B4", "C5", "D4", "C4", "C");
882 assert_eq!(
883 closes.closes_incorrectly().unwrap(),
884 !closes.clausula_vera().unwrap()
885 );
886 let unkeyed = VoiceLeadingQuartet::from_names("D", "D", "D", "F#").unwrap();
887 assert!(unkeyed.opens_incorrectly().is_err());
888 assert!(unkeyed.closes_incorrectly().is_err());
889 }
890
891 #[test]
892 fn a_modal_opening_needs_a_perfect_interval_and_a_tonic_or_dominant() {
893 let opening = |v1n1, v1n2, v2n1, v2n2, key: &str| {
895 VoiceLeadingQuartet::from_names(v1n1, v1n2, v2n1, v2n2)
896 .unwrap()
897 .with_key(Key::from_tonic(key).unwrap())
898 .modal_opening()
899 .unwrap()
900 };
901 assert!(opening("D", "D", "D", "F#", "D"));
902 assert!(opening("B", "A", "G#", "A", "A"));
903 assert!(opening("A", "A", "F#", "D", "A"));
904 assert!(!opening("C#", "C#", "D", "E", "A"));
905 assert!(!opening("B", "B", "A", "A", "C"));
906
907 assert!(
909 VoiceLeadingQuartet::from_names("D", "D", "D", "F#")
910 .unwrap()
911 .modal_opening()
912 .is_err()
913 );
914 }
915
916 #[test]
917 #[allow(clippy::type_complexity)]
918 fn proper_resolution_leaps_and_clausula_vera_match_music21() {
919 let quartet = |a: &str, b: &str, c: &str, d: &str, key: Option<&str>| {
920 let built = VoiceLeadingQuartet::from_names(a, b, c, d).unwrap();
921 match key {
922 Some(key) => built.with_key(Key::from_tonic(key).unwrap()),
923 None => built,
924 }
925 };
926 let cases: [(
927 &str,
928 &str,
929 &str,
930 &str,
931 Option<&str>,
932 bool,
933 bool,
934 Option<bool>,
935 ); 20] = [
936 ("C4", "B3", "F4", "E4", None, true, false, None),
937 ("C4", "B3", "F4", "E4", Some("C"), true, false, Some(false)),
938 ("B3", "C4", "F4", "E4", Some("C"), true, false, Some(false)),
939 ("B3", "C4", "F4", "E4", None, false, false, None),
940 ("F4", "E4", "B3", "C4", Some("C"), true, false, Some(false)),
941 ("F4", "E4", "B3", "C4", None, true, false, None),
942 ("G3", "C4", "F4", "E4", Some("C"), true, false, Some(false)),
943 ("G3", "C4", "F4", "E4", Some("G"), true, false, Some(false)),
944 ("C4", "C4", "E4", "E4", Some("C"), true, false, Some(false)),
945 ("D4", "F4", "F4", "A4", None, true, true, None),
946 ("C4", "F4", "E4", "F4", None, true, false, None),
947 ("C4", "E4", "E4", "G4", None, true, true, None),
948 ("C4", "E4", "E4", "C4", None, true, false, None),
949 ("B3", "C4", "D4", "C4", Some("C"), true, false, Some(true)),
950 ("B3", "C4", "D4", "C4", Some("G"), true, false, Some(false)),
951 ("B3", "C4", "D4", "C5", Some("C"), true, false, Some(false)),
952 ("F4", "E4", "G3", "C4", Some("C"), true, false, Some(false)),
953 ("F4", "E4", "B3", "C4", Some("F"), true, false, Some(false)),
954 ("D4", "C4", "B3", "C4", Some("C"), true, false, Some(true)),
955 ("C4", "D4", "G4", "F4", None, true, false, None),
956 ];
957 for (a, b, c, d, key, proper, leap, clausula) in cases {
958 let vlq = quartet(a, b, c, d, key);
959 assert_eq!(
960 vlq.is_proper_resolution().unwrap(),
961 proper,
962 "{a} {b} {c} {d} {key:?}"
963 );
964 assert_eq!(
965 vlq.leap_not_set_with_step(),
966 leap,
967 "{a} {b} {c} {d} {key:?}"
968 );
969 match clausula {
970 Some(expected) => assert_eq!(
971 vlq.clausula_vera().unwrap(),
972 expected,
973 "{a} {b} {c} {d} {key:?}"
974 ),
975 None => assert!(vlq.clausula_vera().is_err(), "{a} {b} {c} {d}"),
976 }
977 assert_eq!(vlq.key().is_some(), key.is_some());
978 }
979 }
980 use super::*;
981
982 struct Expected {
983 motion: MotionType,
984 with_anti_parallel: MotionType,
985 flags: [bool; 15],
986 }
987
988 #[test]
989 fn quartets_match_music21() {
990 use MotionType::*;
991 let t = true;
992 let f = false;
993 let cases = [
994 (
995 ("C4", "D4", "C3", "D3"),
996 Expected {
997 motion: Parallel,
998 with_anti_parallel: Parallel,
999 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
1000 },
1001 ),
1002 (
1003 ("C4", "D4", "E3", "F3"),
1004 Expected {
1005 motion: Parallel,
1006 with_anti_parallel: Parallel,
1007 flags: [f, f, t, t, f, f, f, f, f, f, f, f, f, f, f],
1008 },
1009 ),
1010 (
1011 ("C4", "G4", "C3", "C3"),
1012 Expected {
1013 motion: Oblique,
1014 with_anti_parallel: Oblique,
1015 flags: [f, t, f, f, f, f, f, f, f, f, f, f, f, f, f],
1016 },
1017 ),
1018 (
1019 ("C4", "C4", "C3", "C3"),
1020 Expected {
1021 motion: NoMotion,
1022 with_anti_parallel: NoMotion,
1023 flags: [t, f, f, f, f, f, f, f, f, f, f, f, f, f, f],
1024 },
1025 ),
1026 (
1027 ("C4", "D4", "G3", "F3"),
1028 Expected {
1029 motion: Contrary,
1030 with_anti_parallel: Contrary,
1031 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
1032 },
1033 ),
1034 (
1035 ("C4", "C5", "F3", "F4"),
1036 Expected {
1037 motion: Parallel,
1038 with_anti_parallel: Parallel,
1039 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, t, f],
1040 },
1041 ),
1042 (
1043 ("C5", "D5", "C4", "D4"),
1044 Expected {
1045 motion: Parallel,
1046 with_anti_parallel: Parallel,
1047 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
1048 },
1049 ),
1050 (
1051 ("C4", "D4", "G4", "A4"),
1052 Expected {
1053 motion: Parallel,
1054 with_anti_parallel: Parallel,
1055 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, t, t],
1056 },
1057 ),
1058 (
1059 ("C5", "D5", "G4", "E5"),
1060 Expected {
1061 motion: Similar,
1062 with_anti_parallel: Similar,
1063 flags: [f, f, t, f, f, f, f, f, f, f, f, f, f, t, t],
1064 },
1065 ),
1066 (
1067 ("E4", "F4", "C4", "A3"),
1068 Expected {
1069 motion: Contrary,
1070 with_anti_parallel: Contrary,
1071 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
1072 },
1073 ),
1074 (
1075 ("G4", "F4", "C4", "D4"),
1076 Expected {
1077 motion: Contrary,
1078 with_anti_parallel: Contrary,
1079 flags: [f, f, f, f, t, f, t, f, f, f, f, f, f, f, f],
1080 },
1081 ),
1082 (
1083 ("C4", "C#4", "C3", "C#3"),
1084 Expected {
1085 motion: Parallel,
1086 with_anti_parallel: Parallel,
1087 flags: [f, f, t, t, f, f, f, f, f, t, f, f, f, f, f],
1088 },
1089 ),
1090 (
1091 ("C4", "B3", "F3", "G3"),
1092 Expected {
1093 motion: Contrary,
1094 with_anti_parallel: Contrary,
1095 flags: [f, f, f, f, t, f, t, f, f, f, f, f, f, f, f],
1096 },
1097 ),
1098 (
1099 ("D5", "A5", "G3", "D4"),
1100 Expected {
1101 motion: Parallel,
1102 with_anti_parallel: Parallel,
1103 flags: [f, f, t, t, f, f, f, f, t, f, f, f, f, f, f],
1104 },
1105 ),
1106 (
1107 ("A4", "B4", "F4", "E4"),
1108 Expected {
1109 motion: Contrary,
1110 with_anti_parallel: Contrary,
1111 flags: [f, f, f, f, t, t, f, f, f, f, f, f, f, f, f],
1112 },
1113 ),
1114 (
1115 ("C5", "C5", "E4", "F4"),
1116 Expected {
1117 motion: Oblique,
1118 with_anti_parallel: Oblique,
1119 flags: [f, t, f, f, f, f, f, f, f, f, f, f, f, f, f],
1120 },
1121 ),
1122 ];
1123 for ((a, b, c, d), expected) in cases {
1124 let quartet = VoiceLeadingQuartet::from_names(a, b, c, d).unwrap();
1125 let label = format!("{a} {b} / {c} {d}");
1126 assert_eq!(quartet.motion_type(false), expected.motion, "{label}");
1127 assert_eq!(
1128 quartet.motion_type(true),
1129 expected.with_anti_parallel,
1130 "{label}"
1131 );
1132 let actual = [
1133 quartet.no_motion(),
1134 quartet.oblique_motion(),
1135 quartet.similar_motion(),
1136 quartet.parallel_motion(None, false),
1137 quartet.contrary_motion(),
1138 quartet.outward_contrary_motion(),
1139 quartet.inward_contrary_motion(),
1140 quartet.anti_parallel_motion(None),
1141 quartet.parallel_fifth(),
1142 quartet.parallel_octave(),
1143 quartet.parallel_unison(),
1144 quartet.hidden_fifth(),
1145 quartet.hidden_octave(),
1146 quartet.voice_overlap(),
1147 quartet.voice_crossing(),
1148 ];
1149 assert_eq!(actual, expected.flags, "{label}");
1150 }
1151 }
1152
1153 #[test]
1154 fn anti_parallel_fifths_are_contrary_unless_asked_for() {
1155 let quartet = VoiceLeadingQuartet::from_names("G4", "D5", "C4", "G3").unwrap();
1156 assert_eq!(quartet.motion_type(false), MotionType::Contrary);
1157 assert_eq!(quartet.motion_type(true), MotionType::AntiParallel);
1158 assert!(quartet.parallel_fifth());
1159 assert_eq!(MotionType::AntiParallel.to_string(), "Anti-Parallel");
1160 }
1161
1162 #[test]
1163 fn hidden_intervals_need_similar_motion_into_a_perfect_interval() {
1164 let quartet = VoiceLeadingQuartet::from_names("E4", "G4", "C4", "C3").unwrap();
1165 assert!(!quartet.hidden_fifth());
1166 let quartet = VoiceLeadingQuartet::from_names("E4", "D5", "C4", "G4").unwrap();
1167 assert!(quartet.hidden_fifth());
1168 assert!(!quartet.hidden_octave());
1169 let quartet = VoiceLeadingQuartet::from_names("E4", "C5", "C4", "C4").unwrap();
1170 assert!(!quartet.hidden_octave());
1171 }
1172
1173 #[test]
1176 fn a_score_is_walked_into_its_quartets() {
1177 use crate::{Duration, Note, Rest};
1178
1179 let line = |notes: &[(&str, FloatType)]| {
1180 let mut part = Stream::with_kind(StreamKind::Part);
1181 for (name, length) in notes {
1182 let duration = Duration::new(*length).unwrap();
1183 if name.is_empty() {
1184 part.push(Rest::new(duration));
1185 } else {
1186 part.push(Note::from_name(*name).unwrap().with_duration(duration));
1187 }
1188 }
1189 part
1190 };
1191 let mut score = Stream::with_kind(StreamKind::Score);
1192 score.insert(
1193 0.0,
1194 line(&[("C5", 1.0), ("D5", 1.0), ("", 1.0), ("E5", 1.0)]),
1195 );
1196 score.insert(0.0, line(&[("C4", 2.0), ("G3", 2.0)]));
1197
1198 let names = |options| -> Vec<String> {
1199 iterate_all_voice_leading_quartets(&score, options)
1200 .unwrap()
1201 .iter()
1202 .map(|quartet| {
1203 [
1204 quartet.v1n1(),
1205 quartet.v1n2(),
1206 quartet.v2n1(),
1207 quartet.v2n2(),
1208 ]
1209 .map(Pitch::name_with_octave)
1210 .join(" ")
1211 })
1212 .collect()
1213 };
1214 assert_eq!(names(QuartetOptions::default()), ["C5 D5 C4 C4"]);
1218 let strict = QuartetOptions {
1219 include_oblique: false,
1220 ..QuartetOptions::default()
1221 };
1222 assert!(names(strict).is_empty());
1223
1224 let sounding = verticality_at(&score, 1.5);
1225 assert_eq!(sounding.len(), 2);
1226 assert_eq!(sounding[0].1[0].pitches()[0].name_with_octave(), "D5");
1227 assert_eq!(sounding[1].1[0].pitches()[0].name_with_octave(), "C4");
1228 assert!(verticality_at(&score, 9.0).is_empty());
1229 }
1230}