music21_rs/key/
keysignature.rs1use 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
36pub 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
43pub 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
65pub 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
86pub 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)]
93pub struct KeySignature {
97 sharps: IntegerType,
98}
99
100impl KeySignature {
101 pub fn new(sharps: IntegerType) -> Self {
103 Self { sharps }
104 }
105
106 pub fn sharps(&self) -> IntegerType {
108 self.sharps
109 }
110
111 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 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}