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 options = tuning
196 .strings()
197 .iter()
198 .map(|string| string_choices(string.pitch_space, &target_pitch_classes))
199 .collect::<Vec<_>>();
200
201 let mut best: Option<Candidate> = None;
202 let mut current = Vec::with_capacity(tuning.len());
203 collect_candidates(
204 &options,
205 0,
206 &mut current,
207 &target_pitch_spaces,
208 &target_pitch_classes,
209 root_pitch_class,
210 tuning,
211 &mut best,
212 );
213
214 best.map(|candidate| candidate.fingering)
215}
216
217fn string_choices(
228 open_pitch_space: IntegerType,
229 target_pitch_classes: &BTreeSet<u8>,
230) -> Vec<StringChoice> {
231 let mut choices = vec![StringChoice {
232 fret: None,
233 pitch_space: None,
234 pitch_class: None,
235 }];
236
237 for fret in 0..=MAX_FRET {
238 let pitch_space = open_pitch_space + IntegerType::from(fret);
239 let pitch_class = pitch_space.rem_euclid(12) as u8;
240 if target_pitch_classes.contains(&pitch_class) {
241 choices.push(StringChoice {
242 fret: Some(fret),
243 pitch_space: Some(pitch_space),
244 pitch_class: Some(pitch_class),
245 });
246 }
247 }
248
249 choices
250}
251
252fn collect_candidates(
253 options: &[Vec<StringChoice>],
254 string_index: usize,
255 current: &mut Vec<StringChoice>,
256 target_pitch_spaces: &BTreeSet<IntegerType>,
257 target_pitch_classes: &BTreeSet<u8>,
258 root_pitch_class: Option<u8>,
259 tuning: &GuitarTuning,
260 best: &mut Option<Candidate>,
261) {
262 if string_index == options.len() {
263 let bound = best.as_ref().map_or(usize::MAX, |best| best.score);
264 if let Some(candidate) = score_candidate(
265 current,
266 target_pitch_spaces,
267 target_pitch_classes,
268 root_pitch_class,
269 tuning,
270 bound,
271 ) && best
272 .as_ref()
273 .is_none_or(|current| candidate.score < current.score)
274 {
275 *best = Some(candidate);
276 }
277 return;
278 }
279
280 for choice in &options[string_index] {
281 if let Some(fret) = choice.fret.filter(|fret| *fret > 0) {
286 let (mut low, mut high) = (fret, fret);
287 for played in current.iter().filter_map(|c| c.fret.filter(|f| *f > 0)) {
288 low = low.min(played);
289 high = high.max(played);
290 }
291 if high - low > MAX_FRET_SPAN {
292 continue;
293 }
294 }
295
296 current.push(choice.clone());
297 collect_candidates(
298 options,
299 string_index + 1,
300 current,
301 target_pitch_spaces,
302 target_pitch_classes,
303 root_pitch_class,
304 tuning,
305 best,
306 );
307 current.pop();
308 }
309}
310
311fn unreachable_same_fret_pairs(fretted_positions: &[(usize, u8)]) -> usize {
312 let mut count = 0;
313 for (index, (left_string, fret)) in fretted_positions.iter().enumerate() {
314 for (right_string, right_fret) in fretted_positions.iter().skip(index + 1) {
315 if right_fret != fret || right_string.saturating_sub(*left_string) < 3 {
316 continue;
317 }
318 let crossed = fretted_positions.iter().any(|(between, between_fret)| {
319 between > left_string && between < right_string && between_fret > fret
320 });
321 if crossed {
322 count += 1;
323 }
324 }
325 }
326 count
327}
328
329fn score_candidate(
330 choices: &[StringChoice],
331 target_pitch_spaces: &BTreeSet<IntegerType>,
332 target_pitch_classes: &BTreeSet<u8>,
333 root_pitch_class: Option<u8>,
334 tuning: &GuitarTuning,
335 bound: usize,
336) -> Option<Candidate> {
337 let sounding_indices = choices
338 .iter()
339 .enumerate()
340 .filter_map(|(index, choice)| choice.fret.map(|_| index))
341 .collect::<Vec<_>>();
342 if sounding_indices.is_empty() {
343 return None;
344 }
345
346 let covered_pitch_spaces = choices
347 .iter()
348 .filter_map(|choice| choice.pitch_space)
349 .collect::<BTreeSet<_>>();
350 let omitted_pitch_spaces = target_pitch_spaces
351 .difference(&covered_pitch_spaces)
352 .copied()
353 .collect::<Vec<_>>();
354 let covered_pitch_classes = choices
355 .iter()
356 .filter_map(|choice| choice.pitch_class)
357 .collect::<BTreeSet<_>>();
358 let omitted_pitch_classes = target_pitch_classes
359 .difference(&covered_pitch_classes)
360 .copied()
361 .collect::<Vec<_>>();
362 let fretted = choices
363 .iter()
364 .filter_map(|choice| choice.fret.filter(|fret| *fret > 0))
365 .collect::<Vec<_>>();
366 let base_fret = fretted.iter().copied().min().unwrap_or(0);
367 let fret_span = fretted
368 .iter()
369 .copied()
370 .max()
371 .zip(fretted.iter().copied().min())
372 .map(|(max, min)| max - min)
373 .unwrap_or(0);
374 let muted_count = choices
375 .iter()
376 .filter(|choice| choice.fret.is_none())
377 .count();
378 let internal_mutes = internal_muted_string_count(choices, &sounding_indices);
379 let bass_pitch_class = sounding_indices
380 .first()
381 .and_then(|index| choices[*index].pitch_class);
382 let root_is_missing =
383 root_pitch_class.is_some_and(|root| !covered_pitch_classes.contains(&root));
384 let bass_is_not_root =
385 root_pitch_class.is_some_and(|root| bass_pitch_class.is_some_and(|bass| bass != root));
386 let duplicate_count = sounding_indices
387 .len()
388 .saturating_sub(covered_pitch_spaces.len());
389 let fret_sum = fretted.iter().map(|fret| *fret as usize).sum::<usize>();
390
391 let open_strings = choices
398 .iter()
399 .filter(|choice| choice.fret == Some(0))
400 .count();
401
402 let cheap_floor = omitted_pitch_classes.len() * 1000
406 + usize::from(root_is_missing) * 400
407 + internal_mutes * 120
408 + muted_count * 30;
409 const MAX_OPEN_BONUS: usize = 6 * 12;
413 if cheap_floor.saturating_sub(MAX_OPEN_BONUS) >= bound {
414 return None;
415 }
416
417 let finger_assignment = finger_assignment(choices)?;
418
419 let unreachable = unreachable_same_fret_pairs(
420 &choices
421 .iter()
422 .enumerate()
423 .filter_map(|(index, choice)| {
424 choice
425 .fret
426 .filter(|fret| *fret > 0)
427 .map(|fret| (index, fret))
428 })
429 .collect::<Vec<_>>(),
430 );
431
432 let penalties = unreachable * 300
433 + omitted_pitch_classes.len() * 1000
434 + usize::from(root_is_missing) * 400
435 + internal_mutes * 120
436 + muted_count * 30
437 + usize::from(bass_is_not_root) * 60
438 + fret_span as usize * 12
439 + base_fret as usize * 6
440 + fretted.len() * 2
441 + duplicate_count
442 + fret_sum;
443
444 let score = if base_fret <= 2 {
450 penalties.saturating_sub(open_strings * 12)
451 } else {
452 penalties + open_strings * 20
453 };
454
455 let strings = choices
456 .iter()
457 .enumerate()
458 .map(|(index, choice)| {
459 let tuning_string = &tuning.strings()[index];
460 GuitarStringFingering {
461 string_number: (tuning.len() - index) as u8,
462 string_name: tuning_string.name.clone(),
463 open_pitch_space: tuning_string.pitch_space,
464 open_pitch_class: tuning_string.pitch_class,
465 fret: choice.fret,
466 finger: finger_assignment[index],
467 pitch_space: choice.pitch_space,
468 pitch_class: choice.pitch_class,
469 }
470 })
471 .collect::<Vec<_>>();
472
473 Some(Candidate {
474 fingering: GuitarFingering {
475 strings,
476 base_fret,
477 fret_span,
478 covered_pitch_spaces: covered_pitch_spaces.into_iter().collect(),
479 omitted_pitch_spaces,
480 covered_pitch_classes: covered_pitch_classes.into_iter().collect(),
481 omitted_pitch_classes,
482 },
483 score,
484 })
485}
486
487fn finger_assignment(choices: &[StringChoice]) -> Option<Vec<Option<u8>>> {
488 let fretted_positions = choices
489 .iter()
490 .enumerate()
491 .filter_map(|(string_index, choice)| {
492 choice
493 .fret
494 .filter(|fret| *fret > 0)
495 .map(|fret| (string_index, fret))
496 })
497 .collect::<Vec<_>>();
498
499 if fretted_positions.is_empty() {
500 return Some(vec![None; choices.len()]);
501 }
502
503 if !fret_span_is_reachable(&fretted_positions) {
504 return None;
505 }
506
507 if fretted_positions.len() <= 4 {
508 let groups = fretted_positions
509 .iter()
510 .enumerate()
511 .map(|(position_index, (string_index, fret))| FingerGroup {
512 fret: *fret,
513 start: *string_index,
514 covers: vec![position_index],
515 })
516 .collect::<Vec<_>>();
517 return assignment_from_groups(groups, choices.len(), &fretted_positions);
518 }
519
520 let barres = possible_barres(choices, &fretted_positions);
521 let mut best: Option<(usize, usize, Vec<FingerGroup>)> = None;
522 choose_barres(&barres, 0, Vec::new(), &fretted_positions, &mut best);
523
524 let (_, _, groups) = best?;
525 assignment_from_groups(groups, choices.len(), &fretted_positions)
526}
527
528fn fret_span_is_reachable(fretted_positions: &[(usize, u8)]) -> bool {
529 let Some(lowest) = fretted_positions.iter().map(|(_, fret)| *fret).min() else {
530 return true;
531 };
532 let highest = fretted_positions
533 .iter()
534 .map(|(_, fret)| *fret)
535 .max()
536 .unwrap_or(lowest);
537 let span = highest - lowest;
538
539 span <= if lowest >= 5 { 4 } else { 3 }
540}
541
542fn assignment_from_groups(
543 mut groups: Vec<FingerGroup>,
544 string_count: usize,
545 fretted_positions: &[(usize, u8)],
546) -> Option<Vec<Option<u8>>> {
547 if groups.len() > 4 {
548 return None;
549 }
550
551 groups.sort_by(|left, right| {
552 left.fret
553 .cmp(&right.fret)
554 .then_with(|| left.start.cmp(&right.start))
555 });
556
557 let mut assignment = vec![None; string_count];
558 for (finger_index, group) in groups.into_iter().enumerate() {
559 let finger = (finger_index + 1) as u8;
560 for position_index in group.covers {
561 let string_index = fretted_positions[position_index].0;
562 assignment[string_index] = Some(finger);
563 }
564 }
565 Some(assignment)
566}
567
568fn possible_barres(choices: &[StringChoice], fretted_positions: &[(usize, u8)]) -> Vec<Barre> {
569 let frets = fretted_positions
570 .iter()
571 .map(|(_, fret)| *fret)
572 .collect::<BTreeSet<_>>();
573 let mut barres = Vec::new();
574
575 for fret in frets {
576 for start in 0..choices.len() {
577 for end in (start + 1)..choices.len() {
578 if !barre_range_is_clear(choices, fret, start, end) {
579 continue;
580 }
581
582 let covers = fretted_positions
583 .iter()
584 .enumerate()
585 .filter_map(|(position_index, (string_index, position_fret))| {
586 (*position_fret == fret && (start..=end).contains(string_index))
587 .then_some(position_index)
588 })
589 .collect::<Vec<_>>();
590
591 if covers.len() >= 2 {
592 barres.push(Barre {
593 fret,
594 start,
595 end,
596 covers,
597 });
598 }
599 }
600 }
601 }
602
603 barres
604}
605
606fn barre_range_is_clear(choices: &[StringChoice], fret: u8, start: usize, end: usize) -> bool {
607 choices[start..=end]
608 .iter()
609 .all(|choice| choice.fret.is_some_and(|string_fret| string_fret >= fret))
610}
611
612fn choose_barres(
613 barres: &[Barre],
614 index: usize,
615 selected: Vec<Barre>,
616 fretted_positions: &[(usize, u8)],
617 best: &mut Option<(usize, usize, Vec<FingerGroup>)>,
618) {
619 if index == barres.len() {
620 let Some(groups) = finger_groups_from_barres(selected, fretted_positions) else {
621 return;
622 };
623 if groups.len() > 4 {
624 return;
625 }
626
627 let barre_span = groups
628 .iter()
629 .filter(|group| group.covers.len() > 1)
630 .map(|group| group.covers.len())
631 .sum::<usize>();
632 let key = (groups.len(), usize::MAX - barre_span);
633 if best
634 .as_ref()
635 .is_none_or(|(best_count, best_barre_score, _)| key < (*best_count, *best_barre_score))
636 {
637 *best = Some((key.0, key.1, groups));
638 }
639 return;
640 }
641
642 choose_barres(barres, index + 1, selected.clone(), fretted_positions, best);
643
644 let mut next = selected;
645 next.push(barres[index].clone());
646 choose_barres(barres, index + 1, next, fretted_positions, best);
647}
648
649fn finger_groups_from_barres(
650 barres: Vec<Barre>,
651 fretted_positions: &[(usize, u8)],
652) -> Option<Vec<FingerGroup>> {
653 let mut covered = vec![false; fretted_positions.len()];
654 let mut groups = Vec::new();
655
656 for barre in barres {
657 if barre.covers.iter().any(|position| covered[*position]) {
658 return None;
659 }
660 for position in &barre.covers {
661 covered[*position] = true;
662 }
663 groups.push(FingerGroup {
664 fret: barre.fret,
665 start: barre.start.min(barre.end),
666 covers: barre.covers,
667 });
668 }
669
670 for (position_index, (string_index, fret)) in fretted_positions.iter().enumerate() {
671 if !covered[position_index] {
672 groups.push(FingerGroup {
673 fret: *fret,
674 start: *string_index,
675 covers: vec![position_index],
676 });
677 }
678 }
679
680 Some(groups)
681}
682
683fn internal_muted_string_count(choices: &[StringChoice], sounding_indices: &[usize]) -> usize {
684 let Some(first) = sounding_indices.first() else {
685 return 0;
686 };
687 let Some(last) = sounding_indices.last() else {
688 return 0;
689 };
690
691 choices[*first..=*last]
692 .iter()
693 .filter(|choice| choice.fret.is_none())
694 .count()
695}
696
697fn pitch_class(pitch: &Pitch) -> u8 {
698 (pitch.ps().round() as IntegerType).rem_euclid(12) as u8
699}
700
701fn pitch_space(pitch: &Pitch) -> crate::Result<IntegerType> {
702 let pitch_space = pitch.ps();
703 let rounded = pitch_space.round();
704 if (pitch_space - rounded).abs() > FloatType::EPSILON {
705 return Err(crate::Error::Chord(
706 "guitar fingering requires chromatic pitches".to_string(),
707 ));
708 }
709 Ok(rounded as IntegerType)
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn choices(frets: &[Option<u8>]) -> Vec<StringChoice> {
717 frets
718 .iter()
719 .copied()
720 .map(|fret| StringChoice {
721 fret,
722 pitch_space: None,
723 pitch_class: None,
724 })
725 .collect()
726 }
727
728 #[test]
729 fn finger_assignment_rejects_more_than_four_independent_fingers() {
730 assert!(
731 finger_assignment(&choices(&[
732 Some(1),
733 Some(2),
734 Some(3),
735 Some(4),
736 Some(5),
737 None
738 ]))
739 .is_none()
740 );
741 }
742
743 #[test]
744 fn finger_assignment_accepts_barre_shapes() {
745 let assignment = finger_assignment(&choices(&[
746 Some(1),
747 Some(3),
748 Some(3),
749 Some(2),
750 Some(1),
751 Some(1),
752 ]))
753 .unwrap();
754
755 assert_eq!(
756 assignment.iter().flatten().collect::<BTreeSet<_>>().len(),
757 3
758 );
759 }
760
761 #[test]
762 fn finger_assignment_rejects_low_position_five_fret_stretches() {
763 assert!(
764 finger_assignment(&choices(&[Some(1), Some(2), Some(3), Some(5), None, None]))
765 .is_none()
766 );
767 }
768
769 #[test]
770 fn finger_assignment_allows_higher_position_extended_reaches() {
771 let assignment =
772 finger_assignment(&choices(&[Some(5), Some(7), Some(8), Some(9), None, None])).unwrap();
773
774 assert_eq!(assignment.iter().flatten().count(), 4);
775 }
776
777 #[test]
778 fn finger_assignment_does_not_barre_over_open_string() {
779 let assignment = finger_assignment(&choices(&[
780 Some(1),
781 Some(2),
782 Some(0),
783 Some(3),
784 Some(1),
785 Some(1),
786 ]))
787 .unwrap();
788
789 assert!(assignment.iter().flatten().collect::<BTreeSet<_>>().len() <= 4);
790 assert_ne!(assignment[0], assignment[4]);
791 }
792}