1use std::sync::LazyLock;
15
16use crate::chord::Chord;
17use crate::error::{Error, Result};
18use crate::interval::Interval;
19use crate::pitch::Pitch;
20
21#[derive(Clone, Debug, Default, PartialEq)]
24pub struct ChordInfo {
25 pub bass: Option<Pitch>,
27 pub root: Option<Pitch>,
29 pub third: Option<Pitch>,
31 pub fifth: Option<Pitch>,
33 pub seventh: Option<Pitch>,
35}
36
37impl ChordInfo {
38 pub fn of(chord: &Chord) -> Self {
40 Self {
41 bass: chord.bass().cloned(),
42 root: chord.root().cloned(),
43 third: chord.chord_step(3).cloned(),
44 fifth: chord.chord_step(5).cloned(),
45 seventh: chord.chord_step(7).cloned(),
46 }
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub enum AugmentedSixth {
55 French,
57 German,
59 Swiss,
61}
62
63impl AugmentedSixth {
64 pub fn of(chord: &Chord) -> Result<Self> {
70 if !chord.is_augmented_sixth(false) {
71 return Err(Error::FiguredBass(
72 "Possibility is not an augmented sixth chord.".to_string(),
73 ));
74 }
75 if chord.is_italian_augmented_sixth(false, false) {
76 return Err(Error::FiguredBass(
77 "Italian augmented sixth resolution not supported in this method.".to_string(),
78 ));
79 }
80 if chord.is_french_augmented_sixth(false) {
81 Ok(Self::French)
82 } else if chord.is_german_augmented_sixth(false) {
83 Ok(Self::German)
84 } else if chord.is_swiss_augmented_sixth(false) {
85 Ok(Self::Swiss)
86 } else {
87 Err(Error::FiguredBass("Unknown augSixthType: None".to_string()))
88 }
89 }
90
91 pub fn number(self) -> u8 {
93 match self {
94 Self::French => 1,
95 Self::German => 2,
96 Self::Swiss => 3,
97 }
98 }
99
100 pub fn from_number(number: u8) -> Option<Self> {
102 match number {
103 1 => Some(Self::French),
104 2 => Some(Self::German),
105 3 => Some(Self::Swiss),
106 _ => None,
107 }
108 }
109}
110
111static STEPS: LazyLock<Vec<(&'static str, Interval)>> = LazyLock::new(|| {
113 ["P1", "m2", "-m2", "M2", "-M2", "A1", "d1", "d2", "P4"]
114 .into_iter()
115 .map(|name| (name, Interval::from_name(name).expect("a resolution step")))
116 .collect()
117});
118
119fn step(name: &str) -> &'static Interval {
120 STEPS
121 .iter()
122 .find(|(known, _)| *known == name)
123 .map(|(_, interval)| interval)
124 .expect("every resolution step is in the table")
125}
126
127type Rule<'a> = (Box<dyn Fn(&Pitch) -> bool + 'a>, &'static str);
129
130fn resolve(possibility: &[Pitch], rules: &[Rule<'_>]) -> Result<Vec<Pitch>> {
133 possibility
134 .iter()
135 .map(|pitch| {
136 let name = rules
137 .iter()
138 .find(|(applies, _)| applies(pitch))
139 .map_or("P1", |(_, step)| *step);
140 pitch.transpose(step(name))
141 })
142 .collect()
143}
144
145fn named<'a>(note: Option<&'a Pitch>) -> impl Fn(&Pitch) -> bool + 'a {
147 move |pitch| note.is_some_and(|note| pitch.name() == note.name())
148}
149
150fn move_named(
152 possibility: &[Pitch],
153 moves: &[(Option<&Pitch>, &'static str)],
154) -> Result<Vec<Pitch>> {
155 let rules: Vec<Rule<'_>> = moves
156 .iter()
157 .map(|&(note, step)| -> Rule<'_> { (Box::new(named(note)), step) })
158 .collect();
159 resolve(possibility, &rules)
160}
161
162fn info_or_read(
166 possibility: &[Pitch],
167 info: Option<&ChordInfo>,
168 check: impl Fn(&Chord) -> Result<()>,
169) -> Result<ChordInfo> {
170 match info {
171 Some(info) => Ok(info.clone()),
172 None => {
173 let chord = Chord::new(possibility)?;
174 check(&chord)?;
175 Ok(ChordInfo::of(&chord))
176 }
177 }
178}
179
180fn augmented_sixth_notes(
182 possibility: &[Pitch],
183 kind: Option<AugmentedSixth>,
184 info: Option<&ChordInfo>,
185) -> Result<(AugmentedSixth, ChordInfo)> {
186 let mut kind = kind;
187 let info = match info {
188 Some(info) => info.clone(),
189 None => {
190 let chord = Chord::new(possibility)?;
191 if kind.is_none() {
192 kind = Some(AugmentedSixth::of(&chord)?);
193 } else if !chord.is_augmented_sixth(false) {
194 return Err(Error::FiguredBass(
195 "Possibility is not an augmented sixth chord.".to_string(),
196 ));
197 }
198 ChordInfo::of(&chord)
199 }
200 };
201 let kind = kind.ok_or_else(|| Error::FiguredBass("Unknown augSixthType: None".to_string()))?;
202 Ok((kind, info))
203}
204
205fn augmented_sixth_parts(kind: AugmentedSixth, info: &ChordInfo) -> [Option<&Pitch>; 4] {
209 match kind {
210 AugmentedSixth::French | AugmentedSixth::Swiss => [
211 info.bass.as_ref(),
212 info.third.as_ref(),
213 info.seventh.as_ref(),
214 info.root.as_ref(),
215 ],
216 AugmentedSixth::German => [
217 info.bass.as_ref(),
218 info.root.as_ref(),
219 info.fifth.as_ref(),
220 info.seventh.as_ref(),
221 ],
222 }
223}
224
225pub fn augmented_sixth_to_dominant(
237 possibility: &[Pitch],
238 kind: Option<AugmentedSixth>,
239 info: Option<&ChordInfo>,
240) -> Result<Vec<Pitch>> {
241 resolve_augmented_sixth(
242 possibility,
243 kind,
244 info,
245 "-m2",
246 &[
247 (AugmentedSixth::Swiss, "d1"),
248 (AugmentedSixth::German, "-m2"),
249 ],
250 )
251}
252
253pub fn augmented_sixth_to_major_tonic(
260 possibility: &[Pitch],
261 kind: Option<AugmentedSixth>,
262 info: Option<&ChordInfo>,
263) -> Result<Vec<Pitch>> {
264 resolve_augmented_sixth(
265 possibility,
266 kind,
267 info,
268 "P1",
269 &[
270 (AugmentedSixth::French, "M2"),
271 (AugmentedSixth::German, "A1"),
272 (AugmentedSixth::Swiss, "m2"),
273 ],
274 )
275}
276
277pub fn augmented_sixth_to_minor_tonic(
284 possibility: &[Pitch],
285 kind: Option<AugmentedSixth>,
286 info: Option<&ChordInfo>,
287) -> Result<Vec<Pitch>> {
288 resolve_augmented_sixth(
289 possibility,
290 kind,
291 info,
292 "P1",
293 &[
294 (AugmentedSixth::French, "m2"),
295 (AugmentedSixth::Swiss, "d2"),
296 ],
297 )
298}
299
300fn resolve_augmented_sixth(
304 possibility: &[Pitch],
305 kind: Option<AugmentedSixth>,
306 info: Option<&ChordInfo>,
307 fifth: &'static str,
308 other: &[(AugmentedSixth, &'static str)],
309) -> Result<Vec<Pitch>> {
310 let (kind, info) = augmented_sixth_notes(possibility, kind, info)?;
311 let [bass, root, fifth_note, other_note] = augmented_sixth_parts(kind, &info);
312 let mut moves = vec![(bass, "-m2"), (root, "m2"), (fifth_note, fifth)];
313 if let Some(&(_, step)) = other.iter().find(|(listed, _)| *listed == kind) {
314 moves.push((other_note, step));
315 }
316 move_named(possibility, &moves)
317}
318
319fn dominant_seventh(chord: &Chord) -> Result<()> {
320 if chord.is_dominant_seventh() {
321 Ok(())
322 } else {
323 Err(Error::FiguredBass(
324 "Possibility is not a dominant seventh chord.".to_string(),
325 ))
326 }
327}
328
329fn dominant_seventh_in_root_position(chord: &Chord) -> Result<()> {
330 dominant_seventh(chord)?;
331 if chord.inversion() == Some(0) {
332 Ok(())
333 } else {
334 Err(Error::FiguredBass(
335 "Possibility must be in root position.".to_string(),
336 ))
337 }
338}
339
340fn diminished_seventh(chord: &Chord) -> Result<()> {
341 if chord.is_diminished_seventh() {
342 Ok(())
343 } else {
344 Err(Error::FiguredBass(
345 "Possibility is not a fully diminished seventh chord.".to_string(),
346 ))
347 }
348}
349
350fn seventh_notes(info: &ChordInfo) -> [Option<&Pitch>; 5] {
352 [
353 info.bass.as_ref(),
354 info.root.as_ref(),
355 info.third.as_ref(),
356 info.fifth.as_ref(),
357 info.seventh.as_ref(),
358 ]
359}
360
361pub fn dominant_seventh_to_major_tonic(
370 possibility: &[Pitch],
371 resolve_v43_to_i6: bool,
372 info: Option<&ChordInfo>,
373) -> Result<Vec<Pitch>> {
374 dominant_seventh_to_tonic(possibility, resolve_v43_to_i6, info, "M2", "-M2", "-m2")
375}
376
377pub fn dominant_seventh_to_minor_tonic(
384 possibility: &[Pitch],
385 resolve_v43_to_i6: bool,
386 info: Option<&ChordInfo>,
387) -> Result<Vec<Pitch>> {
388 dominant_seventh_to_tonic(possibility, resolve_v43_to_i6, info, "m2", "-M2", "-M2")
389}
390
391fn dominant_seventh_to_tonic(
392 possibility: &[Pitch],
393 resolve_v43_to_i6: bool,
394 info: Option<&ChordInfo>,
395 fifth_up: &'static str,
396 fifth_down: &'static str,
397 seventh_down: &'static str,
398) -> Result<Vec<Pitch>> {
399 let info = info_or_read(possibility, info, dominant_seventh)?;
400 let [bass, root, third, fifth, seventh] = seventh_notes(&info);
401 let rules: Vec<Rule<'_>> = vec![
402 (
403 Box::new(move |pitch: &Pitch| named(root)(pitch) && bass == Some(pitch)),
404 "P4",
405 ),
406 (Box::new(named(third)), "m2"),
407 (
408 Box::new(move |pitch: &Pitch| resolve_v43_to_i6 && named(fifth)(pitch)),
409 fifth_up,
410 ),
411 (Box::new(named(fifth)), fifth_down),
412 (
413 Box::new(move |pitch: &Pitch| resolve_v43_to_i6 && named(seventh)(pitch)),
414 "M2",
415 ),
416 (Box::new(named(seventh)), seventh_down),
417 ];
418 resolve(possibility, &rules)
419}
420
421pub fn dominant_seventh_to_major_submediant(
429 possibility: &[Pitch],
430 info: Option<&ChordInfo>,
431) -> Result<Vec<Pitch>> {
432 let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
433 let [_, root, third, fifth, seventh] = seventh_notes(&info);
434 move_named(
435 possibility,
436 &[
437 (root, "m2"),
438 (third, "m2"),
439 (fifth, "-M2"),
440 (seventh, "-M2"),
441 ],
442 )
443}
444
445pub fn dominant_seventh_to_minor_submediant(
452 possibility: &[Pitch],
453 info: Option<&ChordInfo>,
454) -> Result<Vec<Pitch>> {
455 let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
456 let [_, root, third, fifth, seventh] = seventh_notes(&info);
457 move_named(
458 possibility,
459 &[
460 (root, "M2"),
461 (third, "m2"),
462 (fifth, "-M2"),
463 (seventh, "-m2"),
464 ],
465 )
466}
467
468pub fn dominant_seventh_to_major_subdominant(
475 possibility: &[Pitch],
476 info: Option<&ChordInfo>,
477) -> Result<Vec<Pitch>> {
478 let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
479 let [_, root, third, fifth, _] = seventh_notes(&info);
480 move_named(possibility, &[(root, "M2"), (third, "m2"), (fifth, "-M2")])
481}
482
483pub fn dominant_seventh_to_minor_subdominant(
490 possibility: &[Pitch],
491 info: Option<&ChordInfo>,
492) -> Result<Vec<Pitch>> {
493 let info = info_or_read(possibility, info, dominant_seventh_in_root_position)?;
494 let [_, root, third, fifth, _] = seventh_notes(&info);
495 move_named(possibility, &[(root, "m2"), (third, "m2"), (fifth, "-M2")])
496}
497
498pub fn diminished_seventh_to_major_tonic(
507 possibility: &[Pitch],
508 doubled_root: bool,
509 info: Option<&ChordInfo>,
510) -> Result<Vec<Pitch>> {
511 diminished_seventh_to_tonic(possibility, doubled_root, info, "M2", "-m2")
512}
513
514pub fn diminished_seventh_to_minor_tonic(
521 possibility: &[Pitch],
522 doubled_root: bool,
523 info: Option<&ChordInfo>,
524) -> Result<Vec<Pitch>> {
525 diminished_seventh_to_tonic(possibility, doubled_root, info, "m2", "-M2")
526}
527
528fn diminished_seventh_to_tonic(
529 possibility: &[Pitch],
530 doubled_root: bool,
531 info: Option<&ChordInfo>,
532 third_up: &'static str,
533 fifth_down: &'static str,
534) -> Result<Vec<Pitch>> {
535 let info = info_or_read(possibility, info, diminished_seventh)?;
536 let [_, root, third, fifth, seventh] = seventh_notes(&info);
537 let rules: Vec<Rule<'_>> = vec![
538 (Box::new(named(root)), "m2"),
539 (
540 Box::new(move |pitch: &Pitch| doubled_root && named(third)(pitch)),
541 "-M2",
542 ),
543 (Box::new(named(third)), third_up),
544 (Box::new(named(fifth)), fifth_down),
545 (Box::new(named(seventh)), "-m2"),
546 ];
547 resolve(possibility, &rules)
548}
549
550pub fn diminished_seventh_to_major_subdominant(
557 possibility: &[Pitch],
558 info: Option<&ChordInfo>,
559) -> Result<Vec<Pitch>> {
560 let info = info_or_read(possibility, info, diminished_seventh)?;
561 let [_, root, third, _, seventh] = seventh_notes(&info);
562 move_named(
563 possibility,
564 &[(root, "m2"), (third, "-M2"), (seventh, "A1")],
565 )
566}
567
568pub fn diminished_seventh_to_minor_subdominant(
575 possibility: &[Pitch],
576 info: Option<&ChordInfo>,
577) -> Result<Vec<Pitch>> {
578 let info = info_or_read(possibility, info, diminished_seventh)?;
579 let [_, root, third, _, _] = seventh_notes(&info);
580 move_named(possibility, &[(root, "m2"), (third, "-M2")])
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 fn sixths() -> [Vec<Pitch>; 3] {
589 let chord = |names: [&str; 4]| names.map(|name| Pitch::from_name(name).unwrap()).to_vec();
590 [
591 chord(["G#4", "E4", "D4", "B-2"]),
592 chord(["G#4", "F4", "D4", "B-2"]),
593 chord(["G#4", "E#4", "D4", "B-2"]),
594 ]
595 }
596
597 fn names(pitches: Result<Vec<Pitch>>) -> Vec<String> {
598 pitches
599 .unwrap()
600 .iter()
601 .map(Pitch::name_with_octave)
602 .collect()
603 }
604
605 #[test]
606 fn every_augmented_sixth_resolves_as_music21_resolves_it() {
607 for sixth in sixths() {
608 assert_eq!(
609 names(augmented_sixth_to_dominant(&sixth, None, None)),
610 ["A4", "E4", "C#4", "A2"]
611 );
612 assert_eq!(
613 names(augmented_sixth_to_major_tonic(&sixth, None, None)),
614 ["A4", "F#4", "D4", "A2"]
615 );
616 assert_eq!(
617 names(augmented_sixth_to_minor_tonic(&sixth, None, None)),
618 ["A4", "F4", "D4", "A2"]
619 );
620 }
621 }
622}