1use super::Chord;
2use crate::{
3 defaults::{FloatType, IntegerType},
4 pitch::Pitch,
5};
6use std::collections::BTreeSet;
7
8const STANDARD_TUNING: [&str; 6] = ["E2", "A2", "D3", "G3", "B3", "E4"];
9const MAX_FRET: u8 = 12;
10const MAX_FRET_SPAN: u8 = 4;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct GuitarTuningString {
19 pub name: String,
21 pub pitch_space: IntegerType,
23 pub pitch_class: u8,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct GuitarTuning {
34 strings: Vec<GuitarTuningString>,
35}
36
37impl GuitarTuning {
38 pub fn new<I, S>(strings: I) -> crate::Result<Self>
40 where
41 I: IntoIterator<Item = S>,
42 S: AsRef<str>,
43 {
44 let strings = strings
45 .into_iter()
46 .map(|name| {
47 let pitch = Pitch::from_name(name.as_ref())?;
48 let pitch_space = pitch_space(&pitch)?;
49 Ok(GuitarTuningString {
50 name: pitch.name_with_octave(),
51 pitch_space,
52 pitch_class: pitch_class(&pitch),
53 })
54 })
55 .collect::<crate::Result<Vec<_>>>()?;
56
57 if strings.is_empty() {
58 return Err(crate::Error::Chord(
59 "guitar tuning must contain at least one string".to_string(),
60 ));
61 }
62 if strings.len() > u8::MAX as usize {
63 return Err(crate::Error::Chord(
64 "guitar tuning cannot contain more than 255 strings".to_string(),
65 ));
66 }
67
68 Ok(Self { strings })
69 }
70
71 pub fn standard() -> Self {
73 Self::new(STANDARD_TUNING).expect("standard guitar tuning should be valid")
74 }
75
76 pub fn strings(&self) -> &[GuitarTuningString] {
78 &self.strings
79 }
80
81 fn len(&self) -> usize {
82 self.strings.len()
83 }
84}
85
86impl Default for GuitarTuning {
87 fn default() -> Self {
88 Self::standard()
89 }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct GuitarStringFingering {
96 pub string_number: u8,
98 pub string_name: String,
100 pub open_pitch_space: IntegerType,
102 pub open_pitch_class: u8,
104 pub fret: Option<u8>,
106 pub finger: Option<u8>,
110 pub pitch_space: Option<IntegerType>,
112 pub pitch_class: Option<u8>,
114}
115
116#[derive(Clone, Debug, Eq, PartialEq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119pub struct GuitarFingering {
120 pub strings: Vec<GuitarStringFingering>,
122 pub base_fret: u8,
124 pub fret_span: u8,
126 pub covered_pitch_spaces: Vec<IntegerType>,
128 pub omitted_pitch_spaces: Vec<IntegerType>,
130 pub covered_pitch_classes: Vec<u8>,
132 pub omitted_pitch_classes: Vec<u8>,
134}
135
136#[derive(Clone, Debug)]
137struct StringChoice {
138 fret: Option<u8>,
139 pitch_space: Option<IntegerType>,
140 pitch_class: Option<u8>,
141}
142
143#[derive(Clone, Debug)]
144struct Candidate {
145 fingering: GuitarFingering,
146 score: usize,
147}
148
149#[derive(Clone, Debug)]
150struct Barre {
151 fret: u8,
152 start: usize,
153 end: usize,
154 covers: Vec<usize>,
155}
156
157#[derive(Clone, Debug)]
158struct FingerGroup {
159 fret: u8,
160 start: usize,
161 covers: Vec<usize>,
162}
163
164pub(crate) fn suggested_guitar_fingering(chord: &Chord) -> Option<GuitarFingering> {
165 suggested_guitar_fingering_with_tuning(chord, &GuitarTuning::standard())
166}
167
168pub(crate) fn suggested_guitar_fingering_with_tuning(
169 chord: &Chord,
170 tuning: &GuitarTuning,
171) -> Option<GuitarFingering> {
172 if chord.pitches().iter().any(|pitch| {
173 let ps = pitch.ps();
174 (ps - ps.round()).abs() > FloatType::EPSILON
175 }) {
176 return None;
177 }
178
179 let target_pitch_spaces = chord
180 .pitches()
181 .iter()
182 .map(pitch_space)
183 .collect::<crate::Result<BTreeSet<_>>>()
184 .ok()?;
185 if target_pitch_spaces.is_empty() {
186 return None;
187 }
188 let target_pitch_classes = chord.pitch_classes().into_iter().collect::<BTreeSet<_>>();
189
190 let root_pitch_class = chord
191 .root_pitch_name()
192 .and_then(|name| Pitch::from_name(name).ok())
193 .map(|pitch| pitch_class(&pitch));
194
195 let mut candidates = Vec::new();
196 for base_fret in 0..=(MAX_FRET - MAX_FRET_SPAN) {
197 let options = tuning
198 .strings()
199 .iter()
200 .map(|string| string_choices(string.pitch_space, &target_pitch_spaces, base_fret))
201 .collect::<Vec<_>>();
202 collect_candidates(
203 &options,
204 0,
205 Vec::with_capacity(tuning.len()),
206 &target_pitch_spaces,
207 &target_pitch_classes,
208 root_pitch_class,
209 tuning,
210 &mut candidates,
211 );
212 }
213
214 candidates.sort_by(|left, right| {
215 left.score
216 .cmp(&right.score)
217 .then_with(|| {
218 left.fingering
219 .omitted_pitch_classes
220 .len()
221 .cmp(&right.fingering.omitted_pitch_classes.len())
222 })
223 .then_with(|| left.fingering.base_fret.cmp(&right.fingering.base_fret))
224 .then_with(|| left.fingering.fret_span.cmp(&right.fingering.fret_span))
225 });
226 candidates
227 .into_iter()
228 .next()
229 .map(|candidate| candidate.fingering)
230}
231
232fn string_choices(
233 open_pitch_space: IntegerType,
234 target: &BTreeSet<IntegerType>,
235 base_fret: u8,
236) -> Vec<StringChoice> {
237 let mut choices = vec![StringChoice {
238 fret: None,
239 pitch_space: None,
240 pitch_class: None,
241 }];
242 let first_fret = if base_fret == 0 { 0 } else { base_fret };
243 let last_fret = (base_fret + MAX_FRET_SPAN).min(MAX_FRET);
244
245 for fret in first_fret..=last_fret {
246 let pitch_space = open_pitch_space + IntegerType::from(fret);
247 if target.contains(&pitch_space) {
248 choices.push(StringChoice {
249 fret: Some(fret),
250 pitch_space: Some(pitch_space),
251 pitch_class: Some(pitch_space.rem_euclid(12) as u8),
252 });
253 }
254 }
255
256 choices
257}
258
259fn collect_candidates(
260 options: &[Vec<StringChoice>],
261 string_index: usize,
262 current: Vec<StringChoice>,
263 target_pitch_spaces: &BTreeSet<IntegerType>,
264 target_pitch_classes: &BTreeSet<u8>,
265 root_pitch_class: Option<u8>,
266 tuning: &GuitarTuning,
267 candidates: &mut Vec<Candidate>,
268) {
269 if string_index == options.len() {
270 if let Some(candidate) = score_candidate(
271 current,
272 target_pitch_spaces,
273 target_pitch_classes,
274 root_pitch_class,
275 tuning,
276 ) {
277 candidates.push(candidate);
278 }
279 return;
280 }
281
282 for choice in &options[string_index] {
283 let mut next = current.clone();
284 next.push(choice.clone());
285 collect_candidates(
286 options,
287 string_index + 1,
288 next,
289 target_pitch_spaces,
290 target_pitch_classes,
291 root_pitch_class,
292 tuning,
293 candidates,
294 );
295 }
296}
297
298fn score_candidate(
299 choices: Vec<StringChoice>,
300 target_pitch_spaces: &BTreeSet<IntegerType>,
301 target_pitch_classes: &BTreeSet<u8>,
302 root_pitch_class: Option<u8>,
303 tuning: &GuitarTuning,
304) -> Option<Candidate> {
305 let sounding_indices = choices
306 .iter()
307 .enumerate()
308 .filter_map(|(index, choice)| choice.fret.map(|_| index))
309 .collect::<Vec<_>>();
310 if sounding_indices.is_empty() {
311 return None;
312 }
313
314 let covered_pitch_spaces = choices
315 .iter()
316 .filter_map(|choice| choice.pitch_space)
317 .collect::<BTreeSet<_>>();
318 let omitted_pitch_spaces = target_pitch_spaces
319 .difference(&covered_pitch_spaces)
320 .copied()
321 .collect::<Vec<_>>();
322 let covered_pitch_classes = choices
323 .iter()
324 .filter_map(|choice| choice.pitch_class)
325 .collect::<BTreeSet<_>>();
326 let omitted_pitch_classes = target_pitch_classes
327 .difference(&covered_pitch_classes)
328 .copied()
329 .collect::<Vec<_>>();
330 let fretted = choices
331 .iter()
332 .filter_map(|choice| choice.fret.filter(|fret| *fret > 0))
333 .collect::<Vec<_>>();
334 let finger_assignment = finger_assignment(&choices)?;
335 let base_fret = fretted.iter().copied().min().unwrap_or(0);
336 let fret_span = fretted
337 .iter()
338 .copied()
339 .max()
340 .zip(fretted.iter().copied().min())
341 .map(|(max, min)| max - min)
342 .unwrap_or(0);
343 let muted_count = choices
344 .iter()
345 .filter(|choice| choice.fret.is_none())
346 .count();
347 let internal_mutes = internal_muted_string_count(&choices, &sounding_indices);
348 let bass_pitch_class = sounding_indices
349 .first()
350 .and_then(|index| choices[*index].pitch_class);
351 let root_is_missing =
352 root_pitch_class.is_some_and(|root| !covered_pitch_classes.contains(&root));
353 let bass_is_not_root =
354 root_pitch_class.is_some_and(|root| bass_pitch_class.is_some_and(|bass| bass != root));
355 let duplicate_count = sounding_indices
356 .len()
357 .saturating_sub(covered_pitch_spaces.len());
358 let fret_sum = fretted.iter().map(|fret| *fret as usize).sum::<usize>();
359
360 let mut score = omitted_pitch_spaces.len() * 1000
361 + usize::from(root_is_missing) * 250
362 + usize::from(bass_is_not_root) * 45
363 + internal_mutes * 40
364 + muted_count * 6
365 + fret_span as usize * 8
366 + base_fret as usize * 3
367 + fretted.len() * 2
368 + duplicate_count
369 + fret_sum;
370
371 if covered_pitch_spaces.len() == target_pitch_spaces.len() {
372 score = score.saturating_sub(50);
373 }
374
375 let strings = choices
376 .into_iter()
377 .enumerate()
378 .map(|(index, choice)| {
379 let tuning_string = &tuning.strings()[index];
380 GuitarStringFingering {
381 string_number: (tuning.len() - index) as u8,
382 string_name: tuning_string.name.clone(),
383 open_pitch_space: tuning_string.pitch_space,
384 open_pitch_class: tuning_string.pitch_class,
385 fret: choice.fret,
386 finger: finger_assignment[index],
387 pitch_space: choice.pitch_space,
388 pitch_class: choice.pitch_class,
389 }
390 })
391 .collect::<Vec<_>>();
392
393 Some(Candidate {
394 fingering: GuitarFingering {
395 strings,
396 base_fret,
397 fret_span,
398 covered_pitch_spaces: covered_pitch_spaces.into_iter().collect(),
399 omitted_pitch_spaces,
400 covered_pitch_classes: covered_pitch_classes.into_iter().collect(),
401 omitted_pitch_classes,
402 },
403 score,
404 })
405}
406
407fn finger_assignment(choices: &[StringChoice]) -> Option<Vec<Option<u8>>> {
408 let fretted_positions = choices
409 .iter()
410 .enumerate()
411 .filter_map(|(string_index, choice)| {
412 choice
413 .fret
414 .filter(|fret| *fret > 0)
415 .map(|fret| (string_index, fret))
416 })
417 .collect::<Vec<_>>();
418
419 if fretted_positions.is_empty() {
420 return Some(vec![None; choices.len()]);
421 }
422
423 if !fret_span_is_reachable(&fretted_positions) {
424 return None;
425 }
426
427 if fretted_positions.len() <= 4 {
428 let groups = fretted_positions
429 .iter()
430 .enumerate()
431 .map(|(position_index, (string_index, fret))| FingerGroup {
432 fret: *fret,
433 start: *string_index,
434 covers: vec![position_index],
435 })
436 .collect::<Vec<_>>();
437 return assignment_from_groups(groups, choices.len(), &fretted_positions);
438 }
439
440 let barres = possible_barres(choices, &fretted_positions);
441 let mut best: Option<(usize, usize, Vec<FingerGroup>)> = None;
442 choose_barres(&barres, 0, Vec::new(), &fretted_positions, &mut best);
443
444 let (_, _, groups) = best?;
445 assignment_from_groups(groups, choices.len(), &fretted_positions)
446}
447
448fn fret_span_is_reachable(fretted_positions: &[(usize, u8)]) -> bool {
449 let Some(lowest) = fretted_positions.iter().map(|(_, fret)| *fret).min() else {
450 return true;
451 };
452 let highest = fretted_positions
453 .iter()
454 .map(|(_, fret)| *fret)
455 .max()
456 .unwrap_or(lowest);
457 let span = highest - lowest;
458
459 span <= if lowest >= 5 { 4 } else { 3 }
460}
461
462fn assignment_from_groups(
463 mut groups: Vec<FingerGroup>,
464 string_count: usize,
465 fretted_positions: &[(usize, u8)],
466) -> Option<Vec<Option<u8>>> {
467 if groups.len() > 4 {
468 return None;
469 }
470
471 groups.sort_by(|left, right| {
472 left.fret
473 .cmp(&right.fret)
474 .then_with(|| left.start.cmp(&right.start))
475 });
476
477 let mut assignment = vec![None; string_count];
478 for (finger_index, group) in groups.into_iter().enumerate() {
479 let finger = (finger_index + 1) as u8;
480 for position_index in group.covers {
481 let string_index = fretted_positions[position_index].0;
482 assignment[string_index] = Some(finger);
483 }
484 }
485 Some(assignment)
486}
487
488fn possible_barres(choices: &[StringChoice], fretted_positions: &[(usize, u8)]) -> Vec<Barre> {
489 let frets = fretted_positions
490 .iter()
491 .map(|(_, fret)| *fret)
492 .collect::<BTreeSet<_>>();
493 let mut barres = Vec::new();
494
495 for fret in frets {
496 for start in 0..choices.len() {
497 for end in (start + 1)..choices.len() {
498 if !barre_range_is_clear(choices, fret, start, end) {
499 continue;
500 }
501
502 let covers = fretted_positions
503 .iter()
504 .enumerate()
505 .filter_map(|(position_index, (string_index, position_fret))| {
506 (*position_fret == fret && (start..=end).contains(string_index))
507 .then_some(position_index)
508 })
509 .collect::<Vec<_>>();
510
511 if covers.len() >= 2 {
512 barres.push(Barre {
513 fret,
514 start,
515 end,
516 covers,
517 });
518 }
519 }
520 }
521 }
522
523 barres
524}
525
526fn barre_range_is_clear(choices: &[StringChoice], fret: u8, start: usize, end: usize) -> bool {
527 choices[start..=end]
528 .iter()
529 .all(|choice| choice.fret.is_some_and(|string_fret| string_fret >= fret))
530}
531
532fn choose_barres(
533 barres: &[Barre],
534 index: usize,
535 selected: Vec<Barre>,
536 fretted_positions: &[(usize, u8)],
537 best: &mut Option<(usize, usize, Vec<FingerGroup>)>,
538) {
539 if index == barres.len() {
540 let Some(groups) = finger_groups_from_barres(selected, fretted_positions) else {
541 return;
542 };
543 if groups.len() > 4 {
544 return;
545 }
546
547 let barre_span = groups
548 .iter()
549 .filter(|group| group.covers.len() > 1)
550 .map(|group| group.covers.len())
551 .sum::<usize>();
552 let key = (groups.len(), usize::MAX - barre_span);
553 if best
554 .as_ref()
555 .is_none_or(|(best_count, best_barre_score, _)| key < (*best_count, *best_barre_score))
556 {
557 *best = Some((key.0, key.1, groups));
558 }
559 return;
560 }
561
562 choose_barres(barres, index + 1, selected.clone(), fretted_positions, best);
563
564 let mut next = selected;
565 next.push(barres[index].clone());
566 choose_barres(barres, index + 1, next, fretted_positions, best);
567}
568
569fn finger_groups_from_barres(
570 barres: Vec<Barre>,
571 fretted_positions: &[(usize, u8)],
572) -> Option<Vec<FingerGroup>> {
573 let mut covered = vec![false; fretted_positions.len()];
574 let mut groups = Vec::new();
575
576 for barre in barres {
577 if barre.covers.iter().any(|position| covered[*position]) {
578 return None;
579 }
580 for position in &barre.covers {
581 covered[*position] = true;
582 }
583 groups.push(FingerGroup {
584 fret: barre.fret,
585 start: barre.start.min(barre.end),
586 covers: barre.covers,
587 });
588 }
589
590 for (position_index, (string_index, fret)) in fretted_positions.iter().enumerate() {
591 if !covered[position_index] {
592 groups.push(FingerGroup {
593 fret: *fret,
594 start: *string_index,
595 covers: vec![position_index],
596 });
597 }
598 }
599
600 Some(groups)
601}
602
603fn internal_muted_string_count(choices: &[StringChoice], sounding_indices: &[usize]) -> usize {
604 let Some(first) = sounding_indices.first() else {
605 return 0;
606 };
607 let Some(last) = sounding_indices.last() else {
608 return 0;
609 };
610
611 choices[*first..=*last]
612 .iter()
613 .filter(|choice| choice.fret.is_none())
614 .count()
615}
616
617fn pitch_class(pitch: &Pitch) -> u8 {
618 (pitch.ps().round() as IntegerType).rem_euclid(12) as u8
619}
620
621fn pitch_space(pitch: &Pitch) -> crate::Result<IntegerType> {
622 let pitch_space = pitch.ps();
623 let rounded = pitch_space.round();
624 if (pitch_space - rounded).abs() > FloatType::EPSILON {
625 return Err(crate::Error::Chord(
626 "guitar fingering requires chromatic pitches".to_string(),
627 ));
628 }
629 Ok(rounded as IntegerType)
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 fn choices(frets: &[Option<u8>]) -> Vec<StringChoice> {
637 frets
638 .iter()
639 .copied()
640 .map(|fret| StringChoice {
641 fret,
642 pitch_space: None,
643 pitch_class: None,
644 })
645 .collect()
646 }
647
648 #[test]
649 fn finger_assignment_rejects_more_than_four_independent_fingers() {
650 assert!(
651 finger_assignment(&choices(&[
652 Some(1),
653 Some(2),
654 Some(3),
655 Some(4),
656 Some(5),
657 None
658 ]))
659 .is_none()
660 );
661 }
662
663 #[test]
664 fn finger_assignment_accepts_barre_shapes() {
665 let assignment = finger_assignment(&choices(&[
666 Some(1),
667 Some(3),
668 Some(3),
669 Some(2),
670 Some(1),
671 Some(1),
672 ]))
673 .unwrap();
674
675 assert_eq!(
676 assignment.iter().flatten().collect::<BTreeSet<_>>().len(),
677 3
678 );
679 }
680
681 #[test]
682 fn finger_assignment_rejects_low_position_five_fret_stretches() {
683 assert!(
684 finger_assignment(&choices(&[Some(1), Some(2), Some(3), Some(5), None, None]))
685 .is_none()
686 );
687 }
688
689 #[test]
690 fn finger_assignment_allows_higher_position_extended_reaches() {
691 let assignment =
692 finger_assignment(&choices(&[Some(5), Some(7), Some(8), Some(9), None, None])).unwrap();
693
694 assert_eq!(assignment.iter().flatten().count(), 4);
695 }
696
697 #[test]
698 fn finger_assignment_does_not_barre_over_open_string() {
699 let assignment = finger_assignment(&choices(&[
700 Some(1),
701 Some(2),
702 Some(0),
703 Some(3),
704 Some(1),
705 Some(1),
706 ]))
707 .unwrap();
708
709 assert!(assignment.iter().flatten().collect::<BTreeSet<_>>().len() <= 4);
710 assert_ne!(assignment[0], assignment[4]);
711 }
712}