Skip to main content

music21_rs/figuredbass/
possibility.rs

1//! The rules a possibility is held to: music21's `figuredBass.possibility`.
2//!
3//! A possibility is one way of voicing a figured-bass chord: a pitch for each
4//! part, from the highest part down to the bass, which is last. Unlike a
5//! chord's notes, the order says which part sings which pitch, and parts may
6//! cross, so the highest pitch need not be first.
7//!
8//! The rules come in two kinds. Those over one possibility say whether a
9//! voicing is acceptable on its own; those over two say whether moving from
10//! one voicing to the next is.
11
12use std::sync::LazyLock;
13
14use crate::chord::Chord;
15use crate::defaults::{FloatType, IntegerType};
16use crate::error::{Error, Result};
17use crate::interval::Interval;
18use crate::pitch::Pitch;
19use crate::voiceleading::VoiceLeadingQuartet;
20
21/// The highest pitch a part may reach unless told otherwise: `B5`.
22pub static DEFAULT_MAX_PITCH: LazyLock<Pitch> =
23    LazyLock::new(|| Pitch::from_name("B5").expect("B5 is a pitch name"));
24
25/// Whether any part sings higher than a part above it.
26///
27/// ```
28/// use music21_rs::Pitch;
29/// use music21_rs::figuredbass::possibility::voice_crossing;
30///
31/// let pitches = |names: &[&str]| -> Vec<Pitch> {
32///     names.iter().map(|name| Pitch::from_name(*name).unwrap()).collect()
33/// };
34/// assert!(!voice_crossing(&pitches(&["G5", "C5", "E4", "C4"])));
35/// assert!(voice_crossing(&pitches(&["C5", "G5", "E4", "C4"])));
36/// ```
37pub fn voice_crossing(possibility: &[Pitch]) -> bool {
38    possibility.iter().enumerate().any(|(index, higher)| {
39        possibility[index + 1..]
40            .iter()
41            .any(|lower| higher.ps() < lower.ps())
42    })
43}
44
45/// Whether the possibility leaves out any of `pitch_names`, the names of
46/// the notes its chord has to sound.
47pub fn is_incomplete<S: AsRef<str>>(possibility: &[Pitch], pitch_names: &[S]) -> bool {
48    pitch_names.iter().any(|name| {
49        !possibility
50            .iter()
51            .any(|pitch| pitch.is_named(name.as_ref()))
52    })
53}
54
55/// Whether every two of the upper parts -- all but the bass -- lie within
56/// `max_semitone_separation` of each other; with no limit, they always do.
57/// music21's default is twelve, the span a hand can play.
58pub fn upper_parts_within_limit(
59    possibility: &[Pitch],
60    max_semitone_separation: Option<IntegerType>,
61) -> bool {
62    let Some(limit) = max_semitone_separation else {
63        return true;
64    };
65    let upper = &possibility[..possibility.len().saturating_sub(1)];
66    upper.iter().enumerate().all(|(index, higher)| {
67        upper[index + 1..]
68            .iter()
69            .all(|lower| (higher.ps() - lower.ps()).abs() <= FloatType::from(limit))
70    })
71}
72
73/// Whether no part sings above `max_pitch`, which is
74/// [`DEFAULT_MAX_PITCH`] unless a caller says otherwise.
75pub fn pitches_within_limit(possibility: &[Pitch], max_pitch: &Pitch) -> bool {
76    possibility.iter().all(|pitch| pitch.ps() <= max_pitch.ps())
77}
78
79/// Whether each part named in `part_pitch_limits` sings exactly the pitch
80/// it is held to. Parts are numbered from one, the highest.
81///
82/// # Errors
83///
84/// A part number the possibility has no part for.
85pub fn limit_part_to_pitch(
86    possibility: &[Pitch],
87    part_pitch_limits: &[(usize, Pitch)],
88) -> Result<bool> {
89    for (part, pitch) in part_pitch_limits {
90        if possibility_part(possibility, *part)? != pitch {
91            return Ok(false);
92        }
93    }
94    Ok(true)
95}
96
97/// The pitch part `part` sings, counting the highest part as one.
98fn possibility_part(possibility: &[Pitch], part: usize) -> Result<&Pitch> {
99    part.checked_sub(1)
100        .and_then(|index| possibility.get(index))
101        .ok_or_else(|| {
102            Error::FiguredBass(format!(
103                "a possibility of {} parts has no part {part}",
104                possibility.len()
105            ))
106        })
107}
108
109/// Whether the pitch-space distance between `a` and `b` is a whole number of
110/// `semitones` past some octaves.
111fn apart_by(a: &Pitch, b: &Pitch, semitones: FloatType) -> bool {
112    (a.ps() - b.ps()).abs() % 12.0 == semitones
113}
114
115/// Whether any two parts move in parallel fifths from `a` to `b`.
116///
117/// # Errors
118///
119/// Two possibilities with different numbers of parts.
120pub fn parallel_fifths(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
121    parallel(a, b, 7.0, VoiceLeadingQuartet::parallel_fifth)
122}
123
124/// Whether any two parts move in parallel octaves from `a` to `b`.
125///
126/// # Errors
127///
128/// Two possibilities with different numbers of parts.
129pub fn parallel_octaves(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
130    parallel(a, b, 0.0, VoiceLeadingQuartet::parallel_octave)
131}
132
133fn parallel(
134    a: &[Pitch],
135    b: &[Pitch],
136    semitones: FloatType,
137    rule: fn(&VoiceLeadingQuartet) -> bool,
138) -> Result<bool> {
139    let pairs = part_pairs(a, b)?;
140    for (index, (higher_a, higher_b)) in pairs.iter().enumerate() {
141        for (lower_a, lower_b) in &pairs[index + 1..] {
142            if !apart_by(higher_a, lower_a, semitones) || !apart_by(higher_b, lower_b, semitones) {
143                continue;
144            }
145            let quartet = VoiceLeadingQuartet::new(
146                (*lower_a).clone(),
147                (*lower_b).clone(),
148                (*higher_a).clone(),
149                (*higher_b).clone(),
150            )?;
151            if rule(&quartet) {
152                return Ok(true);
153            }
154        }
155    }
156    Ok(false)
157}
158
159/// Whether the outer parts, the highest and the bass, move by similar motion
160/// into a fifth from `a` to `b`.
161///
162/// # Errors
163///
164/// Two possibilities with different numbers of parts, or none.
165pub fn hidden_fifths(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
166    hidden(a, b, 7.0, VoiceLeadingQuartet::hidden_fifth)
167}
168
169/// Whether the outer parts, the highest and the bass, move by similar motion
170/// into an octave from `a` to `b`.
171///
172/// # Errors
173///
174/// Two possibilities with different numbers of parts, or none.
175pub fn hidden_octaves(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
176    hidden(a, b, 0.0, VoiceLeadingQuartet::hidden_octave)
177}
178
179fn hidden(
180    a: &[Pitch],
181    b: &[Pitch],
182    semitones: FloatType,
183    rule: fn(&VoiceLeadingQuartet) -> bool,
184) -> Result<bool> {
185    let pairs = part_pairs(a, b)?;
186    let (Some((highest_a, highest_b)), Some((lowest_a, lowest_b))) = (pairs.first(), pairs.last())
187    else {
188        return Err(Error::FiguredBass(
189            "a possibility with no parts has no outer parts".to_string(),
190        ));
191    };
192    if !apart_by(highest_b, lowest_b, semitones) {
193        return Ok(false);
194    }
195    let quartet = VoiceLeadingQuartet::new(
196        (*lowest_a).clone(),
197        (*lowest_b).clone(),
198        (*highest_a).clone(),
199        (*highest_b).clone(),
200    )?;
201    Ok(rule(&quartet))
202}
203
204/// Whether moving from `a` to `b` takes any part past where a part above it
205/// was, or below where a part beneath it was.
206///
207/// # Errors
208///
209/// Two possibilities with different numbers of parts.
210pub fn voice_overlap(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
211    let pairs = part_pairs(a, b)?;
212    Ok(pairs
213        .iter()
214        .enumerate()
215        .any(|(index, (higher_a, higher_b))| {
216            pairs[index + 1..].iter().any(|(lower_a, lower_b)| {
217                lower_b.ps() > higher_a.ps() || higher_b.ps() < lower_a.ps()
218            })
219        }))
220}
221
222/// Whether each part named in `limits` moves no further than the semitones
223/// beside it from `a` to `b`. Parts are numbered from one, the highest.
224///
225/// # Errors
226///
227/// A part number either possibility has no part for.
228pub fn part_movements_within_limits(
229    a: &[Pitch],
230    b: &[Pitch],
231    limits: &[(usize, IntegerType)],
232) -> Result<bool> {
233    for (part, limit) in limits {
234        let from = possibility_part(a, *part)?;
235        let to = possibility_part(b, *part)?;
236        if (to.ps() - from.ps()).abs() > FloatType::from(*limit) {
237            return Ok(false);
238        }
239    }
240    Ok(true)
241}
242
243/// Whether every part but the bass sings the same pitch in `a` and `b`.
244///
245/// # Errors
246///
247/// Two possibilities with different numbers of parts.
248pub fn upper_parts_same(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
249    let pairs = part_pairs(a, b)?;
250    Ok(pairs[..pairs.len().saturating_sub(1)]
251        .iter()
252        .all(|(from, to)| from == to))
253}
254
255/// Whether each part named in `parts` sings the same pitch in `a` and `b`;
256/// with no parts named, they always do. Parts are numbered from one, the
257/// highest.
258///
259/// # Errors
260///
261/// Two possibilities with different numbers of parts, or a part number they
262/// have no part for.
263pub fn parts_same(a: &[Pitch], b: &[Pitch], parts: Option<&[usize]>) -> Result<bool> {
264    let Some(parts) = parts else {
265        return Ok(true);
266    };
267    part_pairs(a, b)?;
268    for part in parts {
269        if possibility_part(a, *part)? != possibility_part(b, *part)? {
270            return Ok(false);
271        }
272    }
273    Ok(true)
274}
275
276/// The notes of an Italian augmented sixth an Italian-sixth resolution is
277/// judged by: its bass, root, third and fifth.
278#[derive(Clone, Debug, PartialEq)]
279pub struct ItalianSixth {
280    /// The chord's bass.
281    pub bass: Pitch,
282    /// The chord's root.
283    pub root: Pitch,
284    /// The chord's third.
285    pub third: Pitch,
286    /// The chord's fifth.
287    pub fifth: Pitch,
288}
289
290impl ItalianSixth {
291    /// The bass, root, third and fifth of the Italian augmented sixth
292    /// `possibility` spells.
293    ///
294    /// # Errors
295    ///
296    /// A possibility that spells no Italian augmented sixth.
297    pub fn of(possibility: &[Pitch]) -> Result<Self> {
298        Self::from_chord(&Chord::new(possibility)?)
299    }
300
301    /// The bass, root, third and fifth of `chord`, an Italian augmented
302    /// sixth.
303    ///
304    /// # Errors
305    ///
306    /// A chord that is no Italian augmented sixth.
307    pub fn from_chord(chord: &Chord) -> Result<Self> {
308        let refused =
309            || Error::FiguredBass("possibA does not spell out an It+6 chord.".to_string());
310        if !chord.is_italian_augmented_sixth(false, false) {
311            return Err(refused());
312        }
313        let (Some(bass), Some(root), Some(third), Some(fifth)) = (
314            chord.bass(),
315            chord.root(),
316            chord.chord_step(3),
317            chord.chord_step(5),
318        ) else {
319            return Err(refused());
320        };
321        Ok(Self {
322            bass: bass.clone(),
323            root: root.clone(),
324            third: third.clone(),
325            fifth: fifth.clone(),
326        })
327    }
328}
329
330/// Whether moving from `a`, an Italian augmented sixth, to `b` resolves it:
331/// the bass falls a semitone, the root rises one, the third falls one and the
332/// fifth moves by a third or a second. With `restrict_doublings`, the root
333/// may resolve only once and the third not at all.
334///
335/// `sixth` is the chord's notes where the caller has them already, and is
336/// read off `a` where not.
337///
338/// # Errors
339///
340/// An `a` that spells no Italian augmented sixth when `sixth` is not given.
341pub fn could_be_italian_a6_resolution(
342    a: &[Pitch],
343    b: &[Pitch],
344    sixth: Option<&ItalianSixth>,
345    restrict_doublings: bool,
346) -> Result<bool> {
347    let read;
348    let sixth = match sixth {
349        Some(sixth) => sixth,
350        None => {
351            read = ItalianSixth::of(a)?;
352            &read
353        }
354    };
355    const FIFTH_MOVES: [&str; 4] = ["M3", "m3", "M2", "m-2"];
356    let mut root_resolved = false;
357    for (from, to) in a.iter().zip(b) {
358        if from.name() == sixth.fifth.name() {
359            if from == to {
360                continue;
361            }
362            if (from.ps() - to.ps()).abs() > 4.0 {
363                return Ok(false);
364            }
365            let moved = Interval::between_pitches(from, to)?.directed_simple_name();
366            if !FIFTH_MOVES.contains(&moved.as_str()) {
367                return Ok(false);
368            }
369        } else if from.name() == sixth.bass.name() && from == &sixth.bass {
370            if from.ps() - to.ps() != 1.0
371                || Interval::between_pitches(from, to)?.directed_name() != "m-2"
372            {
373                return Ok(false);
374            }
375        } else if from.name() == sixth.root.name() {
376            if root_resolved && restrict_doublings {
377                return Ok(false);
378            }
379            if to.ps() - from.ps() != 1.0
380                || Interval::between_pitches(from, to)?.directed_name() != "m2"
381            {
382                return Ok(false);
383            }
384            root_resolved = true;
385        } else if from.name() == sixth.third.name() {
386            if restrict_doublings {
387                return Ok(false);
388            }
389            if from.ps() - to.ps() != 1.0
390                || Interval::between_pitches(from, to)?.directed_name() != "m-2"
391            {
392                return Ok(false);
393            }
394        }
395    }
396    Ok(true)
397}
398
399/// Each part's pitch in `a` beside its pitch in `b`.
400///
401/// # Errors
402///
403/// Two possibilities with different numbers of parts.
404pub fn part_pairs<'a>(a: &'a [Pitch], b: &'a [Pitch]) -> Result<Vec<(&'a Pitch, &'a Pitch)>> {
405    if a.len() != b.len() {
406        return Err(Error::FiguredBass(format!(
407            "possibilities of {} and {} parts cannot be paired",
408            a.len(),
409            b.len()
410        )));
411    }
412    Ok(a.iter().zip(b).collect())
413}