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