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
18#[derive(Clone, Debug)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[must_use]
22pub struct Key {
23 tonic_pitch: Pitch,
24 mode: String,
25 sharps: IntegerType,
26 #[cfg_attr(feature = "serde", serde(default))]
27 color: Option<String>,
28}
29
30impl Key {
31 pub(crate) fn new(tonic_pitch: Pitch, mode: &str, sharps: IntegerType) -> Self {
32 Self {
33 tonic_pitch,
34 mode: mode.to_string(),
35 sharps,
36 color: None,
37 }
38 }
39
40 pub fn color(&self) -> Option<&str> {
42 self.color.as_deref()
43 }
44
45 pub fn set_color(&mut self, color: Option<String>) {
47 self.color = color;
48 }
49
50 pub fn from_tonic_mode<'a, M>(tonic: &str, mode: M) -> Result<Self>
59 where
60 M: Into<Option<&'a str>>,
61 {
62 let mode = mode.into();
63 let (tonic, resolved_mode) = match mode {
64 Some(mode) => (tonic.to_string(), mode.to_lowercase()),
65 None if tonic.contains('m') => (tonic.replace('m', ""), "minor".to_string()),
66 None if tonic.contains('M') => (tonic.replace('M', ""), "major".to_string()),
67 None if tonic.chars().all(|ch| !ch.is_ascii_uppercase()) => {
68 (tonic.to_string(), "minor".to_string())
69 }
70 None => (tonic.to_string(), "major".to_string()),
71 };
72 let tonic_pitch = Pitch::from_name(tonic.as_str())?;
73
74 let sharps = pitch_to_sharps(&tonic_pitch, Some(&resolved_mode))?;
75 Ok(Self::new(tonic_pitch, &resolved_mode, sharps))
76 }
77
78 pub fn from_tonic(tonic: &str) -> Result<Self> {
80 Self::from_tonic_mode(tonic, None::<&str>)
81 }
82
83 pub fn tonic(&self) -> Pitch {
85 self.tonic_pitch.clone()
86 }
87
88 pub fn tonic_pitch(&self) -> &Pitch {
90 &self.tonic_pitch
91 }
92
93 pub fn mode(&self) -> &str {
95 &self.mode
96 }
97
98 pub fn transpose(&self, interval: &Interval) -> Result<Self> {
101 let mut moved = self
102 .key_signature()
103 .transpose(interval)?
104 .try_as_key(Some(&self.mode), None)?;
105 moved.color = self.color.clone();
106 Ok(moved)
107 }
108
109 pub fn scale(&self) -> Scale {
121 Scale::new(self.scale_type(), self.tonic_pitch.clone())
122 }
123
124 fn scale_type(&self) -> ScaleType {
128 const MODES: [ScaleType; 7] = [
129 ScaleType::Major,
130 ScaleType::Dorian,
131 ScaleType::Phrygian,
132 ScaleType::Lydian,
133 ScaleType::Mixolydian,
134 ScaleType::Minor,
135 ScaleType::Locrian,
136 ];
137 let major_step = (4 * self.sharps).rem_euclid(7);
138 let tonic_step = self.tonic_pitch.step_index();
139 MODES[(tonic_step - major_step).rem_euclid(7) as usize]
140 }
141
142 pub fn tonic_pitch_name_with_case(&self) -> String {
145 let name = self.tonic_pitch.name();
147 let mut letters = name.chars();
148 let step = letters.next().unwrap_or_default();
149 let step = match self.mode.as_str() {
150 "major" => step.to_ascii_uppercase(),
151 "minor" => step.to_ascii_lowercase(),
152 _ => step,
153 };
154 format!("{step}{}", letters.as_str())
155 }
156
157 pub fn sharps(&self) -> IntegerType {
159 self.sharps
160 }
161
162 pub fn key_signature(&self) -> KeySignature {
164 KeySignature::new(self.sharps)
165 }
166
167 pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
175 if degree == 0 {
176 return Err(Error::Key("scale degrees start at 1".to_string()));
177 }
178 let within = (degree - 1) % 7;
179 let octaves_up = IntegerType::try_from((degree - 1) / 7)
180 .map_err(|_| Error::Key(format!("degree {degree} is out of range")))?;
181 let mut pitch = self.scale().pitch_at_degree(within as IntegerType + 1)?;
182 let octave = pitch.octave().unwrap_or(4) + octaves_up;
183 pitch.set_octave(Some(octave));
184 Ok(pitch)
185 }
186
187 pub fn pitches(&self) -> Result<Vec<Pitch>> {
189 (1..=8)
190 .map(|degree| self.pitch_from_degree(degree))
191 .collect()
192 }
193
194 pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
196 self.chord_of_thirds(degree, 3)
197 }
198
199 pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
201 self.chord_of_thirds(degree, 4)
202 }
203
204 fn chord_of_thirds(&self, degree: usize, notes: usize) -> Result<Chord> {
205 let pitches = (0..notes)
206 .map(|third| self.pitch_from_degree(degree + 2 * third))
207 .collect::<Result<Vec<_>>>()?;
208 Chord::new(pitches.as_slice())
209 }
210
211 pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
213 (1..=7)
214 .map(|degree| self.triad_from_degree(degree))
215 .collect()
216 }
217
218 pub fn harmonized_sevenths(&self) -> Result<Vec<Chord>> {
220 (1..=7)
221 .map(|degree| self.seventh_chord_from_degree(degree))
222 .collect()
223 }
224
225 pub fn derive_by_degree(&self, degree: usize, pitch: &Pitch) -> Result<Self> {
230 self.derive_by_degree_of(self.scale_type(), degree, pitch)
231 }
232
233 pub fn derive_by_degree_of(
242 &self,
243 scale_type: ScaleType,
244 degree: usize,
245 pitch: &Pitch,
246 ) -> Result<Self> {
247 let scale =
248 Scale::new(scale_type, self.tonic_pitch.clone()).derive_by_degree(degree, pitch)?;
249 let tonic = scale.tonic().clone();
250 let sharps = pitch_to_sharps(&tonic, Some(&self.mode))?;
251 Ok(Self::new(tonic, &self.mode, sharps))
252 }
253
254 pub fn relative(&self) -> Result<Self> {
257 match self.mode.as_str() {
258 "major" => self.key_signature().try_as_key(Some("minor"), None),
259 "minor" => self.key_signature().try_as_key(Some("major"), None),
260 _ => Ok(self.clone()),
261 }
262 }
263
264 pub fn parallel(&self) -> Result<Self> {
266 match self.mode.as_str() {
267 "major" => Self::from_tonic_mode(&self.tonic_pitch.name(), "minor"),
268 "minor" => Self::from_tonic_mode(&self.tonic_pitch.name(), "major"),
269 _ => Ok(self.clone()),
270 }
271 }
272}
273
274impl FromStr for Key {
275 type Err = Error;
276
277 fn from_str(value: &str) -> Result<Self> {
278 parse_key(value)
279 }
280}
281
282impl TryFrom<&str> for Key {
283 type Error = Error;
284
285 fn try_from(value: &str) -> Result<Self> {
286 value.parse()
287 }
288}
289
290impl TryFrom<String> for Key {
291 type Error = Error;
292
293 fn try_from(value: String) -> Result<Self> {
294 value.parse()
295 }
296}
297
298fn parse_key(value: &str) -> Result<Key> {
299 let trimmed = value.trim();
300 if trimmed.is_empty() {
301 return Err(Error::Analysis("key cannot be empty".to_string()));
302 }
303
304 let parts = trimmed.split_whitespace().collect::<Vec<_>>();
305 match parts.as_slice() {
306 [tonic] => {
307 if let Some((tonic, mode)) = split_compact_key_token(tonic) {
308 Key::from_tonic_mode(tonic, Some(mode.as_str()))
309 } else {
310 Key::from_tonic(tonic)
311 }
312 }
313 [tonic, mode] => {
314 let mode = canonical_key_mode(mode);
315 Key::from_tonic_mode(tonic, Some(mode.as_str()))
316 }
317 _ => Err(Error::Analysis(format!(
318 "invalid key {value:?}; use a tonic and optional mode, such as \"C\", \"C major\", or \"Am\""
319 ))),
320 }
321}
322
323fn split_compact_key_token(token: &str) -> Option<(&str, String)> {
324 let lower = token.to_ascii_lowercase();
325 for suffix in ["major", "minor", "maj", "min", "m"] {
326 if lower.ends_with(suffix) && lower.len() > suffix.len() {
327 let tonic_end = token.len() - suffix.len();
328 return Some((&token[..tonic_end], canonical_key_mode(suffix)));
329 }
330 }
331 None
332}
333
334fn canonical_key_mode(mode: &str) -> String {
335 match mode.to_ascii_lowercase().as_str() {
336 "maj" | "major" => "major".to_string(),
337 "m" | "min" | "minor" => "minor".to_string(),
338 other => other.to_string(),
339 }
340}
341
342pub fn convert_key_string_to_music21_key_string(text: &str) -> String {
347 if !text.ends_with('b') || text == "b" {
348 return text.to_string();
349 }
350 if text == "bb" {
351 return "b-".to_string();
352 }
353 if text == "Bb" {
354 return "B-".to_string();
355 }
356 let mut chars = text.chars();
357 let Some(first) = chars.next() else {
358 return text.to_string();
359 };
360 let rest: Vec<char> = chars.collect();
361 if rest.iter().all(|c| *c == 'b') {
362 return format!("{first}{}", "-".repeat(rest.len()));
363 }
364 text.to_string()
365}
366
367#[cfg(test)]
368mod tests {
369
370 #[test]
371 fn a_key_is_read_from_an_owned_name_and_harmonizes_its_degrees() {
372 use super::Key;
373
374 let key = Key::try_from("a".to_string()).unwrap();
375 assert_eq!((key.tonic().name(), key.mode()), ("A".to_string(), "minor"));
376 let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
377 assert_eq!(dorian.mode(), "dorian");
378 let major = Key::try_from("Eb").unwrap();
379 let pitches: Vec<String> = major.pitches().unwrap().iter().map(|p| p.name()).collect();
380 assert_eq!(pitches, ["Eb", "F", "G", "Ab", "Bb", "C", "D", "Eb"]);
381 let sevenths = major.harmonized_sevenths().unwrap();
382 assert_eq!(sevenths.len(), 7);
383 assert_eq!(sevenths[4].pitch_names(), ["Bb", "D", "F", "Ab"]);
384 assert!(Key::try_from("H").is_err());
385 }
386
387 #[test]
388 fn the_pattern_read_by_decides_which_key_has_a_pitch_at_a_degree() {
389 let minor = Key::from_tonic_mode("C", "minor").unwrap();
393 let e = Pitch::from_name("E").unwrap();
394 assert_eq!(minor.derive_by_degree(7, &e).unwrap().tonic().name(), "F#");
395 assert_eq!(
396 minor
397 .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
398 .unwrap()
399 .tonic()
400 .name(),
401 "F"
402 );
403 assert_eq!(
405 minor
406 .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
407 .unwrap()
408 .mode(),
409 "minor"
410 );
411 }
412
413 #[test]
414 fn mode_suffixes_and_semitone_transposition_match_music21() {
415 let e_major = Key::from_tonic("EM").unwrap();
416 assert_eq!(
417 (e_major.tonic().name(), e_major.mode()),
418 ("E".to_string(), "major")
419 );
420 let f_sharp_minor = Key::from_tonic("F#m").unwrap();
421 assert_eq!(
422 (f_sharp_minor.tonic().name(), f_sharp_minor.mode()),
423 ("F#".to_string(), "minor")
424 );
425 let up_a_semitone = Key::from_tonic("e")
426 .unwrap()
427 .transpose(&Interval::from_semitones(1).unwrap())
428 .unwrap();
429 assert_eq!(up_a_semitone.tonic_pitch_name_with_case(), "f");
430 assert_eq!(up_a_semitone.sharps(), -4);
431 let by_name = Key::from_tonic("e")
432 .unwrap()
433 .transpose(&Interval::from_name("m2").unwrap())
434 .unwrap();
435 assert_eq!(by_name.tonic_pitch_name_with_case(), "f");
436 assert!(matches!(
437 pitch_to_sharps(&Pitch::from_name("C~").unwrap(), None),
438 Err(crate::Error::Key(_))
439 ));
440 }
441
442 #[test]
443 fn key_strings_convert_like_music21() {
444 let cases = [
445 ("bb", "b-"),
446 ("b", "b"),
447 ("B", "B"),
448 ("Bb", "B-"),
449 ("a", "a"),
450 ("f#", "f#"),
451 ("F#", "F#"),
452 ("eb", "e-"),
453 ("e-", "e-"),
454 ("Ebb", "E--"),
455 ("Abb", "A--"),
456 ];
457 for (text, expected) in cases {
458 assert_eq!(
459 convert_key_string_to_music21_key_string(text),
460 expected,
461 "{text}"
462 );
463 }
464 }
465
466 #[test]
467 fn derive_by_degree_matches_music21() {
468 let cases: [(&str, usize, &str, &str, &str); 8] = [
469 ("C", 5, "E", "A3", "major"),
470 ("C", 1, "F#4", "F#4", "major"),
471 ("a", 3, "C", "A3", "minor"),
472 ("C", 7, "B-", "Cb4", "major"),
473 ("C", 4, "B", "F#4", "major"),
474 ("D", 2, "C#3", "B2", "major"),
475 ("c", 6, "A-4", "C4", "minor"),
476 ("C", 5, "G4", "C4", "major"),
477 ];
478 for (key, degree, pitch, tonic, mode) in cases {
479 let derived = Key::from_tonic(key)
480 .unwrap()
481 .derive_by_degree(degree, &Pitch::from_name(pitch).unwrap())
482 .unwrap();
483 assert_eq!(
484 derived.tonic_pitch().name_with_octave(),
485 tonic,
486 "{key} {degree} {pitch}"
487 );
488 assert_eq!(derived.mode(), mode, "{key} {degree} {pitch}");
489 }
490 assert_eq!(
491 Key::from_tonic("C")
492 .unwrap()
493 .derive_by_degree(5, &Pitch::from_name("E").unwrap())
494 .unwrap()
495 .sharps(),
496 3
497 );
498 }
499 use super::*;
500 use crate::key::keysignature::pitch_name_to_sharps;
501
502 #[test]
503 fn transposing_a_key_matches_music21() {
504 let cases = [
505 ("C", "M2", "D", 2, "major"),
506 ("c", "P5", "g", -2, "minor"),
507 ("F#", "m2", "G", 1, "major"),
508 ("Bb", "-M3", "Gb", -6, "major"),
509 ("D", "P8", "D", 2, "major"),
510 ("e", "M6", "c#", 4, "minor"),
511 ("C", "-m2", "B", 5, "major"),
512 ];
513 for (key, interval, tonic, sharps, mode) in cases {
514 let moved = Key::from_tonic(key)
515 .unwrap()
516 .transpose(&Interval::from_name(interval).unwrap())
517 .unwrap();
518 assert_eq!(
519 moved.tonic_pitch_name_with_case(),
520 tonic,
521 "{key} {interval}"
522 );
523 assert_eq!(moved.sharps(), sharps, "{key} {interval}");
524 assert_eq!(moved.mode(), mode, "{key} {interval}");
525 }
526 let dorian = Key::from_tonic_mode("D", "dorian").unwrap().scale();
527 assert_eq!(dorian.scale_type(), ScaleType::Dorian);
528 let hypodorian = Key::from_tonic_mode("D", "hypodorian").unwrap();
531 assert_eq!(hypodorian.sharps(), 2);
532 assert_eq!(hypodorian.scale().scale_type(), ScaleType::Major);
533 }
534
535 #[test]
536 fn tonic_names_carry_mode_case() {
537 let cases = [
538 ("C", "C", "major"),
539 ("c", "c", "minor"),
540 ("F#", "F#", "major"),
541 ("f#", "f#", "minor"),
542 ("Bb", "Bb", "major"),
543 ("eb", "eb", "minor"),
544 ];
545 for (input, cased, mode) in cases {
546 let key: Key = input.parse().unwrap();
547 assert_eq!(key.tonic_pitch_name_with_case(), cased, "{input}");
548 assert_eq!(key.mode(), mode, "{input}");
549 }
550 let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
551 assert_eq!(dorian.tonic_pitch_name_with_case(), "D");
552 }
553
554 #[test]
555 fn key_from_tonic_mode() {
556 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
557 assert_eq!(c_major.sharps(), 0);
558 let g_major = Key::from_tonic_mode("G", Some("major")).unwrap();
559 assert_eq!(g_major.sharps(), 1);
560 let a_minor = Key::from_tonic_mode("A", Some("minor")).unwrap();
561 assert_eq!(a_minor.sharps(), 0);
562 let e_phrygian = Key::from_tonic_mode("E", Some("phrygian")).unwrap();
563 assert_eq!(e_phrygian.sharps(), 0);
564 }
565
566 #[test]
567 fn key_from_string_accepts_common_notation() {
568 let c_major: Key = "C major".parse().unwrap();
569 assert_eq!(c_major.tonic().name(), "C");
570 assert_eq!(c_major.mode(), "major");
571
572 let a_minor: Key = "Am".parse().unwrap();
573 assert_eq!(a_minor.tonic().name(), "A");
574 assert_eq!(a_minor.mode(), "minor");
575
576 let b_flat_minor: Key = "Bb minor".parse().unwrap();
577 assert_eq!(b_flat_minor.tonic().name(), "Bb");
578 assert_eq!(b_flat_minor.mode(), "minor");
579 }
580
581 #[test]
582 fn key_scale_degree_and_chords() {
583 let d_major = Key::from_tonic_mode("D", Some("major")).unwrap();
584 assert_eq!(
585 d_major.pitch_from_degree(7).unwrap().name_with_octave(),
586 "C#5"
587 );
588 assert_eq!(
589 d_major.triad_from_degree(1).unwrap().pitched_common_name(),
590 "D-major triad"
591 );
592 assert_eq!(
593 d_major
594 .seventh_chord_from_degree(5)
595 .unwrap()
596 .pitched_common_name(),
597 "A-dominant seventh chord"
598 );
599 }
600
601 #[test]
602 fn key_harmonized_triads() {
603 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
604 let triads = c_major.harmonized_triads().unwrap();
605 assert_eq!(triads.len(), 7);
606 assert_eq!(triads[0].pitched_common_name(), "C-major triad");
607 assert_eq!(triads[4].pitched_common_name(), "G-major triad");
608 }
609
610 #[test]
611 fn key_relative_and_parallel() {
612 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
613 let relative = c_major.relative().unwrap();
614 assert_eq!(relative.mode(), "minor");
615 assert_eq!(relative.tonic().name(), "A");
616
617 let parallel = c_major.parallel().unwrap();
618 assert_eq!(parallel.mode(), "minor");
619 assert_eq!(parallel.tonic().name(), "C");
620 }
621
622 #[test]
623 fn pitch_name_to_sharps_modes() {
624 assert_eq!(pitch_name_to_sharps("C", Some("major")).unwrap(), 0);
625 assert_eq!(pitch_name_to_sharps("E", Some("minor")).unwrap(), 1);
626 assert_eq!(pitch_name_to_sharps("D", Some("dorian")).unwrap(), 0);
627 assert_eq!(pitch_name_to_sharps("A", Some("mixolydian")).unwrap(), 2);
628 }
629}