Skip to main content

music21_rs/key/
keysignature.rs

1use crate::{
2    defaults::IntegerType,
3    error::{Error, Result},
4    interval::{Interval, IntervalArgument},
5    pitch::Pitch,
6    scale::FIFTHS_ORDER_SHARP,
7};
8
9use super::Key;
10
11const MODE_SHARPS_ALTER: [(&str, IntegerType); 9] = [
12    ("major", 0),
13    ("ionian", 0),
14    ("minor", -3),
15    ("aeolian", -3),
16    ("dorian", -2),
17    ("phrygian", -4),
18    ("lydian", 1),
19    ("mixolydian", -1),
20    ("locrian", -5),
21];
22
23fn canonical_mode_for_offset(offset: IntegerType) -> Option<&'static str> {
24    match offset {
25        0 => Some("ionian"),
26        -1 => Some("mixolydian"),
27        -2 => Some("dorian"),
28        -3 => Some("aeolian"),
29        -4 => Some("phrygian"),
30        -5 => Some("locrian"),
31        1 => Some("lydian"),
32        _ => None,
33    }
34}
35
36/// Returns the circle-of-fifths sharp-count offset for a mode name.
37pub fn mode_sharps_alter(mode: &str) -> Option<IntegerType> {
38    MODE_SHARPS_ALTER
39        .iter()
40        .find_map(|(name, value)| (*name == mode.to_lowercase()).then_some(*value))
41}
42
43/// Returns the major-key tonic pitch for a key-signature sharp count.
44pub fn sharps_to_pitch(sharp_count: IntegerType) -> Result<Pitch> {
45    if sharp_count == 0 {
46        return Pitch::new(
47            Some("C".to_string()),
48            None,
49            None,
50            Option::<IntegerType>::None,
51            Option::<IntegerType>::None,
52            None,
53            None,
54            None,
55            None,
56        );
57    }
58
59    let mut pitch = Pitch::new(
60        Some("C".to_string()),
61        None,
62        None,
63        Option::<IntegerType>::None,
64        Option::<IntegerType>::None,
65        None,
66        None,
67        None,
68        None,
69    )?;
70    pitch.octave_setter(None);
71
72    let interval = if sharp_count > 0 {
73        Interval::new(IntervalArgument::Str("P5".to_string()))?
74    } else {
75        Interval::new(IntervalArgument::Str("P-5".to_string()))?
76    };
77
78    for _ in 0..sharp_count.abs() {
79        pitch = pitch.transpose(interval.clone());
80        pitch.octave_setter(None);
81    }
82    Ok(pitch)
83}
84
85/// Returns the key-signature sharp count for a tonic pitch and optional mode.
86pub fn pitch_to_sharps(pitch_value: &Pitch, mode: Option<&str>) -> Result<IntegerType> {
87    let step_index = FIFTHS_ORDER_SHARP
88        .iter()
89        .position(|step| *step == pitch_value.step())
90        .ok_or_else(|| Error::StepName("cannot map step to circle of fifths".to_string()))?;
91
92    let mut sharps = step_index as IntegerType - 1;
93    let accidental_alter = pitch_value.alter().round() as IntegerType;
94    sharps += 7 * accidental_alter;
95
96    if let Some(mode) = mode {
97        let Some(mode_offset) = mode_sharps_alter(mode) else {
98            return Err(Error::Ordinal(format!("unknown mode {mode}")));
99        };
100        sharps += mode_offset;
101    }
102
103    Ok(sharps)
104}
105
106/// Returns the key-signature sharp count for a tonic pitch name and optional mode.
107pub fn pitch_name_to_sharps(pitch_name: &str, mode: Option<&str>) -> Result<IntegerType> {
108    let pitch = Pitch::new(
109        Some(pitch_name.to_string()),
110        None,
111        None,
112        Option::<IntegerType>::None,
113        Option::<IntegerType>::None,
114        None,
115        None,
116        None,
117        None,
118    )?;
119    pitch_to_sharps(&pitch, mode)
120}
121
122#[derive(Clone, Debug)]
123/// A key signature represented by the number of sharps.
124///
125/// Flats are represented as negative sharps, so B-flat major has `-2`.
126pub struct KeySignature {
127    sharps: IntegerType,
128}
129
130impl KeySignature {
131    /// Creates a key signature from a sharp count.
132    pub fn new(sharps: IntegerType) -> Self {
133        Self { sharps }
134    }
135
136    /// Returns the number of sharps, with flats as negative values.
137    pub fn sharps(&self) -> IntegerType {
138        self.sharps
139    }
140
141    /// Converts this signature to a key in the given mode.
142    pub fn as_key(&self, mode: &str) -> Key {
143        self.try_as_key(Some(mode), None).unwrap_or_else(|_| {
144            Key::new(
145                Pitch::new(
146                    Some("C".to_string()),
147                    None,
148                    None,
149                    Option::<IntegerType>::None,
150                    Option::<IntegerType>::None,
151                    None,
152                    None,
153                    None,
154                    None,
155                )
156                .expect("C is valid pitch"),
157                "major",
158                0,
159            )
160        })
161    }
162
163    /// Converts this signature to a key, optionally inferring mode from tonic.
164    pub fn try_as_key(&self, mode: Option<&str>, tonic: Option<&str>) -> Result<Key> {
165        let our_sharps = self.sharps;
166
167        let resolved_mode = if mode.is_none() && tonic.is_none() {
168            "major".to_string()
169        } else if mode.is_none() && tonic.is_some() {
170            let tonic_name = tonic.expect("checked is_some above");
171            let major_sharps = pitch_name_to_sharps(tonic_name, None)?;
172            canonical_mode_for_offset(our_sharps - major_sharps)
173                .ok_or_else(|| {
174                    Error::Ordinal(format!(
175                        "Could not solve mode from sharps={} and tonic={}",
176                        self.sharps, tonic_name
177                    ))
178                })?
179                .to_string()
180        } else {
181            mode.expect("checked is_some above").to_lowercase()
182        };
183
184        let sharp_alteration_from_major = mode_sharps_alter(&resolved_mode)
185            .ok_or_else(|| Error::Ordinal(format!("Mode {resolved_mode} is unknown")))?;
186
187        let tonic_pitch = sharps_to_pitch(our_sharps - sharp_alteration_from_major)?;
188        Ok(Key::new(tonic_pitch, &resolved_mode, our_sharps))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn keysignature_as_key_major_minor() {
198        let ks = KeySignature::new(2);
199        assert_eq!(ks.as_key("major").tonic().name(), "D");
200        assert_eq!(ks.as_key("minor").tonic().name(), "B");
201    }
202
203    #[test]
204    fn keysignature_mode_inference_from_tonic() {
205        let ks = KeySignature::new(0);
206        let key = ks.try_as_key(None, Some("D")).unwrap();
207        assert_eq!(key.mode(), "dorian");
208        assert_eq!(key.tonic().name(), "D");
209    }
210
211    #[test]
212    fn sharps_to_pitch_roundtrip() {
213        let f_sharp = sharps_to_pitch(6).unwrap();
214        assert_eq!(f_sharp.name(), "F#");
215        let b_flat = sharps_to_pitch(-2).unwrap();
216        assert_eq!(b_flat.name(), "B-");
217    }
218}