1use super::*;
5
6impl Chord {
7 pub fn resolution_chord(&self, tonic: &str, mode: Option<&str>) -> Result<Option<Self>> {
15 Ok(self.resolution_chords(tonic, mode)?.into_iter().next())
16 }
17
18 pub fn resolution_chords(&self, tonic: &str, mode: Option<&str>) -> Result<Vec<Self>> {
27 let key = Key::from_tonic_mode(tonic, mode)?;
28 self.resolution_chords_in_key(&key)
29 }
30
31 pub fn resolution_chords_in_key(&self, key: &Key) -> Result<Vec<Self>> {
33 if self.is_contextual_augmented_sixth(key)? {
34 return Ok(vec![
35 self.place_resolution_near_source(key.triad_from_degree(5)?)?,
36 ]);
37 }
38
39 let mut resolutions = Vec::new();
40
41 let dominant_resolution = if self.is_dominant_function_sonority() {
42 self.resolve_by_root_motion(key, 5)?
43 } else {
44 None
45 };
46 if let Some(chord) = dominant_resolution {
47 resolutions.push(chord);
48 }
49
50 let leading_tone_resolution = if self.is_leading_tone_function_sonority() {
51 self.resolve_by_root_motion(key, 1)?
52 } else {
53 None
54 };
55 if let Some(chord) = leading_tone_resolution {
56 resolutions.push(chord);
57 }
58
59 Ok(Self::deduplicate_resolution_chords(resolutions))
60 }
61
62 pub fn resolution_suggestions_in_key(
64 &self,
65 key: &Key,
66 ) -> Result<Vec<ChordResolutionSuggestion>> {
67 let mut suggestions = Vec::new();
68 let mut seen = std::collections::BTreeSet::new();
69 let key_name = Self::display_key_name(key);
70
71 if self.is_contextual_augmented_sixth(key)? {
72 Self::push_resolution_suggestion(
73 key.triad_from_degree(5)?,
74 format!("augmented-sixth resolution in {key_name}"),
75 &mut suggestions,
76 &mut seen,
77 );
78 return Ok(suggestions);
79 }
80
81 if self.is_dominant_function_sonority()
82 && let Some(chord) = self.resolve_by_root_motion(key, 5)?
83 {
84 Self::push_resolution_suggestion(
85 chord,
86 format!("dominant resolution in {key_name}"),
87 &mut suggestions,
88 &mut seen,
89 );
90 }
91
92 if self.is_leading_tone_function_sonority()
93 && let Some(chord) = self.resolve_by_root_motion(key, 1)?
94 {
95 Self::push_resolution_suggestion(
96 chord,
97 format!("leading-tone resolution in {key_name}"),
98 &mut suggestions,
99 &mut seen,
100 );
101 }
102
103 Ok(suggestions)
104 }
105
106 pub fn resolution_suggestions(&self) -> Result<Vec<ChordResolutionSuggestion>> {
114 let mut suggestions = Vec::new();
115 let mut seen = std::collections::BTreeSet::new();
116
117 let augmented_contexts = self.augmented_sixth_contexts()?;
118 if !augmented_contexts.is_empty() {
119 for (tonic, mode) in augmented_contexts {
120 let context = format!(
121 "augmented-sixth resolution in {} {mode}",
122 Self::display_tonic_name(tonic)
123 );
124 self.add_resolution_suggestions_for_key(
125 tonic,
126 mode,
127 context,
128 &mut suggestions,
129 &mut seen,
130 )?;
131 }
132 return Ok(suggestions);
133 }
134
135 if let Some(root_pc) = self.find_root_pitch().map(root::pitch_class) {
136 if self.is_dominant_function_sonority() {
137 let tonic = Self::pitch_class_name((root_pc + 5) % 12);
138 for mode in ["major", "minor"] {
139 let context = format!(
140 "dominant resolution to {} {mode}",
141 Self::display_tonic_name(tonic)
142 );
143 self.add_resolution_suggestions_for_key(
144 tonic,
145 mode,
146 context,
147 &mut suggestions,
148 &mut seen,
149 )?;
150 }
151 }
152
153 if self.is_leading_tone_function_sonority() {
154 let tonic = Self::pitch_class_name((root_pc + 1) % 12);
155 for mode in ["major", "minor"] {
156 let context = format!(
157 "leading-tone resolution to {} {mode}",
158 Self::display_tonic_name(tonic)
159 );
160 self.add_resolution_suggestions_for_key(
161 tonic,
162 mode,
163 context,
164 &mut suggestions,
165 &mut seen,
166 )?;
167 }
168 }
169 }
170
171 Ok(suggestions)
172 }
173
174 pub fn can_be_dominant_v(&self) -> bool {
177 self.is_major_triad() || self.is_dominant_seventh()
178 }
179
180 pub fn can_be_tonic(&self) -> bool {
183 self.is_major_triad() || self.is_minor_triad()
184 }
185
186 pub(super) fn resolve_by_root_motion(&self, key: &Key, semitones: u8) -> Result<Option<Self>> {
187 let Some(root_pitch) = self.find_root_pitch() else {
188 return Ok(None);
189 };
190 let target_pc = (root::pitch_class(root_pitch) + semitones) % 12;
191 Self::triad_for_key_pitch_class(key, target_pc)?
192 .map(|chord| self.place_resolution_near_source(chord))
193 .transpose()
194 }
195
196 pub(super) fn triad_for_key_pitch_class(key: &Key, target_pc: u8) -> Result<Option<Self>> {
197 for degree in 1..=7 {
198 let degree_pitch = key.pitch_from_degree(degree)?;
199 if root::pitch_class(°ree_pitch) == target_pc {
200 return Ok(Some(key.triad_from_degree(degree)?));
201 }
202 }
203 Ok(None)
204 }
205
206 pub(super) fn place_resolution_near_source(&self, resolution: Self) -> Result<Self> {
207 let Some(source_center) = Self::pitch_center(&self.pitches()) else {
208 return Ok(resolution);
209 };
210 let Some(resolution_center) = Self::pitch_center(&resolution.pitches()) else {
211 return Ok(resolution);
212 };
213
214 let octave_shift = ((source_center - resolution_center) / 12.0).round() as IntegerType;
215 if octave_shift == 0 {
216 return Ok(resolution);
217 }
218
219 let pitches = resolution
220 .pitches()
221 .into_iter()
222 .map(|pitch| {
223 let octave = pitch
224 .octave()
225 .unwrap_or_else(|| (pitch.ps().round() as IntegerType).div_euclid(12) - 1);
226 Pitch::from_name_and_octave(pitch.name(), octave + octave_shift)
227 })
228 .collect::<Result<Vec<_>>>()?;
229
230 Chord::new(pitches.as_slice())
231 }
232
233 pub(super) fn pitch_center(pitches: &[Pitch]) -> Option<FloatType> {
234 if pitches.is_empty() {
235 return None;
236 }
237
238 Some(pitches.iter().map(Pitch::ps).sum::<FloatType>() / pitches.len() as FloatType)
239 }
240
241 pub(super) fn deduplicate_resolution_chords(chords: Vec<Self>) -> Vec<Self> {
242 let mut seen = std::collections::BTreeSet::new();
243 let mut deduped = Vec::new();
244
245 for chord in chords {
246 if seen.insert(chord.pitch_classes()) {
247 deduped.push(chord);
248 }
249 }
250
251 deduped
252 }
253
254 pub(super) fn augmented_sixth_contexts(&self) -> Result<Vec<(&'static str, &'static str)>> {
255 if !self.has_augmented_sixth_spelling() {
256 return Ok(Vec::new());
257 }
258
259 let mut contexts = Vec::new();
260 for tonic in CANDIDATE_TONICS {
261 for mode in ["major", "minor"] {
262 let key = Key::from_tonic_mode(tonic, Some(mode))?;
263 if self.is_contextual_augmented_sixth(&key)? {
264 contexts.push((tonic, mode));
265 }
266 }
267 }
268 Ok(contexts)
269 }
270
271 pub(super) fn push_resolution_suggestion(
272 chord: Chord,
273 key_context: String,
274 suggestions: &mut Vec<ChordResolutionSuggestion>,
275 seen: &mut std::collections::BTreeSet<(String, String)>,
276 ) {
277 let pitched_common_name = chord.pitched_common_name();
278 if seen.insert((pitched_common_name, key_context.clone())) {
279 suggestions.push(ChordResolutionSuggestion { chord, key_context });
280 }
281 }
282
283 pub(super) fn has_augmented_sixth_spelling(&self) -> bool {
284 for (index, lower) in self.notes.iter().enumerate() {
285 for upper in self.notes.iter().skip(index + 1) {
286 if Self::is_directed_augmented_sixth(&lower.pitch, &upper.pitch)
287 || Self::is_directed_augmented_sixth(&upper.pitch, &lower.pitch)
288 {
289 return true;
290 }
291 }
292 }
293 false
294 }
295
296 pub(super) fn is_directed_augmented_sixth(lower: &Pitch, upper: &Pitch) -> bool {
297 let generic_interval = (root::step_num(upper) - root::step_num(lower)).rem_euclid(7) + 1;
298 let semitones = ((upper.ps().round() as IntegerType) - (lower.ps().round() as IntegerType))
299 .rem_euclid(12);
300 generic_interval == 6 && semitones == 10
301 }
302
303 pub(super) fn add_resolution_suggestions_for_key(
304 &self,
305 tonic: &str,
306 mode: &str,
307 key_context: String,
308 suggestions: &mut Vec<ChordResolutionSuggestion>,
309 seen: &mut std::collections::BTreeSet<(String, String)>,
310 ) -> Result<()> {
311 for chord in self.resolution_chords(tonic, Some(mode))? {
312 Self::push_resolution_suggestion(chord, key_context.clone(), suggestions, seen);
313 }
314 Ok(())
315 }
316
317 pub(super) fn is_dominant_function_sonority(&self) -> bool {
318 let names = self.common_names_with_primary();
319 let has_explicit_dominant_name = names.iter().any(|name| {
320 matches!(
321 name.as_str(),
322 "dominant seventh chord"
323 | "major minor seventh chord"
324 | "incomplete dominant-seventh chord"
325 )
326 });
327 let has_dominant_family_name = names
328 .iter()
329 .any(|name| name.contains("dominant") || name == "major-minor");
330
331 has_explicit_dominant_name
332 || (has_dominant_family_name && self.has_intervals_above_root(&[4, 10]))
333 }
334
335 pub(super) fn is_leading_tone_function_sonority(&self) -> bool {
336 let names = self.common_names_with_primary();
337 let has_explicit_leading_tone_name = names.iter().any(|name| {
338 matches!(
339 name.as_str(),
340 "diminished triad"
341 | "diminished seventh chord"
342 | "half-diminished seventh chord"
343 | "incomplete half-diminished seventh chord"
344 )
345 });
346 let has_diminished_family_name = names.iter().any(|name| name.contains("diminished"));
347
348 has_explicit_leading_tone_name
349 || (has_diminished_family_name && self.has_intervals_above_root(&[3, 6]))
350 }
351
352 pub(super) fn is_contextual_augmented_sixth(&self, key: &Key) -> Result<bool> {
353 let chord_pcs = self.pitch_class_set();
354 if chord_pcs.len() < 3 || chord_pcs.len() > 4 {
355 return Ok(false);
356 }
357
358 let tonic_pc = root::pitch_class(&key.pitch_from_degree(1)?);
359 let second_pc = root::pitch_class(&key.pitch_from_degree(2)?);
360 let third_pc = root::pitch_class(&key.pitch_from_degree(3)?);
361 let fourth_pc = root::pitch_class(&key.pitch_from_degree(4)?);
362 let sixth_pc = root::pitch_class(&key.pitch_from_degree(6)?);
363
364 let raised_fourth_pc = (fourth_pc + 1) % 12;
365 let lowered_sixth_pc = if (sixth_pc + 12 - tonic_pc) % 12 == 9 {
366 (sixth_pc + 11) % 12
367 } else {
368 sixth_pc
369 };
370
371 if !chord_pcs.contains(&lowered_sixth_pc) || !chord_pcs.contains(&raised_fourth_pc) {
372 return Ok(false);
373 }
374
375 if self
376 .common_names_with_primary()
377 .iter()
378 .any(|name| name.contains("augmented sixth chord"))
379 {
380 return Ok(true);
381 }
382
383 let lowered_third_pc = if (third_pc + 12 - tonic_pc) % 12 == 4 {
384 (third_pc + 11) % 12
385 } else {
386 third_pc
387 };
388 let raised_second_pc = (second_pc + 1) % 12;
389 let allowed_pcs = [
390 lowered_sixth_pc,
391 raised_fourth_pc,
392 tonic_pc,
393 second_pc,
394 lowered_third_pc,
395 raised_second_pc,
396 ];
397
398 Ok(chord_pcs.contains(&tonic_pc)
399 && chord_pcs
400 .iter()
401 .all(|pc| allowed_pcs.iter().any(|allowed| allowed == pc)))
402 }
403}
404
405#[derive(Debug, Clone)]
406#[must_use]
408pub struct ChordResolutionSuggestion {
409 pub chord: Chord,
411 pub key_context: String,
413}
414
415pub(super) const CANDIDATE_TONICS: [&str; 12] = [
416 "C", "D-", "D", "E-", "E", "F", "F#", "G", "A-", "A", "B-", "B",
417];