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_cardinality() != self.unique_pitch_name_count()
60 }
61
62 pub fn is_triad(&self) -> bool {
65 self.unique_pitch_name_count() == 3 && self.third().is_some() && self.fifth().is_some()
66 }
67
68 pub fn is_seventh(&self) -> bool {
71 self.unique_pitch_name_count() == 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.is_triad() || self.has_any_enharmonic_spelled_pitches() {
236 return false;
237 }
238 let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
239 else {
240 return false;
241 };
242 let spelled = semitones_above(root, third) == third_semitones
243 && semitones_above(root, fifth) == fifth_semitones;
244 debug_assert!(!spelled || self.forte_address() == Some(address));
250 spelled
251 }
252
253 pub(super) fn is_incomplete_triad_of_type(
254 &self,
255 address: (u8, u8),
256 third_semitones: u8,
257 ) -> bool {
258 if self
259 .forte_address()
260 .is_none_or(|(card, index, _)| (card, index) != address)
261 {
262 return false;
263 }
264 let (Some(root), Some(_)) = (self.root(), self.third()) else {
265 return false;
266 };
267 self.pitch_refs()
268 .all(|pitch| [0, third_semitones].contains(&semitones_above(root, pitch)))
269 }
270
271 pub fn is_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
275 match self.pitch_class_cardinality() {
276 3 => self.is_italian_augmented_sixth(permit_any_inversion, false),
277 4 => {
278 self.is_french_augmented_sixth(permit_any_inversion)
279 || self.is_german_augmented_sixth(permit_any_inversion)
280 || self.is_swiss_augmented_sixth(permit_any_inversion)
281 }
282 _ => false,
283 }
284 }
285
286 pub fn is_italian_augmented_sixth(
289 &self,
290 permit_any_inversion: bool,
291 restrict_doublings: bool,
292 ) -> bool {
293 if !self.is_augmented_sixth_of_type(
294 (3, 8, 1),
295 1,
296 permit_any_inversion,
297 &AUGMENTED_SIXTHS.italian,
298 ) {
299 return false;
300 }
301 if !restrict_doublings {
302 return true;
303 }
304 let (Some(root), Some(third), Some(fifth)) = (self.root(), self.third(), self.fifth())
305 else {
306 return false;
307 };
308 self.pitch_refs().all(|pitch| {
309 pitch.name() == fifth.name() || std::ptr::eq(pitch, third) || std::ptr::eq(pitch, root)
310 })
311 }
312
313 pub fn is_french_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
316 self.is_augmented_sixth_of_type(
317 (4, 25, 0),
318 2,
319 permit_any_inversion,
320 &AUGMENTED_SIXTHS.french,
321 )
322 }
323
324 pub fn is_german_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
327 self.is_augmented_sixth_of_type(
328 (4, 27, -1),
329 1,
330 permit_any_inversion,
331 &AUGMENTED_SIXTHS.german,
332 )
333 }
334
335 pub fn is_swiss_augmented_sixth(&self, permit_any_inversion: bool) -> bool {
338 self.is_augmented_sixth_of_type(
339 (4, 27, -1),
340 2,
341 permit_any_inversion,
342 &AUGMENTED_SIXTHS.swiss,
343 )
344 }
345
346 pub fn is_ninth(&self) -> bool {
349 self.unique_pitch_name_count() == 5
350 && self.third().is_some()
351 && self.fifth().is_some()
352 && self.seventh().is_some()
353 && self.chord_step(2).is_some()
354 }
355
356 pub fn is_transpositionally_symmetrical(&self, require_intervallic_evenness: bool) -> bool {
360 let Some((card, index, _)) = self.forte_address() else {
361 return self.notes.is_empty();
362 };
363 if card == 1 {
364 return require_intervallic_evenness;
365 }
366 const EVEN: [(u8, u8); 5] = [(2, 6), (3, 12), (4, 28), (6, 35), (12, 1)];
367 const UNEVEN: [(u8, u8); 10] = [
368 (4, 9),
369 (4, 25),
370 (6, 7),
371 (6, 20),
372 (6, 30),
373 (8, 9),
374 (8, 25),
375 (8, 28),
376 (9, 12),
377 (10, 6),
378 ];
379 EVEN.contains(&(card, index))
380 || (!require_intervallic_evenness && UNEVEN.contains(&(card, index)))
381 }
382
383 pub fn has_any_repeated_diatonic_note(&self) -> bool {
386 let steps = self
387 .pitch_refs()
388 .map(Pitch::step)
389 .collect::<std::collections::BTreeSet<_>>();
390 steps.len() != self.unique_pitch_name_count()
391 }
392
393 pub(super) fn is_augmented_sixth_of_type(
394 &self,
395 address: (u8, u8, i8),
396 required_inversion: u8,
397 permit_any_inversion: bool,
398 intervals: &[[Interval; 2]],
399 ) -> bool {
400 if self.forte_address() != Some(address) || self.has_any_enharmonic_spelled_pitches() {
401 return false;
402 }
403 if !permit_any_inversion && self.inversion() != Some(required_inversion) {
404 return false;
405 }
406 let Some(root) = self.root() else {
407 return false;
408 };
409 let steps = [self.third(), self.fifth(), self.seventh()];
410 intervals.iter().zip(steps).all(|(accepted, step)| {
411 step.and_then(|pitch| Interval::between_pitches(root, pitch).ok())
412 .is_some_and(|interval| {
413 accepted.iter().any(|candidate| {
414 candidate.directed_simple_key() == interval.directed_simple_key()
415 })
416 })
417 })
418 }
419
420 pub(super) fn has_intervals_above_root(&self, intervals: &[u8]) -> bool {
421 let Some(root_pitch) = self.find_root_pitch() else {
422 return false;
423 };
424 let root_pc = root::pitch_class(root_pitch);
425 let chord_pcs = self.pitch_class_set();
426 intervals
427 .iter()
428 .all(|interval| chord_pcs.contains(&((root_pc + interval) % 12)))
429 }
430
431 pub fn interval_from_chord_step(&self, step: u8) -> Option<Interval> {
436 let root = self.root()?;
437 self.pitch_refs()
438 .filter_map(|pitch| Interval::between_pitches(root, pitch).ok())
439 .find(|interval| interval.mod7() == IntegerType::from(step))
440 }
441}
442
443#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
446#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
447pub enum TriadQuality {
448 Major,
450 Minor,
452 Augmented,
454 Diminished,
456 Other,
458}
459
460impl TriadQuality {
461 pub fn as_str(self) -> &'static str {
463 match self {
464 Self::Major => "major",
465 Self::Minor => "minor",
466 Self::Augmented => "augmented",
467 Self::Diminished => "diminished",
468 Self::Other => "other",
469 }
470 }
471}
472
473impl Display for TriadQuality {
474 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
475 f.write_str(self.as_str())
476 }
477}
478
479pub(super) fn fold_chord_step(step: u8) -> u8 {
480 if step >= 8 { step - 7 } else { step }
481}
482
483pub(super) fn diatonic_steps_above(root: &Pitch, pitch: &Pitch) -> u8 {
484 ((root::step_num(pitch) - root::step_num(root)).rem_euclid(7) + 1) as u8
485}
486
487pub(super) fn semitones_above(root: &Pitch, pitch: &Pitch) -> u8 {
488 (root::pitch_class(pitch) + 12 - root::pitch_class(root)) % 12
489}
490
491pub(super) struct AugmentedSixthIntervals {
492 italian: [[Interval; 2]; 2],
493 french: [[Interval; 2]; 3],
494 german: [[Interval; 2]; 3],
495 swiss: [[Interval; 2]; 3],
496}
497
498pub(super) static AUGMENTED_SIXTHS: LazyLock<AugmentedSixthIntervals> = LazyLock::new(|| {
502 let pair = |up: &str, down: &str| {
503 [
504 Interval::from_name(up).expect("augmented sixth intervals parse"),
505 Interval::from_name(down).expect("augmented sixth intervals parse"),
506 ]
507 };
508 AugmentedSixthIntervals {
509 italian: [pair("d3", "A-6"), pair("d5", "A-4")],
510 french: [pair("M3", "m-6"), pair("d5", "A-4"), pair("m7", "M-2")],
511 german: [pair("d3", "A-6"), pair("d5", "A-4"), pair("d7", "A-2")],
512 swiss: [pair("m3", "M-6"), pair("dd5", "AA-4"), pair("d7", "A-2")],
513 }
514});