1use super::*;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub(super) struct Music21FigureMatch {
8 kind: &'static str,
9 notation: &'static str,
10 abbreviation: &'static str,
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub(super) struct Music21ChordAnalysis {
15 d3: Option<u8>,
16 d5: Option<u8>,
17 d7: Option<u8>,
18 d9: Option<u8>,
19 d11: Option<u8>,
20 d13: Option<u8>,
21 is_triad: bool,
22 is_seventh: bool,
23}
24
25pub(crate) fn chord_symbol_spellings(chord: &Chord) -> Vec<String> {
29 ChordSymbolFigure::from_chord(chord)
30 .map(|figure| figure.to_string())
31 .into_iter()
32 .collect()
33}
34
35pub(crate) fn chord_symbol_spellings_with_root(chord: &Chord, root: u8) -> Vec<String> {
38 let Some(root) = chord
39 .pitches()
40 .into_iter()
41 .find(|pitch| pitch_class(pitch) == root % 12)
42 else {
43 return Vec::new();
44 };
45 ChordSymbolFigure::from_chord_with_root(chord, &root)
46 .map(|figure| figure.to_string())
47 .into_iter()
48 .collect()
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
60#[must_use]
61pub struct ChordSymbolFigure {
62 pub root: String,
67 pub kind: &'static str,
69 pub abbreviation: &'static str,
71 pub bass: Option<String>,
73 pub additions: Vec<String>,
75 pub omissions: Vec<String>,
79}
80
81impl ChordSymbolFigure {
82 pub fn from_chord(chord: &Chord) -> Option<Self> {
85 let pitches = chord.pitches();
86 let microtonal = pitches
87 .iter()
88 .any(|pitch| (pitch.ps() - pitch.ps().round()).abs() > FloatType::EPSILON);
89 if pitches.is_empty() || microtonal {
90 return None;
91 }
92 let root = chord.root()?.clone();
93 if pitches.len() == 1 {
94 return Some(Self {
95 root: root.name(),
96 kind: "pedal",
97 abbreviation: "pedal",
98 bass: None,
99 additions: Vec::new(),
100 omissions: Vec::new(),
101 });
102 }
103 let matched = identify_music21_chord_type(&Music21ChordAnalysis::of(chord))?;
104 let bass = chord.bass()?.clone();
105 let inverted = pitch_class(&bass) != pitch_class(&root);
106 let (root, kind, abbreviation, notation) = if inverted && matched.kind == "suspended-second"
107 {
108 (bass.clone(), "suspended-fourth", "sus", "1,4,5")
109 } else {
110 (root, matched.kind, matched.abbreviation, matched.notation)
111 };
112 let bass = (pitch_class(&bass) != pitch_class(&root)).then(|| bass.name());
113 let mut perfect = kind_pitch_names(&root, notation).ok()?;
114 perfect.extend(bass.clone());
118 let present: BTreeSet<String> = pitches.iter().map(Pitch::name).collect();
119 let (additions, omissions) = if perfect.is_superset(&present) {
120 (Vec::new(), Vec::new())
121 } else {
122 (
123 present.difference(&perfect).cloned().collect(),
124 perfect.difference(&present).cloned().collect(),
125 )
126 };
127 Some(Self {
128 root: root.name(),
129 kind,
130 abbreviation,
131 bass,
132 additions,
133 omissions,
134 })
135 }
136
137 pub fn from_chord_with_root(chord: &Chord, root: &Pitch) -> Option<Self> {
140 let mut chord = chord.clone();
141 chord.set_root(Some(root.clone()));
142 Self::from_chord(&chord)
143 }
144
145 pub fn written_with(&self, abbreviation: &str) -> String {
148 let mut figure = format!("{}{abbreviation}", self.root);
149 if let Some(bass) = &self.bass {
150 figure.push('/');
151 figure.push_str(bass);
152 }
153 if !self.additions.is_empty() {
154 figure.push_str("add");
155 figure.push_str(&self.additions.join(","));
156 if !self.omissions.is_empty() {
157 figure.push_str(",omit");
158 figure.push_str(&self.omissions.join(","));
159 }
160 }
161 figure
162 }
163}
164
165impl std::fmt::Display for ChordSymbolFigure {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 f.write_str(&self.written_with(self.abbreviation))
168 }
169}
170
171impl Music21ChordAnalysis {
172 fn of(chord: &Chord) -> Self {
175 let step = |degree: u8| chord.semitones_from_chord_step(degree);
176 Self {
177 d3: step(3),
178 d5: step(5),
179 d7: step(7),
180 d9: step(2),
181 d11: step(4),
182 d13: step(6),
183 is_triad: chord.is_triad(),
184 is_seventh: chord.is_seventh(),
185 }
186 }
187}
188pub(super) fn identify_music21_chord_type(
189 analysis: &Music21ChordAnalysis,
190) -> Option<Music21FigureMatch> {
191 let mut matched = None;
192
193 for chord_type in MUSIC21_CHORD_TYPES {
194 let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
195 let is_match = match chord_degrees.len() {
196 2 if analysis.is_triad => {
197 compare_music21_degrees(&[analysis.d3, analysis.d5], &chord_degrees, &[])
198 }
199 3 if analysis.is_seventh => compare_music21_degrees(
200 &[analysis.d3, analysis.d5, analysis.d7],
201 &chord_degrees,
202 &[],
203 ),
204 4 if music21_truthy(analysis.d9)
205 && !music21_truthy(analysis.d11)
206 && !music21_truthy(analysis.d13) =>
207 {
208 compare_music21_degrees(
209 &[analysis.d3, analysis.d5, analysis.d7, analysis.d9],
210 &chord_degrees,
211 &[5],
212 )
213 }
214 5 if music21_truthy(analysis.d11) && !music21_truthy(analysis.d13) => {
215 compare_music21_degrees(
216 &[
217 analysis.d3,
218 analysis.d5,
219 analysis.d7,
220 analysis.d9,
221 analysis.d11,
222 ],
223 &chord_degrees,
224 &[3, 5],
225 )
226 }
227 6 if music21_truthy(analysis.d13) => compare_music21_degrees(
228 &[
229 analysis.d3,
230 analysis.d5,
231 analysis.d7,
232 analysis.d9,
233 analysis.d11,
234 analysis.d13,
235 ],
236 &chord_degrees,
237 &[5, 11, 9],
238 ),
239 _ => false,
240 };
241
242 if is_match {
243 matched = Some(Music21FigureMatch {
244 kind: chord_type.kind,
245 notation: chord_type.notation,
246 abbreviation: chord_type.abbreviation,
247 });
248 }
249 }
250
251 if matched.is_some() {
252 return matched;
253 }
254
255 let mut number_of_matched_degrees = 0;
256 for chord_type in MUSIC21_CHORD_TYPES {
257 let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
258 let mut degrees = degree_numbers_for_notation(chord_type.notation)?;
259 degrees.sort_unstable();
260 let to_compare = degrees
261 .into_iter()
262 .filter(|degree| *degree != 1)
263 .map(|degree| analysis_value_for_degree(analysis, degree))
264 .collect::<Vec<_>>();
265
266 if compare_music21_degrees(&to_compare, &chord_degrees, &[])
267 && number_of_matched_degrees < chord_degrees.len()
268 {
269 number_of_matched_degrees = chord_degrees.len();
270 matched = Some(Music21FigureMatch {
271 kind: chord_type.kind,
272 notation: chord_type.notation,
273 abbreviation: chord_type.abbreviation,
274 });
275 }
276 }
277
278 matched
279}
280
281pub(super) fn compare_music21_degrees(
282 in_chord_nums: &[Option<u8>],
283 given_chord_nums: &[u8],
284 permitted_omissions: &[u8],
285) -> bool {
286 if given_chord_nums.len() > in_chord_nums.len() {
287 return false;
288 }
289
290 for (index, expected) in given_chord_nums.iter().enumerate() {
291 if in_chord_nums[index] == Some(*expected) {
292 continue;
293 }
294
295 let (degree, natural) = match index {
296 0 => (3, 4),
297 1 => (5, 7),
298 2 => (7, 11),
299 3 => (9, 2),
300 4 => (11, 5),
301 5 => (13, 9),
302 _ => return false,
303 };
304
305 if !(permitted_omissions.contains(°ree)
306 && *expected == natural
307 && in_chord_nums[index].is_none())
308 {
309 return false;
310 }
311 }
312
313 true
314}
315
316pub(super) fn music21_truthy(value: Option<u8>) -> bool {
317 value.is_some_and(|value| value != 0)
318}
319
320pub(super) fn analysis_value_for_degree(analysis: &Music21ChordAnalysis, degree: u8) -> Option<u8> {
321 match degree {
322 2 | 9 => analysis.d9,
323 3 => analysis.d3,
324 4 | 11 => analysis.d11,
325 5 => analysis.d5,
326 6 | 13 => analysis.d13,
327 7 => analysis.d7,
328 _ => None,
329 }
330}
331
332pub fn chord_symbol_figure_from_chord(chord: &Chord) -> Result<Option<String>> {
341 if chord.notes().is_empty() {
342 return Ok(Some(String::new()));
343 }
344 Ok(ChordSymbolFigure::from_chord(chord).map(|figure| figure.to_string()))
345}
346
347#[must_use]
354pub fn chord_symbol_kind_from_chord(chord: &Chord) -> Option<&'static str> {
355 ChordSymbolFigure::from_chord(chord).map(|figure| figure.kind)
356}
357pub fn chord_symbol_from_chord(chord: &Chord) -> Result<Option<ChordSymbol>> {
360 match chord_symbol_figure_from_chord(chord)? {
361 Some(figure) if !figure.is_empty() => Ok(Some(ChordSymbol::parse(figure)?)),
362 _ => Ok(None),
363 }
364}