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, diatonicscale::DiatonicScale},
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}
27
28impl Key {
29 pub(crate) fn new(tonic_pitch: Pitch, mode: &str, sharps: IntegerType) -> Self {
30 Self {
31 tonic_pitch,
32 mode: mode.to_string(),
33 sharps,
34 }
35 }
36
37 pub fn from_tonic_mode<'a, M>(tonic: &str, mode: M) -> Result<Self>
46 where
47 M: Into<Option<&'a str>>,
48 {
49 let mode = mode.into();
50 let (tonic, resolved_mode) = match mode {
51 Some(mode) => (tonic.to_string(), mode.to_lowercase()),
52 None if tonic.contains('m') => (tonic.replace('m', ""), "minor".to_string()),
53 None if tonic.contains('M') => (tonic.replace('M', ""), "major".to_string()),
54 None if tonic.chars().all(|ch| !ch.is_ascii_uppercase()) => {
55 (tonic.to_string(), "minor".to_string())
56 }
57 None => (tonic.to_string(), "major".to_string()),
58 };
59 let tonic_pitch = Pitch::from_name(tonic.as_str())?;
60
61 let sharps = pitch_to_sharps(&tonic_pitch, Some(&resolved_mode))?;
62 Ok(Self::new(tonic_pitch, &resolved_mode, sharps))
63 }
64
65 pub fn from_tonic(tonic: &str) -> Result<Self> {
67 Self::from_tonic_mode(tonic, None::<&str>)
68 }
69
70 pub fn tonic(&self) -> Pitch {
72 self.tonic_pitch.clone()
73 }
74
75 pub fn tonic_pitch(&self) -> &Pitch {
77 &self.tonic_pitch
78 }
79
80 pub fn mode(&self) -> &str {
82 &self.mode
83 }
84
85 pub fn transpose(&self, interval: &Interval) -> Result<Self> {
87 self.key_signature()
88 .transpose(interval)?
89 .try_as_key(Some(&self.mode), None)
90 }
91
92 pub fn as_scale(&self) -> Result<Scale> {
96 let scale_type = match self.mode.as_str() {
97 "major" | "ionian" => ScaleType::Major,
98 "minor" | "aeolian" => ScaleType::Minor,
99 "dorian" => ScaleType::Dorian,
100 "phrygian" => ScaleType::Phrygian,
101 "lydian" => ScaleType::Lydian,
102 "mixolydian" => ScaleType::Mixolydian,
103 "locrian" => ScaleType::Locrian,
104 other => {
105 return Err(Error::Key(format!("no scale type for mode {other}")));
106 }
107 };
108 Ok(Scale::new(scale_type, self.tonic_pitch.clone()))
109 }
110
111 pub fn tonic_pitch_name_with_case(&self) -> String {
114 let name = self.tonic_pitch.name();
115 match self.mode.as_str() {
116 "major" => name.to_uppercase(),
117 "minor" => name.to_lowercase(),
118 _ => name,
119 }
120 }
121
122 pub fn sharps(&self) -> IntegerType {
124 self.sharps
125 }
126
127 pub fn key_signature(&self) -> KeySignature {
129 KeySignature::new(self.sharps)
130 }
131
132 pub fn scale(&self) -> DiatonicScale {
134 DiatonicScale::new(self.tonic_pitch.clone(), self.sharps, &self.mode)
135 }
136
137 pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
139 self.scale().pitch_from_degree(degree)
140 }
141
142 pub fn pitches(&self) -> Result<Vec<Pitch>> {
144 self.scale().pitches()
145 }
146
147 pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
149 self.scale().triad_from_degree(degree)
150 }
151
152 pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
154 self.scale().seventh_chord_from_degree(degree)
155 }
156
157 pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
159 (1..=7)
160 .map(|degree| self.triad_from_degree(degree))
161 .collect()
162 }
163
164 pub fn harmonized_sevenths(&self) -> Result<Vec<Chord>> {
166 (1..=7)
167 .map(|degree| self.seventh_chord_from_degree(degree))
168 .collect()
169 }
170
171 pub fn derive_by_degree(&self, degree: usize, pitch: &Pitch) -> Result<Self> {
176 self.derive_by_degree_of(self.as_scale()?.scale_type(), degree, pitch)
177 }
178
179 pub fn derive_by_degree_of(
188 &self,
189 scale_type: ScaleType,
190 degree: usize,
191 pitch: &Pitch,
192 ) -> Result<Self> {
193 let scale =
194 Scale::new(scale_type, self.tonic_pitch.clone()).derive_by_degree(degree, pitch)?;
195 let tonic = scale.tonic().clone();
196 let sharps = pitch_to_sharps(&tonic, Some(&self.mode))?;
197 Ok(Self::new(tonic, &self.mode, sharps))
198 }
199
200 pub fn relative(&self) -> Result<Self> {
203 match self.mode.as_str() {
204 "major" => self.key_signature().try_as_key(Some("minor"), None),
205 "minor" => self.key_signature().try_as_key(Some("major"), None),
206 _ => Ok(self.clone()),
207 }
208 }
209
210 pub fn parallel(&self) -> Result<Self> {
212 match self.mode.as_str() {
213 "major" => Self::from_tonic_mode(&self.tonic_pitch.name(), "minor"),
214 "minor" => Self::from_tonic_mode(&self.tonic_pitch.name(), "major"),
215 _ => Ok(self.clone()),
216 }
217 }
218}
219
220impl FromStr for Key {
221 type Err = Error;
222
223 fn from_str(value: &str) -> Result<Self> {
224 parse_key(value)
225 }
226}
227
228impl TryFrom<&str> for Key {
229 type Error = Error;
230
231 fn try_from(value: &str) -> Result<Self> {
232 value.parse()
233 }
234}
235
236impl TryFrom<String> for Key {
237 type Error = Error;
238
239 fn try_from(value: String) -> Result<Self> {
240 value.parse()
241 }
242}
243
244fn parse_key(value: &str) -> Result<Key> {
245 let trimmed = value.trim();
246 if trimmed.is_empty() {
247 return Err(Error::Analysis("key cannot be empty".to_string()));
248 }
249
250 let parts = trimmed.split_whitespace().collect::<Vec<_>>();
251 match parts.as_slice() {
252 [tonic] => {
253 if let Some((tonic, mode)) = split_compact_key_token(tonic) {
254 Key::from_tonic_mode(tonic, Some(mode.as_str()))
255 } else {
256 Key::from_tonic(tonic)
257 }
258 }
259 [tonic, mode] => {
260 let mode = canonical_key_mode(mode);
261 Key::from_tonic_mode(tonic, Some(mode.as_str()))
262 }
263 _ => Err(Error::Analysis(format!(
264 "invalid key {value:?}; use a tonic and optional mode, such as \"C\", \"C major\", or \"Am\""
265 ))),
266 }
267}
268
269fn split_compact_key_token(token: &str) -> Option<(&str, String)> {
270 let lower = token.to_ascii_lowercase();
271 for suffix in ["major", "minor", "maj", "min", "m"] {
272 if lower.ends_with(suffix) && lower.len() > suffix.len() {
273 let tonic_end = token.len() - suffix.len();
274 return Some((&token[..tonic_end], canonical_key_mode(suffix)));
275 }
276 }
277 None
278}
279
280fn canonical_key_mode(mode: &str) -> String {
281 match mode.to_ascii_lowercase().as_str() {
282 "maj" | "major" => "major".to_string(),
283 "m" | "min" | "minor" => "minor".to_string(),
284 other => other.to_string(),
285 }
286}
287
288pub fn convert_key_string_to_music21_key_string(text: &str) -> String {
293 if !text.ends_with('b') || text == "b" {
294 return text.to_string();
295 }
296 if text == "bb" {
297 return "b-".to_string();
298 }
299 if text == "Bb" {
300 return "B-".to_string();
301 }
302 let mut chars = text.chars();
303 let Some(first) = chars.next() else {
304 return text.to_string();
305 };
306 let rest: Vec<char> = chars.collect();
307 if rest.iter().all(|c| *c == 'b') {
308 return format!("{first}{}", "-".repeat(rest.len()));
309 }
310 text.to_string()
311}
312
313#[cfg(test)]
314mod tests {
315
316 #[test]
317 fn a_key_is_read_from_an_owned_name_and_harmonizes_its_degrees() {
318 use super::Key;
319
320 let key = Key::try_from("a".to_string()).unwrap();
321 assert_eq!((key.tonic().name(), key.mode()), ("A".to_string(), "minor"));
322 let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
323 assert_eq!(dorian.mode(), "dorian");
324 let major = Key::try_from("E-").unwrap();
325 let pitches: Vec<String> = major.pitches().unwrap().iter().map(|p| p.name()).collect();
326 assert_eq!(pitches, ["E-", "F", "G", "A-", "B-", "C", "D", "E-"]);
327 let sevenths = major.harmonized_sevenths().unwrap();
328 assert_eq!(sevenths.len(), 7);
329 assert_eq!(sevenths[4].pitch_names(), ["B-", "D", "F", "A-"]);
330 assert!(Key::try_from("H").is_err());
331 }
332
333 #[test]
334 fn the_pattern_read_by_decides_which_key_has_a_pitch_at_a_degree() {
335 let minor = Key::from_tonic_mode("C", "minor").unwrap();
339 let e = Pitch::from_name("E").unwrap();
340 assert_eq!(minor.derive_by_degree(7, &e).unwrap().tonic().name(), "F#");
341 assert_eq!(
342 minor
343 .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
344 .unwrap()
345 .tonic()
346 .name(),
347 "F"
348 );
349 assert_eq!(
351 minor
352 .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
353 .unwrap()
354 .mode(),
355 "minor"
356 );
357 }
358
359 #[test]
360 fn mode_suffixes_and_semitone_transposition_match_music21() {
361 let e_major = Key::from_tonic("EM").unwrap();
362 assert_eq!(
363 (e_major.tonic().name(), e_major.mode()),
364 ("E".to_string(), "major")
365 );
366 let f_sharp_minor = Key::from_tonic("F#m").unwrap();
367 assert_eq!(
368 (f_sharp_minor.tonic().name(), f_sharp_minor.mode()),
369 ("F#".to_string(), "minor")
370 );
371 let up_a_semitone = Key::from_tonic("e")
372 .unwrap()
373 .transpose(&Interval::from_semitones(1).unwrap())
374 .unwrap();
375 assert_eq!(up_a_semitone.tonic_pitch_name_with_case(), "f");
376 assert_eq!(up_a_semitone.sharps(), -4);
377 let by_name = Key::from_tonic("e")
378 .unwrap()
379 .transpose(&Interval::from_name("m2").unwrap())
380 .unwrap();
381 assert_eq!(by_name.tonic_pitch_name_with_case(), "f");
382 assert!(matches!(
383 pitch_to_sharps(&Pitch::from_name("C~").unwrap(), None),
384 Err(crate::Error::Key(_))
385 ));
386 }
387
388 #[test]
389 fn key_strings_convert_like_music21() {
390 let cases = [
391 ("bb", "b-"),
392 ("b", "b"),
393 ("B", "B"),
394 ("Bb", "B-"),
395 ("a", "a"),
396 ("f#", "f#"),
397 ("F#", "F#"),
398 ("eb", "e-"),
399 ("e-", "e-"),
400 ("Ebb", "E--"),
401 ("Abb", "A--"),
402 ];
403 for (text, expected) in cases {
404 assert_eq!(
405 convert_key_string_to_music21_key_string(text),
406 expected,
407 "{text}"
408 );
409 }
410 }
411
412 #[test]
413 fn derive_by_degree_matches_music21() {
414 let cases: [(&str, usize, &str, &str, &str); 8] = [
415 ("C", 5, "E", "A3", "major"),
416 ("C", 1, "F#4", "F#4", "major"),
417 ("a", 3, "C", "A3", "minor"),
418 ("C", 7, "B-", "C-4", "major"),
419 ("C", 4, "B", "F#4", "major"),
420 ("D", 2, "C#3", "B2", "major"),
421 ("c", 6, "A-4", "C4", "minor"),
422 ("C", 5, "G4", "C4", "major"),
423 ];
424 for (key, degree, pitch, tonic, mode) in cases {
425 let derived = Key::from_tonic(key)
426 .unwrap()
427 .derive_by_degree(degree, &Pitch::from_name(pitch).unwrap())
428 .unwrap();
429 assert_eq!(
430 derived.tonic_pitch().name_with_octave(),
431 tonic,
432 "{key} {degree} {pitch}"
433 );
434 assert_eq!(derived.mode(), mode, "{key} {degree} {pitch}");
435 }
436 assert_eq!(
437 Key::from_tonic("C")
438 .unwrap()
439 .derive_by_degree(5, &Pitch::from_name("E").unwrap())
440 .unwrap()
441 .sharps(),
442 3
443 );
444 }
445 use super::*;
446 use crate::key::keysignature::pitch_name_to_sharps;
447
448 #[test]
449 fn transposing_a_key_matches_music21() {
450 let cases = [
451 ("C", "M2", "D", 2, "major"),
452 ("c", "P5", "g", -2, "minor"),
453 ("F#", "m2", "G", 1, "major"),
454 ("B-", "-M3", "G-", -6, "major"),
455 ("D", "P8", "D", 2, "major"),
456 ("e", "M6", "c#", 4, "minor"),
457 ("C", "-m2", "B", 5, "major"),
458 ];
459 for (key, interval, tonic, sharps, mode) in cases {
460 let moved = Key::from_tonic(key)
461 .unwrap()
462 .transpose(&Interval::from_name(interval).unwrap())
463 .unwrap();
464 assert_eq!(
465 moved.tonic_pitch_name_with_case(),
466 tonic,
467 "{key} {interval}"
468 );
469 assert_eq!(moved.sharps(), sharps, "{key} {interval}");
470 assert_eq!(moved.mode(), mode, "{key} {interval}");
471 }
472 let dorian = Key::from_tonic_mode("D", "dorian")
473 .unwrap()
474 .as_scale()
475 .unwrap();
476 assert_eq!(dorian.scale_type(), ScaleType::Dorian);
477 assert!(
478 Key::from_tonic_mode("D", "hypodorian").is_err()
479 || Key::from_tonic_mode("D", "hypodorian")
480 .unwrap()
481 .as_scale()
482 .is_err()
483 );
484 }
485
486 #[test]
487 fn tonic_names_carry_mode_case() {
488 let cases = [
489 ("C", "C", "major"),
490 ("c", "c", "minor"),
491 ("F#", "F#", "major"),
492 ("f#", "f#", "minor"),
493 ("B-", "B-", "major"),
494 ("e-", "e-", "minor"),
495 ];
496 for (input, cased, mode) in cases {
497 let key: Key = input.parse().unwrap();
498 assert_eq!(key.tonic_pitch_name_with_case(), cased, "{input}");
499 assert_eq!(key.mode(), mode, "{input}");
500 }
501 let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
502 assert_eq!(dorian.tonic_pitch_name_with_case(), "D");
503 }
504
505 #[test]
506 fn key_from_tonic_mode() {
507 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
508 assert_eq!(c_major.sharps(), 0);
509 let g_major = Key::from_tonic_mode("G", Some("major")).unwrap();
510 assert_eq!(g_major.sharps(), 1);
511 let a_minor = Key::from_tonic_mode("A", Some("minor")).unwrap();
512 assert_eq!(a_minor.sharps(), 0);
513 let e_phrygian = Key::from_tonic_mode("E", Some("phrygian")).unwrap();
514 assert_eq!(e_phrygian.sharps(), 0);
515 }
516
517 #[test]
518 fn key_from_string_accepts_common_notation() {
519 let c_major: Key = "C major".parse().unwrap();
520 assert_eq!(c_major.tonic().name(), "C");
521 assert_eq!(c_major.mode(), "major");
522
523 let a_minor: Key = "Am".parse().unwrap();
524 assert_eq!(a_minor.tonic().name(), "A");
525 assert_eq!(a_minor.mode(), "minor");
526
527 let b_flat_minor: Key = "Bb minor".parse().unwrap();
528 assert_eq!(b_flat_minor.tonic().name(), "B-");
529 assert_eq!(b_flat_minor.mode(), "minor");
530 }
531
532 #[test]
533 fn key_scale_degree_and_chords() {
534 let d_major = Key::from_tonic_mode("D", Some("major")).unwrap();
535 assert_eq!(
536 d_major.pitch_from_degree(7).unwrap().name_with_octave(),
537 "C#5"
538 );
539 assert_eq!(
540 d_major.triad_from_degree(1).unwrap().pitched_common_name(),
541 "D-major triad"
542 );
543 assert_eq!(
544 d_major
545 .seventh_chord_from_degree(5)
546 .unwrap()
547 .pitched_common_name(),
548 "A-dominant seventh chord"
549 );
550 }
551
552 #[test]
553 fn key_harmonized_triads() {
554 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
555 let triads = c_major.harmonized_triads().unwrap();
556 assert_eq!(triads.len(), 7);
557 assert_eq!(triads[0].pitched_common_name(), "C-major triad");
558 assert_eq!(triads[4].pitched_common_name(), "G-major triad");
559 }
560
561 #[test]
562 fn key_relative_and_parallel() {
563 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
564 let relative = c_major.relative().unwrap();
565 assert_eq!(relative.mode(), "minor");
566 assert_eq!(relative.tonic().name(), "A");
567
568 let parallel = c_major.parallel().unwrap();
569 assert_eq!(parallel.mode(), "minor");
570 assert_eq!(parallel.tonic().name(), "C");
571 }
572
573 #[test]
574 fn pitch_name_to_sharps_modes() {
575 assert_eq!(pitch_name_to_sharps("C", Some("major")).unwrap(), 0);
576 assert_eq!(pitch_name_to_sharps("E", Some("minor")).unwrap(), 1);
577 assert_eq!(pitch_name_to_sharps("D", Some("dorian")).unwrap(), 0);
578 assert_eq!(pitch_name_to_sharps("A", Some("mixolydian")).unwrap(), 2);
579 }
580}