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::{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/// How two voices move relative to each other.
23#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum MotionType {
26    /// Contrary motion between two spellings of the same simple interval,
27    /// such as a fifth opening out to a twelfth. Only reported when asked for.
28    AntiParallel,
29    /// The voices move in opposite directions.
30    Contrary,
31    /// Neither voice moves.
32    NoMotion,
33    /// One voice holds while the other moves.
34    Oblique,
35    /// The voices move the same way and keep the same generic interval.
36    Parallel,
37    /// The voices move the same way through different intervals.
38    Similar,
39}
40
41impl MotionType {
42    /// Returns music21's label for the motion.
43    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/// What a caller may ask of a parallel motion.
62///
63/// music21 takes either: a number is how wide the interval must be, whatever
64/// it is spelled — `3` for parallel thirds of any quality — and an interval
65/// is the interval itself, spelling and all.
66#[derive(Clone, Debug, PartialEq)]
67pub enum ParallelRequirement {
68    /// However many steps wide, counted inclusively.
69    Wide(IntegerType),
70    /// This interval exactly.
71    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/// Two consecutive notes in each of two voices. Voice one is the upper voice.
87#[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    /// Builds a quartet from the first and second pitch of the upper voice,
102    /// then the first and second pitch of the lower voice.
103    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    /// Builds a quartet from pitch names, in the same order as [`Self::new`].
124    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    /// Attaches the key the progression is heard in, which
134    /// [`Self::is_proper_resolution`] and [`Self::clausula_vera`] consult.
135    pub fn with_key(mut self, key: Key) -> Self {
136        self.key = Some(key);
137        self
138    }
139
140    /// The key the progression is heard in, if one was given.
141    pub fn key(&self) -> Option<&Key> {
142        self.key.as_ref()
143    }
144
145    /// Attaches a key, or takes one away.
146    pub fn set_key(&mut self, key: Option<Key>) {
147        self.key = key;
148    }
149
150    /// Whether a dissonant first interval resolves the way voice-leading
151    /// rules expect: music21's `isProperResolution`. A fourth wants the upper
152    /// voice to fall or stay; a tritone or a minor seventh wants the
153    /// leading tone up, the seventh down and the right contrary motion — and
154    /// with a key set, only when the notes really are those scale degrees.
155    /// Anything else, and no motion at all, is proper.
156    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    /// Whether one voice leaps while the other neither steps nor holds:
210    /// music21's `leapNotSetWithStep`. Two thirds in contrary motion are let
211    /// through.
212    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    /// Whether the two voices open the way sixteenth-century counterpoint
235    /// opens: music21's `modalOpening`. Errors without a key.
236    ///
237    /// One of the two harmonic intervals must be a unison or a fifth — the
238    /// second may be, to allow for an anacrusis — and the pair must establish
239    /// the tonic or the dominant. Which of the two says so is whichever can
240    /// be read at all: music21 asks the first, and only falls to the second
241    /// when the first says nothing.
242    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    /// The opposite of [`Self::modal_opening`]: music21's `opensIncorrectly`.
270    pub fn opens_incorrectly(&self) -> Result<bool> {
271        self.modal_opening().map(|opens| !opens)
272    }
273
274    /// The opposite of [`Self::clausula_vera`]: music21's `closesIncorrectly`.
275    pub fn closes_incorrectly(&self) -> Result<bool> {
276        self.clausula_vera().map(|closes| !closes)
277    }
278
279    /// Whether the two voices close a clausula vera: stepwise contrary
280    /// motion, one voice by a semitone and the other by a tone, onto a unison
281    /// or octave on the tonic. Errors without a key.
282    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    /// The upper voice's first pitch.
302    pub fn v1n1(&self) -> &Pitch {
303        &self.v1n1
304    }
305
306    /// The upper voice's second pitch.
307    pub fn v1n2(&self) -> &Pitch {
308        &self.v1n2
309    }
310
311    /// The lower voice's first pitch.
312    pub fn v2n1(&self) -> &Pitch {
313        &self.v2n1
314    }
315
316    /// The lower voice's second pitch.
317    pub fn v2n2(&self) -> &Pitch {
318        &self.v2n2
319    }
320
321    /// The harmonic intervals from the upper voice to the lower voice, at the
322    /// first and second moment.
323    pub fn vertical_intervals(&self) -> &[Interval; 2] {
324        &self.vertical
325    }
326
327    /// The melodic intervals each voice moves through, upper voice first.
328    pub fn horizontal_intervals(&self) -> &[Interval; 2] {
329        &self.horizontal
330    }
331
332    /// Classifies the motion. Anti-parallel motion is reported as contrary
333    /// unless `allow_anti_parallel` is set.
334    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    /// Returns whether neither voice moves.
351    pub fn no_motion(&self) -> bool {
352        self.horizontal.iter().all(Interval::is_perfect_unison)
353    }
354
355    /// Returns whether exactly one voice holds its pitch.
356    pub fn oblique_motion(&self) -> bool {
357        !self.no_motion() && self.horizontal.iter().any(Interval::is_perfect_unison)
358    }
359
360    /// Returns whether both voices move in the same direction.
361    pub fn similar_motion(&self) -> bool {
362        !self.no_motion() && self.horizontal[0].direction() == self.horizontal[1].direction()
363    }
364
365    /// Returns whether the voices move in the same direction keeping the same
366    /// generic interval. With `required`, the interval must also be that one;
367    /// `allow_octave_displacement` accepts a fifth answered by a twelfth.
368    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    /// Returns whether the voices move in opposite directions.
396    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    /// Returns whether the voices move apart.
403    pub fn outward_contrary_motion(&self) -> bool {
404        self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Ascending
405    }
406
407    /// Returns whether the voices move towards each other.
408    pub fn inward_contrary_motion(&self) -> bool {
409        self.contrary_motion() && self.horizontal[0].direction() == IntervalDirection::Descending
410    }
411
412    /// Returns whether contrary motion lands on the same simple interval it
413    /// left, such as a fifth opening out to a twelfth. With `required`, that
414    /// interval must also be the given one.
415    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    /// Returns whether the voices move in parallel or anti-parallel through
423    /// the given interval, in any octave.
424    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    /// Returns whether the voices move in parallel fifths.
430    pub fn parallel_fifth(&self) -> bool {
431        self.parallel_interval(&PERFECT_FIFTH)
432    }
433
434    /// Returns whether the voices move in parallel octaves.
435    pub fn parallel_octave(&self) -> bool {
436        self.parallel_interval(&PERFECT_OCTAVE)
437    }
438
439    /// Returns whether the voices move in parallel unisons.
440    pub fn parallel_unison(&self) -> bool {
441        self.parallel_interval(&PERFECT_UNISON)
442    }
443
444    /// Returns whether the voices move in parallel unisons or octaves.
445    pub fn parallel_unison_or_octave(&self) -> bool {
446        self.parallel_unison() || self.parallel_octave()
447    }
448
449    /// Returns whether similar motion arrives at the given interval without
450    /// having started from it.
451    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    /// Returns whether similar motion arrives at a perfect fifth.
459    pub fn hidden_fifth(&self) -> bool {
460        self.hidden_interval(&PERFECT_FIFTH)
461    }
462
463    /// Returns whether similar motion arrives at a perfect octave.
464    pub fn hidden_octave(&self) -> bool {
465        self.hidden_interval(&PERFECT_OCTAVE)
466    }
467
468    /// Returns whether a voice moves past where the other voice just was.
469    pub fn voice_overlap(&self) -> bool {
470        self.v1n2.ps() < self.v2n1.ps() || self.v2n2.ps() > self.v1n1.ps()
471    }
472
473    /// Returns whether the lower voice is above the upper voice at either
474    /// moment.
475    pub fn voice_crossing(&self) -> bool {
476        self.v1n1.ps() < self.v2n1.ps() || self.v1n2.ps() < self.v2n2.ps()
477    }
478}
479
480/// Which quartets [`iterate_all_voice_leading_quartets`] gives: music21's
481/// keyword arguments of the same names, with its defaults.
482#[derive(Clone, Copy, Debug, Eq, PartialEq)]
483#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
484pub struct QuartetOptions {
485    /// Whether a voice may come to its note out of a rest. Without it, the
486    /// note before has to stop exactly where the next one starts.
487    pub include_rests: bool,
488    /// Whether a voice that holds or repeats its note counts as moving.
489    pub include_oblique: bool,
490    /// Whether a quartet in which neither voice changes pitch is given.
491    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
504/// One thing on a part's flattened timeline, as the walk sees it.
505struct Span<'a> {
506    offset: FloatType,
507    end: FloatType,
508    part: usize,
509    /// Where the element stands in [`Stream::leaves`].
510    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    /// music21's `classSortOrder`, which decides what comes first among the
520    /// things starting together.
521    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
536/// Each part of a stream as a flat timeline: every element of the part with
537/// its offset from the stream's start and its position in
538/// [`Stream::leaves`], in the order `flatten` would give them. A stream
539/// holding no parts is one part itself.
540fn 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    // Stable, as `flatten` is.
575    for part in &mut parts {
576        part.sort_by(|left, right| left.1.total_cmp(&right.1));
577    }
578    parts
579}
580
581/// Every [`VoiceLeadingQuartet`] in a stream, generally a score: music21's
582/// `iterateAllVoiceLeadingQuartets`.
583///
584/// Wherever a note starts, each voice that moves there is paired with the
585/// note it came from, each voice holding through is paired with itself, and
586/// every two such pairs make a quartet. Only notes make one; a pair with a
587/// chord in it is passed over, as it is upstream.
588///
589/// The quartets come in the order of the score: by offset, then by part,
590/// the first-named voice being the earlier part. music21 orders the voices
591/// within an offset by a counter of when each note object was put into its
592/// stream, which says nothing about the music, so its order within an offset
593/// can differ; the quartets themselves are the same.
594///
595/// A voice comes *from* the first thing in its part at the nearest earlier
596/// offset where anything in that part starts. If that is not a note or
597/// chord -- a rest, or a time signature standing where a note also starts --
598/// the voice has no pair there. That is music21's reading and is kept.
599pub 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
616/// Where the four notes of every quartet [`iterate_all_voice_leading_quartets`]
617/// finds stand in the stream: each quartet as the offset it is found at and
618/// the positions in [`Stream::leaves`] of its `v1n1`, `v1n2`, `v2n1` and
619/// `v2n2`, in that order. This is the walk itself, for a caller that keeps
620/// something of its own beside each note and wants that back rather than a
621/// copy of the pitch; the offset is what music21's `reverse` walks backwards
622/// by, since a voice held through has its notes start earlier.
623///
624/// ```
625/// use music21_rs::{voiceleading::{voice_leading_quartet_positions, QuartetOptions}, Note, Stream, StreamKind};
626///
627/// let mut upper = Stream::with_kind(StreamKind::Part);
628/// upper.insert(0.0, Note::from_name("C5")?);
629/// upper.insert(1.0, Note::from_name("D5")?);
630/// let mut lower = Stream::with_kind(StreamKind::Part);
631/// lower.insert(0.0, Note::from_name("C4")?);
632/// lower.insert(1.0, Note::from_name("B3")?);
633/// let mut score = Stream::with_kind(StreamKind::Score);
634/// score.insert(0.0, upper);
635/// score.insert(0.0, lower);
636/// assert_eq!(voice_leading_quartet_positions(&score, QuartetOptions::default()), [(1.0, [0, 1, 2, 3])]);
637/// # Ok::<(), music21_rs::Error>(())
638/// ```
639pub 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    // Stable, so things starting together stay in part order and then in
657    // the order their part holds them.
658    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            // The nearest earlier offset where this part starts anything,
677            // and the first thing it starts there.
678            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            // A hair of latitude, since offsets here are floats where
693            // music21's are fractions.
694            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
734/// What is sounding or standing in each part at an offset, part by part:
735/// music21's `getVerticalityFromObject`, asked by offset since a stream here
736/// owns its notes and an offset is what says where one is.
737///
738/// A thing counts if it starts there or is still going, and a thing of no
739/// length counts where it stands. Parts holding nothing there are left out,
740/// so each answer carries the index of its part.
741pub 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
757/// [`verticality_at`] by position: for each part holding anything at the
758/// offset, the part's index and the positions in [`Stream::leaves`] of what
759/// it holds there, for a caller that keeps something of its own beside each
760/// element.
761///
762/// ```
763/// use music21_rs::{voiceleading::verticality_positions_at, Note, Stream};
764///
765/// let mut stream = Stream::new();
766/// stream.insert(0.0, Note::from_name("C4")?);
767/// stream.insert(1.0, Note::from_name("D4")?);
768/// assert_eq!(verticality_positions_at(&stream, 0.5), [(0, vec![0])]);
769/// # Ok::<(), music21_rs::Error>(())
770/// ```
771pub 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        // music21's own examples.
851        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        // Without a key there is nothing to be the tonic of.
865        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    /// The walk over whole scores is checked against music21 by
1131    /// `voice_leading_parity`; this is the shape of it on two bars.
1132    #[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        // The upper voice moves while the lower holds; then the lower moves
1172        // under a rest, which leaves it nothing to be paired with; and the
1173        // upper voice comes back out of a rest it has no note to come from.
1174        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}