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 self.semitones_from_chord_step_with_root(step, self.root()?)
34 }
35
36 pub fn has_repeated_chord_step(&self, step: u8) -> bool {
39 let Some(root) = self.root() else {
40 return false;
41 };
42
43 self.has_repeated_step_from(step, root)
44 }
45
46 fn has_repeated_step_from(&self, step: u8, root: &Pitch) -> bool {
49 let step = fold_chord_step(step);
50 let Some(first) = self
51 .chord_step_from(step, root)
52 .map(|pitch| semitones_above(root, pitch))
53 else {
54 return false;
55 };
56 self.pitch_refs().any(|pitch| {
57 diatonic_steps_above(root, pitch) == step && semitones_above(root, pitch) != first
58 })
59 }
60
61 pub fn has_any_enharmonic_spelled_pitches(&self) -> bool {
64 self.pitch_class_cardinality() != self.unique_pitch_name_count()
65 }
66
67 pub fn is_triad(&self) -> bool {
70 self.root().is_some_and(|root| self.is_triad_from(root))
71 }
72
73 pub fn is_seventh(&self) -> bool {
76 self.root().is_some_and(|root| self.is_seventh_from(root))
77 }
78
79 pub(super) fn is_triad_from(&self, root: &Pitch) -> bool {
81 self.unique_pitch_name_count() == 3 && self.has_steps_from(root, &[3, 5])
82 }
83
84 pub(super) fn is_seventh_from(&self, root: &Pitch) -> bool {
86 self.unique_pitch_name_count() == 4 && self.has_steps_from(root, &[3, 5, 7])
87 }
88
89 pub fn is_major_triad(&self) -> bool {
91 self.is_triad_of_type((3, 11, -1), TriadQuality::Major)
92 }
93
94 pub fn is_minor_triad(&self) -> bool {
96 self.is_triad_of_type((3, 11, 1), TriadQuality::Minor)
97 }
98
99 pub fn is_diminished_triad(&self) -> bool {
101 self.is_triad_of_type((3, 10, 0), TriadQuality::Diminished)
102 }
103
104 pub fn is_augmented_triad(&self) -> bool {
106 self.is_triad_of_type((3, 12, 0), TriadQuality::Augmented)
107 }
108
109 pub fn is_seventh_of_type(&self, semitones: &[u8]) -> bool {
112 if self.unique_pitch_name_count() != 4 {
115 return false;
116 }
117
118 let Some(root) = self.root() else {
119 return false;
120 };
121
122 self.seventh_of_type_from(semitones, root)
123 }
124
125 pub(crate) fn seventh_of_type_from(&self, semitones: &[u8], root: &Pitch) -> bool {
128 if !self.is_seventh_from(root) {
129 return false;
130 }
131
132 self.pitch_refs()
133 .all(|pitch| semitones.contains(&semitones_above(root, pitch)))
134 }
135
136 pub fn is_dominant_seventh(&self) -> bool {
139 self.is_seventh_of_type(&[0, 4, 7, 10])
140 }
141
142 pub fn is_half_diminished_seventh(&self) -> bool {
144 self.is_seventh_of_type(&[0, 3, 6, 10])
145 }
146
147 pub fn is_diminished_seventh(&self) -> bool {
149 self.is_seventh_of_type(&[0, 3, 6, 9])
150 }
151
152 pub fn is_incomplete_major_triad(&self) -> bool {
154 self.is_incomplete_triad_of_type((2, 4), 4)
155 }
156
157 pub fn is_incomplete_minor_triad(&self) -> bool {
159 self.is_incomplete_triad_of_type((2, 3), 3)
160 }
161
162 pub fn contains_triad(&self) -> bool {
165 self.root()
166 .is_some_and(|root| self.has_steps_from(root, &[3, 5]))
167 }
168
169 pub fn contains_seventh(&self) -> bool {
171 self.root()
172 .is_some_and(|root| self.has_steps_from(root, &[3, 5, 7]))
173 }
174
175 pub fn quality(&self) -> TriadQuality {
179 let Some(root) = self.root() else {
181 return TriadQuality::Other;
182 };
183
184 let Some(third) = self.semitones_from_chord_step_with_root(3, root) else {
185 return TriadQuality::Other;
186 };
187 if [3, 5].into_iter().any(|step| {
190 self.chord_step_from(step, root)
191 .is_some_and(|pitch| ((pitch.ps() - root.ps()) * 100.0).round() % 100.0 != 0.0)
192 }) {
193 return TriadQuality::Other;
194 }
195 if self.has_repeated_step_from(1, root) || self.has_repeated_step_from(3, root) {
196 return TriadQuality::Other;
197 }
198 let Some(fifth) = self.semitones_from_chord_step_with_root(5, root) else {
199 return match third {
200 4 => TriadQuality::Major,
201 3 => TriadQuality::Minor,
202 _ => TriadQuality::Other,
203 };
204 };
205 if self.has_repeated_step_from(5, root) {
206 return TriadQuality::Other;
207 }
208 match (third, fifth) {
209 (4, 7) => TriadQuality::Major,
210 (3, 7) => TriadQuality::Minor,
211 (4, 8) => TriadQuality::Augmented,
212 (3, 6) => TriadQuality::Diminished,
213 _ => TriadQuality::Other,
214 }
215 }
216
217 pub fn is_consonant(&self) -> bool {
221 let distinct = self.remove_redundant_pitch_names();
222 match distinct.notes.len() {
223 1 => true,
224 2 => {
225 let closed = self.closed_position(None, false).remove_redundant_pitches();
226 Interval::between_pitches(&closed.notes[0].pitch, &closed.notes[1].pitch)
227 .is_ok_and(|interval| interval.is_consonant())
228 }
229 3 => (self.is_major_triad() || self.is_minor_triad()) && self.inversion() != Some(2),
230 _ => false,
231 }
232 }
233
234 pub fn chord_step_with_root(&self, step: u8, root: &Pitch) -> Option<&Pitch> {
238 self.chord_step_from(step, root)
239 }
240
241 pub fn semitones_from_chord_step_with_root(&self, step: u8, root: &Pitch) -> Option<u8> {
244 let pitch = self.chord_step_from(step, root)?;
245 Some(semitones_above(root, pitch))
246 }
247
248 pub fn inversion_from_root(&self, root: &Pitch) -> Option<u8> {
251 self.inversion_with_root(root)
252 }
253
254 pub(super) fn has_steps_from(&self, root: &Pitch, steps: &[u8]) -> bool {
258 steps
259 .iter()
260 .all(|&step| self.chord_step_from(step, root).is_some())
261 }
262
263 pub(crate) fn chord_step_from(&self, step: u8, root: &Pitch) -> Option<&Pitch> {
264 let step = fold_chord_step(step);
265 self.pitch_refs()
266 .find(|pitch| diatonic_steps_above(root, pitch) == step)
267 }
268
269 pub(super) fn is_triad_of_type(&self, address: (u8, u8, i8), quality: TriadQuality) -> bool {
270 if self.unique_pitch_name_count() != 3 {
273 return false;
274 }
275
276 let Some(root) = self.root() else {
277 return false;
278 };
279
280 let spelled = self.spelled_triad_from(root) == Some(quality);
281 debug_assert!(!spelled || self.forte_address() == Some(address));
287 spelled
288 }
289
290 pub(crate) fn spelled_triad_from(&self, root: &Pitch) -> Option<TriadQuality> {
294 if !self.is_triad_from(root) || self.has_any_enharmonic_spelled_pitches() {
295 return None;
296 }
297
298 let third = semitones_above(root, self.chord_step_from(3, root)?);
299 let fifth = semitones_above(root, self.chord_step_from(5, root)?);
300 match (third, fifth) {
301 (4, 7) => Some(TriadQuality::Major),
302 (3, 7) => Some(TriadQuality::Minor),
303 (3, 6) => Some(TriadQuality::Diminished),
304 (4, 8) => Some(TriadQuality::Augmented),
305 _ => None,
306 }
307 }
308
309 pub(super) fn is_incomplete_triad_of_type(
310 &self,
311 address: (u8, u8),
312 third_semitones: u8,
313 ) -> bool {
314 if self
315 .forte_address()
316 .is_none_or(|(card, index, _)| (card, index) != address)
317 {
318 return false;
319 }
320 let Some(root) = self.root() else {
321 return false;
322 };
323 if self.chord_step_from(3, root).is_none() {
324 return false;
325 }
326
327 self.pitch_refs()
328 .all(|pitch| [0, third_semitones].contains(&semitones_above(root, pitch)))
329 }
330
331 pub fn is_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
335 match self.pitch_class_cardinality() {
336 3 => self.is_italian_augmented_sixth(permit_any_inversion, false),
337 4 => {
338 let found = self.forte_address();
340 self.is_french_from(found, permit_any_inversion)
341 || self.is_german_from(found, permit_any_inversion)
342 || self.is_swiss_from(found, permit_any_inversion)
343 }
344 _ => false,
345 }
346 }
347
348 pub fn is_italian_augmented_sixth(
351 &self,
352 permit_any_inversion: bool,
353 restrict_doublings: bool,
354 ) -> bool {
355 let Some(root) = self.augmented_sixth_root(
356 self.forte_address(),
357 (3, 8, 1),
358 1,
359 permit_any_inversion,
360 &AUGMENTED_SIXTHS.italian,
361 ) else {
362 return false;
363 };
364 if !restrict_doublings {
365 return true;
366 }
367
368 let (Some(third), Some(fifth)) =
370 (self.chord_step_from(3, root), self.chord_step_from(5, root))
371 else {
372 return false;
373 };
374 self.pitch_refs().all(|pitch| {
375 pitch.name() == fifth.name() || std::ptr::eq(pitch, third) || std::ptr::eq(pitch, root)
376 })
377 }
378
379 pub fn is_french_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
382 self.is_french_from(self.forte_address(), permit_any_inversion)
383 }
384
385 pub(super) fn is_french_from(
388 &self,
389 found: Option<(u8, u8, i8)>,
390 permit_any_inversion: bool,
391 ) -> bool {
392 self.augmented_sixth_root(
393 found,
394 (4, 25, 0),
395 2,
396 permit_any_inversion,
397 &AUGMENTED_SIXTHS.french,
398 )
399 .is_some()
400 }
401
402 pub fn is_german_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
405 self.is_german_from(self.forte_address(), permit_any_inversion)
406 }
407
408 pub(super) fn is_german_from(
411 &self,
412 found: Option<(u8, u8, i8)>,
413 permit_any_inversion: bool,
414 ) -> bool {
415 self.augmented_sixth_root(
416 found,
417 (4, 27, -1),
418 1,
419 permit_any_inversion,
420 &AUGMENTED_SIXTHS.german,
421 )
422 .is_some()
423 }
424
425 pub fn is_swiss_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
428 self.is_swiss_from(self.forte_address(), permit_any_inversion)
429 }
430
431 pub(super) fn is_swiss_from(
434 &self,
435 found: Option<(u8, u8, i8)>,
436 permit_any_inversion: bool,
437 ) -> bool {
438 self.augmented_sixth_root(
439 found,
440 (4, 27, -1),
441 2,
442 permit_any_inversion,
443 &AUGMENTED_SIXTHS.swiss,
444 )
445 .is_some()
446 }
447
448 pub fn is_ninth(&self) -> bool {
451 self.unique_pitch_name_count() == 5
452 && self
453 .root()
454 .is_some_and(|root| self.has_steps_from(root, &[3, 5, 7, 2]))
455 }
456
457 pub fn is_transpositionally_symmetrical(&self, require_intervallic_evenness: bool) -> bool {
461 let Some((card, index, _)) = self.forte_address() else {
462 return self.notes.is_empty();
463 };
464 if card == 1 {
465 return require_intervallic_evenness;
466 }
467 const EVEN: [(u8, u8); 5] = [(2, 6), (3, 12), (4, 28), (6, 35), (12, 1)];
468 const UNEVEN: [(u8, u8); 10] = [
469 (4, 9),
470 (4, 25),
471 (6, 7),
472 (6, 20),
473 (6, 30),
474 (8, 9),
475 (8, 25),
476 (8, 28),
477 (9, 12),
478 (10, 6),
479 ];
480 EVEN.contains(&(card, index))
481 || (!require_intervallic_evenness && UNEVEN.contains(&(card, index)))
482 }
483
484 pub fn has_any_repeated_diatonic_note(&self) -> bool {
487 let steps = self
488 .pitch_refs()
489 .map(Pitch::step)
490 .collect::<std::collections::BTreeSet<_>>();
491 steps.len() != self.unique_pitch_name_count()
492 }
493
494 pub(super) fn augmented_sixth_root(
500 &self,
501 found: Option<(u8, u8, i8)>,
502 address: (u8, u8, i8),
503 required_inversion: u8,
504 permit_any_inversion: bool,
505 intervals: &[[Interval; 2]],
506 ) -> Option<&Pitch> {
507 if found != Some(address) || self.has_any_enharmonic_spelled_pitches() {
508 return None;
509 }
510
511 let root = self.root()?;
512 if !permit_any_inversion && self.inversion_with_root(root) != Some(required_inversion) {
513 return None;
514 }
515
516 let steps = [3, 5, 7].map(|step| self.chord_step_from(step, root));
517 let matches = intervals.iter().zip(steps).all(|(accepted, step)| {
518 step.and_then(|pitch| Interval::between_pitches(root, pitch).ok())
519 .is_some_and(|interval| {
520 accepted.iter().any(|candidate| {
521 candidate.directed_simple_key() == interval.directed_simple_key()
522 })
523 })
524 });
525 matches.then_some(root)
526 }
527
528 pub(super) fn has_intervals_above_root(&self, intervals: &[u8]) -> bool {
529 let Some(root_pitch) = self.find_root_pitch() else {
530 return false;
531 };
532 let root_pc = root::pitch_class(root_pitch);
533 let chord_pcs = self.pitch_class_set();
534 intervals
535 .iter()
536 .all(|interval| chord_pcs.contains(&((root_pc + interval) % 12)))
537 }
538
539 pub fn interval_from_chord_step(&self, step: u8) -> Option<Interval> {
544 let root = self.root()?;
545 self.pitch_refs()
546 .filter_map(|pitch| Interval::between_pitches(root, pitch).ok())
547 .find(|interval| interval.mod7() == IntegerType::from(step))
548 }
549}
550
551#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
554#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
555pub enum TriadQuality {
556 Major,
558 Minor,
560 Augmented,
562 Diminished,
564 Other,
566}
567
568impl TriadQuality {
569 pub fn as_str(self) -> &'static str {
571 match self {
572 Self::Major => "major",
573 Self::Minor => "minor",
574 Self::Augmented => "augmented",
575 Self::Diminished => "diminished",
576 Self::Other => "other",
577 }
578 }
579}
580
581impl Display for TriadQuality {
582 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
583 f.write_str(self.as_str())
584 }
585}
586
587pub(super) fn fold_chord_step(step: u8) -> u8 {
588 if step >= 8 { step - 7 } else { step }
589}
590
591pub(super) fn diatonic_steps_above(root: &Pitch, pitch: &Pitch) -> u8 {
592 ((root::step_num(pitch) - root::step_num(root)).rem_euclid(7) + 1) as u8
593}
594
595pub(super) fn semitones_above(root: &Pitch, pitch: &Pitch) -> u8 {
596 (root::pitch_class(pitch) + 12 - root::pitch_class(root)) % 12
597}
598
599pub(super) struct AugmentedSixthIntervals {
600 italian: [[Interval; 2]; 2],
601 french: [[Interval; 2]; 3],
602 german: [[Interval; 2]; 3],
603 swiss: [[Interval; 2]; 3],
604}
605
606pub(super) static AUGMENTED_SIXTHS: LazyLock<AugmentedSixthIntervals> = LazyLock::new(|| {
610 let pair = |up: &str, down: &str| {
611 [
612 Interval::from_name(up).expect("augmented sixth intervals parse"),
613 Interval::from_name(down).expect("augmented sixth intervals parse"),
614 ]
615 };
616 AugmentedSixthIntervals {
617 italian: [pair("d3", "A-6"), pair("d5", "A-4")],
618 french: [pair("M3", "m-6"), pair("d5", "A-4"), pair("m7", "M-2")],
619 german: [pair("d3", "A-6"), pair("d5", "A-4"), pair("d7", "A-2")],
620 swiss: [pair("m3", "M-6"), pair("dd5", "AA-4"), pair("d7", "A-2")],
621 }
622});