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, diatonicscale::DiatonicScale},
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}
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    /// Builds a key from a tonic and mode.
38    ///
39    /// Pass a mode string such as `"major"`, `"minor"`, `"dorian"`, or
40    /// `None::<&str>` to infer major/minor from tonic case.
41    /// Builds a key from a tonic name and a mode. Without a mode the name
42    /// decides, as music21's `Key` reads it: a trailing `m` is minor and a
43    /// trailing `M` major (`F#m`, `EM`), otherwise a lower-case name is minor
44    /// and an upper-case one major.
45    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    /// Builds a key and infers major/minor from tonic case.
66    pub fn from_tonic(tonic: &str) -> Result<Self> {
67        Self::from_tonic_mode(tonic, None::<&str>)
68    }
69
70    /// Returns a cloned tonic pitch.
71    pub fn tonic(&self) -> Pitch {
72        self.tonic_pitch.clone()
73    }
74
75    /// Returns a borrowed tonic pitch.
76    pub fn tonic_pitch(&self) -> &Pitch {
77        &self.tonic_pitch
78    }
79
80    /// Returns the key mode.
81    pub fn mode(&self) -> &str {
82        &self.mode
83    }
84
85    /// Returns this key moved by the interval, keeping its mode.
86    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    /// Returns the [`Scale`] for this key's mode on its tonic. Major and
93    /// minor cover ionian and aeolian; the other church modes map to their
94    /// scale types, and any other mode is an error.
95    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    /// Returns the tonic name in music21's case convention: upper case for
112    /// major, lower case for minor, unchanged for other modes.
113    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    /// Returns the number of sharps in the key signature.
123    pub fn sharps(&self) -> IntegerType {
124        self.sharps
125    }
126
127    /// Returns the matching key signature.
128    pub fn key_signature(&self) -> KeySignature {
129        KeySignature::new(self.sharps)
130    }
131
132    /// Returns the diatonic scale for this key.
133    pub fn scale(&self) -> DiatonicScale {
134        DiatonicScale::new(self.tonic_pitch.clone(), self.sharps, &self.mode)
135    }
136
137    /// Returns the pitch at a one-based scale degree.
138    pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
139        self.scale().pitch_from_degree(degree)
140    }
141
142    /// Returns scale pitches from degree 1 through the octave.
143    pub fn pitches(&self) -> Result<Vec<Pitch>> {
144        self.scale().pitches()
145    }
146
147    /// Builds the diatonic triad on a one-based degree.
148    pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
149        self.scale().triad_from_degree(degree)
150    }
151
152    /// Builds the diatonic seventh chord on a one-based degree.
153    pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
154        self.scale().seventh_chord_from_degree(degree)
155    }
156
157    /// Returns all seven diatonic triads.
158    pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
159        (1..=7)
160            .map(|degree| self.triad_from_degree(degree))
161            .collect()
162    }
163
164    /// Returns all seven diatonic seventh chords.
165    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    /// Returns the relative major or minor key when applicable.
172    /// Returns the key of the same mode in which `pitch` is the given scale
173    /// degree: music21's `deriveByDegree`, so C major with `E` as degree 5 is
174    /// A major and A minor with `C` as degree 3 is A minor again.
175    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    /// The same, reading the degree off a scale pattern the caller chooses
180    /// rather than the key's own.
181    ///
182    /// music21 spells this as a settable `.abstract` on the key: the seventh
183    /// degree of a minor key is a whole tone below the tonic in the natural
184    /// form and a semitone below it in the harmonic form, so the key that has
185    /// `E` as its seventh is F-sharp minor by one reading and F minor by the
186    /// other. The key that comes back keeps this one's mode either way.
187    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    /// Returns the relative major or minor key -- the one sharing this key's
201    /// signature. A mode that is neither answers with itself.
202    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    /// Returns the parallel major or minor key when applicable.
211    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
288/// music21's `convertKeyStringToMusic21KeyString`: a key written with `b`
289/// for flat, as in `Bb` or `eb`, rewritten with music21's `-`, so `Bb` is
290/// `B-` and `bb` is `b-`. A lone `b` is B, and anything without a trailing
291/// `b` is returned as it is.
292pub 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        // music21's own example: the minor key whose seventh degree is E is
336        // F-sharp minor in the natural form and F minor in the harmonic one,
337        // where the seventh is only a semitone below the tonic.
338        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        // The mode of the key that comes back is this key's, either way.
350        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}