1use super::*;
6
7impl Chord {
8 pub fn chord_step(&self, step: u8) -> Option<&Pitch> {
12 self.chord_step_from(step, self.root()?)
13 }
14
15 pub fn third(&self) -> Option<&Pitch> {
17 self.chord_step(3)
18 }
19
20 pub fn fifth(&self) -> Option<&Pitch> {
22 self.chord_step(5)
23 }
24
25 pub fn seventh(&self) -> Option<&Pitch> {
27 self.chord_step(7)
28 }
29
30 pub fn semitones_from_chord_step(&self, step: u8) -> Option<u8> {
33 let root = self.root()?;
34 let pitch = self.chord_step_from(step, root)?;
35 Some(semitones_above(root, pitch))
36 }
37
38 pub fn has_repeated_chord_step(&self, step: u8) -> bool {
41 let Some(root) = self.root() else {
42 return false;
43 };
44 let step = fold_chord_step(step);
45 let Some(first) = self
46 .chord_step_from(step, root)
47 .map(|pitch| semitones_above(root, pitch))
48 else {
49 return false;
50 };
51 self.pitch_refs().any(|pitch| {
52 diatonic_steps_above(root, pitch) == step && semitones_above(root, pitch) != first
53 })
54 }
55
56 pub fn has_any_enharmonic_spelled_pitches(&self) -> bool {
59 self.pitch_class_set().len() != self.unique_pitch_names().len()
60 }
61
62 pub fn is_triad(&self) -> bool {
65 self.unique_pitch_names().len() == 3 && self.third().is_some() && self.fifth().is_some()
66 }
67
68 pub fn is_seventh(&self) -> bool {
71 self.unique_pitch_names().len() == 4
72 && self.third().is_some()
73 && self.fifth().is_some()
74 && self.seventh().is_some()
75 }
76
77 pub fn is_major_triad(&self) -> bool {
79 self.is_triad_of_type((3, 11, -1), 4, 7)
80 }
81
82 pub fn is_minor_triad(&self) -> bool {
84 self.is_triad_of_type((3, 11, 1), 3, 7)
85 }
86
87 pub fn is_diminished_triad(&self) -> bool {
89 self.is_triad_of_type((3, 10, 0), 3, 6)
90 }
91
92 pub fn is_augmented_triad(&self) -> bool {
94 self.is_triad_of_type((3, 12, 0), 4, 8)
95 }
96
97 pub fn is_seventh_of_type(&self, semitones: &[u8]) -> bool {
100 if !self.is_seventh() {
101 return false;
102 }
103 let Some(root) = self.root() else {
104 return false;
105 };
106 self.pitch_refs()
107 .all(|pitch| semitones.contains(&semitones_above(root, pitch)))
108 }
109
110 pub fn is_dominant_seventh(&self) -> bool {
113 self.is_seventh_of_type(&[0, 4, 7, 10])
114 }
115
116 pub fn is_half_diminished_seventh(&self) -> bool {
118 self.is_seventh_of_type(&[0, 3, 6, 10])
119 }
120
121 pub fn is_diminished_seventh(&self) -> bool {
123 self.is_seventh_of_type(&[0, 3, 6, 9])
124 }
125
126 pub fn is_incomplete_major_triad(&self) -> bool {
128 self.is_incomplete_triad_of_type((2, 4), 4)
129 }
130
131 pub fn is_incomplete_minor_triad(&self) -> bool {
133 self.is_incomplete_triad_of_type((2, 3), 3)
134 }
135
136 pub fn contains_triad(&self) -> bool {
139 self.third().is_some() && self.fifth().is_some()
140 }
141
142 pub fn contains_seventh(&self) -> bool {
144 self.contains_triad() && self.seventh().is_some()
145 }
146
147 pub fn quality(&self) -> TriadQuality {
151 let Some(third) = self.semitones_from_chord_step(3) else {
152 return TriadQuality::Other;
153 };
154 if self.root().is_some_and(|root| {
157 [3, 5].into_iter().any(|step| {
158 self.chord_step_from(step, root)
159 .is_some_and(|pitch| ((pitch.ps() - root.ps()) * 100.0).round() % 100.0 != 0.0)
160 })
161 }) {
162 return TriadQuality::Other;
163 }
164 if self.has_repeated_chord_step(1) || self.has_repeated_chord_step(3) {
165 return TriadQuality::Other;
166 }
167 let Some(fifth) = self.semitones_from_chord_step(5) else {
168 return match third {
169 4 => TriadQuality::Major,
170 3 => TriadQuality::Minor,
171 _ => TriadQuality::Other,
172 };
173 };
174 if self.has_repeated_chord_step(5) {
175 return TriadQuality::Other;
176 }
177 match (third, fifth) {
178 (4, 7) => TriadQuality::Major,
179 (3, 7) => TriadQuality::Minor,
180 (4, 8) => TriadQuality::Augmented,
181 (3, 6) => TriadQuality::Diminished,
182 _ => TriadQuality::Other,
183 }
184 }
185
186 pub fn is_consonant(&self) -> bool {
190 let distinct = self.remove_redundant_pitch_names();
191 match distinct.notes.len() {
192 1 => true,
193 2 => {
194 let closed = self.closed_position(None, false).remove_redundant_pitches();
195 Interval::between_pitches(&closed.notes[0].pitch, &closed.notes[1].pitch)
196 .is_ok_and(|interval| interval.is_consonant())
197 }
198 3 => (self.is_major_triad() || self.is_minor_triad()) && self.inversion() != Some(2),
199 _ => false,
200 }
201 }
202
203 pub fn chord_step_with_root(&self, step: u8, root: &Pitch) -> Option<&Pitch> {
207 self.chord_step_from(step, root)
208 }
209
210 pub fn semitones_from_chord_step_with_root(&self, step: u8, root: &Pitch) -> Option<u8> {
213 let pitch = self.chord_step_from(step, root)?;
214 Some(semitones_above(root, pitch))
215 }
216
217 pub fn inversion_from_root(&self, root: &Pitch) -> Option<u8> {
220 self.inversion_with_root(root)
221 }
222
223 pub(crate) fn chord_step_from(&self, step: u8, root: &Pitch) -> Option<&Pitch> {
224 let step = fold_chord_step(step);
225 self.pitch_refs()
226 .find(|pitch| diatonic_steps_above(root, pitch) == step)
227 }
228
229 pub(super) fn is_triad_of_type(
230 &self,
231 address: (u8, u8, i8),
232 third_semitones: u8,
233 fifth_semitones: u8,
234 ) -> bool {
235 if self.forte_address() != Some(address) {
236 return false;
237 }
238 if !self.is_triad() || self.has_any_enharmonic_spelled_pitches() {
239 return false;
240 }
241 let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
242 else {
243 return false;
244 };
245 semitones_above(root, third) == third_semitones
246 && semitones_above(root, fifth) == fifth_semitones
247 }
248
249 pub(super) fn is_incomplete_triad_of_type(
250 &self,
251 address: (u8, u8),
252 third_semitones: u8,
253 ) -> bool {
254 if self
255 .forte_address()
256 .is_none_or(|(card, index, _)| (card, index) != address)
257 {
258 return false;
259 }
260 let (Some(root), Some(_)) = (self.root(), self.third()) else {
261 return false;
262 };
263 self.pitch_refs()
264 .all(|pitch| [0, third_semitones].contains(&semitones_above(root, pitch)))
265 }
266
267 pub fn is_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
271 match self.pitch_class_cardinality() {
272 3 => self.is_italian_augmented_sixth(permit_any_inversion, false),
273 4 => {
274 self.is_french_augmented_sixth(permit_any_inversion)
275 || self.is_german_augmented_sixth(permit_any_inversion)
276 || self.is_swiss_augmented_sixth(permit_any_inversion)
277 }
278 _ => false,
279 }
280 }
281
282 pub fn is_italian_augmented_sixth(
285 &self,
286 permit_any_inversion: bool,
287 restrict_doublings: bool,
288 ) -> bool {
289 if !self.is_augmented_sixth_of_type(
290 (3, 8, 1),
291 1,
292 permit_any_inversion,
293 &AUGMENTED_SIXTHS.italian,
294 ) {
295 return false;
296 }
297 if !restrict_doublings {
298 return true;
299 }
300 let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
301 else {
302 return false;
303 };
304 self.pitch_refs().all(|pitch| {
305 pitch.name() == fifth.name() || std::ptr::eq(pitch, third) || std::ptr::eq(pitch, root)
306 })
307 }
308
309 pub fn is_french_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
312 self.is_augmented_sixth_of_type(
313 (4, 25, 0),
314 2,
315 permit_any_inversion,
316 &AUGMENTED_SIXTHS.french,
317 )
318 }
319
320 pub fn is_german_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
323 self.is_augmented_sixth_of_type(
324 (4, 27, -1),
325 1,
326 permit_any_inversion,
327 &AUGMENTED_SIXTHS.german,
328 )
329 }
330
331 pub fn is_swiss_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
334 self.is_augmented_sixth_of_type(
335 (4, 27, -1),
336 2,
337 permit_any_inversion,
338 &AUGMENTED_SIXTHS.swiss,
339 )
340 }
341
342 pub fn is_ninth(&self) -> bool {
345 self.unique_pitch_names().len() == 5
346 && self.third().is_some()
347 && self.fifth().is_some()
348 && self.seventh().is_some()
349 && self.chord_step(2).is_some()
350 }
351
352 pub fn is_transpositionally_symmetrical(&self, require_intervallic_evenness: bool) -> bool {
356 let Some((card, index, _)) = self.forte_address() else {
357 return self.notes.is_empty();
358 };
359 if card == 1 {
360 return require_intervallic_evenness;
361 }
362 const EVEN: [(u8, u8); 5] = [(2, 6), (3, 12), (4, 28), (6, 35), (12, 1)];
363 const UNEVEN: [(u8, u8); 10] = [
364 (4, 9),
365 (4, 25),
366 (6, 7),
367 (6, 20),
368 (6, 30),
369 (8, 9),
370 (8, 25),
371 (8, 28),
372 (9, 12),
373 (10, 6),
374 ];
375 EVEN.contains(&(card, index))
376 || (!require_intervallic_evenness && UNEVEN.contains(&(card, index)))
377 }
378
379 pub fn has_any_repeated_diatonic_note(&self) -> bool {
382 let steps = self
383 .pitch_refs()
384 .map(Pitch::step)
385 .collect::<std::collections::BTreeSet<_>>();
386 steps.len() != self.unique_pitch_names().len()
387 }
388
389 pub(super) fn is_augmented_sixth_of_type(
390 &self,
391 address: (u8, u8, i8),
392 required_inversion: u8,
393 permit_any_inversion: bool,
394 intervals: &[[Interval; 2]],
395 ) -> bool {
396 if self.forte_address() != Some(address) || self.has_any_enharmonic_spelled_pitches() {
397 return false;
398 }
399 if !permit_any_inversion && self.inversion() != Some(required_inversion) {
400 return false;
401 }
402 let Some(root) = self.root() else {
403 return false;
404 };
405 let steps = [self.third(), self.fifth(), self.seventh()];
406 intervals.iter().zip(steps).all(|(accepted, step)| {
407 step.and_then(|pitch| Interval::between_pitches(root, pitch).ok())
408 .is_some_and(|interval| {
409 accepted.iter().any(|candidate| {
410 candidate.directed_simple_key() == interval.directed_simple_key()
411 })
412 })
413 })
414 }
415
416 pub(super) fn has_intervals_above_root(&self, intervals: &[u8]) -> bool {
417 let Some(root_pitch) = self.find_root_pitch() else {
418 return false;
419 };
420 let root_pc = root::pitch_class(root_pitch);
421 let chord_pcs = self.pitch_class_set();
422 intervals
423 .iter()
424 .all(|interval| chord_pcs.contains(&((root_pc + interval) % 12)))
425 }
426
427 pub fn interval_from_chord_step(&self, step: u8) -> Option<Interval> {
432 let root = self.root()?;
433 self.pitch_refs()
434 .filter_map(|pitch| Interval::between_pitches(root, pitch).ok())
435 .find(|interval| interval.mod7() == IntegerType::from(step))
436 }
437}
438
439#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
442#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
443pub enum TriadQuality {
444 Major,
446 Minor,
448 Augmented,
450 Diminished,
452 Other,
454}
455
456impl TriadQuality {
457 pub fn as_str(self) -> &'static str {
459 match self {
460 Self::Major => "major",
461 Self::Minor => "minor",
462 Self::Augmented => "augmented",
463 Self::Diminished => "diminished",
464 Self::Other => "other",
465 }
466 }
467}
468
469impl Display for TriadQuality {
470 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
471 f.write_str(self.as_str())
472 }
473}
474
475pub(super) fn fold_chord_step(step: u8) -> u8 {
476 if step >= 8 { step - 7 } else { step }
477}
478
479pub(super) fn diatonic_steps_above(root: &Pitch, pitch: &Pitch) -> u8 {
480 ((root::step_num(pitch) - root::step_num(root)).rem_euclid(7) + 1) as u8
481}
482
483pub(super) fn semitones_above(root: &Pitch, pitch: &Pitch) -> u8 {
484 (root::pitch_class(pitch) + 12 - root::pitch_class(root)) % 12
485}
486
487pub(super) struct AugmentedSixthIntervals {
488 italian: [[Interval; 2]; 2],
489 french: [[Interval; 2]; 3],
490 german: [[Interval; 2]; 3],
491 swiss: [[Interval; 2]; 3],
492}
493
494pub(super) static AUGMENTED_SIXTHS: LazyLock<AugmentedSixthIntervals> = LazyLock::new(|| {
498 let pair = |up: &str, down: &str| {
499 [
500 Interval::from_name(up).expect("augmented sixth intervals parse"),
501 Interval::from_name(down).expect("augmented sixth intervals parse"),
502 ]
503 };
504 AugmentedSixthIntervals {
505 italian: [pair("d3", "A-6"), pair("d5", "A-4")],
506 french: [pair("M3", "m-6"), pair("d5", "A-4"), pair("m7", "M-2")],
507 german: [pair("d3", "A-6"), pair("d5", "A-4"), pair("d7", "A-2")],
508 swiss: [pair("m3", "M-6"), pair("dd5", "AA-4"), pair("d7", "A-2")],
509 }
510});