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
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::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
85pub 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
106pub 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)]
123pub struct KeySignature {
127 sharps: IntegerType,
128}
129
130impl KeySignature {
131 pub fn new(sharps: IntegerType) -> Self {
133 Self { sharps }
134 }
135
136 pub fn sharps(&self) -> IntegerType {
138 self.sharps
139 }
140
141 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 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}