Skip to main content

music21_rs/figuredbass/
mod.rs

1//! Figured bass: the numbers written under a bass note, and what they mean.
2//!
3//! A column of figures says how far above the bass each note of the chord
4//! stands, and an accidental beside a number says how that note is spelled.
5//! Most of a column is left out in practice — a bare `7` means a seventh
6//! chord in root position, fifth and third and all — so a column is read in
7//! two forms: as written, and expanded to every note it stands for.
8//!
9//! This is the port of music21's `figuredBass.notation`, and it is what
10//! [`crate::roman::RomanNumeral`] reads its own digits with.
11
12use std::fmt;
13
14pub mod possibility;
15pub mod realizer;
16pub mod resolution;
17pub mod rules;
18pub mod scale;
19pub mod segment;
20
21use crate::{
22    defaults::IntegerType,
23    error::{Error, Result},
24    pitch::{Accidental, Pitch},
25};
26
27/// The number a figure carries when it is nothing but an extender line:
28/// music21's `EXTENDER_SENTINEL`.
29pub const EXTENDER: IntegerType = -1;
30
31/// The shorthand a column is written in, and every note it stands for.
32///
33/// music21's `shorthandNotation`. A column not listed here is read as it was
34/// written.
35const SHORTHAND: &[(&[Option<IntegerType>], &[IntegerType])] = &[
36    // A column written as nothing at all, or as a bare accidental, is the
37    // triad over its bass.
38    (&[None], &[5, 3]),
39    (&[Some(5)], &[5, 3]),
40    (&[Some(6)], &[6, 3]),
41    (&[Some(7)], &[7, 5, 3]),
42    (&[Some(9)], &[9, 7, 5, 3]),
43    (&[Some(11)], &[11, 9, 7, 5, 3]),
44    (&[Some(13)], &[13, 11, 9, 7, 5, 3]),
45    (&[Some(6), Some(5)], &[6, 5, 3]),
46    (&[Some(4), Some(3)], &[6, 4, 3]),
47    (&[Some(4), Some(2)], &[6, 4, 2]),
48    (&[Some(2)], &[6, 4, 2]),
49];
50
51/// The marks figured bass writes that music21's `Accidental` does not know by
52/// itself, and what each stands for: music21's `specialModifiers`.
53const SPECIAL: &[(&str, &str)] = &[
54    ("+", "#"),
55    ("/", "-"),
56    ("\\", "#"),
57    ("b", "-"),
58    ("bb", "--"),
59    ("bbb", "---"),
60    ("bbbb", "-----"),
61    ("++", "##"),
62    ("+++", "###"),
63    ("++++", "####"),
64    ("\u{266f}", "#"),
65    ("\u{266e}", "n"),
66    ("\u{266d}", "-"),
67];
68
69/// The accidental written beside a figure: music21's `Modifier`.
70///
71/// Figured bass has its own marks — a `+` raises and a `/` lowers — so the
72/// string it was written with is kept alongside the accidental it means.
73#[derive(Clone, Debug, Default, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75#[must_use]
76pub struct Modifier {
77    written: Option<String>,
78    accidental: Option<Accidental>,
79}
80
81impl Modifier {
82    /// Reads one, which may be nothing at all: neither `None` nor the empty
83    /// string says anything about how the note is spelled.
84    pub fn new(written: Option<&str>) -> Result<Self> {
85        let Some(mark) = written.filter(|mark| !mark.is_empty()) else {
86            return Ok(Self {
87                written: written.map(str::to_string),
88                accidental: None,
89            });
90        };
91        let accidental = match Accidental::new(mark) {
92            Ok(accidental) => accidental,
93            Err(_) => {
94                let Some((_, spelled)) = SPECIAL.iter().find(|(special, _)| *special == mark)
95                else {
96                    return Err(Error::Notation(format!(
97                        "Figure modifier unsupported in music21: {mark}"
98                    )));
99                };
100                Accidental::new(*spelled)?
101            }
102        };
103        Ok(Self {
104            written: Some(mark.to_string()),
105            accidental: Some(accidental),
106        })
107    }
108
109    /// The mark as it was written.
110    pub fn written(&self) -> Option<&str> {
111        self.written.as_deref()
112    }
113
114    /// The accidental it stands for, and nothing where nothing was written.
115    pub fn accidental(&self) -> Option<&Accidental> {
116        self.accidental.as_ref()
117    }
118
119    /// The same note spelled as this modifier asks.
120    ///
121    /// A written natural replaces whatever was there; anything else is added
122    /// to it, so a sharp against a flattened note raises it to a natural.
123    pub fn modify(&self, pitch: &Pitch) -> Result<Pitch> {
124        let Some(accidental) = &self.accidental else {
125            return Ok(pitch.clone());
126        };
127        let mut modified = pitch.clone();
128        let alter = if accidental.alter() == 0.0 || pitch.written_accidental().is_none() {
129            accidental.alter()
130        } else {
131            pitch.accidental().alter() + accidental.alter()
132        };
133        modified.set_accidental(Accidental::new(alter)?);
134        Ok(modified)
135    }
136
137    /// The name of a pitch spelled as this modifier asks: music21's
138    /// `modifyPitchName`, so a sharp makes `D` into `D#`.
139    pub fn modify_pitch_name(&self, name: &str) -> Result<String> {
140        Ok(self.modify(&Pitch::from_name(name)?)?.name())
141    }
142}
143
144/// A pitch from its name: music21's `convertToPitch`, which raises a
145/// `ValueError` rather than a pitch error for a name it cannot read.
146pub fn convert_to_pitch(name: &str) -> Result<Pitch> {
147    Pitch::from_name(name)
148        .map_err(|_| Error::Value(format!("Cannot convert string {name} to a music21 Pitch.")))
149}
150
151impl fmt::Display for Modifier {
152    /// music21's `_reprInternal`: the mark and the accidental it means.
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let written = match &self.written {
155            Some(written) => written.as_str(),
156            None => "None",
157        };
158        match &self.accidental {
159            Some(accidental) => write!(f, "{written} {}", accidental.name()),
160            None => write!(f, "{written} None"),
161        }
162    }
163}
164
165/// One figure of a column: a number above the bass and the accidental written
166/// beside it.
167#[derive(Clone, Debug, PartialEq)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169#[must_use]
170pub struct Figure {
171    number: Option<IntegerType>,
172    modifier: Modifier,
173    extender: bool,
174}
175
176impl Figure {
177    /// One figure. A number of `None` is a figure written as a bare
178    /// accidental, which stands for the third.
179    pub fn new(number: Option<IntegerType>, modifier: Modifier, extender: bool) -> Self {
180        Self {
181            number,
182            modifier,
183            extender,
184        }
185    }
186
187    /// How far above the bass the note stands, counted inclusively.
188    pub fn number(&self) -> Option<IntegerType> {
189        self.number
190    }
191
192    /// Puts the figure at another number.
193    pub fn set_number(&mut self, number: Option<IntegerType>) {
194        self.number = number;
195    }
196
197    /// The accidental written beside the number.
198    pub fn modifier(&self) -> &Modifier {
199        &self.modifier
200    }
201
202    /// Whether a line carries the figure on from the note before.
203    pub fn has_extender(&self) -> bool {
204        self.extender
205    }
206
207    /// Whether the figure is nothing but that line: music21 reads a number of
208    /// one as no number at all, so an extender on it carries the whole
209    /// column forward.
210    pub fn is_pure_extender(&self) -> bool {
211        self.number == Some(1) && self.extender
212    }
213}
214
215impl fmt::Display for Figure {
216    /// music21's `_reprInternal`.
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        if self.is_pure_extender() {
219            return write!(f, "pure-extender <Modifier {}>", self.modifier);
220        }
221        let number = match self.number {
222            Some(EXTENDER) => "_".to_string(),
223            Some(number) => number.to_string(),
224            None => "None".to_string(),
225        };
226        let extender = if self.extender { "(extender)" } else { "" };
227        write!(f, "{number}{extender} <Modifier {}>", self.modifier)
228    }
229}
230
231/// One column of figured bass, as written and as it stands expanded.
232///
233/// music21's `figuredBass.notation.Notation`. The figures are given as one
234/// string, comma-separated: `'7,5,#3'`, `'6,4'`, `'4+,2'`.
235#[derive(Clone, Debug, Default, PartialEq)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237#[must_use]
238pub struct Notation {
239    column: String,
240    figure_strings: Vec<String>,
241    original_numbers: Vec<Option<IntegerType>>,
242    original_modifiers: Vec<Option<String>>,
243    numbers: Vec<Option<IntegerType>>,
244    modifier_strings: Vec<Option<String>>,
245    extenders: Vec<bool>,
246    figures: Vec<Figure>,
247    figures_as_written: Vec<Figure>,
248}
249
250impl Notation {
251    /// Reads a column.
252    pub fn parse(column: &str) -> Result<Self> {
253        let mut notation = Self {
254            column: column.to_string(),
255            ..Self::default()
256        };
257        notation.read_column()?;
258        notation.expand();
259        notation.build_figures()?;
260        Ok(notation)
261    }
262
263    /// music21's `_parseNotationColumn`: every comma-separated figure split
264    /// into its number and the mark beside it.
265    fn read_column(&mut self) -> Result<()> {
266        for written in self.column.split(',') {
267            let written = written.trim();
268            self.figure_strings.push(written.to_string());
269            let digits: String = written
270                .chars()
271                .filter(|letter| letter.is_ascii_digit() || *letter == '_')
272                .collect();
273            let marks: String = written
274                .chars()
275                .filter(|letter| !letter.is_ascii_digit() && *letter != '_')
276                .collect();
277
278            let mut number = None;
279            let mut extender = false;
280            if !digits.is_empty() {
281                if digits == "_" {
282                    number = Some(EXTENDER);
283                    extender = true;
284                } else if digits.contains('_') {
285                    extender = true;
286                    number = digits.trim_matches('_').parse().ok();
287                } else {
288                    number = digits.parse().ok();
289                }
290            }
291            self.original_numbers.push(number);
292            self.original_modifiers
293                .push((!marks.is_empty()).then_some(marks));
294            self.extenders.push(extender);
295        }
296        self.numbers = self.original_numbers.clone();
297        self.modifier_strings = self.original_modifiers.clone();
298        Ok(())
299    }
300
301    /// music21's `_translateToLonghand`: a column written in shorthand stands
302    /// for every note the shorthand leaves out, and each mark stays with the
303    /// number it was written against.
304    fn expand(&mut self) {
305        let longhand = SHORTHAND
306            .iter()
307            .find(|(shorthand, _)| *shorthand == self.numbers.as_slice())
308            .map(|(_, longhand)| longhand.to_vec());
309        let Some(longhand) = longhand else {
310            // A figure written as a bare accidental is the third.
311            self.numbers = self
312                .numbers
313                .iter()
314                .map(|number| Some(number.unwrap_or(3)))
315                .collect();
316            return;
317        };
318        let against: Vec<IntegerType> = self
319            .numbers
320            .iter()
321            .map(|number| number.unwrap_or(3))
322            .collect();
323        self.modifier_strings = longhand
324            .iter()
325            .map(|number| {
326                against
327                    .iter()
328                    .position(|written| written == number)
329                    .and_then(|index| self.modifier_strings[index].clone())
330            })
331            .collect();
332        self.numbers = longhand.into_iter().map(Some).collect();
333    }
334
335    /// music21's `_getFigures`: the numbers and their marks, paired up.
336    fn build_figures(&mut self) -> Result<()> {
337        for (index, number) in self.numbers.iter().enumerate() {
338            let modifier = Modifier::new(self.modifier_strings[index].as_deref())?;
339            let extender = self.extenders.get(index).copied().unwrap_or(false);
340            self.figures.push(Figure::new(*number, modifier, extender));
341        }
342        for (index, number) in self.original_numbers.iter().enumerate() {
343            let modifier = Modifier::new(self.original_modifiers[index].as_deref())?;
344            self.figures_as_written
345                .push(Figure::new(*number, modifier, false));
346        }
347        Ok(())
348    }
349
350    /// The column as it was written.
351    pub fn column(&self) -> &str {
352        &self.column
353    }
354
355    /// Each figure of the written column, as a string.
356    pub fn figure_strings(&self) -> &[String] {
357        &self.figure_strings
358    }
359
360    /// The numbers as written, before the shorthand was expanded.
361    pub fn original_numbers(&self) -> &[Option<IntegerType>] {
362        &self.original_numbers
363    }
364
365    /// The marks as written.
366    pub fn original_modifiers(&self) -> &[Option<String>] {
367        &self.original_modifiers
368    }
369
370    /// The numbers of the expanded column.
371    pub fn numbers(&self) -> &[Option<IntegerType>] {
372        &self.numbers
373    }
374
375    /// The marks of the expanded column.
376    pub fn modifier_strings(&self) -> &[Option<String>] {
377        &self.modifier_strings
378    }
379
380    /// Whether any figure carries a line on from the note before.
381    pub fn has_extenders(&self) -> bool {
382        self.extenders.iter().any(|extender| *extender)
383    }
384
385    /// Which figures carry such a line.
386    pub fn extenders(&self) -> &[bool] {
387        &self.extenders
388    }
389
390    /// The figures of the expanded column.
391    pub fn figures(&self) -> &[Figure] {
392        &self.figures
393    }
394
395    /// The figures of the column as it was written.
396    pub fn figures_as_written(&self) -> &[Figure] {
397        &self.figures_as_written
398    }
399
400    /// The accidentals of the expanded column, one per figure.
401    pub fn modifiers(&self) -> Vec<&Modifier> {
402        self.figures.iter().map(Figure::modifier).collect()
403    }
404}
405
406impl fmt::Display for Notation {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        f.write_str(&self.column)
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn a_column_keeps_what_it_was_written_with_beside_what_it_means() {
418        let notation = Notation::parse("6,#").unwrap();
419        assert_eq!(notation.column(), "6,#");
420        assert_eq!(notation.figures_as_written().len(), 2);
421        assert_eq!(notation.original_modifiers(), [None, Some("#".to_string())]);
422        assert_eq!(notation.extenders(), [false, false]);
423        assert_eq!(notation.modifiers().len(), notation.figures().len());
424        assert_eq!(notation.modifiers()[1].written(), Some("#"));
425        assert_eq!(notation.modifiers()[0].written(), None);
426        assert!(notation.to_string().contains("6,#"));
427
428        let mut figure = Figure::new(Some(6), Modifier::new(None).unwrap(), true);
429        assert!(figure.has_extender());
430        assert!(!figure.is_pure_extender());
431        figure.set_number(Some(1));
432        assert!(figure.is_pure_extender());
433        figure.set_number(None);
434        assert_eq!(figure.number(), None);
435    }
436
437    /// music21's own examples for `modifyPitchName` and `convertToPitch`.
438    #[test]
439    fn a_modifier_respells_a_pitch_name() {
440        assert_eq!(
441            Modifier::new(Some("#"))
442                .unwrap()
443                .modify_pitch_name("D")
444                .unwrap(),
445            "D#"
446        );
447        assert_eq!(
448            Modifier::new(Some("-"))
449                .unwrap()
450                .modify_pitch_name("F")
451                .unwrap(),
452            "Fb"
453        );
454        assert_eq!(
455            Modifier::new(Some("n"))
456                .unwrap()
457                .modify_pitch_name("C#")
458                .unwrap(),
459            "C"
460        );
461        assert_eq!(
462            Modifier::new(None)
463                .unwrap()
464                .modify_pitch_name("Bb")
465                .unwrap(),
466            "Bb"
467        );
468        assert!(
469            Modifier::new(Some("#"))
470                .unwrap()
471                .modify_pitch_name("H")
472                .is_err()
473        );
474
475        assert_eq!(convert_to_pitch("C5").unwrap().to_string(), "C5");
476        assert!(matches!(convert_to_pitch("nonsense"), Err(Error::Value(_))));
477    }
478
479    #[test]
480    fn a_column_expands_out_of_its_shorthand() {
481        let notation = Notation::parse("4+,2").unwrap();
482        assert_eq!(notation.figure_strings(), ["4+", "2"]);
483        assert_eq!(notation.original_numbers(), [Some(4), Some(2)]);
484        assert_eq!(notation.numbers(), [Some(6), Some(4), Some(2)]);
485        assert_eq!(
486            notation.modifier_strings(),
487            [None, Some("+".to_string()), None]
488        );
489        assert_eq!(
490            notation.figures()[1].to_string(),
491            "4 <Modifier + sharp>",
492            "the mark stays with the number it was written against"
493        );
494    }
495
496    #[test]
497    fn a_bare_mark_is_the_third() {
498        let notation = Notation::parse("-6, -").unwrap();
499        assert_eq!(notation.original_numbers(), [Some(6), None]);
500        assert_eq!(notation.numbers(), [Some(6), Some(3)]);
501        assert_eq!(notation.figures()[1].to_string(), "3 <Modifier - flat>");
502    }
503
504    #[test]
505    fn figured_bass_reads_its_own_marks() {
506        // A `+` raises and a `/` lowers, which no accidental name would.
507        assert_eq!(
508            Modifier::new(Some("+"))
509                .unwrap()
510                .accidental()
511                .map(|accidental| accidental.name()),
512            Some("sharp")
513        );
514        assert_eq!(
515            Modifier::new(Some("/"))
516                .unwrap()
517                .accidental()
518                .map(|accidental| accidental.name()),
519            Some("flat")
520        );
521        assert!(Modifier::new(Some("")).unwrap().accidental().is_none());
522        assert!(Modifier::new(None).unwrap().accidental().is_none());
523        assert!(Modifier::new(Some("zzz")).is_err());
524    }
525
526    #[test]
527    fn an_extender_carries_the_figure_on() {
528        let notation = Notation::parse("7_").unwrap();
529        assert!(notation.has_extenders());
530        let pure = Figure::new(Some(1), Modifier::new(Some("#")).unwrap(), true);
531        assert!(pure.is_pure_extender());
532        assert_eq!(pure.to_string(), "pure-extender <Modifier # sharp>");
533    }
534}