Skip to main content

music21_rs/key/
mod.rs

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
15/// Key-signature conversion and spelling helpers.
16pub mod keysignature;
17
18#[derive(Clone, Debug)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20/// A tonal key with a tonic pitch and mode.
21#[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    /// The colour the key's signature is drawn in, if one was said.
41    pub fn color(&self) -> Option<&str> {
42        self.color.as_deref()
43    }
44
45    /// Sets the colour the key's signature is drawn in.
46    pub fn set_color(&mut self, color: Option<String>) {
47        self.color = color;
48    }
49
50    /// Builds a key from a tonic and mode.
51    ///
52    /// Pass a mode string such as `"major"`, `"minor"`, `"dorian"`, or
53    /// `None::<&str>` to infer major/minor from tonic case.
54    /// Builds a key from a tonic name and a mode. Without a mode the name
55    /// decides, as music21's `Key` reads it: a trailing `m` is minor and a
56    /// trailing `M` major (`F#m`, `EM`), otherwise a lower-case name is minor
57    /// and an upper-case one major.
58    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    /// Builds a key and infers major/minor from tonic case.
79    pub fn from_tonic(tonic: &str) -> Result<Self> {
80        Self::from_tonic_mode(tonic, None::<&str>)
81    }
82
83    /// Returns a cloned tonic pitch.
84    pub fn tonic(&self) -> Pitch {
85        self.tonic_pitch.clone()
86    }
87
88    /// Returns a borrowed tonic pitch.
89    pub fn tonic_pitch(&self) -> &Pitch {
90        &self.tonic_pitch
91    }
92
93    /// Returns the key mode.
94    pub fn mode(&self) -> &str {
95        &self.mode
96    }
97
98    /// Returns this key moved by the interval, keeping its mode and the
99    /// colour it is drawn in.
100    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    /// The scale this key is written in: its tonic, and the diatonic mode
110    /// that tonic stands on within the key's own signature.
111    ///
112    /// The mode is read off the signature rather than the mode's name, so
113    /// every key has one. The church modes come out as themselves, D dorian
114    /// as `ScaleType::Dorian` on D, and a mode music21 keeps no signature for
115    /// takes the major signature on its tonic, as music21 gives it, and so
116    /// reads as major.
117    ///
118    /// music21's own `Key.getScale` answers the relative major for a church
119    /// mode, a C major scale for D dorian. The crate answers the mode.
120    pub fn scale(&self) -> Scale {
121        Scale::new(self.scale_type(), self.tonic_pitch.clone())
122    }
123
124    /// Which of the seven diatonic modes the tonic stands on in the key's
125    /// signature: how many letters it sits above the signature's major tonic,
126    /// which is four letters on for every sharp.
127    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    /// Returns the tonic name in music21's case convention: upper case for
143    /// major, lower case for minor, unchanged for other modes.
144    pub fn tonic_pitch_name_with_case(&self) -> String {
145        // Only the step takes the case: B-flat minor is `bb`, not `BB`.
146        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    /// Returns the number of sharps in the key signature.
158    pub fn sharps(&self) -> IntegerType {
159        self.sharps
160    }
161
162    /// Returns the matching key signature.
163    pub fn key_signature(&self) -> KeySignature {
164        KeySignature::new(self.sharps)
165    }
166
167    /// The pitch on a one-based scale degree, counting on up past the
168    /// octave: in C major the ninth degree is `D5`, which is what stacking
169    /// thirds on the upper degrees needs. A tonic given with no octave is
170    /// read in octave 4.
171    ///
172    /// [`Scale::pitch_at_degree`] is music21's `pitchFromDegree`, which reads
173    /// the eighth degree as the tonic again.
174    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    /// The scale's pitches from the tonic up to its octave, eight in all.
188    pub fn pitches(&self) -> Result<Vec<Pitch>> {
189        (1..=8)
190            .map(|degree| self.pitch_from_degree(degree))
191            .collect()
192    }
193
194    /// The diatonic triad on a one-based degree.
195    pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
196        self.chord_of_thirds(degree, 3)
197    }
198
199    /// The diatonic seventh chord on a one-based degree.
200    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    /// Returns all seven diatonic triads.
212    pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
213        (1..=7)
214            .map(|degree| self.triad_from_degree(degree))
215            .collect()
216    }
217
218    /// Returns all seven diatonic seventh chords.
219    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    /// Returns the relative major or minor key when applicable.
226    /// Returns the key of the same mode in which `pitch` is the given scale
227    /// degree: music21's `deriveByDegree`, so C major with `E` as degree 5 is
228    /// A major and A minor with `C` as degree 3 is A minor again.
229    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    /// The same, reading the degree off a scale pattern the caller chooses
234    /// rather than the key's own.
235    ///
236    /// music21 spells this as a settable `.abstract` on the key: the seventh
237    /// degree of a minor key is a whole tone below the tonic in the natural
238    /// form and a semitone below it in the harmonic form, so the key that has
239    /// `E` as its seventh is F-sharp minor by one reading and F minor by the
240    /// other. The key that comes back keeps this one's mode either way.
241    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    /// Returns the relative major or minor key -- the one sharing this key's
255    /// signature. A mode that is neither answers with itself.
256    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    /// Returns the parallel major or minor key when applicable.
265    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
342/// music21's `convertKeyStringToMusic21KeyString`: a key written with `b`
343/// for flat, as in `Bb` or `eb`, rewritten with music21's `-`, so `Bb` is
344/// `B-` and `bb` is `b-`. A lone `b` is B, and anything without a trailing
345/// `b` is returned as it is.
346pub 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        // music21's own example: the minor key whose seventh degree is E is
390        // F-sharp minor in the natural form and F minor in the harmonic one,
391        // where the seventh is only a semitone below the tonic.
392        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        // The mode of the key that comes back is this key's, either way.
404        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        // music21 gives a mode it keeps no signature for the major one on
529        // its tonic, so D hypodorian is written with D major's two sharps.
530        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}