1use crate::core::{
4 base::{HasDescription, HasName, HasStaticName},
5 interval::{HasIntervals, Interval},
6 mode::Mode,
7 mode_kind::ModeKind,
8 modifier::Degree,
9 note::Note,
10 scale::Scale,
11 scale_kind::ScaleKind,
12};
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17pub trait HasRelativeScale {
21 fn relative_scale(&self) -> Vec<Interval>;
27}
28
29pub trait HasRelativeChord {
31 fn relative_chord(&self) -> Vec<Interval>;
37}
38
39pub trait HasScaleCandidates {
41 fn scale_candidates(&self) -> Vec<ScaleCandidate>;
44}
45
46#[derive(Debug, Clone, PartialEq)]
50#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
51pub enum ScaleCandidate {
52 Mode {
54 kind: ModeKind,
56 rank: u8,
58 reason: &'static str,
60 },
61 Scale {
63 kind: ScaleKind,
65 rank: u8,
67 reason: &'static str,
69 },
70}
71
72impl ScaleCandidate {
73 pub fn rank(&self) -> u8 {
75 match self {
76 ScaleCandidate::Mode { rank, .. } => *rank,
77 ScaleCandidate::Scale { rank, .. } => *rank,
78 }
79 }
80
81 pub fn reason(&self) -> &'static str {
83 match self {
84 ScaleCandidate::Mode { reason, .. } => reason,
85 ScaleCandidate::Scale { reason, .. } => reason,
86 }
87 }
88
89 pub fn name(&self) -> String {
91 match self {
92 ScaleCandidate::Mode { kind, .. } => kind.name(),
93 ScaleCandidate::Scale { kind, .. } => kind.name(),
94 }
95 }
96
97 pub fn description(&self) -> &'static str {
99 match self {
100 ScaleCandidate::Mode { kind, .. } => kind.description(),
101 ScaleCandidate::Scale { kind, .. } => kind.description(),
102 }
103 }
104
105 pub fn notes(&self, root: Note) -> Vec<Note> {
107 match self {
108 ScaleCandidate::Mode { kind, .. } => Mode::new(root, *kind).notes(),
109 ScaleCandidate::Scale { kind, .. } => Scale::new(root, *kind).notes(),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub enum IntervalCollectionKind {
118 Mode(ModeKind),
120 Scale(ScaleKind),
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct IntervalCandidate {
127 pub kind: IntervalCollectionKind,
129 pub rank: u8,
131 pub reason: &'static str,
133}
134
135impl IntervalCandidate {
136 pub fn to_scale_candidate(&self) -> ScaleCandidate {
138 match self.kind {
139 IntervalCollectionKind::Mode(kind) => ScaleCandidate::Mode {
140 kind,
141 rank: self.rank,
142 reason: self.reason,
143 },
144 IntervalCollectionKind::Scale(kind) => ScaleCandidate::Scale {
145 kind,
146 rank: self.rank,
147 reason: self.reason,
148 },
149 }
150 }
151}
152
153#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug, Ord, PartialOrd)]
157#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
158#[repr(u8)]
159pub enum KnownChord {
160 Unknown,
162 Major,
164 Minor,
166 Major7,
168 Dominant(Degree),
170 MinorMajor7,
172 MinorDominant(Degree),
174 DominantSharp11(Degree),
176 Augmented,
178 AugmentedMajor7,
180 AugmentedDominant(Degree),
182 HalfDiminished(Degree),
184 Diminished,
186 DominantFlat9(Degree),
188 DominantSharp9(Degree),
190 AugmentedDominantFlat9(Degree),
192 MinorDominantFlat13(Degree),
194 MinorDominantFlat9Flat13(Degree),
196 Sharp11,
198}
199
200impl HasDescription for KnownChord {
203 fn description(&self) -> &'static str {
204 match self {
205 KnownChord::Unknown => panic!("KnownChord::Unknown should never be used in description()"),
206 KnownChord::Major => "major",
207 KnownChord::Minor => "minor",
208 KnownChord::Major7 => "major 7",
209 KnownChord::Dominant(_) => "dominant",
210 KnownChord::MinorMajor7 => "minor major 7",
211 KnownChord::MinorDominant(_) => "minor 7",
212 KnownChord::DominantSharp11(_) => "dominant sharp 11",
213 KnownChord::Augmented => "augmented",
214 KnownChord::AugmentedMajor7 => "augmented major 7",
215 KnownChord::AugmentedDominant(_) => "augmented dominant",
216 KnownChord::HalfDiminished(_) => "half diminished",
217 KnownChord::Diminished => "diminished",
218 KnownChord::DominantFlat9(_) => "dominant flat 9",
219 KnownChord::DominantSharp9(_) => "dominant sharp 9",
220 KnownChord::AugmentedDominantFlat9(_) => "augmented dominant flat 9",
221 KnownChord::MinorDominantFlat13(_) => "minor dominant flat 13",
222 KnownChord::MinorDominantFlat9Flat13(_) => "minor dominant flat 9 flat 13",
223 KnownChord::Sharp11 => "sharp 11",
224 }
225 }
226}
227
228impl HasRelativeScale for KnownChord {
229 fn relative_scale(&self) -> Vec<Interval> {
230 match self {
231 KnownChord::Unknown => panic!("KnownChord::Unknown should never be used in relative_scale()"),
232 KnownChord::Major => vec![
233 Interval::PerfectUnison,
234 Interval::MajorSecond,
235 Interval::MajorThird,
236 Interval::PerfectFourth,
237 Interval::PerfectFifth,
238 Interval::MajorSixth,
239 Interval::MajorSeventh,
240 ],
241 KnownChord::Minor => vec![
242 Interval::PerfectUnison,
243 Interval::MajorSecond,
244 Interval::MinorThird,
245 Interval::PerfectFourth,
246 Interval::PerfectFifth,
247 Interval::MinorSixth,
248 Interval::MinorSeventh,
249 ],
250 KnownChord::Major7 => vec![
251 Interval::PerfectUnison,
252 Interval::MajorSecond,
253 Interval::MajorThird,
254 Interval::PerfectFourth,
255 Interval::PerfectFifth,
256 Interval::MajorSixth,
257 Interval::MajorSeventh,
258 ],
259 KnownChord::Dominant(_) => vec![
260 Interval::PerfectUnison,
261 Interval::MajorSecond,
262 Interval::MajorThird,
263 Interval::PerfectFourth,
264 Interval::PerfectFifth,
265 Interval::MajorSixth,
266 Interval::MinorSeventh,
267 ],
268 KnownChord::MinorMajor7 => vec![
269 Interval::PerfectUnison,
270 Interval::MajorSecond,
271 Interval::MinorThird,
272 Interval::PerfectFourth,
273 Interval::PerfectFifth,
274 Interval::MajorSixth,
275 Interval::MajorSeventh,
276 ],
277 KnownChord::MinorDominant(_) => vec![
278 Interval::PerfectUnison,
279 Interval::MajorSecond,
280 Interval::MinorThird,
281 Interval::PerfectFourth,
282 Interval::PerfectFifth,
283 Interval::MajorSixth,
284 Interval::MinorSeventh,
285 ],
286 KnownChord::DominantSharp11(_) => vec![
287 Interval::PerfectUnison,
288 Interval::MajorSecond,
289 Interval::MajorThird,
290 Interval::AugmentedFourth,
291 Interval::PerfectFifth,
292 Interval::MajorSixth,
293 Interval::MinorSeventh,
294 ],
295 KnownChord::Augmented => vec![
296 Interval::PerfectUnison,
297 Interval::MajorSecond,
298 Interval::MajorThird,
299 Interval::PerfectFourth,
300 Interval::AugmentedFifth,
301 Interval::MajorSixth,
302 Interval::MajorSeventh,
303 ],
304 KnownChord::AugmentedMajor7 => vec![
305 Interval::PerfectUnison,
306 Interval::MajorSecond,
307 Interval::MajorThird,
308 Interval::AugmentedFourth,
309 Interval::AugmentedFifth,
310 Interval::MajorSixth,
311 Interval::MajorSeventh,
312 ],
313 KnownChord::AugmentedDominant(_) => vec![
314 Interval::PerfectUnison,
315 Interval::MajorSecond,
316 Interval::MajorThird,
317 Interval::AugmentedFourth,
318 Interval::AugmentedFifth,
319 Interval::AugmentedSixth,
320 ],
321 KnownChord::HalfDiminished(_) => vec![
322 Interval::PerfectUnison,
323 Interval::MinorSecond,
324 Interval::MinorThird,
325 Interval::PerfectFourth,
326 Interval::DiminishedFifth,
327 Interval::MinorSixth,
328 Interval::MinorSeventh,
329 ],
330 KnownChord::Diminished => vec![
331 Interval::PerfectUnison,
332 Interval::MajorSecond,
333 Interval::MinorThird,
334 Interval::PerfectFourth,
335 Interval::DiminishedFifth,
336 Interval::MinorSixth,
337 Interval::DiminishedSeventh,
338 Interval::MajorSeventh,
339 ],
340 KnownChord::DominantFlat9(_) => vec![
341 Interval::PerfectUnison,
342 Interval::MinorSecond,
343 Interval::MinorThird,
344 Interval::MajorThird,
345 Interval::AugmentedFourth,
346 Interval::PerfectFifth,
347 Interval::MajorSixth,
348 Interval::MinorSeventh,
349 ],
350 KnownChord::DominantSharp9(_) => vec![
351 Interval::PerfectUnison,
352 Interval::MinorSecond,
353 Interval::MinorThird,
354 Interval::DiminishedFourth,
355 Interval::DiminishedFifth,
356 Interval::MinorSixth,
357 Interval::MinorSeventh,
358 ],
359 KnownChord::AugmentedDominantFlat9(_) => vec![
360 Interval::PerfectUnison,
361 Interval::MinorSecond,
362 Interval::MinorThird,
363 Interval::MajorThird,
364 Interval::AugmentedFourth,
365 Interval::AugmentedFifth,
366 Interval::MinorSeventh,
367 ],
368 KnownChord::MinorDominantFlat13(_) => vec![
369 Interval::PerfectUnison,
370 Interval::MajorSecond,
371 Interval::MinorThird,
372 Interval::PerfectFourth,
373 Interval::PerfectFifth,
374 Interval::MinorSixth,
375 Interval::MinorSeventh,
376 ],
377 KnownChord::MinorDominantFlat9Flat13(_) => vec![
378 Interval::PerfectUnison,
379 Interval::MinorSecond,
380 Interval::MinorThird,
381 Interval::PerfectFourth,
382 Interval::PerfectFifth,
383 Interval::MinorSixth,
384 Interval::MinorSeventh,
385 ],
386 KnownChord::Sharp11 => vec![
387 Interval::PerfectUnison,
388 Interval::MajorSecond,
389 Interval::MajorThird,
390 Interval::AugmentedFourth,
391 Interval::PerfectFifth,
392 Interval::MajorSixth,
393 Interval::MajorSeventh,
394 ],
395 }
396 }
397}
398
399impl HasRelativeChord for KnownChord {
400 fn relative_chord(&self) -> Vec<Interval> {
401 self.intervals().to_vec()
402 }
403}
404
405impl HasName for KnownChord {
406 fn name(&self) -> String {
407 match self {
408 KnownChord::Unknown => panic!("KnownChord::Unknown should never be used in name()"),
409 KnownChord::Major => "".to_owned(),
410 KnownChord::Minor => "m".to_owned(),
411 KnownChord::Major7 => "maj7".to_owned(),
412 KnownChord::Dominant(d) => d.static_name().to_owned(),
413 KnownChord::MinorMajor7 => "m(maj7)".to_owned(),
414 KnownChord::MinorDominant(d) => format!("m{}", d.static_name()),
415 KnownChord::DominantSharp11(d) => format!("{}(♯11)", d.static_name()),
416 KnownChord::Augmented => "+".to_owned(),
417 KnownChord::AugmentedMajor7 => "+(maj7)".to_owned(),
418 KnownChord::AugmentedDominant(d) => format!("+{}", d.static_name()),
419 KnownChord::HalfDiminished(d) => format!("m{}(♭5)", d.static_name()),
420 KnownChord::Diminished => "dim".to_owned(),
421 KnownChord::DominantFlat9(d) => format!("{}(♭9)", d.static_name()),
422 KnownChord::DominantSharp9(d) => format!("{}(♯9)", d.static_name()),
423 KnownChord::AugmentedDominantFlat9(d) => format!("+{}(♭9)", d.static_name()),
424 KnownChord::MinorDominantFlat13(d) => format!("m{}(♭13)", d.static_name()),
425 KnownChord::MinorDominantFlat9Flat13(d) => format!("{}(♭9)(♭13)", d.static_name()),
426 KnownChord::Sharp11 => "(♯11)".to_owned(),
427 }
428 }
429}
430
431impl KnownChord {
432 pub fn scale_interval_candidates(&self) -> &'static [IntervalCandidate] {
434 match self {
435 KnownChord::Unknown => &[],
436 KnownChord::Major => MAJOR_CANDIDATES,
437 KnownChord::Minor => MINOR_CANDIDATES,
438 KnownChord::Major7 => MAJOR7_CANDIDATES,
439 KnownChord::Dominant(_) => DOMINANT_CANDIDATES,
440 KnownChord::MinorMajor7 => MINOR_MAJOR7_CANDIDATES,
441 KnownChord::MinorDominant(_) => MINOR_DOMINANT_CANDIDATES,
442 KnownChord::DominantSharp11(_) => DOMINANT_SHARP11_CANDIDATES,
443 KnownChord::Augmented => AUGMENTED_CANDIDATES,
444 KnownChord::AugmentedMajor7 => AUGMENTED_MAJOR7_CANDIDATES,
445 KnownChord::AugmentedDominant(_) => AUGMENTED_DOMINANT_CANDIDATES,
446 KnownChord::HalfDiminished(_) => HALF_DIMINISHED_CANDIDATES,
447 KnownChord::Diminished => DIMINISHED_CANDIDATES,
448 KnownChord::DominantFlat9(_) => DOMINANT_FLAT9_CANDIDATES,
449 KnownChord::DominantSharp9(_) => DOMINANT_SHARP9_CANDIDATES,
450 KnownChord::AugmentedDominantFlat9(_) => AUGMENTED_DOMINANT_FLAT9_CANDIDATES,
451 KnownChord::MinorDominantFlat13(_) => MINOR_DOMINANT_FLAT13_CANDIDATES,
452 KnownChord::MinorDominantFlat9Flat13(_) => MINOR_DOMINANT_FLAT9_FLAT13_CANDIDATES,
453 KnownChord::Sharp11 => SHARP11_CANDIDATES,
454 }
455 }
456}
457
458impl HasScaleCandidates for KnownChord {
459 fn scale_candidates(&self) -> Vec<ScaleCandidate> {
460 self.scale_interval_candidates().iter().map(IntervalCandidate::to_scale_candidate).collect()
461 }
462}
463
464impl HasIntervals for KnownChord {
465 fn intervals(&self) -> &'static [Interval] {
466 match self {
467 KnownChord::Unknown => &[],
468 KnownChord::Major => MAJOR_INTERVALS,
469 KnownChord::Minor => MINOR_INTERVALS,
470 KnownChord::Major7 => MAJOR7_INTERVALS,
471 KnownChord::Dominant(_) => DOMINANT_INTERVALS,
472 KnownChord::MinorMajor7 => MINOR_MAJOR7_INTERVALS,
473 KnownChord::MinorDominant(_) => MINOR_DOMINANT_INTERVALS,
474 KnownChord::DominantSharp11(_) => DOMINANT_SHARP11_INTERVALS,
475 KnownChord::Augmented => AUGMENTED_INTERVALS,
476 KnownChord::AugmentedMajor7 => AUGMENTED_MAJOR7_INTERVALS,
477 KnownChord::AugmentedDominant(_) => AUGMENTED_DOMINANT_INTERVALS,
478 KnownChord::AugmentedDominantFlat9(_) => AUGMENTED_DOMINANT_FLAT9_INTERVALS,
479 KnownChord::HalfDiminished(_) => HALF_DIMINISHED_INTERVALS,
480 KnownChord::Diminished => DIMINISHED_INTERVALS,
481 KnownChord::DominantFlat9(_) => DOMINANT_FLAT9_INTERVALS,
482 KnownChord::DominantSharp9(_) => DOMINANT_SHARP9_INTERVALS,
483 KnownChord::MinorDominantFlat13(_) => MINOR_DOMINANT_FLAT13_INTERVALS,
484 KnownChord::MinorDominantFlat9Flat13(_) => MINOR_DOMINANT_FLAT9_FLAT13_INTERVALS,
485 KnownChord::Sharp11 => SHARP11_INTERVALS,
486 }
487 }
488}
489
490static MAJOR_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth];
493static MINOR_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth];
494static MAJOR7_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MajorSeventh];
495static DOMINANT_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MinorSeventh];
496static MINOR_MAJOR7_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth, Interval::MajorSeventh];
497static MINOR_DOMINANT_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth, Interval::MinorSeventh];
498static DOMINANT_SHARP11_INTERVALS: &[Interval] = &[
499 Interval::PerfectUnison,
500 Interval::MajorThird,
501 Interval::PerfectFifth,
502 Interval::MinorSeventh,
503 Interval::AugmentedEleventh,
504];
505static AUGMENTED_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth];
506static AUGMENTED_MAJOR7_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth, Interval::MajorSeventh];
507static AUGMENTED_DOMINANT_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth, Interval::MinorSeventh];
508static AUGMENTED_DOMINANT_FLAT9_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth, Interval::MinorSeventh, Interval::MinorNinth];
509static HALF_DIMINISHED_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::DiminishedFifth, Interval::MinorSeventh];
510static DIMINISHED_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::DiminishedFifth, Interval::DiminishedSeventh];
511static DOMINANT_FLAT9_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MinorSeventh, Interval::MinorNinth];
512static DOMINANT_SHARP9_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MinorSeventh, Interval::AugmentedNinth];
513static MINOR_DOMINANT_FLAT13_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth, Interval::MinorSeventh, Interval::MinorThirteenth];
514static MINOR_DOMINANT_FLAT9_FLAT13_INTERVALS: &[Interval] = &[
515 Interval::PerfectUnison,
516 Interval::MinorThird,
517 Interval::PerfectFifth,
518 Interval::MinorSeventh,
519 Interval::MinorNinth,
520 Interval::MinorThirteenth,
521];
522static SHARP11_INTERVALS: &[Interval] = &[
523 Interval::PerfectUnison,
524 Interval::MajorThird,
525 Interval::PerfectFifth,
526 Interval::MajorSeventh,
527 Interval::AugmentedEleventh,
528];
529
530static MAJOR_CANDIDATES: &[IntervalCandidate] = &[
533 IntervalCandidate {
534 kind: IntervalCollectionKind::Mode(ModeKind::Ionian),
535 rank: 1,
536 reason: "Default diatonic major scale for functional harmony in tonal contexts; works over I, IV, V progressions; avoid dwelling on the 4th scale degree as it can clash with the major 3rd in the triad",
537 },
538 IntervalCandidate {
539 kind: IntervalCollectionKind::Scale(ScaleKind::MajorPentatonic),
540 rank: 2,
541 reason: "Safe major melody that avoids the 4th and 7th scale degrees completely; eliminates avoid-note concerns; perfect for pop, rock, and country hooks where you want guaranteed consonance",
542 },
543 IntervalCandidate {
544 kind: IntervalCollectionKind::Mode(ModeKind::Lydian),
545 rank: 3,
546 reason: "Bright major sound with ♯11 sheen; major with ♯4 (4th mode of major scale); common in modern jazz and film scoring; lean into the ♯4/♯11 as the characteristic color tone that distinguishes it from Ionian",
547 },
548 IntervalCandidate {
549 kind: IntervalCollectionKind::Mode(ModeKind::Mixolydian),
550 rank: 4,
551 reason: "Adds ♭7 over a major triad for blues, rock, and modal flavor; creates a dominant-like quality without needing to resolve; stylistic choice for a looser, groovier feel",
552 },
553];
554
555static MINOR_CANDIDATES: &[IntervalCandidate] = &[
556 IntervalCandidate {
557 kind: IntervalCollectionKind::Mode(ModeKind::Aeolian),
558 rank: 1,
559 reason: "Default natural minor scale for functional harmony; 6th mode of major scale; provides the classic melancholic, sad tonality with ♭6; contains ♭3, ♭6, and ♭7 for full minor character",
560 },
561 IntervalCandidate {
562 kind: IntervalCollectionKind::Scale(ScaleKind::MinorPentatonic),
563 rank: 2,
564 reason: "Safe minor melody that avoids the ♭2 and ♭6 scale degrees; the go-to scale for blues, rock, and pentatonic-based improvisation; simple and effective with no avoid-note concerns",
565 },
566 IntervalCandidate {
567 kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
568 rank: 3,
569 reason: "Essential blues vocabulary built on minor pentatonic with added ♯4 blue note; phrasing-driven rather than theoretical; creates that characteristic blues 'bend' and expressive quality",
570 },
571 IntervalCandidate {
572 kind: IntervalCollectionKind::Mode(ModeKind::Dorian),
573 rank: 4,
574 reason: "Minor mode with raised 6th (♮6) that provides a brighter, jazzier lift compared to Aeolian; 2nd mode of major scale; less sad than natural minor; common choice for jazz and funk minor sounds",
575 },
576 IntervalCandidate {
577 kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
578 rank: 5,
579 reason: "Minor mode with a lowered 2nd (♭2) that creates an exotic, Spanish, or Flamenco flavor; 3rd mode of major scale; dark and modal with a distinctly non-functional character",
580 },
581 IntervalCandidate {
582 kind: IntervalCollectionKind::Scale(ScaleKind::HarmonicMinor),
583 rank: 6,
584 reason: "Classical minor scale with raised 7th (♮7) for strong V-i resolution; the ♭6 creates tension and augmented-second interval; essential for traditional minor key harmony",
585 },
586 IntervalCandidate {
587 kind: IntervalCollectionKind::Scale(ScaleKind::MelodicMinor),
588 rank: 7,
589 reason: "Modern jazz minor with both ♮6 and ♮7 for smooth ascending melodic motion; eliminates the augmented second of harmonic minor; bright and sophisticated minor color",
590 },
591];
592
593static MAJOR7_CANDIDATES: &[IntervalCandidate] = &[
594 IntervalCandidate {
595 kind: IntervalCollectionKind::Mode(ModeKind::Ionian),
596 rank: 1,
597 reason: "Default major 7 scale from functional harmony; Ionian mode provides the natural maj7 chord tones; watch out for the 4th scale degree which can clash as an avoid-note over the maj7 chord",
598 },
599 IntervalCandidate {
600 kind: IntervalCollectionKind::Mode(ModeKind::Lydian),
601 rank: 2,
602 reason: "Bright major 7 sound with ♯11 sheen; major 7 with ♯4 (4th mode of major scale); the modern jazz choice for maj7 chords; the ♯4/♯11 is the color tone that makes this sparkle; eliminates the avoid-note issue of the natural 4th",
603 },
604 IntervalCandidate {
605 kind: IntervalCollectionKind::Scale(ScaleKind::MajorPentatonic),
606 rank: 3,
607 reason: "Safe major 7 melody that completely avoids both the 4th and 7th scale degrees; provides consonant, hook-friendly melodic material with zero avoid-note concerns",
608 },
609];
610
611static DOMINANT_CANDIDATES: &[IntervalCandidate] = &[
612 IntervalCandidate {
613 kind: IntervalCollectionKind::Mode(ModeKind::Mixolydian),
614 rank: 1,
615 reason: "Default dominant 7 scale; 5th mode of major scale; provides major 3rd with ♭7 for functional V chord resolution; the bread-and-butter choice for tonal dominant chords in ii-V-I progressions",
616 },
617 IntervalCandidate {
618 kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
619 rank: 2,
620 reason: "Blues vocabulary over dominant 7 chords; includes the ♯4 blue note for characteristic blues phrasing; phrasing-driven rather than theoretical; perfect for blues turnarounds and rock contexts",
621 },
622 IntervalCandidate {
623 kind: IntervalCollectionKind::Mode(ModeKind::LydianDominant),
624 rank: 3,
625 reason: "Dominant 7 with ♯11 color (Mixolydian with ♯4); 4th mode of melodic minor; sophisticated modern jazz sound; use when you want bright ♯11 upper-structure tension on your V chord; also called Acoustic scale",
626 },
627 IntervalCandidate {
628 kind: IntervalCollectionKind::Mode(ModeKind::MixolydianFlat6),
629 rank: 4,
630 reason: "Dominant scale with ♭13 (♭6) for darker tension; 5th mode of melodic minor; creates a minor-leaning dominant sound; useful when the V chord needs to lean toward a minor resolution",
631 },
632 IntervalCandidate {
633 kind: IntervalCollectionKind::Scale(ScaleKind::WholeTone),
634 rank: 5,
635 reason: "Symmetrical whole tone scale for augmented dominant (#5) color; creates dreamy, floating, harmonically ambiguous sound; every note is a whole step apart; use for suspended, unresolved V7#5 or V+7 sounds",
636 },
637 IntervalCandidate {
638 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
639 rank: 6,
640 reason: "Classic dominant diminished (half-whole octatonic); supports ♭9, ♯9, ♯11, and 13 simultaneously; use when the V chord feels 'hot' and needs maximum upper-structure tension; distinct from Altered mode",
641 },
642 IntervalCandidate {
643 kind: IntervalCollectionKind::Mode(ModeKind::Altered),
644 rank: 7,
645 reason: "The ultimate altered dominant tool (7th mode of melodic minor); provides maximum tension with ♭9, ♯9, ♭5, and ♯5 (♭13) all available; strongest when resolving to minor i or in modern jazz contexts needing maximum harmonic pull",
646 },
647];
648
649static MINOR_MAJOR7_CANDIDATES: &[IntervalCandidate] = &[
650 IntervalCandidate {
651 kind: IntervalCollectionKind::Scale(ScaleKind::MelodicMinor),
652 rank: 1,
653 reason: "Primary source scale for minor-major 7 chords; melodic minor provides the ♮6 and ♮7 needed for this chord quality; creates smooth ascending melodic lines in classical and jazz contexts",
654 },
655 IntervalCandidate {
656 kind: IntervalCollectionKind::Scale(ScaleKind::HarmonicMinor),
657 rank: 2,
658 reason: "Alternative source for mMaj7 with a more exotic flavor; harmonic minor provides the ♮7 but retains the ♭6 creating characteristic tension and the augmented second interval between ♭6 and ♮7",
659 },
660];
661
662static MINOR_DOMINANT_CANDIDATES: &[IntervalCandidate] = &[
663 IntervalCandidate {
664 kind: IntervalCollectionKind::Mode(ModeKind::Dorian),
665 rank: 1,
666 reason: "Default minor 7 scale with raised 6th (♮6) that lifts and brightens the sound; 2nd mode of major scale; the classic jazz ii chord sound in ii-V-I progressions; more optimistic than Aeolian",
667 },
668 IntervalCandidate {
669 kind: IntervalCollectionKind::Scale(ScaleKind::MinorPentatonic),
670 rank: 2,
671 reason: "Safe minor 7 melody that avoids the ♭2 and ♭6 scale degrees for guaranteed consonance; simple and effective choice; the go-to for straightforward, bluesy minor 7 improvisation",
672 },
673 IntervalCandidate {
674 kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
675 rank: 3,
676 reason: "Blues vocabulary over minor 7 chords with the characteristic ♯4 blue note; stylistic phrasing choice for blues, rock, and R&B contexts; phrasing-driven rather than purely harmonic",
677 },
678 IntervalCandidate {
679 kind: IntervalCollectionKind::Mode(ModeKind::Aeolian),
680 rank: 4,
681 reason: "Natural minor over m7 chords; includes the ♭6 for darker, more melancholic color compared to Dorian; functional and traditional; creates a more classically minor feel",
682 },
683 IntervalCandidate {
684 kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
685 rank: 5,
686 reason: "Modal minor 7 with lowered 2nd (♭2) for exotic, Spanish, or Flamenco flavor; 3rd mode of major scale; creates a distinctly modal, non-functional character; dark and mysterious",
687 },
688];
689
690static DOMINANT_SHARP11_CANDIDATES: &[IntervalCandidate] = &[
691 IntervalCandidate {
692 kind: IntervalCollectionKind::Mode(ModeKind::LydianDominant),
693 rank: 1,
694 reason: "The defining scale for V7♯11 chords; dominant 7 with ♯11 (Mixolydian with ♯4); 4th mode of melodic minor; sophisticated modern jazz sound for dominant chords with upper-structure tension; also called Acoustic scale",
695 },
696 IntervalCandidate {
697 kind: IntervalCollectionKind::Mode(ModeKind::Mixolydian),
698 rank: 2,
699 reason: "Basic dominant fallback that de-emphasizes the ♯11 extension; provides functional V7 sound (dominant 7 with ♭7); when you want simpler, more traditional dominant approach without the ♯11 color",
700 },
701 IntervalCandidate {
702 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
703 rank: 3,
704 reason: "Adds even more tension options on top of the V7♯11; half-whole diminished (dominant diminished) provides ♭9, ♯9, ♯11, and 13 simultaneously; use for hot, dense dominant sound with maximum upper-structure complexity",
705 },
706];
707
708static AUGMENTED_CANDIDATES: &[IntervalCandidate] = &[
709 IntervalCandidate {
710 kind: IntervalCollectionKind::Mode(ModeKind::IonianSharp5),
711 rank: 1,
712 reason: "Major scale with raised 5th (♯5); 3rd mode of harmonic minor; provides a functional augmented triad sound while maintaining major scale characteristics; the classic augmented triad source",
713 },
714 IntervalCandidate {
715 kind: IntervalCollectionKind::Scale(ScaleKind::WholeTone),
716 rank: 2,
717 reason: "Symmetrical whole tone scale where every augmented triad shares the same notes; creates a dreamy, floating, harmonically ambiguous quality; all notes are whole steps apart; perfect for suspended augmented sounds",
718 },
719 IntervalCandidate {
720 kind: IntervalCollectionKind::Mode(ModeKind::LydianAugmented),
721 rank: 3,
722 reason: "Major scale with both ♯4 and ♯5; 3rd mode of melodic minor; provides a bright, exotic augmented color with Lydian characteristics; sophisticated choice for modern jazz augmented triads",
723 },
724];
725
726static AUGMENTED_MAJOR7_CANDIDATES: &[IntervalCandidate] = &[
727 IntervalCandidate {
728 kind: IntervalCollectionKind::Mode(ModeKind::LydianAugmented),
729 rank: 1,
730 reason: "3rd mode of melodic minor; provides major 7 with both ♯4/♯11 and ♯5; bright, exotic, and sophisticated augmented major 7 sound; the modern jazz choice for this chord quality",
731 },
732 IntervalCandidate {
733 kind: IntervalCollectionKind::Mode(ModeKind::IonianSharp5),
734 rank: 2,
735 reason: "Major 7 with raised 5th from harmonic minor; more functional and traditional approach to augmented major 7; retains natural 4th unlike Lydian Augmented",
736 },
737];
738
739static AUGMENTED_DOMINANT_CANDIDATES: &[IntervalCandidate] = &[
740 IntervalCandidate {
741 kind: IntervalCollectionKind::Scale(ScaleKind::WholeTone),
742 rank: 1,
743 reason: "Primary scale for augmented dominant 7 (V+7 or V7♯5) chords; symmetrical whole tone scale naturally provides the ♯5 augmented quality; creates dreamy, floating dominant color; perfect for unresolved, suspended V+7 sounds",
744 },
745 IntervalCandidate {
746 kind: IntervalCollectionKind::Mode(ModeKind::LydianDominant),
747 rank: 2,
748 reason: "Dominant 7 with ♯11 (Mixolydian with ♯4) that can accommodate or bend toward ♯5; flexible modern dominant sound from melodic minor; use when you want V7 character with option to lean into augmented implications",
749 },
750];
751
752static HALF_DIMINISHED_CANDIDATES: &[IntervalCandidate] = &[
753 IntervalCandidate {
754 kind: IntervalCollectionKind::Mode(ModeKind::Locrian),
755 rank: 1,
756 reason: "Default half-diminished (m7♭5) scale; 7th mode of major scale; provides the functional ii°7 sound in minor key ii°-V-i progressions; classic diminished 5th with minor 7th",
757 },
758 IntervalCandidate {
759 kind: IntervalCollectionKind::Mode(ModeKind::LocrianNatural2),
760 rank: 2,
761 reason: "Half-diminished with raised 2nd (♮2) for smoother melodic motion; 6th mode of melodic minor; eliminates the difficult ♭2 interval; modern jazz choice for m7♭5 chords",
762 },
763 IntervalCandidate {
764 kind: IntervalCollectionKind::Mode(ModeKind::LocrianNatural6),
765 rank: 3,
766 reason: "Half-diminished with raised 6th (♮6 or ♮13) for a brighter color; 2nd mode of harmonic minor; provides major 6th/13th extension while maintaining the ♭5; exotic flavor",
767 },
768];
769
770static DIMINISHED_CANDIDATES: &[IntervalCandidate] = &[
771 IntervalCandidate {
772 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedWholeHalf),
773 rank: 1,
774 reason: "Symmetrical diminished 7 scale with whole-half step pattern; primary choice for fully diminished 7 chords; every diminished 7 chord shares three others a minor third apart in this scale",
775 },
776 IntervalCandidate {
777 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
778 rank: 2,
779 reason: "Alternative diminished scale with half-whole step pattern; more commonly used for dominant 7 chords but can work as a passing chord option for dim7; provides different color than W-H pattern",
780 },
781];
782
783static DOMINANT_FLAT9_CANDIDATES: &[IntervalCandidate] = &[
784 IntervalCandidate {
785 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
786 rank: 1,
787 reason: "Primary scale for V7♭9 chords using symmetrical half-whole diminished (dominant diminished) pattern; provides ♭9, ♯9, ♯11, and 13 simultaneously; rich dominant tension palette for modern jazz and classical harmony",
788 },
789 IntervalCandidate {
790 kind: IntervalCollectionKind::Mode(ModeKind::PhrygianDominant),
791 rank: 2,
792 reason: "Phrygian with major 3rd creates Spanish or Phrygian dominant sound; 5th mode of harmonic minor; characteristic ♭2/♭9 with major 3rd fingerprint; exotic, Middle Eastern, or Flamenco flavor; strong V in minor keys",
793 },
794];
795
796static DOMINANT_SHARP9_CANDIDATES: &[IntervalCandidate] = &[
797 IntervalCandidate {
798 kind: IntervalCollectionKind::Mode(ModeKind::Altered),
799 rank: 1,
800 reason: "The altered dominant scale (7th mode of melodic minor); provides all alterations including ♭9, ♯9, ♭5, and ♯5 (♭13); maximum tension with strongest resolution pull; the ultimate V7♯9 choice for modern jazz and chromatic harmony",
801 },
802 IntervalCandidate {
803 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
804 rank: 2,
805 reason: "Symmetrical half-whole diminished (dominant diminished) providing ♭9, ♯9, ♯11, and 13; comprehensive dominant tension palette; hot V7 sound with dense upper-structure options; works when you need maximum harmonic complexity",
806 },
807 IntervalCandidate {
808 kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
809 rank: 3,
810 reason: "Blues vocabulary that naturally includes the ♯9 sound through the ♯4 blue note; phrasing-driven stylistic choice rather than theoretical; perfect for blues and rock contexts where ♯9 is part of the blues language",
811 },
812];
813
814static AUGMENTED_DOMINANT_FLAT9_CANDIDATES: &[IntervalCandidate] = &[
815 IntervalCandidate {
816 kind: IntervalCollectionKind::Mode(ModeKind::Altered),
817 rank: 1,
818 reason: "Primary scale for V7(♭9)(♯5) chords; the altered dominant scale (7th mode of melodic minor) provides all necessary alterations including ♭9, ♯5/♭13, and ♭5; maximum tension with both flat 9 and sharp 5 present; creates the strongest resolution pull in modern jazz and chromatic harmony",
819 },
820 IntervalCandidate {
821 kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
822 rank: 2,
823 reason: "Symmetrical half-whole diminished (dominant diminished) that supports V7(♭9)(♯5); provides ♭9, ♯9, ♯11, and 13 simultaneously; rich dominant tension palette; works well when you need the ♭9 with option to imply or approach the ♯5",
824 },
825 IntervalCandidate {
826 kind: IntervalCollectionKind::Mode(ModeKind::PhrygianDominant),
827 rank: 3,
828 reason: "Spanish or Phrygian dominant sound (5th mode of harmonic minor) emphasizing the characteristic ♭9; exotic, Middle Eastern, or Flamenco flavor; while it has natural 5th, it can approach or bend toward ♯5 in phrasing; strong V in minor keys",
829 },
830];
831
832static MINOR_DOMINANT_FLAT13_CANDIDATES: &[IntervalCandidate] = &[
833 IntervalCandidate {
834 kind: IntervalCollectionKind::Mode(ModeKind::Aeolian),
835 rank: 1,
836 reason:
837 "Natural minor scale over minor 7 chords; provides the ♭6/♭13 extension naturally; functional sound for minor dominant chords where you need the flat 13 characteristic clearly present",
838 },
839 IntervalCandidate {
840 kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
841 rank: 2,
842 reason: "Modal minor 7 with both ♭2 and ♭6/♭13; 3rd mode of major scale; creates an exotic, darker minor dominant with Spanish or modal flavor; both characteristic tensions present",
843 },
844];
845
846static MINOR_DOMINANT_FLAT9_FLAT13_CANDIDATES: &[IntervalCandidate] = &[
847 IntervalCandidate {
848 kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
849 rank: 1,
850 reason: "Phrygian mode provides both characteristic tensions: ♭2/♭9 and ♭6/♭13; 3rd mode of major scale; creates exotic, modal, Spanish or Flamenco flavor; dark and mysterious minor dominant sound",
851 },
852 IntervalCandidate {
853 kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
854 rank: 2,
855 reason: "Blues scale offers simpler, more phrasing-driven vocabulary; stylistic choice rather than literal interval match; use when you want blues vocabulary over this complex minor dominant chord symbol",
856 },
857];
858
859static SHARP11_CANDIDATES: &[IntervalCandidate] = &[
860 IntervalCandidate {
861 kind: IntervalCollectionKind::Mode(ModeKind::Lydian),
862 rank: 1,
863 reason: "Major 7 with ♯11 extension; major with ♯4 (4th mode of major scale); the bright, modern sound where ♯4/♯11 is the defining color tone; sophisticated jazz and film score choice; eliminates the avoid-note issue of natural 4th",
864 },
865 IntervalCandidate {
866 kind: IntervalCollectionKind::Mode(ModeKind::Ionian),
867 rank: 2,
868 reason: "Major 7 fallback that de-emphasizes the ♯11 extension; returns to functional harmony; use when you want to downplay the ♯11 color or need a simpler, more traditional major 7 approach",
869 },
870 IntervalCandidate {
871 kind: IntervalCollectionKind::Scale(ScaleKind::MajorPentatonic),
872 rank: 3,
873 reason: "Safe major 7 melody that avoids both the 4th and 7th scale degrees; provides consonant melodic material with zero avoid-note concerns; perfect for hooks and melodic lines over maj7♯11 chords",
874 },
875];
876
877#[cfg(test)]
880mod tests {
881 use super::*;
882 use crate::core::modifier::Degree;
883 use pretty_assertions::assert_eq;
884
885 #[test]
886 fn test_dominant_chord_candidates() {
887 let candidates = KnownChord::Dominant(Degree::Seven).scale_candidates();
888 assert!(!candidates.is_empty(), "G7 should have scale candidates");
889
890 match &candidates[0] {
891 ScaleCandidate::Mode { kind, rank, .. } => {
892 assert_eq!(*kind, ModeKind::Mixolydian);
893 assert_eq!(*rank, 1);
894 }
895 _ => panic!("First candidate for G7 should be a Mode"),
896 }
897
898 match &candidates[1] {
899 ScaleCandidate::Scale { kind, rank, .. } => {
900 assert_eq!(*kind, ScaleKind::Blues);
901 assert_eq!(*rank, 2);
902 }
903 _ => panic!("Second candidate for G7 should be a Scale"),
904 }
905
906 match &candidates[2] {
907 ScaleCandidate::Mode { kind, rank, .. } => {
908 assert_eq!(*kind, ModeKind::LydianDominant);
909 assert_eq!(*rank, 3);
910 }
911 _ => panic!("Third candidate for G7 should be a Mode"),
912 }
913 }
914
915 #[test]
916 fn test_dominant_sharp11_candidates() {
917 let candidates = KnownChord::DominantSharp11(Degree::Seven).scale_candidates();
918 assert!(!candidates.is_empty(), "G7#11 should have scale candidates");
919
920 match &candidates[0] {
921 ScaleCandidate::Mode { kind, rank, .. } => {
922 assert_eq!(*kind, ModeKind::LydianDominant);
923 assert_eq!(*rank, 1);
924 }
925 _ => panic!("First candidate for G7#11 should be a Mode"),
926 }
927 }
928
929 #[test]
930 fn test_dominant_flat9_candidates() {
931 let candidates = KnownChord::DominantFlat9(Degree::Seven).scale_candidates();
932 assert!(!candidates.is_empty(), "G7b9 should have scale candidates");
933
934 match &candidates[0] {
935 ScaleCandidate::Scale { kind, rank, .. } => {
936 assert_eq!(*kind, ScaleKind::DiminishedHalfWhole);
937 assert_eq!(*rank, 1);
938 }
939 ScaleCandidate::Mode { kind, rank, .. } => {
940 assert_eq!(*kind, ModeKind::PhrygianDominant);
941 assert_eq!(*rank, 1);
942 }
943 }
944 }
945
946 #[test]
947 fn test_dominant_sharp9_candidates() {
948 let candidates = KnownChord::DominantSharp9(Degree::Seven).scale_candidates();
949 assert!(!candidates.is_empty(), "G7#9 should have scale candidates");
950
951 match &candidates[0] {
952 ScaleCandidate::Mode { kind, rank, .. } => {
953 assert_eq!(*kind, ModeKind::Altered);
954 assert_eq!(*rank, 1);
955 }
956 _ => panic!("First candidate for G7#9 should be a Mode"),
957 }
958 }
959
960 #[test]
961 fn test_augmented_dominant_flat9_candidates() {
962 let candidates = KnownChord::AugmentedDominantFlat9(Degree::Seven).scale_candidates();
963 assert!(!candidates.is_empty(), "G7(b9)(#5) should have scale candidates");
964
965 match &candidates[0] {
966 ScaleCandidate::Mode { kind, rank, .. } => {
967 assert_eq!(*kind, ModeKind::Altered);
968 assert_eq!(*rank, 1);
969 }
970 _ => panic!("First candidate for G7(b9)(#5) should be a Mode"),
971 }
972
973 match &candidates[1] {
974 ScaleCandidate::Scale { kind, rank, .. } => {
975 assert_eq!(*kind, ScaleKind::DiminishedHalfWhole);
976 assert_eq!(*rank, 2);
977 }
978 _ => panic!("Second candidate for G7(b9)(#5) should be a Scale"),
979 }
980 }
981
982 #[test]
983 fn test_half_diminished_candidates() {
984 let candidates = KnownChord::HalfDiminished(Degree::Seven).scale_candidates();
985 assert!(!candidates.is_empty(), "Cm7b5 should have scale candidates");
986
987 match &candidates[0] {
988 ScaleCandidate::Mode { kind, rank, .. } => {
989 assert_eq!(*kind, ModeKind::Locrian);
990 assert_eq!(*rank, 1);
991 }
992 _ => panic!("First candidate for Cm7b5 should be a Mode"),
993 }
994 }
995
996 #[test]
997 fn test_augmented_dominant_candidates() {
998 let candidates = KnownChord::AugmentedDominant(Degree::Seven).scale_candidates();
999 assert!(!candidates.is_empty(), "Augmented dominant should have scale candidates");
1000
1001 match &candidates[0] {
1002 ScaleCandidate::Scale { kind, rank, .. } => {
1003 assert_eq!(*kind, ScaleKind::WholeTone);
1004 assert_eq!(*rank, 1);
1005 }
1006 _ => panic!("First candidate for augmented dominant should be a Scale"),
1007 }
1008 }
1009
1010 #[test]
1011 fn test_major_chord_candidates() {
1012 let candidates = KnownChord::Major.scale_candidates();
1013 assert!(candidates.len() >= 3, "Major chord should have at least 3 candidates");
1014
1015 match &candidates[0] {
1016 ScaleCandidate::Mode { kind, rank, .. } => {
1017 assert_eq!(*kind, ModeKind::Ionian);
1018 assert_eq!(*rank, 1);
1019 }
1020 _ => panic!("First candidate for Major should be Ionian mode"),
1021 }
1022
1023 match &candidates[1] {
1024 ScaleCandidate::Scale { kind, rank, .. } => {
1025 assert_eq!(*kind, ScaleKind::MajorPentatonic);
1026 assert_eq!(*rank, 2);
1027 }
1028 _ => panic!("Second candidate for Major should be MajorPentatonic scale"),
1029 }
1030
1031 match &candidates[2] {
1032 ScaleCandidate::Mode { kind, rank, .. } => {
1033 assert_eq!(*kind, ModeKind::Lydian);
1034 assert_eq!(*rank, 3);
1035 }
1036 _ => panic!("Third candidate for Major should be Lydian mode"),
1037 }
1038 }
1039
1040 #[test]
1041 fn test_minor_chord_candidates() {
1042 let candidates = KnownChord::Minor.scale_candidates();
1043 assert!(candidates.len() >= 3, "Minor chord should have at least 3 candidates");
1044
1045 match &candidates[0] {
1046 ScaleCandidate::Mode { kind, rank, .. } => {
1047 assert_eq!(*kind, ModeKind::Aeolian);
1048 assert_eq!(*rank, 1);
1049 }
1050 _ => panic!("First candidate for Minor should be Aeolian mode"),
1051 }
1052
1053 match &candidates[1] {
1054 ScaleCandidate::Scale { kind, rank, .. } => {
1055 assert_eq!(*kind, ScaleKind::MinorPentatonic);
1056 assert_eq!(*rank, 2);
1057 }
1058 _ => panic!("Second candidate for Minor should be MinorPentatonic scale"),
1059 }
1060
1061 match &candidates[2] {
1062 ScaleCandidate::Scale { kind, rank, .. } => {
1063 assert_eq!(*kind, ScaleKind::Blues);
1064 assert_eq!(*rank, 3);
1065 }
1066 _ => panic!("Third candidate for Minor should be Blues scale"),
1067 }
1068 }
1069
1070 #[test]
1071 fn test_minor_dominant_candidates() {
1072 let candidates = KnownChord::MinorDominant(Degree::Seven).scale_candidates();
1073 assert!(candidates.len() >= 3, "Minor dominant should have at least 3 candidates");
1074
1075 match &candidates[0] {
1076 ScaleCandidate::Mode { kind, rank, .. } => {
1077 assert_eq!(*kind, ModeKind::Dorian);
1078 assert_eq!(*rank, 1);
1079 }
1080 _ => panic!("First candidate for Minor dominant should be Dorian mode"),
1081 }
1082
1083 match &candidates[1] {
1084 ScaleCandidate::Scale { kind, rank, .. } => {
1085 assert_eq!(*kind, ScaleKind::MinorPentatonic);
1086 assert_eq!(*rank, 2);
1087 }
1088 _ => panic!("Second candidate for Minor dominant should be MinorPentatonic scale"),
1089 }
1090
1091 match &candidates[2] {
1092 ScaleCandidate::Scale { kind, rank, .. } => {
1093 assert_eq!(*kind, ScaleKind::Blues);
1094 assert_eq!(*rank, 3);
1095 }
1096 _ => panic!("Third candidate for Minor dominant should be Blues scale"),
1097 }
1098 }
1099
1100 #[test]
1101 fn test_interval_candidates_kinds_and_order() {
1102 let all_variants = vec![
1104 KnownChord::Unknown,
1105 KnownChord::Major,
1106 KnownChord::Minor,
1107 KnownChord::Major7,
1108 KnownChord::Dominant(Degree::Seven),
1109 KnownChord::MinorMajor7,
1110 KnownChord::MinorDominant(Degree::Seven),
1111 KnownChord::DominantSharp11(Degree::Seven),
1112 KnownChord::Augmented,
1113 KnownChord::AugmentedMajor7,
1114 KnownChord::AugmentedDominant(Degree::Seven),
1115 KnownChord::HalfDiminished(Degree::Seven),
1116 KnownChord::Diminished,
1117 KnownChord::DominantFlat9(Degree::Seven),
1118 KnownChord::DominantSharp9(Degree::Seven),
1119 KnownChord::AugmentedDominantFlat9(Degree::Seven),
1120 KnownChord::MinorDominantFlat13(Degree::Seven),
1121 KnownChord::MinorDominantFlat9Flat13(Degree::Seven),
1122 KnownChord::Sharp11,
1123 ];
1124
1125 for known_chord in all_variants {
1126 let candidates = known_chord.scale_interval_candidates();
1127
1128 for (i, candidate) in candidates.iter().enumerate() {
1130 let expected_rank = (i + 1) as u8;
1131 assert_eq!(
1132 candidate.rank, expected_rank,
1133 "{:?}: Expected rank {} at position {}, got {}",
1134 known_chord, expected_rank, i, candidate.rank
1135 );
1136
1137 assert!(!candidate.reason.is_empty(), "{:?}: Candidate at position {} has empty reason", known_chord, i);
1139
1140 let scale_candidates = known_chord.scale_candidates();
1142 if i < scale_candidates.len() {
1143 match (&candidate.kind, &scale_candidates[i]) {
1144 (IntervalCollectionKind::Mode(mk), ScaleCandidate::Mode { kind, .. }) => {
1145 assert_eq!(mk, kind, "{:?}: Mode kind mismatch at position {}", known_chord, i);
1146 }
1147 (IntervalCollectionKind::Scale(sk), ScaleCandidate::Scale { kind, .. }) => {
1148 assert_eq!(sk, kind, "{:?}: Scale kind mismatch at position {}", known_chord, i);
1149 }
1150 _ => panic!("{:?}: Kind type mismatch at position {}", known_chord, i),
1151 }
1152 }
1153 }
1154
1155 let scale_candidates = known_chord.scale_candidates();
1157 assert_eq!(candidates.len(), scale_candidates.len(), "{:?}: Mismatch in candidate count", known_chord);
1158 }
1159 }
1160}