Skip to main content

music21_rs/chord/
guitar.rs

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/// Open-string pitch data for a guitar tuning.
13///
14/// Tunings are ordered from low string to high string and use concrete pitches,
15/// not just pitch classes, so fingering generation can respect octaves.
16#[derive(Clone, Debug, Eq, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct GuitarTuningString {
19    /// Open-string pitch name, including octave.
20    pub name: String,
21    /// Open-string pitch space, where C4 is 60.
22    pub pitch_space: IntegerType,
23    /// Open-string pitch class.
24    pub pitch_class: u8,
25}
26
27/// Guitar tuning used for fingering generation.
28///
29/// Strings are ordered from low to high, for example standard six-string guitar
30/// tuning is `["E2", "A2", "D3", "G3", "B3", "E4"]`.
31#[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    /// Builds a tuning from low-to-high open-string pitch names.
39    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    /// Returns standard six-string guitar tuning.
72    pub fn standard() -> Self {
73        Self::new(STANDARD_TUNING).expect("standard guitar tuning should be valid")
74    }
75
76    /// Returns the tuning strings from low to high.
77    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/// One string in a suggested guitar fingering.
93#[derive(Clone, Debug, Eq, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct GuitarStringFingering {
96    /// Guitar string number, where the lowest string has the highest number.
97    pub string_number: u8,
98    /// Open-string pitch name, including octave.
99    pub string_name: String,
100    /// Open-string pitch space, where C4 is 60.
101    pub open_pitch_space: IntegerType,
102    /// Open-string pitch class.
103    pub open_pitch_class: u8,
104    /// Fret to play, or `None` for a muted string.
105    pub fret: Option<u8>,
106    /// Suggested fretting finger, where `1` is index and `4` is pinky.
107    ///
108    /// Open and muted strings do not use a finger.
109    pub finger: Option<u8>,
110    /// Sounding pitch space, or `None` for a muted string.
111    pub pitch_space: Option<IntegerType>,
112    /// Sounding pitch class, or `None` for a muted string.
113    pub pitch_class: Option<u8>,
114}
115
116/// A suggested guitar fingering for a chord.
117#[derive(Clone, Debug, Eq, PartialEq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119pub struct GuitarFingering {
120    /// String fingerings from low string to high string.
121    pub strings: Vec<GuitarStringFingering>,
122    /// Lowest fretted position in the shape. `0` means the shape uses open strings.
123    pub base_fret: u8,
124    /// Distance between the lowest and highest fretted notes.
125    pub fret_span: u8,
126    /// Pitch spaces sounded by the fingering.
127    pub covered_pitch_spaces: Vec<IntegerType>,
128    /// Chord pitch spaces not present in the fingering.
129    pub omitted_pitch_spaces: Vec<IntegerType>,
130    /// Pitch classes sounded by the fingering.
131    pub covered_pitch_classes: Vec<u8>,
132    /// Chord pitch classes not present in the fingering.
133    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}