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::from_name("C".to_string());
47    }
48
49    let mut pitch = Pitch::from_name("C".to_string())?;
50    pitch.octave_setter(None);
51
52    let interval = if sharp_count > 0 {
53        Interval::new(IntervalArgument::Str("P5".to_string()))?
54    } else {
55        Interval::new(IntervalArgument::Str("P-5".to_string()))?
56    };
57
58    for _ in 0..sharp_count.abs() {
59        pitch = pitch.transpose(&interval);
60        pitch.octave_setter(None);
61    }
62    Ok(pitch)
63}
64
65/// Returns the key-signature sharp count for a tonic pitch and optional mode.
66pub fn pitch_to_sharps(pitch_value: &Pitch, mode: Option<&str>) -> Result<IntegerType> {
67    let step_index = FIFTHS_ORDER_SHARP
68        .iter()
69        .position(|step| *step == pitch_value.step())
70        .ok_or_else(|| Error::StepName("cannot map step to circle of fifths".to_string()))?;
71
72    let mut sharps = step_index as IntegerType - 1;
73    let accidental_alter = pitch_value.alter().round() as IntegerType;
74    sharps += 7 * accidental_alter;
75
76    if let Some(mode) = mode {
77        let Some(mode_offset) = mode_sharps_alter(mode) else {
78            return Err(Error::Ordinal(format!("unknown mode {mode}")));
79        };
80        sharps += mode_offset;
81    }
82
83    Ok(sharps)
84}
85
86/// Returns the key-signature sharp count for a tonic pitch name and optional mode.
87pub fn pitch_name_to_sharps(pitch_name: &str, mode: Option<&str>) -> Result<IntegerType> {
88    let pitch = Pitch::from_name(pitch_name.to_string())?;
89    pitch_to_sharps(&pitch, mode)
90}
91
92#[derive(Clone, Debug)]
93/// A key signature represented by the number of sharps.
94///
95/// Flats are represented as negative sharps, so B-flat major has `-2`.
96pub struct KeySignature {
97    sharps: IntegerType,
98}
99
100impl KeySignature {
101    /// Creates a key signature from a sharp count.
102    pub fn new(sharps: IntegerType) -> Self {
103        Self { sharps }
104    }
105
106    /// Returns the number of sharps, with flats as negative values.
107    pub fn sharps(&self) -> IntegerType {
108        self.sharps
109    }
110
111    /// Converts this signature to a key in the given mode.
112    pub fn as_key(&self, mode: &str) -> Key {
113        self.try_as_key(Some(mode), None).unwrap_or_else(|_| {
114            Key::new(
115                Pitch::from_name("C".to_string()).expect("C is valid pitch"),
116                "major",
117                0,
118            )
119        })
120    }
121
122    /// Converts this signature to a key, optionally inferring mode from tonic.
123    pub fn try_as_key(&self, mode: Option<&str>, tonic: Option<&str>) -> Result<Key> {
124        let our_sharps = self.sharps;
125
126        let resolved_mode = if mode.is_none() && tonic.is_none() {
127            "major".to_string()
128        } else if mode.is_none() && tonic.is_some() {
129            let tonic_name = tonic.expect("checked is_some above");
130            let major_sharps = pitch_name_to_sharps(tonic_name, None)?;
131            canonical_mode_for_offset(our_sharps - major_sharps)
132                .ok_or_else(|| {
133                    Error::Ordinal(format!(
134                        "Could not solve mode from sharps={} and tonic={}",
135                        self.sharps, tonic_name
136                    ))
137                })?
138                .to_string()
139        } else {
140            mode.expect("checked is_some above").to_lowercase()
141        };
142
143        let sharp_alteration_from_major = mode_sharps_alter(&resolved_mode)
144            .ok_or_else(|| Error::Ordinal(format!("Mode {resolved_mode} is unknown")))?;
145
146        let tonic_pitch = sharps_to_pitch(our_sharps - sharp_alteration_from_major)?;
147        Ok(Key::new(tonic_pitch, &resolved_mode, our_sharps))
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn keysignature_as_key_major_minor() {
157        let ks = KeySignature::new(2);
158        assert_eq!(ks.as_key("major").tonic().name(), "D");
159        assert_eq!(ks.as_key("minor").tonic().name(), "B");
160    }
161
162    #[test]
163    fn keysignature_mode_inference_from_tonic() {
164        let ks = KeySignature::new(0);
165        let key = ks.try_as_key(None, Some("D")).unwrap();
166        assert_eq!(key.mode(), "dorian");
167        assert_eq!(key.tonic().name(), "D");
168    }
169
170    #[test]
171    fn sharps_to_pitch_roundtrip() {
172        let f_sharp = sharps_to_pitch(6).unwrap();
173        assert_eq!(f_sharp.name(), "F#");
174        let b_flat = sharps_to_pitch(-2).unwrap();
175        assert_eq!(b_flat.name(), "B-");
176    }
177}