1use 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
21pub static DEFAULT_MAX_PITCH: LazyLock<Pitch> =
23 LazyLock::new(|| Pitch::from_name("B5").expect("B5 is a pitch name"));
24
25pub 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
45pub 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
55pub 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
73pub fn pitches_within_limit(possibility: &[Pitch], max_pitch: &Pitch) -> bool {
76 possibility.iter().all(|pitch| pitch.ps() <= max_pitch.ps())
77}
78
79pub 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
97fn 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
109fn apart_by(a: &Pitch, b: &Pitch, semitones: FloatType) -> bool {
112 (a.ps() - b.ps()).abs() % 12.0 == semitones
113}
114
115pub fn parallel_fifths(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
121 parallel(a, b, 7.0, VoiceLeadingQuartet::parallel_fifth)
122}
123
124pub 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
159pub fn hidden_fifths(a: &[Pitch], b: &[Pitch]) -> Result<bool> {
166 hidden(a, b, 7.0, VoiceLeadingQuartet::hidden_fifth)
167}
168
169pub 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
204pub 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
222pub 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
243pub 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
255pub 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#[derive(Clone, Debug, PartialEq)]
279pub struct ItalianSixth {
280 pub bass: Pitch,
282 pub root: Pitch,
284 pub third: Pitch,
286 pub fifth: Pitch,
288}
289
290impl ItalianSixth {
291 pub fn of(possibility: &[Pitch]) -> Result<Self> {
298 Self::from_chord(&Chord::new(possibility)?)
299 }
300
301 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
330pub 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
399pub 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}