1use super::*;
5use crate::scale::{Scale, ScaleType};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub(super) struct Music21FigureMatch {
9 kind: &'static str,
10 notation: &'static str,
11 abbreviation: &'static str,
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub(super) struct Music21ChordAnalysis {
16 d3: Option<u8>,
17 d5: Option<u8>,
18 d7: Option<u8>,
19 d9: Option<u8>,
20 d11: Option<u8>,
21 d13: Option<u8>,
22 is_triad: bool,
23 is_seventh: bool,
24}
25
26pub(crate) fn chord_symbol_spellings(chord: &Chord) -> Vec<String> {
30 ChordSymbolFigure::from_chord(chord)
31 .map(|figure| figure.to_string())
32 .into_iter()
33 .collect()
34}
35
36pub(crate) fn chord_symbol_spellings_with_root(chord: &Chord, root: u8) -> Vec<String> {
39 let Some(root) = chord
40 .pitches()
41 .into_iter()
42 .find(|pitch| pitch_class(pitch) == root % 12)
43 else {
44 return Vec::new();
45 };
46 ChordSymbolFigure::from_chord_with_root(chord, &root)
47 .map(|figure| figure.to_string())
48 .into_iter()
49 .collect()
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
61#[must_use]
62pub struct ChordSymbolFigure {
63 pub root: String,
68 pub kind: &'static str,
70 pub abbreviation: &'static str,
72 pub bass: Option<String>,
74 pub additions: Vec<ChordAlteration>,
82 pub omissions: Vec<u8>,
88}
89
90impl ChordSymbolFigure {
91 pub fn from_chord(chord: &Chord) -> Option<Self> {
94 let pitches = chord.pitches();
95 let microtonal = pitches
96 .iter()
97 .any(|pitch| (pitch.ps() - pitch.ps().round()).abs() > FloatType::EPSILON);
98 if pitches.is_empty() || microtonal {
99 return None;
100 }
101 let root = chord.root()?.clone();
102 if pitches.len() == 1 {
103 return Some(Self {
104 root: root.name(),
105 kind: "pedal",
106 abbreviation: "pedal",
107 bass: None,
108 additions: Vec::new(),
109 omissions: Vec::new(),
110 });
111 }
112 let matched = identify_music21_chord_type(&Music21ChordAnalysis::of(chord))?;
113 let bass = chord.bass()?.clone();
114 let inverted = pitch_class(&bass) != pitch_class(&root);
115 let (root, kind, abbreviation, notation) = if inverted && matched.kind == "suspended-second"
116 {
117 (bass.clone(), "suspended-fourth", "sus", "1,4,5")
118 } else {
119 (root, matched.kind, matched.abbreviation, matched.notation)
120 };
121 let bass = (pitch_class(&bass) != pitch_class(&root)).then(|| bass.name());
122 let mut perfect = kind_pitch_names(&root, notation).ok()?;
123 perfect.extend(bass.clone());
127 let present: BTreeSet<String> = pitches.iter().map(Pitch::name).collect();
128 let (additions, omissions) = if perfect.is_superset(&present) {
129 (Vec::new(), Vec::new())
130 } else {
131 let major = Scale::new(ScaleType::Major, root.clone());
132 let kind_degrees: Vec<u8> = notation_intervals(notation)
133 .ok()?
134 .into_iter()
135 .map(|(degree, _)| degree)
136 .collect();
137 let degree_and_alter = |name: &String| -> Option<(u8, IntegerType)> {
138 let (degree, accidental) = major
139 .degree_and_accidental_of(&Pitch::from_name(name).ok()?)
140 .ok()?;
141 let mut degree = u8::try_from(degree).ok()?;
142 if matches!(degree, 2 | 4 | 6) && !kind_degrees.contains(°ree) {
143 degree += 7;
144 }
145 Some((
146 degree,
147 accidental.map_or(0, |accidental| accidental.alter() as IntegerType),
148 ))
149 };
150 let mut additions = present
151 .difference(&perfect)
152 .map(degree_and_alter)
153 .collect::<Option<Vec<_>>>()?;
154 additions.sort_unstable();
155 let mut omissions = perfect
156 .difference(&present)
157 .map(|name| degree_and_alter(name).map(|(degree, _)| degree))
158 .collect::<Option<Vec<_>>>()?;
159 omissions.sort_unstable();
160 (
161 additions
162 .into_iter()
163 .map(|(degree, alter)| ChordAlteration::new(degree, alter))
164 .collect(),
165 omissions,
166 )
167 };
168 Some(Self {
169 root: root.name(),
170 kind,
171 abbreviation,
172 bass,
173 additions,
174 omissions,
175 })
176 }
177
178 pub fn from_chord_with_root(chord: &Chord, root: &Pitch) -> Option<Self> {
181 let mut chord = chord.clone();
182 chord.set_root(Some(root.clone()));
183 Self::from_chord(&chord)
184 }
185
186 pub fn written_with(&self, abbreviation: &str) -> String {
189 let mut figure = format!("{}{abbreviation}", self.root);
190 for addition in &self.additions {
191 figure.push_str("add");
192 figure.push_str(&alter_to_chord_symbol_string(addition.semitones()));
193 figure.push_str(&addition.degree().to_string());
194 }
195 for omission in &self.omissions {
196 figure.push_str("omit");
197 figure.push_str(&omission.to_string());
198 }
199 if let Some(bass) = &self.bass {
200 figure.push('/');
201 figure.push_str(bass);
202 }
203 figure
204 }
205}
206
207impl std::fmt::Display for ChordSymbolFigure {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.write_str(&self.written_with(self.abbreviation))
210 }
211}
212
213impl Music21ChordAnalysis {
214 fn of(chord: &Chord) -> Self {
217 let step = |degree: u8| chord.semitones_from_chord_step(degree);
218 Self {
219 d3: step(3),
220 d5: step(5),
221 d7: step(7),
222 d9: step(2),
223 d11: step(4),
224 d13: step(6),
225 is_triad: chord.is_triad(),
226 is_seventh: chord.is_seventh(),
227 }
228 }
229}
230
231pub fn alter_to_chord_symbol_string(alter: IntegerType) -> String {
235 let sign = if alter < 0 { "b" } else { "#" };
236 sign.repeat(alter.unsigned_abs() as usize)
237}
238
239pub(super) fn identify_music21_chord_type(
240 analysis: &Music21ChordAnalysis,
241) -> Option<Music21FigureMatch> {
242 let mut matched = None;
243
244 for chord_type in MUSIC21_CHORD_TYPES {
245 let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
246 let is_match = match chord_degrees.len() {
247 2 if analysis.is_triad => {
248 compare_music21_degrees(&[analysis.d3, analysis.d5], &chord_degrees, &[])
249 }
250 3 if analysis.is_seventh => compare_music21_degrees(
251 &[analysis.d3, analysis.d5, analysis.d7],
252 &chord_degrees,
253 &[],
254 ),
255 4 if music21_truthy(analysis.d9)
256 && !music21_truthy(analysis.d11)
257 && !music21_truthy(analysis.d13) =>
258 {
259 compare_music21_degrees(
260 &[analysis.d3, analysis.d5, analysis.d7, analysis.d9],
261 &chord_degrees,
262 &[5],
263 )
264 }
265 5 if music21_truthy(analysis.d11) && !music21_truthy(analysis.d13) => {
266 compare_music21_degrees(
267 &[
268 analysis.d3,
269 analysis.d5,
270 analysis.d7,
271 analysis.d9,
272 analysis.d11,
273 ],
274 &chord_degrees,
275 &[3, 5],
276 )
277 }
278 6 if music21_truthy(analysis.d13) => compare_music21_degrees(
279 &[
280 analysis.d3,
281 analysis.d5,
282 analysis.d7,
283 analysis.d9,
284 analysis.d11,
285 analysis.d13,
286 ],
287 &chord_degrees,
288 &[5, 11, 9],
289 ),
290 _ => false,
291 };
292
293 if is_match {
294 matched = Some(Music21FigureMatch {
295 kind: chord_type.kind,
296 notation: chord_type.notation,
297 abbreviation: chord_type.abbreviation,
298 });
299 }
300 }
301
302 if matched.is_some() {
303 return matched;
304 }
305
306 let mut number_of_matched_degrees = 0;
307 for chord_type in MUSIC21_CHORD_TYPES {
308 let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
309 let mut degrees = degree_numbers_for_notation(chord_type.notation)?;
310 degrees.sort_unstable();
311 let to_compare = degrees
312 .into_iter()
313 .filter(|degree| *degree != 1)
314 .map(|degree| analysis_value_for_degree(analysis, degree))
315 .collect::<Vec<_>>();
316
317 if compare_music21_degrees(&to_compare, &chord_degrees, &[])
318 && number_of_matched_degrees < chord_degrees.len()
319 {
320 number_of_matched_degrees = chord_degrees.len();
321 matched = Some(Music21FigureMatch {
322 kind: chord_type.kind,
323 notation: chord_type.notation,
324 abbreviation: chord_type.abbreviation,
325 });
326 }
327 }
328
329 matched
330}
331
332pub(super) fn compare_music21_degrees(
333 in_chord_nums: &[Option<u8>],
334 given_chord_nums: &[u8],
335 permitted_omissions: &[u8],
336) -> bool {
337 if given_chord_nums.len() > in_chord_nums.len() {
338 return false;
339 }
340
341 for (index, expected) in given_chord_nums.iter().enumerate() {
342 if in_chord_nums[index] == Some(*expected) {
343 continue;
344 }
345
346 let (degree, natural) = match index {
347 0 => (3, 4),
348 1 => (5, 7),
349 2 => (7, 11),
350 3 => (9, 2),
351 4 => (11, 5),
352 5 => (13, 9),
353 _ => return false,
354 };
355
356 if !(permitted_omissions.contains(°ree)
357 && *expected == natural
358 && in_chord_nums[index].is_none())
359 {
360 return false;
361 }
362 }
363
364 true
365}
366
367pub(super) fn music21_truthy(value: Option<u8>) -> bool {
368 value.is_some_and(|value| value != 0)
369}
370
371pub(super) fn analysis_value_for_degree(analysis: &Music21ChordAnalysis, degree: u8) -> Option<u8> {
372 match degree {
373 2 | 9 => analysis.d9,
374 3 => analysis.d3,
375 4 | 11 => analysis.d11,
376 5 => analysis.d5,
377 6 | 13 => analysis.d13,
378 7 => analysis.d7,
379 _ => None,
380 }
381}
382
383pub fn chord_symbol_figure_from_chord(chord: &Chord) -> Result<Option<String>> {
392 if chord.notes().is_empty() {
393 return Ok(Some(String::new()));
394 }
395 Ok(ChordSymbolFigure::from_chord(chord).map(|figure| figure.to_string()))
396}
397
398#[must_use]
405pub fn chord_symbol_kind_from_chord(chord: &Chord) -> Option<&'static str> {
406 ChordSymbolFigure::from_chord(chord).map(|figure| figure.kind)
407}
408pub fn chord_symbol_from_chord(chord: &Chord) -> Result<Option<ChordSymbol>> {
411 match chord_symbol_figure_from_chord(chord)? {
412 Some(figure) if !figure.is_empty() => Ok(Some(ChordSymbol::parse(figure)?)),
413 _ => Ok(None),
414 }
415}