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
38pub 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
45pub 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
87pub 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
108pub 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)]
125pub struct KeySignature {
129 music21object: Music21Object,
130 sharps: IntegerType,
131}
132
133impl KeySignature {
134 pub fn new(sharps: IntegerType) -> Self {
136 Self {
137 music21object: Music21Object::new(),
138 sharps,
139 }
140 }
141
142 pub fn sharps(&self) -> IntegerType {
144 self.sharps
145 }
146
147 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 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}