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