Skip to main content

music21_rs/key/
mod.rs

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