1pub use keysignature::{
2 KeySignature, mode_sharps_alter, pitch_name_to_sharps, pitch_to_sharps, sharps_to_pitch,
3};
4
5use crate::{
6 chord::Chord,
7 defaults::IntegerType,
8 error::{Error, Result},
9 interval::Interval,
10 pitch::Pitch,
11 scale::{Scale, ScaleType},
12};
13use std::str::FromStr;
14
15pub mod keysignature;
17
18pub const MODES: [&str; 15] = [
21 "major",
22 "minor",
23 "ionian",
24 "dorian",
25 "phrygian",
26 "lydian",
27 "mixolydian",
28 "aeolian",
29 "locrian",
30 "hypodorian",
31 "hypophrygian",
32 "hypolydian",
33 "hypomixolydian",
34 "hypoaeolian",
35 "hypolocrian",
36];
37
38#[derive(Clone, Debug)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[must_use]
42pub struct Key {
43 tonic_pitch: Pitch,
44 mode: String,
45 sharps: IntegerType,
46 #[cfg_attr(feature = "serde", serde(default))]
47 color: Option<String>,
48}
49
50impl Key {
51 pub(crate) fn new(tonic_pitch: Pitch, mode: &str, sharps: IntegerType) -> Self {
52 Self {
53 tonic_pitch,
54 mode: mode.to_string(),
55 sharps,
56 color: None,
57 }
58 }
59
60 pub fn color(&self) -> Option<&str> {
62 self.color.as_deref()
63 }
64
65 pub fn set_color(&mut self, color: Option<String>) {
67 self.color = color;
68 }
69
70 pub fn from_tonic_mode<'a, M>(tonic: &str, mode: M) -> Result<Self>
78 where
79 M: Into<Option<&'a str>>,
80 {
81 let mode = mode.into();
82 let (tonic, resolved_mode) = match mode {
83 Some(mode) => (tonic.to_string(), mode.to_lowercase()),
84 None if tonic.contains('m') => (tonic.replace('m', ""), "minor".to_string()),
85 None if tonic.contains('M') => (tonic.replace('M', ""), "major".to_string()),
86 None if tonic.chars().all(|ch| !ch.is_ascii_uppercase()) => {
87 (tonic.to_string(), "minor".to_string())
88 }
89 None => (tonic.to_string(), "major".to_string()),
90 };
91 if !MODES.contains(&resolved_mode.as_str()) {
92 return Err(Error::Key(format!(
93 "Cannot create a scale of the following mode: {resolved_mode:?}"
94 )));
95 }
96 let tonic_pitch = Pitch::from_name(tonic.as_str())?;
97
98 let sharps = pitch_to_sharps(&tonic_pitch, Some(&resolved_mode))?;
99 Ok(Self::new(tonic_pitch, &resolved_mode, sharps))
100 }
101
102 pub fn from_tonic(tonic: &str) -> Result<Self> {
104 Self::from_tonic_mode(tonic, None::<&str>)
105 }
106
107 pub fn tonic(&self) -> Pitch {
109 self.tonic_pitch.clone()
110 }
111
112 pub fn tonic_pitch(&self) -> &Pitch {
114 &self.tonic_pitch
115 }
116
117 pub fn mode(&self) -> &str {
119 &self.mode
120 }
121
122 pub fn transpose(&self, interval: &Interval) -> Result<Self> {
125 let mut moved = self
126 .key_signature()
127 .transpose(interval)?
128 .try_as_key(Some(&self.mode), None)?;
129 moved.color = self.color.clone();
130 Ok(moved)
131 }
132
133 pub fn scale(&self) -> Scale {
145 Scale::new(self.scale_type(), self.tonic_pitch.clone())
146 }
147
148 fn scale_type(&self) -> ScaleType {
152 const MODES: [ScaleType; 7] = [
153 ScaleType::Major,
154 ScaleType::Dorian,
155 ScaleType::Phrygian,
156 ScaleType::Lydian,
157 ScaleType::Mixolydian,
158 ScaleType::Minor,
159 ScaleType::Locrian,
160 ];
161 let major_step = (4 * self.sharps).rem_euclid(7);
162 let tonic_step = self.tonic_pitch.step_index();
163 MODES[(tonic_step - major_step).rem_euclid(7) as usize]
164 }
165
166 pub fn tonic_pitch_name_with_case(&self) -> String {
169 let name = self.tonic_pitch.name();
171 let mut letters = name.chars();
172 let step = letters.next().unwrap_or_default();
173 let step = match self.mode.as_str() {
174 "major" => step.to_ascii_uppercase(),
175 "minor" => step.to_ascii_lowercase(),
176 _ => step,
177 };
178 format!("{step}{}", letters.as_str())
179 }
180
181 pub fn sharps(&self) -> IntegerType {
183 self.sharps
184 }
185
186 pub fn key_signature(&self) -> KeySignature {
188 KeySignature::new(self.sharps)
189 }
190
191 pub fn with_sharps(mut self, sharps: IntegerType) -> Self {
204 self.sharps = sharps;
205 self
206 }
207
208 pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
216 if degree == 0 {
217 return Err(Error::Key("scale degrees start at 1".to_string()));
218 }
219 let within = (degree - 1) % 7;
220 let octaves_up = IntegerType::try_from((degree - 1) / 7)
221 .map_err(|_| Error::Key(format!("degree {degree} is out of range")))?;
222 let mut pitch = self.scale().pitch_at_degree(within as IntegerType + 1)?;
223 let octave = pitch.octave().unwrap_or(4) + octaves_up;
224 pitch.set_octave(Some(octave));
225 Ok(pitch)
226 }
227
228 pub fn pitches(&self) -> Result<Vec<Pitch>> {
230 (1..=8)
231 .map(|degree| self.pitch_from_degree(degree))
232 .collect()
233 }
234
235 pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
237 self.chord_of_thirds(degree, 3)
238 }
239
240 pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
242 self.chord_of_thirds(degree, 4)
243 }
244
245 fn chord_of_thirds(&self, degree: usize, notes: usize) -> Result<Chord> {
246 let pitches = (0..notes)
247 .map(|third| self.pitch_from_degree(degree + 2 * third))
248 .collect::<Result<Vec<_>>>()?;
249 Chord::new(pitches.as_slice())
250 }
251
252 pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
254 (1..=7)
255 .map(|degree| self.triad_from_degree(degree))
256 .collect()
257 }
258
259 pub fn harmonized_sevenths(&self) -> Result<Vec<Chord>> {
261 (1..=7)
262 .map(|degree| self.seventh_chord_from_degree(degree))
263 .collect()
264 }
265
266 pub fn derive_by_degree(&self, degree: usize, pitch: &Pitch) -> Result<Self> {
271 self.derive_by_degree_of(self.scale_type(), degree, pitch)
272 }
273
274 pub fn derive_by_degree_of(
283 &self,
284 scale_type: ScaleType,
285 degree: usize,
286 pitch: &Pitch,
287 ) -> Result<Self> {
288 let scale =
289 Scale::new(scale_type, self.tonic_pitch.clone()).derive_by_degree(degree, pitch)?;
290 let tonic = scale.tonic().clone();
291 let sharps = pitch_to_sharps(&tonic, Some(&self.mode))?;
292 Ok(Self::new(tonic, &self.mode, sharps))
293 }
294
295 pub fn relative(&self) -> Result<Self> {
298 match self.mode.as_str() {
299 "major" => self.key_signature().try_as_key(Some("minor"), None),
300 "minor" => self.key_signature().try_as_key(Some("major"), None),
301 _ => Ok(self.clone()),
302 }
303 }
304
305 pub fn parallel(&self) -> Result<Self> {
307 match self.mode.as_str() {
308 "major" => Self::from_tonic_mode(&self.tonic_pitch.name(), "minor"),
309 "minor" => Self::from_tonic_mode(&self.tonic_pitch.name(), "major"),
310 _ => Ok(self.clone()),
311 }
312 }
313}
314
315impl FromStr for Key {
316 type Err = Error;
317
318 fn from_str(value: &str) -> Result<Self> {
319 parse_key(value)
320 }
321}
322
323impl TryFrom<&str> for Key {
324 type Error = Error;
325
326 fn try_from(value: &str) -> Result<Self> {
327 value.parse()
328 }
329}
330
331impl TryFrom<String> for Key {
332 type Error = Error;
333
334 fn try_from(value: String) -> Result<Self> {
335 value.parse()
336 }
337}
338
339fn parse_key(value: &str) -> Result<Key> {
340 let trimmed = value.trim();
341 if trimmed.is_empty() {
342 return Err(Error::Analysis("key cannot be empty".to_string()));
343 }
344
345 let parts = trimmed.split_whitespace().collect::<Vec<_>>();
346 match parts.as_slice() {
347 [tonic] => {
348 if let Some((tonic, mode)) = split_compact_key_token(tonic) {
349 Key::from_tonic_mode(tonic, Some(mode.as_str()))
350 } else {
351 Key::from_tonic(tonic)
352 }
353 }
354 [tonic, mode] => {
355 let mode = canonical_key_mode(mode);
356 Key::from_tonic_mode(tonic, Some(mode.as_str()))
357 }
358 _ => Err(Error::Analysis(format!(
359 "invalid key {value:?}; use a tonic and optional mode, such as \"C\", \"C major\", or \"Am\""
360 ))),
361 }
362}
363
364fn split_compact_key_token(token: &str) -> Option<(&str, String)> {
365 let lower = token.to_ascii_lowercase();
366 for suffix in ["major", "minor", "maj", "min", "m"] {
367 if lower.ends_with(suffix) && lower.len() > suffix.len() {
368 let tonic_end = token.len() - suffix.len();
369 return Some((&token[..tonic_end], canonical_key_mode(suffix)));
370 }
371 }
372 None
373}
374
375fn canonical_key_mode(mode: &str) -> String {
376 match mode.to_ascii_lowercase().as_str() {
377 "maj" | "major" => "major".to_string(),
378 "m" | "min" | "minor" => "minor".to_string(),
379 other => other.to_string(),
380 }
381}
382
383pub fn convert_key_string_to_music21_key_string(text: &str) -> String {
388 if !text.ends_with('b') || text == "b" {
389 return text.to_string();
390 }
391 if text == "bb" {
392 return "b-".to_string();
393 }
394 if text == "Bb" {
395 return "B-".to_string();
396 }
397 let mut chars = text.chars();
398 let Some(first) = chars.next() else {
399 return text.to_string();
400 };
401 let rest: Vec<char> = chars.collect();
402 if rest.iter().all(|c| *c == 'b') {
403 return format!("{first}{}", "-".repeat(rest.len()));
404 }
405 text.to_string()
406}
407
408#[cfg(test)]
409mod tests {
410
411 #[test]
412 fn a_key_is_read_from_an_owned_name_and_harmonizes_its_degrees() {
413 use super::Key;
414
415 let key = Key::try_from("a".to_string()).unwrap();
416 assert_eq!((key.tonic().name(), key.mode()), ("A".to_string(), "minor"));
417 let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
418 assert_eq!(dorian.mode(), "dorian");
419 let major = Key::try_from("Eb").unwrap();
420 let pitches: Vec<String> = major.pitches().unwrap().iter().map(|p| p.name()).collect();
421 assert_eq!(pitches, ["Eb", "F", "G", "Ab", "Bb", "C", "D", "Eb"]);
422 let sevenths = major.harmonized_sevenths().unwrap();
423 assert_eq!(sevenths.len(), 7);
424 assert_eq!(sevenths[4].pitch_names(), ["Bb", "D", "F", "Ab"]);
425 assert!(Key::try_from("H").is_err());
426 }
427
428 #[test]
429 fn the_pattern_read_by_decides_which_key_has_a_pitch_at_a_degree() {
430 let minor = Key::from_tonic_mode("C", "minor").unwrap();
434 let e = Pitch::from_name("E").unwrap();
435 assert_eq!(minor.derive_by_degree(7, &e).unwrap().tonic().name(), "F#");
436 assert_eq!(
437 minor
438 .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
439 .unwrap()
440 .tonic()
441 .name(),
442 "F"
443 );
444 assert_eq!(
446 minor
447 .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
448 .unwrap()
449 .mode(),
450 "minor"
451 );
452 }
453
454 #[test]
455 fn mode_suffixes_and_semitone_transposition_match_music21() {
456 let e_major = Key::from_tonic("EM").unwrap();
457 assert_eq!(
458 (e_major.tonic().name(), e_major.mode()),
459 ("E".to_string(), "major")
460 );
461 let f_sharp_minor = Key::from_tonic("F#m").unwrap();
462 assert_eq!(
463 (f_sharp_minor.tonic().name(), f_sharp_minor.mode()),
464 ("F#".to_string(), "minor")
465 );
466 let up_a_semitone = Key::from_tonic("e")
467 .unwrap()
468 .transpose(&Interval::from_semitones(1).unwrap())
469 .unwrap();
470 assert_eq!(up_a_semitone.tonic_pitch_name_with_case(), "f");
471 assert_eq!(up_a_semitone.sharps(), -4);
472 let by_name = Key::from_tonic("e")
473 .unwrap()
474 .transpose(&Interval::from_name("m2").unwrap())
475 .unwrap();
476 assert_eq!(by_name.tonic_pitch_name_with_case(), "f");
477 assert!(matches!(
478 pitch_to_sharps(&Pitch::from_name("C~").unwrap(), None),
479 Err(crate::Error::Key(_))
480 ));
481 }
482
483 #[test]
484 fn key_strings_convert_like_music21() {
485 let cases = [
486 ("bb", "b-"),
487 ("b", "b"),
488 ("B", "B"),
489 ("Bb", "B-"),
490 ("a", "a"),
491 ("f#", "f#"),
492 ("F#", "F#"),
493 ("eb", "e-"),
494 ("e-", "e-"),
495 ("Ebb", "E--"),
496 ("Abb", "A--"),
497 ];
498 for (text, expected) in cases {
499 assert_eq!(
500 convert_key_string_to_music21_key_string(text),
501 expected,
502 "{text}"
503 );
504 }
505 }
506
507 #[test]
508 fn derive_by_degree_matches_music21() {
509 let cases: [(&str, usize, &str, &str, &str); 8] = [
510 ("C", 5, "E", "A3", "major"),
511 ("C", 1, "F#4", "F#4", "major"),
512 ("a", 3, "C", "A3", "minor"),
513 ("C", 7, "B-", "Cb4", "major"),
514 ("C", 4, "B", "F#4", "major"),
515 ("D", 2, "C#3", "B2", "major"),
516 ("c", 6, "A-4", "C4", "minor"),
517 ("C", 5, "G4", "C4", "major"),
518 ];
519 for (key, degree, pitch, tonic, mode) in cases {
520 let derived = Key::from_tonic(key)
521 .unwrap()
522 .derive_by_degree(degree, &Pitch::from_name(pitch).unwrap())
523 .unwrap();
524 assert_eq!(
525 derived.tonic_pitch().name_with_octave(),
526 tonic,
527 "{key} {degree} {pitch}"
528 );
529 assert_eq!(derived.mode(), mode, "{key} {degree} {pitch}");
530 }
531 assert_eq!(
532 Key::from_tonic("C")
533 .unwrap()
534 .derive_by_degree(5, &Pitch::from_name("E").unwrap())
535 .unwrap()
536 .sharps(),
537 3
538 );
539 }
540 use super::*;
541 use crate::key::keysignature::pitch_name_to_sharps;
542
543 #[test]
544 fn transposing_a_key_matches_music21() {
545 let cases = [
546 ("C", "M2", "D", 2, "major"),
547 ("c", "P5", "g", -2, "minor"),
548 ("F#", "m2", "G", 1, "major"),
549 ("Bb", "-M3", "Gb", -6, "major"),
550 ("D", "P8", "D", 2, "major"),
551 ("e", "M6", "c#", 4, "minor"),
552 ("C", "-m2", "B", 5, "major"),
553 ];
554 for (key, interval, tonic, sharps, mode) in cases {
555 let moved = Key::from_tonic(key)
556 .unwrap()
557 .transpose(&Interval::from_name(interval).unwrap())
558 .unwrap();
559 assert_eq!(
560 moved.tonic_pitch_name_with_case(),
561 tonic,
562 "{key} {interval}"
563 );
564 assert_eq!(moved.sharps(), sharps, "{key} {interval}");
565 assert_eq!(moved.mode(), mode, "{key} {interval}");
566 }
567 let dorian = Key::from_tonic_mode("D", "dorian").unwrap().scale();
568 assert_eq!(dorian.scale_type(), ScaleType::Dorian);
569 let hypodorian = Key::from_tonic_mode("D", "hypodorian").unwrap();
572 assert_eq!(hypodorian.sharps(), 2);
573 assert_eq!(hypodorian.scale().scale_type(), ScaleType::Major);
574 }
575
576 #[test]
577 fn tonic_names_carry_mode_case() {
578 let cases = [
579 ("C", "C", "major"),
580 ("c", "c", "minor"),
581 ("F#", "F#", "major"),
582 ("f#", "f#", "minor"),
583 ("Bb", "Bb", "major"),
584 ("eb", "eb", "minor"),
585 ];
586 for (input, cased, mode) in cases {
587 let key: Key = input.parse().unwrap();
588 assert_eq!(key.tonic_pitch_name_with_case(), cased, "{input}");
589 assert_eq!(key.mode(), mode, "{input}");
590 }
591 let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
592 assert_eq!(dorian.tonic_pitch_name_with_case(), "D");
593 }
594
595 #[test]
596 fn key_from_tonic_mode() {
597 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
598 assert_eq!(c_major.sharps(), 0);
599 let g_major = Key::from_tonic_mode("G", Some("major")).unwrap();
600 assert_eq!(g_major.sharps(), 1);
601 let a_minor = Key::from_tonic_mode("A", Some("minor")).unwrap();
602 assert_eq!(a_minor.sharps(), 0);
603 let e_phrygian = Key::from_tonic_mode("E", Some("phrygian")).unwrap();
604 assert_eq!(e_phrygian.sharps(), 0);
605 }
606
607 #[test]
608 fn a_mode_music21_builds_no_scale_for_is_refused() {
609 for mode in [
610 "major",
611 "Minor",
612 "ionian",
613 "aeolian",
614 "dorian",
615 "phrygian",
616 "lydian",
617 "mixolydian",
618 "locrian",
619 "hypodorian",
620 "hypophrygian",
621 "hypolydian",
622 "hypomixolydian",
623 "hypoaeolian",
624 "hypolocrian",
625 ] {
626 assert!(Key::from_tonic_mode("c", mode).is_ok(), "{mode}");
627 }
628 for mode in ["blues-like", "", "major #(system \"touch x\") %"] {
629 assert!(Key::from_tonic_mode("c", mode).is_err(), "{mode}");
630 }
631 }
632
633 #[test]
634 fn key_from_string_accepts_common_notation() {
635 let c_major: Key = "C major".parse().unwrap();
636 assert_eq!(c_major.tonic().name(), "C");
637 assert_eq!(c_major.mode(), "major");
638
639 let a_minor: Key = "Am".parse().unwrap();
640 assert_eq!(a_minor.tonic().name(), "A");
641 assert_eq!(a_minor.mode(), "minor");
642
643 let b_flat_minor: Key = "Bb minor".parse().unwrap();
644 assert_eq!(b_flat_minor.tonic().name(), "Bb");
645 assert_eq!(b_flat_minor.mode(), "minor");
646 }
647
648 #[test]
649 fn key_scale_degree_and_chords() {
650 let d_major = Key::from_tonic_mode("D", Some("major")).unwrap();
651 assert_eq!(
652 d_major.pitch_from_degree(7).unwrap().name_with_octave(),
653 "C#5"
654 );
655 assert_eq!(
656 d_major.triad_from_degree(1).unwrap().pitched_common_name(),
657 "D-major triad"
658 );
659 assert_eq!(
660 d_major
661 .seventh_chord_from_degree(5)
662 .unwrap()
663 .pitched_common_name(),
664 "A-dominant seventh chord"
665 );
666 }
667
668 #[test]
669 fn key_harmonized_triads() {
670 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
671 let triads = c_major.harmonized_triads().unwrap();
672 assert_eq!(triads.len(), 7);
673 assert_eq!(triads[0].pitched_common_name(), "C-major triad");
674 assert_eq!(triads[4].pitched_common_name(), "G-major triad");
675 }
676
677 #[test]
678 fn key_relative_and_parallel() {
679 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
680 let relative = c_major.relative().unwrap();
681 assert_eq!(relative.mode(), "minor");
682 assert_eq!(relative.tonic().name(), "A");
683
684 let parallel = c_major.parallel().unwrap();
685 assert_eq!(parallel.mode(), "minor");
686 assert_eq!(parallel.tonic().name(), "C");
687 }
688
689 #[test]
690 fn pitch_name_to_sharps_modes() {
691 assert_eq!(pitch_name_to_sharps("C", Some("major")).unwrap(), 0);
692 assert_eq!(pitch_name_to_sharps("E", Some("minor")).unwrap(), 1);
693 assert_eq!(pitch_name_to_sharps("D", Some("dorian")).unwrap(), 0);
694 assert_eq!(pitch_name_to_sharps("A", Some("mixolydian")).unwrap(), 2);
695 }
696}