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