Skip to main content

music21_rs/
tablature.rs

1//! Notes on the strings and frets of a fretted instrument: music21's
2//! `tablature`.
3//!
4//! ```
5//! use music21_rs::tablature::{FretBoard, FretNote};
6//!
7//! let board = FretBoard::guitar(vec![FretNote::new(Some(1), Some(2), Some(2))], 4);
8//! let pitches = board.pitches()?;
9//! assert_eq!(pitches[5].as_ref().map(|pitch| pitch.name_with_octave()), Some("F#4".to_string()));
10//! # Ok::<(), music21_rs::Error>(())
11//! ```
12
13use std::fmt;
14
15use crate::{
16    chordsymbol::ChordSymbol,
17    defaults::{FloatType, IntegerType},
18    error::{Error, Result},
19    pitch::{Pitch, microtone::ordinal_suffix},
20};
21
22/// A finger on a string at a fret: music21's `FretNote`. Strings count
23/// from the highest-sounding, one, as guitarists count them.
24#[derive(Clone, Debug, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct FretNote {
27    /// The string.
28    pub string: Option<IntegerType>,
29    /// The fret, nought for the open string.
30    pub fret: Option<IntegerType>,
31    /// The finger.
32    pub fingering: Option<IntegerType>,
33    /// Whether the finger is shown: music21's `displayFingerNumber`.
34    pub display_finger_number: bool,
35}
36
37impl FretNote {
38    /// A note on a string at a fret, played by a finger, the finger shown.
39    pub fn new(
40        string: Option<IntegerType>,
41        fret: Option<IntegerType>,
42        fingering: Option<IntegerType>,
43    ) -> Self {
44        Self {
45            string,
46            fret,
47            fingering,
48            display_finger_number: true,
49        }
50    }
51}
52
53/// Written as music21 writes the note inside its representation: `6th
54/// string, 133rd fret, 2nd finger`, leaving out what is not given.
55impl fmt::Display for FretNote {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        let parts: Vec<String> = [
58            (self.string, "string"),
59            (self.fret, "fret"),
60            (self.fingering, "finger"),
61        ]
62        .into_iter()
63        .filter_map(|(value, what)| {
64            value.map(|value| format!("{value}{} {what}", ordinal_suffix(value)))
65        })
66        .collect();
67        f.write_str(&parts.join(", "))
68    }
69}
70
71/// A fretted instrument's strings, the notes stopped on them, and their
72/// open pitches: music21's `FretBoard`.
73#[derive(Clone, Debug, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct FretBoard {
76    /// How many strings there are.
77    pub strings: usize,
78    /// The notes stopped, in no particular order.
79    pub fret_notes: Vec<FretNote>,
80    /// How many frets a diagram shows.
81    pub display_frets: usize,
82    /// Each string's open pitch, lowest string first.
83    pub tuning: Vec<Pitch>,
84}
85
86fn tuned(names: &[&str]) -> Vec<Pitch> {
87    names
88        .iter()
89        .map(|name| Pitch::from_name(*name).expect("a tuning names pitches"))
90        .collect()
91}
92
93impl FretBoard {
94    /// A board of so many strings with these notes, not yet tuned.
95    pub fn new(strings: usize, fret_notes: Vec<FretNote>, display_frets: usize) -> Self {
96        Self {
97            strings,
98            fret_notes,
99            display_frets,
100            tuning: Vec::new(),
101        }
102    }
103
104    /// A guitar's six strings, E2 A2 D3 G3 B3 E4: music21's
105    /// `GuitarFretBoard`.
106    pub fn guitar(fret_notes: Vec<FretNote>, display_frets: usize) -> Self {
107        Self {
108            tuning: tuned(&["E2", "A2", "D3", "G3", "B3", "E4"]),
109            ..Self::new(6, fret_notes, display_frets)
110        }
111    }
112
113    /// A ukulele's four strings, re-entrant G4 C4 E4 A4: music21's
114    /// `UkeleleFretBoard`.
115    pub fn ukulele(fret_notes: Vec<FretNote>, display_frets: usize) -> Self {
116        Self {
117            tuning: tuned(&["G4", "C4", "E4", "A4"]),
118            ..Self::new(4, fret_notes, display_frets)
119        }
120    }
121
122    /// A bass guitar's four strings, E1 A1 D2 G2: music21's
123    /// `BassGuitarFretBoard`.
124    pub fn bass_guitar(fret_notes: Vec<FretNote>, display_frets: usize) -> Self {
125        Self {
126            tuning: tuned(&["E1", "A1", "D2", "G2"]),
127            ..Self::new(4, fret_notes, display_frets)
128        }
129    }
130
131    /// A mandolin's four courses, G3 D4 A4 E5: music21's
132    /// `MandolinFretBoard`.
133    pub fn mandolin(fret_notes: Vec<FretNote>, display_frets: usize) -> Self {
134        Self {
135            tuning: tuned(&["G3", "D4", "A4", "E5"]),
136            ..Self::new(4, fret_notes, display_frets)
137        }
138    }
139
140    /// The note on a string, the first if several are: music21's
141    /// `getFretNoteByString`.
142    pub fn fret_note_by_string(&self, string: IntegerType) -> Option<&FretNote> {
143        self.fret_notes
144            .iter()
145            .find(|note| note.string == Some(string))
146    }
147
148    /// The note on each string that has one, lowest-sounding string first:
149    /// music21's `fretNotesLowestFirst`.
150    pub fn fret_notes_lowest_first(&self) -> Vec<&FretNote> {
151        (1..=self.strings as IntegerType)
152            .rev()
153            .filter_map(|string| self.fret_note_by_string(string))
154            .collect()
155    }
156
157    /// The pitch each string sounds, lowest string first, nothing for a
158    /// string with no note: music21's `getPitches`. A note's pitch is its
159    /// string's open pitch raised by its fret, spelled as music21 spells a
160    /// pitch made from a number. Strings are found as music21 finds them,
161    /// counting back from the highest, so a note naming no string is read
162    /// as on the lowest.
163    ///
164    /// # Errors
165    ///
166    /// A board whose tuning is not one pitch a string, or a note on a
167    /// string the board has not got.
168    pub fn pitches(&self) -> Result<Vec<Option<Pitch>>> {
169        if self.tuning.len() != self.strings {
170            return Err(Error::Tablature(format!(
171                "Tuning must be set first, tuned for {} notes, on a {} string instrument",
172                self.tuning.len(),
173                self.strings
174            )));
175        }
176        let mut pitches: Vec<Option<Pitch>> = vec![None; self.strings];
177        for note in &self.fret_notes {
178            // music21 indexes the strings with `-string`, as Python indexes a
179            // list: string nought is the first entry and string one the
180            // last.
181            let python_index = -i64::from(note.string.unwrap_or(0));
182            let index = if python_index < 0 {
183                python_index + self.strings as i64
184            } else {
185                python_index
186            };
187            let index = usize::try_from(index)
188                .ok()
189                .filter(|index| *index < self.strings)
190                .ok_or_else(|| Error::Tablature("list index out of range".to_string()))?;
191            let open = self.tuning[index].ps();
192            let sounding = open + note.fret.unwrap_or(0) as FloatType;
193            pitches[index] = Some(Pitch::from_number(sounding)?);
194        }
195        Ok(pitches)
196    }
197}
198
199/// The fret a diagram starts at, and where its number is written: music21's
200/// `FirstFret`.
201#[derive(Clone, Debug, PartialEq, Eq)]
202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
203pub struct FirstFret {
204    /// The fret.
205    pub fret: IntegerType,
206    /// Where its number goes, `right` by default.
207    pub location: String,
208}
209
210impl FirstFret {
211    /// A first fret with its number on the right.
212    pub fn new(fret: IntegerType) -> Self {
213        Self {
214            fret,
215            location: "right".to_string(),
216        }
217    }
218}
219
220/// A chord symbol with a fret diagram: music21's `ChordWithFretBoard`.
221#[derive(Clone, Debug, PartialEq)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
223pub struct ChordWithFretBoard {
224    /// The symbol.
225    pub chord_symbol: ChordSymbol,
226    /// The diagram.
227    pub fret_board: FretBoard,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn fret_notes_are_written_and_ordered_as_music21_does() {
236        // music21's own tests of tablature.
237        assert_eq!(
238            FretNote::new(Some(6), Some(133), None).to_string(),
239            "6th string, 133rd fret"
240        );
241        assert_eq!(FretNote::new(None, None, None).string, None);
242        let board = FretBoard::new(
243            6,
244            vec![
245                FretNote::new(Some(1), Some(2), Some(2)),
246                FretNote::new(Some(2), Some(1), Some(1)),
247            ],
248            4,
249        );
250        let strings: Vec<Option<IntegerType>> = board
251            .fret_notes_lowest_first()
252            .iter()
253            .map(|note| note.string)
254            .collect();
255        assert_eq!(strings, [Some(2), Some(1)]);
256        assert!(board.pitches().is_err());
257    }
258
259    #[test]
260    fn a_board_sounds_its_strings_raised_by_their_frets() -> Result<()> {
261        // music21's GuitarFretBoard doctest: an A major chord.
262        let board = FretBoard::guitar(
263            vec![
264                FretNote::new(Some(3), Some(2), None),
265                FretNote::new(Some(2), Some(2), None),
266                FretNote::new(Some(4), Some(2), None),
267                FretNote::new(Some(5), Some(0), None),
268            ],
269            4,
270        );
271        let names: Vec<Option<String>> = board
272            .pitches()?
273            .iter()
274            .map(|pitch| pitch.as_ref().map(Pitch::name_with_octave))
275            .collect();
276        assert_eq!(
277            names,
278            [
279                None,
280                Some("A2".to_string()),
281                Some("E3".to_string()),
282                Some("A3".to_string()),
283                Some("C#4".to_string()),
284                None
285            ]
286        );
287        Ok(())
288    }
289}