Skip to main content

music21_rs/figuredbass/
scale.rs

1//! The scale a figured bass is read in: music21's
2//! `figuredBass.realizerScale`.
3//!
4//! A figure counts scale steps above the bass, so the same `6` means a
5//! different note in every key. A [`FiguredBassScale`] is the key it is
6//! counted in -- a tonic and one of the five modes music21's realizer knows --
7//! and says which notes a bass and its figures stand for.
8
9use std::fmt;
10use std::sync::LazyLock;
11
12use crate::defaults::IntegerType;
13use crate::error::{Error, Result};
14use crate::figuredbass::Notation;
15use crate::interval::Interval;
16use crate::key::keysignature::{KeySignature, pitch_to_sharps};
17use crate::pitch::Pitch;
18use crate::scale::{Scale, ScaleType};
19
20/// The modes a figured bass may be realized in.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum FiguredBassMode {
24    /// Major.
25    Major,
26    /// Natural minor.
27    Minor,
28    /// Dorian.
29    Dorian,
30    /// Phrygian.
31    Phrygian,
32    /// Hypophrygian.
33    Hypophrygian,
34}
35
36impl FiguredBassMode {
37    /// Every mode, in music21's order.
38    pub const ALL: [Self; 5] = [
39        Self::Major,
40        Self::Minor,
41        Self::Dorian,
42        Self::Phrygian,
43        Self::Hypophrygian,
44    ];
45
46    /// The mode's name as music21 writes it: `"major"`, `"hypophrygian"`.
47    pub fn name(self) -> &'static str {
48        match self {
49            Self::Major => "major",
50            Self::Minor => "minor",
51            Self::Dorian => "dorian",
52            Self::Phrygian => "phrygian",
53            Self::Hypophrygian => "hypophrygian",
54        }
55    }
56
57    /// The mode music21 names so.
58    ///
59    /// # Errors
60    ///
61    /// A name that is none of the five, with music21's own message.
62    pub fn from_name(name: &str) -> Result<Self> {
63        Self::ALL
64            .into_iter()
65            .find(|mode| mode.name() == name)
66            .ok_or_else(|| Error::FiguredBass(format!("Unsupported scale type-> {name}")))
67    }
68
69    fn scale_type(self) -> ScaleType {
70        match self {
71            Self::Major => ScaleType::Major,
72            Self::Minor => ScaleType::Minor,
73            Self::Dorian => ScaleType::Dorian,
74            Self::Phrygian => ScaleType::Phrygian,
75            Self::Hypophrygian => ScaleType::Hypophrygian,
76        }
77    }
78}
79
80impl fmt::Display for FiguredBassMode {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        formatter.write_str(self.name())
83    }
84}
85
86/// The highest pitch [`FiguredBassScale::pitches`] reaches unless told
87/// otherwise: `B5`.
88static DEFAULT_MAX_PITCH: LazyLock<Pitch> =
89    LazyLock::new(|| Pitch::from_name("B5").expect("B5 is a pitch name"));
90
91/// A diminished octave, the span [`FiguredBassScale::sample_pitches`] fills.
92static DIMINISHED_OCTAVE: LazyLock<Interval> =
93    LazyLock::new(|| Interval::from_name("d8").expect("d8 is an interval name"));
94
95/// The key a figured bass is counted in: a tonic and a mode.
96///
97/// ```
98/// use music21_rs::Pitch;
99/// use music21_rs::figuredbass::Notation;
100/// use music21_rs::figuredbass::scale::FiguredBassScale;
101///
102/// let c_major = FiguredBassScale::default();
103/// let first_inversion = Notation::parse("6")?;
104/// let names = c_major.pitch_names(&Pitch::from_name("D3")?, &first_inversion)?;
105/// assert_eq!(names, ["D", "F", "B"]);
106/// # Ok::<(), music21_rs::Error>(())
107/// ```
108#[derive(Clone, Debug)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110#[must_use]
111pub struct FiguredBassScale {
112    mode: FiguredBassMode,
113    scale: Scale,
114    key_signature: KeySignature,
115}
116
117impl PartialEq for FiguredBassScale {
118    /// Two are equal when they are one mode on one tonic; the key signature
119    /// follows from those.
120    fn eq(&self, other: &Self) -> bool {
121        self.mode == other.mode && self.scale == other.scale
122    }
123}
124
125impl Default for FiguredBassScale {
126    /// C major, as music21's is when given nothing.
127    fn default() -> Self {
128        Self::new(
129            Pitch::from_name("C").expect("C is a pitch name"),
130            FiguredBassMode::Major,
131        )
132        .expect("C major has a key signature")
133    }
134}
135
136impl FiguredBassScale {
137    /// The scale of `mode` on `tonic`.
138    ///
139    /// # Errors
140    ///
141    /// A tonic with no key signature in that mode.
142    pub fn new(tonic: Pitch, mode: FiguredBassMode) -> Result<Self> {
143        let sharps = pitch_to_sharps(&tonic, Some(mode.name()))?;
144        Ok(Self {
145            mode,
146            scale: Scale::new(mode.scale_type(), tonic),
147            key_signature: KeySignature::new(sharps),
148        })
149    }
150
151    /// The mode it is in.
152    pub fn mode(&self) -> FiguredBassMode {
153        self.mode
154    }
155
156    /// The scale itself: music21's `realizerScale`.
157    pub fn scale(&self) -> &Scale {
158        &self.scale
159    }
160
161    /// The key signature the tonic and mode are written with: music21's
162    /// `keySig`.
163    pub fn key_signature(&self) -> &KeySignature {
164        &self.key_signature
165    }
166
167    /// The names of the notes `bass` and `notation` stand for, the bass
168    /// first and the rest in the order the figures give them: music21's
169    /// `getPitchNames`.
170    ///
171    /// A bass the scale does not carry is read as the scale's own note on its
172    /// letter, spelled as the key signature spells it, which is how a
173    /// chromatic bass is still counted from.
174    ///
175    /// # Errors
176    ///
177    /// A bass whose letter the scale cannot find, or a figure its modifier
178    /// cannot be applied to.
179    pub fn pitch_names(&self, bass: &Pitch, notation: &Notation) -> Result<Vec<String>> {
180        let degree = match self.scale.degree_of(bass)? {
181            Some(degree) => degree,
182            None => {
183                let mut spelled = bass.clone();
184                let from_key = self
185                    .key_signature
186                    .accidental_by_step(spelled.step().as_char())?;
187                if from_key.as_ref() != spelled.written_accidental() {
188                    spelled.set_written_accidental(from_key);
189                }
190                self.scale.degree_of(&spelled)?.ok_or_else(|| {
191                    Error::FiguredBass(format!(
192                        "{} is on no degree of {} {}",
193                        bass.name_with_octave(),
194                        self.scale.tonic().name(),
195                        self.mode
196                    ))
197                })?
198            }
199        };
200        let modifiers = notation.modifiers();
201        let mut names = Vec::with_capacity(notation.numbers().len() + 1);
202        for (number, modifier) in notation.numbers().iter().zip(modifiers) {
203            let Some(number) = number else {
204                continue;
205            };
206            let on = (degree as IntegerType + number - 1) % 7;
207            let sample = self.scale.pitch_at_degree(on)?;
208            names.push(modifier.modify_pitch_name(&sample.name())?);
209        }
210        names.push(bass.name());
211        names.reverse();
212        Ok(names)
213    }
214
215    /// Every note `bass` and `notation` stand for from the bass up to, but
216    /// not including, the octave above it: the closest the chord can be
217    /// voiced. music21's `getSamplePitches`.
218    ///
219    /// # Errors
220    ///
221    /// As [`FiguredBassScale::pitch_names`].
222    pub fn sample_pitches(&self, bass: &Pitch, notation: &Notation) -> Result<Vec<Pitch>> {
223        let top = bass.transpose(&DIMINISHED_OCTAVE)?;
224        self.pitches(bass, notation, Some(&top))
225    }
226
227    /// Every note `bass` and `notation` stand for, in every octave from the
228    /// bass up to `max_pitch` (`B5` unless given), both included, lowest
229    /// first: music21's `getPitches`.
230    ///
231    /// # Errors
232    ///
233    /// As [`FiguredBassScale::pitch_names`].
234    pub fn pitches(
235        &self,
236        bass: &Pitch,
237        notation: &Notation,
238        max_pitch: Option<&Pitch>,
239    ) -> Result<Vec<Pitch>> {
240        let max_pitch = max_pitch.unwrap_or(&DEFAULT_MAX_PITCH);
241        let names = self.pitch_names(bass, notation)?;
242        let mut pitches = Vec::new();
243        for name in &names {
244            for octave in 0..=max_pitch.implicit_octave() {
245                let pitch = Pitch::from_name(format!("{name}{octave}"))?;
246                if pitch.ps() >= bass.ps() && pitch.ps() <= max_pitch.ps() {
247                    pitches.push(pitch);
248                }
249            }
250        }
251        pitches.sort_by(|a, b| a.ps().total_cmp(&b.ps()));
252        Ok(pitches)
253    }
254}