Skip to main content

music21_rs/key/
mod.rs

1pub use keysignature::{KeySignature, pitch_name_to_sharps, pitch_to_sharps, sharps_to_pitch};
2
3use crate::{
4    chord::Chord,
5    defaults::IntegerType,
6    error::{Error, Result},
7    pitch::Pitch,
8    scale::diatonicscale::DiatonicScale,
9};
10use std::str::FromStr;
11
12/// Key-signature conversion and spelling helpers.
13pub mod keysignature;
14
15#[derive(Clone, Debug)]
16/// A tonal key with a tonic pitch and mode.
17pub struct Key {
18    tonic_pitch: Pitch,
19    mode: String,
20    sharps: IntegerType,
21}
22
23impl Key {
24    pub(crate) fn new(tonic_pitch: Pitch, mode: &str, sharps: IntegerType) -> Self {
25        Self {
26            tonic_pitch,
27            mode: mode.to_string(),
28            sharps,
29        }
30    }
31
32    /// Builds a key from a tonic and mode.
33    ///
34    /// Pass a mode string such as `"major"`, `"minor"`, `"dorian"`, or
35    /// `None::<&str>` to infer major/minor from tonic case.
36    pub fn from_tonic_mode<'a, M>(tonic: &str, mode: M) -> Result<Self>
37    where
38        M: Into<Option<&'a str>>,
39    {
40        let tonic_pitch = Pitch::new(
41            Some(tonic.to_string()),
42            None,
43            None,
44            Option::<IntegerType>::None,
45            Option::<IntegerType>::None,
46            None,
47            None,
48            None,
49            None,
50        )?;
51
52        let mode = mode.into();
53        let resolved_mode = match mode {
54            Some(mode) => mode.to_lowercase(),
55            None => {
56                if tonic.chars().all(|ch| !ch.is_ascii_uppercase()) {
57                    "minor".to_string()
58                } else {
59                    "major".to_string()
60                }
61            }
62        };
63
64        let sharps = pitch_to_sharps(&tonic_pitch, Some(&resolved_mode))?;
65        Ok(Self::new(tonic_pitch, &resolved_mode, sharps))
66    }
67
68    /// Builds a key and infers major/minor from tonic case.
69    pub fn from_tonic(tonic: &str) -> Result<Self> {
70        Self::from_tonic_mode(tonic, None::<&str>)
71    }
72
73    /// Returns a cloned tonic pitch.
74    pub fn tonic(&self) -> Pitch {
75        self.tonic_pitch.clone()
76    }
77
78    /// Returns a borrowed tonic pitch.
79    pub fn tonic_pitch(&self) -> &Pitch {
80        &self.tonic_pitch
81    }
82
83    /// Returns the key mode.
84    pub fn mode(&self) -> &str {
85        &self.mode
86    }
87
88    /// Returns the number of sharps in the key signature.
89    pub fn sharps(&self) -> IntegerType {
90        self.sharps
91    }
92
93    /// Returns the matching key signature.
94    pub fn key_signature(&self) -> KeySignature {
95        KeySignature::new(self.sharps)
96    }
97
98    /// Returns the diatonic scale for this key.
99    pub fn scale(&self) -> DiatonicScale {
100        DiatonicScale::new(self.tonic_pitch.clone(), self.sharps, &self.mode)
101    }
102
103    /// Returns the pitch at a one-based scale degree.
104    pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
105        self.scale().pitch_from_degree(degree)
106    }
107
108    /// Returns scale pitches from degree 1 through the octave.
109    pub fn pitches(&self) -> Result<Vec<Pitch>> {
110        self.scale().pitches()
111    }
112
113    /// Builds the diatonic triad on a one-based degree.
114    pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
115        self.scale().triad_from_degree(degree)
116    }
117
118    /// Builds the diatonic seventh chord on a one-based degree.
119    pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
120        self.scale().seventh_chord_from_degree(degree)
121    }
122
123    /// Returns all seven diatonic triads.
124    pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
125        (1..=7)
126            .map(|degree| self.triad_from_degree(degree))
127            .collect()
128    }
129
130    /// Returns all seven diatonic seventh chords.
131    pub fn harmonized_sevenths(&self) -> Result<Vec<Chord>> {
132        (1..=7)
133            .map(|degree| self.seventh_chord_from_degree(degree))
134            .collect()
135    }
136
137    /// Returns the relative major or minor key when applicable.
138    pub fn relative(&self) -> Result<Self> {
139        match self.mode.as_str() {
140            "major" => self.key_signature().try_as_key(Some("minor"), None),
141            "minor" => self.key_signature().try_as_key(Some("major"), None),
142            _ => Ok(self.clone()),
143        }
144    }
145
146    /// Returns the parallel major or minor key when applicable.
147    pub fn parallel(&self) -> Result<Self> {
148        match self.mode.as_str() {
149            "major" => Self::from_tonic_mode(&self.tonic_pitch.name(), "minor"),
150            "minor" => Self::from_tonic_mode(&self.tonic_pitch.name(), "major"),
151            _ => Ok(self.clone()),
152        }
153    }
154}
155
156impl FromStr for Key {
157    type Err = Error;
158
159    fn from_str(value: &str) -> Result<Self> {
160        parse_key(value)
161    }
162}
163
164impl TryFrom<&str> for Key {
165    type Error = Error;
166
167    fn try_from(value: &str) -> Result<Self> {
168        value.parse()
169    }
170}
171
172impl TryFrom<String> for Key {
173    type Error = Error;
174
175    fn try_from(value: String) -> Result<Self> {
176        value.parse()
177    }
178}
179
180fn parse_key(value: &str) -> Result<Key> {
181    let trimmed = value.trim();
182    if trimmed.is_empty() {
183        return Err(Error::Analysis("key cannot be empty".to_string()));
184    }
185
186    let parts = trimmed.split_whitespace().collect::<Vec<_>>();
187    match parts.as_slice() {
188        [tonic] => {
189            if let Some((tonic, mode)) = split_compact_key_token(tonic) {
190                Key::from_tonic_mode(tonic, Some(mode.as_str()))
191            } else {
192                Key::from_tonic(tonic)
193            }
194        }
195        [tonic, mode] => {
196            let mode = canonical_key_mode(mode);
197            Key::from_tonic_mode(tonic, Some(mode.as_str()))
198        }
199        _ => Err(Error::Analysis(format!(
200            "invalid key {value:?}; use a tonic and optional mode, such as \"C\", \"C major\", or \"Am\""
201        ))),
202    }
203}
204
205fn split_compact_key_token(token: &str) -> Option<(&str, String)> {
206    let lower = token.to_ascii_lowercase();
207    for suffix in ["major", "minor", "maj", "min", "m"] {
208        if lower.ends_with(suffix) && lower.len() > suffix.len() {
209            let tonic_end = token.len() - suffix.len();
210            return Some((&token[..tonic_end], canonical_key_mode(suffix)));
211        }
212    }
213    None
214}
215
216fn canonical_key_mode(mode: &str) -> String {
217    match mode.to_ascii_lowercase().as_str() {
218        "maj" | "major" => "major".to_string(),
219        "m" | "min" | "minor" => "minor".to_string(),
220        other => other.to_string(),
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use crate::key::keysignature::pitch_name_to_sharps;
228
229    #[test]
230    fn key_from_tonic_mode() {
231        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
232        assert_eq!(c_major.sharps(), 0);
233        let g_major = Key::from_tonic_mode("G", Some("major")).unwrap();
234        assert_eq!(g_major.sharps(), 1);
235        let a_minor = Key::from_tonic_mode("A", Some("minor")).unwrap();
236        assert_eq!(a_minor.sharps(), 0);
237        let e_phrygian = Key::from_tonic_mode("E", Some("phrygian")).unwrap();
238        assert_eq!(e_phrygian.sharps(), 0);
239    }
240
241    #[test]
242    fn key_from_string_accepts_common_notation() {
243        let c_major: Key = "C major".parse().unwrap();
244        assert_eq!(c_major.tonic().name(), "C");
245        assert_eq!(c_major.mode(), "major");
246
247        let a_minor: Key = "Am".parse().unwrap();
248        assert_eq!(a_minor.tonic().name(), "A");
249        assert_eq!(a_minor.mode(), "minor");
250
251        let b_flat_minor: Key = "Bb minor".parse().unwrap();
252        assert_eq!(b_flat_minor.tonic().name(), "B-");
253        assert_eq!(b_flat_minor.mode(), "minor");
254    }
255
256    #[test]
257    fn key_scale_degree_and_chords() {
258        let d_major = Key::from_tonic_mode("D", Some("major")).unwrap();
259        assert_eq!(
260            d_major.pitch_from_degree(7).unwrap().name_with_octave(),
261            "C#5"
262        );
263        assert_eq!(
264            d_major.triad_from_degree(1).unwrap().pitched_common_name(),
265            "D-major triad"
266        );
267        assert_eq!(
268            d_major
269                .seventh_chord_from_degree(5)
270                .unwrap()
271                .pitched_common_name(),
272            "A-dominant seventh chord"
273        );
274    }
275
276    #[test]
277    fn key_harmonized_triads() {
278        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
279        let triads = c_major.harmonized_triads().unwrap();
280        assert_eq!(triads.len(), 7);
281        assert_eq!(triads[0].pitched_common_name(), "C-major triad");
282        assert_eq!(triads[4].pitched_common_name(), "G-major triad");
283    }
284
285    #[test]
286    fn key_relative_and_parallel() {
287        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
288        let relative = c_major.relative().unwrap();
289        assert_eq!(relative.mode(), "minor");
290        assert_eq!(relative.tonic().name(), "A");
291
292        let parallel = c_major.parallel().unwrap();
293        assert_eq!(parallel.mode(), "minor");
294        assert_eq!(parallel.tonic().name(), "C");
295    }
296
297    #[test]
298    fn pitch_name_to_sharps_modes() {
299        assert_eq!(pitch_name_to_sharps("C", Some("major")).unwrap(), 0);
300        assert_eq!(pitch_name_to_sharps("E", Some("minor")).unwrap(), 1);
301        assert_eq!(pitch_name_to_sharps("D", Some("dorian")).unwrap(), 0);
302        assert_eq!(pitch_name_to_sharps("A", Some("mixolydian")).unwrap(), 2);
303    }
304}