Skip to main content

music21_rs/key/
keysignature.rs

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