| 1 | //! Chord symbols: parsing (`F#m7b5`, `Bb13#11/D`, `C6/9`), spelling, and identification |
| 2 | //! from a set of notes. |
| 3 | |
| 4 | use anyhow::{Result, bail}; |
| 5 | |
| 6 | use super::interval::Interval; |
| 7 | use super::pitch::Note; |
| 8 | |
| 9 | #[derive(Debug, Clone, PartialEq)] |
| 10 | pub struct Chord { |
| 11 | pub root: Note, |
| 12 | pub bass: Option<Note>, |
| 13 | /// Chord tones above the root, sorted by degree, root (`1`) included. |
| 14 | pub intervals: Vec<Interval>, |
| 15 | pub symbol: String, |
| 16 | } |
| 17 | |
| 18 | #[derive(Default)] |
| 19 | struct Spec { |
| 20 | major: bool, |
| 21 | minor: bool, |
| 22 | dim: bool, |
| 23 | half_dim: bool, |
| 24 | aug: bool, |
| 25 | sus: Option<u8>, |
| 26 | power: bool, |
| 27 | sixth: bool, |
| 28 | seventh: bool, |
| 29 | nine: bool, |
| 30 | eleven: bool, |
| 31 | thirteen: bool, |
| 32 | adds: Vec<Interval>, |
| 33 | no3: bool, |
| 34 | no5: bool, |
| 35 | alt: bool, |
| 36 | /// (accidental, degree number) from tokens such as `b5`, `#9`, `#11`, `b13`. |
| 37 | alterations: Vec<(i32, u32)>, |
| 38 | seen_number: bool, |
| 39 | } |
| 40 | |
| 41 | fn take<'a>(r: &'a str, options: &[&str]) -> Option<&'a str> { |
| 42 | options.iter().find_map(|o| r.strip_prefix(o)) |
| 43 | } |
| 44 | |
| 45 | impl Chord { |
| 46 | pub fn parse(symbol: &str) -> Result<Chord> { |
| 47 | let original = symbol.trim(); |
| 48 | let s = original |
| 49 | .replace("6/9", "69") |
| 50 | .replace(['(', ')', ',', ' '], ""); |
| 51 | let mut chars = s.chars(); |
| 52 | let letter = chars.next().filter(|c| matches!(c, 'A'..='G')); |
| 53 | let Some(letter) = letter else { |
| 54 | bail!( |
| 55 | "not a chord symbol: `{original}` (must start with a note A-G, e.g. Cmaj7, F#m7b5)" |
| 56 | ); |
| 57 | }; |
| 58 | let mut rest = &s[1..]; |
| 59 | let mut acc = 0i8; |
| 60 | if let Some(c) = rest.chars().next() { |
| 61 | match c { |
| 62 | '#' | '♯' => acc = 1, |
| 63 | 'b' | '♭' => acc = -1, |
| 64 | _ => {} |
| 65 | } |
| 66 | if acc != 0 { |
| 67 | rest = &rest[c.len_utf8()..]; |
| 68 | } |
| 69 | } |
| 70 | let root = Note::parse(&format!( |
| 71 | "{letter}{}", |
| 72 | match acc { |
| 73 | 1 => "#", |
| 74 | -1 => "b", |
| 75 | _ => "", |
| 76 | } |
| 77 | ))?; |
| 78 | |
| 79 | let mut bass = None; |
| 80 | if let Some((head, tail)) = rest.rsplit_once('/') { |
| 81 | match Note::parse(tail) { |
| 82 | Ok(n) => bass = Some(n), |
| 83 | Err(_) => bail!("bad bass note `{tail}` in `{original}`"), |
| 84 | } |
| 85 | rest = head; |
| 86 | } |
| 87 | |
| 88 | let spec = parse_suffix(rest, original)?; |
| 89 | Ok(Chord { |
| 90 | root, |
| 91 | bass, |
| 92 | intervals: build_intervals(&spec), |
| 93 | symbol: original.to_string(), |
| 94 | }) |
| 95 | } |
| 96 | |
| 97 | pub fn notes(&self) -> Vec<Note> { |
| 98 | self.intervals |
| 99 | .iter() |
| 100 | .map(|i| self.root.up(i.steps, i.semis)) |
| 101 | .collect() |
| 102 | } |
| 103 | |
| 104 | pub fn formula(&self) -> String { |
| 105 | self.intervals |
| 106 | .iter() |
| 107 | .map(|i| i.degree_token()) |
| 108 | .collect::<Vec<_>>() |
| 109 | .join(" ") |
| 110 | } |
| 111 | |
| 112 | /// Pitch classes of the chord tones, sorted and de-duplicated. |
| 113 | pub fn pcs(&self) -> Vec<i32> { |
| 114 | let mut v: Vec<i32> = self.notes().iter().map(|n| n.pc()).collect(); |
| 115 | v.sort_unstable(); |
| 116 | v.dedup(); |
| 117 | v |
| 118 | } |
| 119 | |
| 120 | pub fn has(&self, steps: usize, semis: i32) -> bool { |
| 121 | self.intervals |
| 122 | .iter() |
| 123 | .any(|i| i.steps == steps && i.semis == semis) |
| 124 | } |
| 125 | |
| 126 | pub fn third(&self) -> Option<Interval> { |
| 127 | self.intervals.iter().copied().find(|i| i.steps == 2) |
| 128 | } |
| 129 | |
| 130 | pub fn fifth(&self) -> Option<Interval> { |
| 131 | self.intervals.iter().copied().find(|i| i.steps == 4) |
| 132 | } |
| 133 | |
| 134 | pub fn seventh(&self) -> Option<Interval> { |
| 135 | self.intervals.iter().copied().find(|i| i.steps == 6) |
| 136 | } |
| 137 | |
| 138 | pub fn is_minor_third(&self) -> bool { |
| 139 | self.has(2, 3) |
| 140 | } |
| 141 | |
| 142 | /// A major third plus a minor seventh: functions as a dominant. |
| 143 | pub fn is_dominant(&self) -> bool { |
| 144 | self.has(2, 4) && self.has(6, 10) |
| 145 | } |
| 146 | |
| 147 | /// Re-spell the chord on a different root (used by transposition). |
| 148 | pub fn with_root(&self, root: Note, bass: Option<Note>) -> Chord { |
| 149 | Chord { |
| 150 | root, |
| 151 | bass, |
| 152 | intervals: self.intervals.clone(), |
| 153 | symbol: String::new(), |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | /// Canonical symbol, e.g. `Bb13#11/D`. |
| 158 | pub fn name(&self) -> String { |
| 159 | let mut s = format!("{}{}", self.root.name(), self.suffix()); |
| 160 | if let Some(b) = self.bass { |
| 161 | s.push('/'); |
| 162 | s.push_str(&b.name()); |
| 163 | } |
| 164 | s |
| 165 | } |
| 166 | |
| 167 | /// The quality suffix in a compact canonical form (`m7b5`, `maj9`, `7#9`, `sus4`, ...). |
| 168 | pub fn suffix(&self) -> String { |
| 169 | let third = self.third().map(|i| i.semis); |
| 170 | let fifth = self.fifth().map(|i| i.semis); |
| 171 | let seventh = self.seventh().map(|i| i.semis); |
| 172 | let nat = |steps: usize, semis: i32| self.has(steps, semis); |
| 173 | let sus2 = self.intervals.iter().any(|i| i.steps == 1 && i.semis == 2) && third.is_none(); |
| 174 | let sus4 = nat(3, 5) && third.is_none(); |
| 175 | let mut out = String::new(); |
| 176 | let mut alts: Vec<String> = Vec::new(); |
| 177 | |
| 178 | // Highest stacked natural extension replaces the plain 7. |
| 179 | let ext = if nat(12, 21) { |
| 180 | Some(13) |
| 181 | } else if nat(10, 17) && nat(8, 14) { |
| 182 | Some(11) |
| 183 | } else if nat(8, 14) { |
| 184 | Some(9) |
| 185 | } else { |
| 186 | None |
| 187 | }; |
| 188 | let dim_triad = third == Some(3) && fifth == Some(6); |
| 189 | match (third, fifth, seventh) { |
| 190 | (Some(3), Some(6), Some(10)) => out.push_str("m7b5"), |
| 191 | (Some(3), Some(6), Some(9)) => out.push_str("dim7"), |
| 192 | (Some(3), Some(6), None) => out.push_str("dim"), |
| 193 | (Some(4), Some(8), None) => out.push_str("aug"), |
| 194 | (Some(4), _, Some(11)) => { |
| 195 | out.push_str("maj"); |
| 196 | out.push_str(&ext.map_or("7".into(), |e| e.to_string())); |
| 197 | } |
| 198 | (Some(3), _, Some(11)) => { |
| 199 | out.push_str("mMaj"); |
| 200 | out.push_str(&ext.map_or("7".into(), |e| e.to_string())); |
| 201 | } |
| 202 | (Some(3), _, Some(10)) => { |
| 203 | out.push('m'); |
| 204 | out.push_str(&ext.map_or("7".into(), |e| e.to_string())); |
| 205 | } |
| 206 | (Some(3), _, Some(9)) => out.push_str("dim7"), |
| 207 | (Some(3), _, None) => out.push('m'), |
| 208 | (Some(4), _, Some(10)) => out.push_str(&ext.map_or("7".into(), |e| e.to_string())), |
| 209 | (None, _, Some(10)) if sus4 || sus2 => { |
| 210 | out.push_str(&ext.map_or("7".into(), |e| e.to_string())); |
| 211 | } |
| 212 | (Some(4), _, Some(9)) => out.push('7'), |
| 213 | _ => {} |
| 214 | } |
| 215 | if third.is_none() { |
| 216 | if sus4 { |
| 217 | out.push_str("sus4"); |
| 218 | } else if sus2 { |
| 219 | out.push_str("sus2"); |
| 220 | } else if fifth.is_some() && self.intervals.len() == 2 { |
| 221 | out.push('5'); |
| 222 | } |
| 223 | } |
| 224 | if matches!(third, Some(3 | 4)) && seventh.is_none() && nat(5, 9) { |
| 225 | out.push_str(if nat(8, 14) { "69" } else { "6" }); |
| 226 | } |
| 227 | if seventh.is_none() && !nat(5, 9) { |
| 228 | for (steps, semis, n) in [(1, 2, 2), (8, 14, 9), (3, 5, 4), (10, 17, 11), (12, 21, 13)] |
| 229 | { |
| 230 | if nat(steps, semis) && !(n == 4 && sus4) && !(n == 2 && sus2) { |
| 231 | alts.push(format!("add{n}")); |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | // Altered tones. |
| 236 | match fifth { |
| 237 | Some(6) if !dim_triad => alts.push("b5".into()), |
| 238 | Some(8) if !(third == Some(4) && seventh.is_none()) => alts.push("#5".into()), |
| 239 | _ => {} |
| 240 | } |
| 241 | for i in &self.intervals { |
| 242 | if i.steps == 8 && i.semis == 13 { |
| 243 | alts.push("b9".into()); |
| 244 | } |
| 245 | if i.steps == 8 && i.semis == 15 { |
| 246 | alts.push("#9".into()); |
| 247 | } |
| 248 | if i.steps == 10 && i.semis == 18 { |
| 249 | alts.push("#11".into()); |
| 250 | } |
| 251 | if i.steps == 12 && i.semis == 20 { |
| 252 | alts.push("b13".into()); |
| 253 | } |
| 254 | } |
| 255 | if third == Some(4) |
| 256 | && seventh == Some(10) |
| 257 | && fifth.is_none() |
| 258 | && ["b9", "#9", "#11", "b13"] |
| 259 | .iter() |
| 260 | .all(|a| alts.iter().any(|x| x == a)) |
| 261 | { |
| 262 | return "7alt".into(); |
| 263 | } |
| 264 | for a in alts { |
| 265 | out.push_str(&a); |
| 266 | } |
| 267 | out |
| 268 | } |
| 269 | |
| 270 | /// Plain-language description of the chord's quality. |
| 271 | pub fn quality(&self) -> String { |
| 272 | let third = self.third().map(|i| i.semis); |
| 273 | let fifth = self.fifth().map(|i| i.semis); |
| 274 | let seventh = self.seventh().map(|i| i.semis); |
| 275 | let ext = if self.has(12, 21) { |
| 276 | "13th" |
| 277 | } else if self.has(10, 17) && self.has(8, 14) { |
| 278 | "11th" |
| 279 | } else if self.has(8, 14) { |
| 280 | "9th" |
| 281 | } else { |
| 282 | "7th" |
| 283 | }; |
| 284 | let sus = if self.has(3, 5) && third.is_none() { |
| 285 | Some("sus4") |
| 286 | } else if self.has(1, 2) && third.is_none() { |
| 287 | Some("sus2") |
| 288 | } else { |
| 289 | None |
| 290 | }; |
| 291 | let base = match (third, fifth, seventh, sus) { |
| 292 | (_, _, Some(10), Some(s)) => format!("dominant {ext} ({s})"), |
| 293 | (_, _, None, Some(s)) => s.to_string(), |
| 294 | (None, Some(_), None, None) => "power chord (no third)".into(), |
| 295 | (Some(3), Some(6), Some(10), _) => "half-diminished (minor 7 flat 5)".into(), |
| 296 | (Some(3), Some(6), Some(9), _) => "diminished 7th".into(), |
| 297 | (Some(3), Some(6), None, _) => "diminished triad".into(), |
| 298 | (Some(4), Some(8), None, _) => "augmented triad".into(), |
| 299 | (Some(4), Some(8), Some(10), _) => "augmented 7th (dominant 7 sharp 5)".into(), |
| 300 | (Some(4), _, Some(11), _) => format!("major {ext}"), |
| 301 | (Some(3), _, Some(11), _) => format!("minor-major {ext}"), |
| 302 | (Some(3), _, Some(10), _) => format!("minor {ext}"), |
| 303 | (Some(4), _, Some(10), _) => format!("dominant {ext}"), |
| 304 | (Some(3), _, None, _) => "minor triad".into(), |
| 305 | (Some(4), _, None, _) => "major triad".into(), |
| 306 | _ => "non-tertian or incomplete chord".into(), |
| 307 | }; |
| 308 | let mut extras: Vec<&str> = Vec::new(); |
| 309 | if seventh.is_none() && self.has(5, 9) { |
| 310 | extras.push("added 6th"); |
| 311 | } |
| 312 | let alts: Vec<String> = self |
| 313 | .intervals |
| 314 | .iter() |
| 315 | .filter(|i| matches!((i.steps, i.semis), (8, 13) | (8, 15) | (10, 18) | (12, 20))) |
| 316 | .map(|i| i.degree_token()) |
| 317 | .collect(); |
| 318 | let mut text = base; |
| 319 | for e in extras { |
| 320 | text.push_str(&format!(", {e}")); |
| 321 | } |
| 322 | if !alts.is_empty() { |
| 323 | text.push_str(&format!(", altered tones: {}", alts.join(" "))); |
| 324 | } |
| 325 | text |
| 326 | } |
| 327 | |
| 328 | /// Inversion / slash-bass description, if the chord has a bass note. |
| 329 | pub fn bass_description(&self) -> Option<String> { |
| 330 | let bass = self.bass?; |
| 331 | if bass == self.root { |
| 332 | return Some("root position".into()); |
| 333 | } |
| 334 | let pos = self.notes().iter().position(|n| n.pc() == bass.pc()); |
| 335 | Some(match pos.map(|p| self.intervals[p]) { |
| 336 | Some(i) if i.steps == 2 => "first inversion (third in the bass)".into(), |
| 337 | Some(i) if i.steps == 4 => "second inversion (fifth in the bass)".into(), |
| 338 | Some(i) if i.steps == 6 => "third inversion (seventh in the bass)".into(), |
| 339 | Some(i) => format!("{} in the bass", i.name()), |
| 340 | None => "slash chord (bass note is not a chord tone)".into(), |
| 341 | }) |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | fn parse_suffix(rest: &str, original: &str) -> Result<Spec> { |
| 346 | let mut st = Spec::default(); |
| 347 | let mut r = rest; |
| 348 | while !r.is_empty() { |
| 349 | // Alterations like b5, #9, #11, b13 (and +5 / -9 once a number has been seen). |
| 350 | let alt_marks: &[char] = if st.seen_number { |
| 351 | &['#', 'b', '+', '-', '♯', '♭'] |
| 352 | } else { |
| 353 | &['#', 'b', '♯', '♭'] |
| 354 | }; |
| 355 | if r.starts_with(alt_marks) { |
| 356 | let body = r.trim_start_matches(alt_marks); |
| 357 | let marks = &r[..r.len() - body.len()]; |
| 358 | let digits: String = body.chars().take_while(char::is_ascii_digit).collect(); |
| 359 | if !digits.is_empty() { |
| 360 | let acc: i32 = marks |
| 361 | .chars() |
| 362 | .map(|c| { |
| 363 | if matches!(c, '#' | '+' | '♯') { |
| 364 | 1 |
| 365 | } else { |
| 366 | -1 |
| 367 | } |
| 368 | }) |
| 369 | .sum(); |
| 370 | let n: u32 = digits.parse()?; |
| 371 | if !matches!(n, 5 | 9 | 11 | 13) { |
| 372 | bail!("unsupported alteration `{marks}{digits}` in `{original}`"); |
| 373 | } |
| 374 | st.alterations.push((acc, n)); |
| 375 | r = &body[digits.len()..]; |
| 376 | continue; |
| 377 | } |
| 378 | } |
| 379 | if let Some(t) = take(r, &["maj", "Maj", "MAJ", "M", "Δ", "△"]) { |
| 380 | st.major = true; |
| 381 | r = t; |
| 382 | } else if let Some(t) = take(r, &["min", "mi", "m", "-", "−"]) { |
| 383 | st.minor = true; |
| 384 | r = t; |
| 385 | } else if let Some(t) = take(r, &["half-dim", "halfdim", "ø", "Ø"]) { |
| 386 | st.half_dim = true; |
| 387 | st.seventh = true; |
| 388 | r = t; |
| 389 | } else if let Some(t) = take(r, &["dim", "°", "º", "o"]) { |
| 390 | st.dim = true; |
| 391 | r = t; |
| 392 | } else if let Some(t) = take(r, &["aug", "+"]) { |
| 393 | st.aug = true; |
| 394 | r = t; |
| 395 | } else if let Some(t) = take(r, &["sus2"]) { |
| 396 | st.sus = Some(2); |
| 397 | r = t; |
| 398 | } else if let Some(t) = take(r, &["sus4", "sus"]) { |
| 399 | st.sus = Some(4); |
| 400 | r = t; |
| 401 | } else if let Some(t) = take(r, &["add", "Add"]) { |
| 402 | let digits: String = t.chars().take_while(char::is_ascii_digit).collect(); |
| 403 | let interval = match digits.as_str() { |
| 404 | "2" => Interval::new(1, 2), |
| 405 | "4" => Interval::new(3, 5), |
| 406 | "6" => Interval::new(5, 9), |
| 407 | "9" => Interval::new(8, 14), |
| 408 | "11" => Interval::new(10, 17), |
| 409 | "13" => Interval::new(12, 21), |
| 410 | _ => bail!("unsupported `add{digits}` in `{original}`"), |
| 411 | }; |
| 412 | st.adds.push(interval); |
| 413 | r = &t[digits.len()..]; |
| 414 | } else if let Some(t) = take(r, &["no3", "omit3"]) { |
| 415 | st.no3 = true; |
| 416 | r = t; |
| 417 | } else if let Some(t) = take(r, &["no5", "omit5"]) { |
| 418 | st.no5 = true; |
| 419 | r = t; |
| 420 | } else if let Some(t) = take(r, &["alt"]) { |
| 421 | st.alt = true; |
| 422 | st.seventh = true; |
| 423 | r = t; |
| 424 | } else if let Some(t) = take(r, &["69"]) { |
| 425 | st.sixth = true; |
| 426 | st.adds.push(Interval::new(8, 14)); |
| 427 | st.seen_number = true; |
| 428 | r = t; |
| 429 | } else if let Some(t) = take(r, &["13"]) { |
| 430 | st.seventh = true; |
| 431 | st.nine = true; |
| 432 | st.thirteen = true; |
| 433 | st.seen_number = true; |
| 434 | r = t; |
| 435 | } else if let Some(t) = take(r, &["11"]) { |
| 436 | st.seventh = true; |
| 437 | st.nine = true; |
| 438 | st.eleven = true; |
| 439 | st.seen_number = true; |
| 440 | r = t; |
| 441 | } else if let Some(t) = take(r, &["9"]) { |
| 442 | st.seventh = true; |
| 443 | st.nine = true; |
| 444 | st.seen_number = true; |
| 445 | r = t; |
| 446 | } else if let Some(t) = take(r, &["7"]) { |
| 447 | st.seventh = true; |
| 448 | st.seen_number = true; |
| 449 | r = t; |
| 450 | } else if let Some(t) = take(r, &["6"]) { |
| 451 | st.sixth = true; |
| 452 | st.seen_number = true; |
| 453 | r = t; |
| 454 | } else if let Some(t) = take(r, &["5"]) { |
| 455 | st.power = true; |
| 456 | st.seen_number = true; |
| 457 | r = t; |
| 458 | } else if let Some(t) = take(r, &["4"]) { |
| 459 | st.adds.push(Interval::new(3, 5)); |
| 460 | st.seen_number = true; |
| 461 | r = t; |
| 462 | } else if let Some(t) = take(r, &["2"]) { |
| 463 | st.adds.push(Interval::new(1, 2)); |
| 464 | st.seen_number = true; |
| 465 | r = t; |
| 466 | } else { |
| 467 | bail!("unrecognized chord suffix `{r}` in `{original}`"); |
| 468 | } |
| 469 | } |
| 470 | if st.minor && st.major && !st.seventh { |
| 471 | // "mM" with no number: treat as minor-major 7th. |
| 472 | st.seventh = true; |
| 473 | } |
| 474 | Ok(st) |
| 475 | } |
| 476 | |
| 477 | fn build_intervals(st: &Spec) -> Vec<Interval> { |
| 478 | let mut v = vec![Interval::new(0, 0)]; |
| 479 | let minor_third = st.minor || st.dim || st.half_dim; |
| 480 | let half_dim_or_dim = st.dim || st.half_dim; |
| 481 | if st.power && !st.seventh && !st.sixth && !st.major && !st.minor && !st.dim { |
| 482 | v.push(Interval::new(4, 7)); |
| 483 | } else { |
| 484 | if !st.no3 { |
| 485 | match st.sus { |
| 486 | Some(2) => v.push(Interval::new(1, 2)), |
| 487 | Some(_) => v.push(Interval::new(3, 5)), |
| 488 | None => v.push(Interval::new(2, if minor_third { 3 } else { 4 })), |
| 489 | } |
| 490 | } |
| 491 | let mut fifth = if half_dim_or_dim { |
| 492 | 6 |
| 493 | } else if st.aug { |
| 494 | 8 |
| 495 | } else { |
| 496 | 7 |
| 497 | }; |
| 498 | for (acc, n) in &st.alterations { |
| 499 | if *n == 5 { |
| 500 | fifth = 7 + acc; |
| 501 | } |
| 502 | } |
| 503 | if !st.no5 && !st.alt { |
| 504 | v.push(Interval::new(4, fifth)); |
| 505 | } |
| 506 | } |
| 507 | if st.sixth { |
| 508 | v.push(Interval::new(5, 9)); |
| 509 | } |
| 510 | if st.seventh { |
| 511 | let semis = if st.dim && !st.half_dim { |
| 512 | 9 |
| 513 | } else if st.major && !st.half_dim { |
| 514 | 11 |
| 515 | } else { |
| 516 | 10 |
| 517 | }; |
| 518 | v.push(Interval::new(6, semis)); |
| 519 | } |
| 520 | let mut nine = st.nine; |
| 521 | let mut eleven = st.eleven; |
| 522 | let mut thirteen = st.thirteen; |
| 523 | if st.thirteen && minor_third && !st.no3 { |
| 524 | eleven = true; |
| 525 | } |
| 526 | for a in &st.adds { |
| 527 | match (a.steps, a.semis) { |
| 528 | (8, 14) => nine = true, |
| 529 | (10, 17) => eleven = true, |
| 530 | (12, 21) => thirteen = true, |
| 531 | _ => v.push(*a), |
| 532 | } |
| 533 | } |
| 534 | let mut altered = |steps: usize, base: i32, flag: &mut bool, n: u32| { |
| 535 | for (acc, num) in &st.alterations { |
| 536 | if *num == n { |
| 537 | v.push(Interval::new(steps, base + acc)); |
| 538 | *flag = false; |
| 539 | } |
| 540 | } |
| 541 | }; |
| 542 | altered(8, 14, &mut nine, 9); |
| 543 | altered(10, 17, &mut eleven, 11); |
| 544 | altered(12, 21, &mut thirteen, 13); |
| 545 | if st.alt { |
| 546 | v.push(Interval::new(8, 13)); |
| 547 | v.push(Interval::new(8, 15)); |
| 548 | v.push(Interval::new(10, 18)); |
| 549 | v.push(Interval::new(12, 20)); |
| 550 | } |
| 551 | if nine { |
| 552 | v.push(Interval::new(8, 14)); |
| 553 | } |
| 554 | if eleven { |
| 555 | v.push(Interval::new(10, 17)); |
| 556 | } |
| 557 | if thirteen { |
| 558 | v.push(Interval::new(12, 21)); |
| 559 | } |
| 560 | v.sort_by_key(|i| (i.steps, i.semis)); |
| 561 | v.dedup(); |
| 562 | v |
| 563 | } |
| 564 | |
| 565 | /// Suffixes tried when identifying a chord from notes, most common first. |
| 566 | const IDENTIFY_SUFFIXES: &[&str] = &[ |
| 567 | "", "m", "7", "maj7", "m7", "dim", "aug", "sus4", "sus2", "5", "6", "m6", "dim7", "m7b5", |
| 568 | "mMaj7", "add9", "madd9", "9", "maj9", "m9", "7sus4", "69", "7b9", "7#9", "7#5", "7b5", "11", |
| 569 | "m11", "13", "maj7#11", "maj13", "m13", "7#11", "7b13", "7alt", "maj7#5", "maj7b5", |
| 570 | ]; |
| 571 | |
| 572 | #[derive(Debug, Clone)] |
| 573 | pub struct Candidate { |
| 574 | pub chord: Chord, |
| 575 | /// 0 is best: root in the bass first, then simpler chord types. |
| 576 | pub rank: usize, |
| 577 | } |
| 578 | |
| 579 | /// Identify chords from note names. The first note is taken as the bass. |
| 580 | pub fn identify(notes: &[Note]) -> Vec<Candidate> { |
| 581 | let Some(&bass) = notes.first() else { |
| 582 | return Vec::new(); |
| 583 | }; |
| 584 | let mut pcs: Vec<i32> = notes.iter().map(|n| n.pc()).collect(); |
| 585 | pcs.sort_unstable(); |
| 586 | pcs.dedup(); |
| 587 | let mut out: Vec<Candidate> = Vec::new(); |
| 588 | let mut seen_roots: Vec<Note> = Vec::new(); |
| 589 | for &root in notes { |
| 590 | if seen_roots.iter().any(|r| r.pc() == root.pc()) { |
| 591 | continue; |
| 592 | } |
| 593 | seen_roots.push(root); |
| 594 | for (idx, suffix) in IDENTIFY_SUFFIXES.iter().enumerate() { |
| 595 | let Ok(mut chord) = Chord::parse(&format!("{}{suffix}", root.name())) else { |
| 596 | continue; |
| 597 | }; |
| 598 | let relative: Vec<i32> = { |
| 599 | let mut v: Vec<i32> = chord.notes().iter().map(|n| n.pc()).collect(); |
| 600 | v.sort_unstable(); |
| 601 | v.dedup(); |
| 602 | v |
| 603 | }; |
| 604 | if relative != pcs { |
| 605 | continue; |
| 606 | } |
| 607 | if bass.pc() != root.pc() { |
| 608 | chord.bass = Some(bass); |
| 609 | } |
| 610 | let rank = if bass.pc() == root.pc() { 0 } else { 100 } + idx; |
| 611 | out.push(Candidate { chord, rank }); |
| 612 | } |
| 613 | } |
| 614 | out.sort_by_key(|c| c.rank); |
| 615 | out |
| 616 | } |
| 617 | |
| 618 | #[cfg(test)] |
| 619 | mod tests { |
| 620 | use super::*; |
| 621 | |
| 622 | fn names(symbol: &str) -> String { |
| 623 | Chord::parse(symbol) |
| 624 | .unwrap() |
| 625 | .notes() |
| 626 | .iter() |
| 627 | .map(|n| n.name()) |
| 628 | .collect::<Vec<_>>() |
| 629 | .join(" ") |
| 630 | } |
| 631 | |
| 632 | #[test] |
| 633 | fn triads_and_sevenths() { |
| 634 | assert_eq!(names("C"), "C E G"); |
| 635 | assert_eq!(names("Cm"), "C Eb G"); |
| 636 | assert_eq!(names("Cdim"), "C Eb Gb"); |
| 637 | assert_eq!(names("Caug"), "C E G#"); |
| 638 | assert_eq!(names("C7"), "C E G Bb"); |
| 639 | assert_eq!(names("Cmaj7"), "C E G B"); |
| 640 | assert_eq!(names("Dm7"), "D F A C"); |
| 641 | assert_eq!(names("Cdim7"), "C Eb Gb Bbb"); |
| 642 | assert_eq!(names("CmMaj7"), "C Eb G B"); |
| 643 | assert_eq!(names("C5"), "C G"); |
| 644 | } |
| 645 | |
| 646 | #[test] |
| 647 | fn half_diminished_spelling() { |
| 648 | assert_eq!(names("F#m7b5"), "F# A C E"); |
| 649 | assert_eq!(names("F#ø7"), "F# A C E"); |
| 650 | assert_eq!(Chord::parse("F#m7b5").unwrap().name(), "F#m7b5"); |
| 651 | } |
| 652 | |
| 653 | #[test] |
| 654 | fn extended_and_altered() { |
| 655 | assert_eq!(names("Bb13#11/D"), "Bb D F Ab C E G"); |
| 656 | assert_eq!(names("C9"), "C E G Bb D"); |
| 657 | assert_eq!(names("C13"), "C E G Bb D A"); |
| 658 | assert_eq!(names("Cm13"), "C Eb G Bb D F A"); |
| 659 | assert_eq!(names("G7b9"), "G B D F Ab"); |
| 660 | assert_eq!(names("G7#9"), "G B D F A#"); |
| 661 | assert_eq!(names("C7(b5)"), "C E Gb Bb"); |
| 662 | assert_eq!(names("C7alt"), "C E Bb Db D# F# Ab"); |
| 663 | assert_eq!(names("Cmaj9"), "C E G B D"); |
| 664 | } |
| 665 | |
| 666 | #[test] |
| 667 | fn sus_add_and_sixth() { |
| 668 | assert_eq!(names("Csus4"), "C F G"); |
| 669 | assert_eq!(names("C7sus4"), "C F G Bb"); |
| 670 | assert_eq!(names("Cadd9"), "C E G D"); |
| 671 | assert_eq!(names("C6/9"), "C E G A D"); |
| 672 | assert_eq!(names("Am6"), "A C E F#"); |
| 673 | } |
| 674 | |
| 675 | #[test] |
| 676 | fn slash_bass_and_inversions() { |
| 677 | let c = Chord::parse("Bb13#11/D").unwrap(); |
| 678 | assert_eq!(c.bass.unwrap().name(), "D"); |
| 679 | assert_eq!( |
| 680 | c.bass_description().unwrap(), |
| 681 | "first inversion (third in the bass)" |
| 682 | ); |
| 683 | assert_eq!(c.name(), "Bb13#11/D"); |
| 684 | let s = Chord::parse("C/F").unwrap(); |
| 685 | assert!(s.bass_description().unwrap().starts_with("slash chord")); |
| 686 | } |
| 687 | |
| 688 | #[test] |
| 689 | fn canonical_names() { |
| 690 | for sym in [ |
| 691 | "Cmaj7", "Dm7", "G7", "Bdim7", "Ebm9", "A7#9", "C6", "Csus4", "F#7b9", |
| 692 | ] { |
| 693 | assert_eq!(Chord::parse(sym).unwrap().name(), sym, "{sym}"); |
| 694 | } |
| 695 | assert_eq!(Chord::parse("C-7").unwrap().name(), "Cm7"); |
| 696 | assert_eq!(Chord::parse("CΔ7").unwrap().name(), "Cmaj7"); |
| 697 | assert_eq!(Chord::parse("C6/9").unwrap().name(), "C69"); |
| 698 | } |
| 699 | |
| 700 | #[test] |
| 701 | fn rejects_garbage() { |
| 702 | assert!(Chord::parse("H7").is_err()); |
| 703 | assert!(Chord::parse("Cxyz").is_err()); |
| 704 | assert!(Chord::parse("C/Q").is_err()); |
| 705 | assert!(Chord::parse("").is_err()); |
| 706 | } |
| 707 | |
| 708 | #[test] |
| 709 | fn identifies_from_notes() { |
| 710 | let n = |s: &str| { |
| 711 | s.split_whitespace() |
| 712 | .map(|x| Note::parse(x).unwrap()) |
| 713 | .collect::<Vec<_>>() |
| 714 | }; |
| 715 | let c = identify(&n("C E G Bb")); |
| 716 | assert_eq!(c[0].chord.name(), "C7"); |
| 717 | let c = identify(&n("E C G")); |
| 718 | assert_eq!(c[0].chord.name(), "C/E"); |
| 719 | // C6 and Am7/C are the same notes; both are reported. |
| 720 | let all: Vec<String> = identify(&n("C E G A")) |
| 721 | .iter() |
| 722 | .map(|c| c.chord.name()) |
| 723 | .collect(); |
| 724 | assert!( |
| 725 | all.contains(&"C6".to_string()) && all.contains(&"Am7/C".to_string()), |
| 726 | "{all:?}" |
| 727 | ); |
| 728 | assert!(identify(&n("C Db D")).is_empty()); |
| 729 | } |
| 730 | } |