Skip to main content

music21/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::Pitch;
99/// use music21::figuredbass::Notation;
100/// use music21::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::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 same, counting its figures in `scale`: music21's `realizerScale`
152    /// assigned. The key signature and mode are left as they were, as
153    /// music21 leaves them.
154    pub fn with_scale(mut self, scale: Scale) -> Self {
155        self.scale = scale;
156        self
157    }
158
159    /// The same, spelling a bass the scale does not carry by
160    /// `key_signature`: music21's `keySig` assigned.
161    pub fn with_key_signature(mut self, key_signature: KeySignature) -> Self {
162        self.key_signature = key_signature;
163        self
164    }
165
166    /// The mode it is in.
167    pub fn mode(&self) -> FiguredBassMode {
168        self.mode
169    }
170
171    /// The scale itself: music21's `realizerScale`.
172    pub fn scale(&self) -> &Scale {
173        &self.scale
174    }
175
176    /// The key signature the tonic and mode are written with: music21's
177    /// `keySig`.
178    pub fn key_signature(&self) -> &KeySignature {
179        &self.key_signature
180    }
181
182    /// The names of the notes `bass` and `notation` stand for, the bass
183    /// first and the rest in the order the figures give them: music21's
184    /// `getPitchNames`.
185    ///
186    /// A bass the scale does not carry is read as the scale's own note on its
187    /// letter, spelled as the key signature spells it, which is how a
188    /// chromatic bass is still counted from.
189    ///
190    /// # Errors
191    ///
192    /// A bass whose letter the scale cannot find, or a figure its modifier
193    /// cannot be applied to.
194    pub fn pitch_names(&self, bass: &Pitch, notation: &Notation) -> Result<Vec<String>> {
195        let degree = match self.scale.degree_of(bass)? {
196            Some(degree) => degree,
197            None => {
198                let mut spelled = bass.clone();
199                let from_key = self
200                    .key_signature
201                    .accidental_by_step(spelled.step().as_char())?;
202                if from_key.as_ref() != spelled.written_accidental() {
203                    spelled.set_written_accidental(from_key);
204                }
205                self.scale.degree_of(&spelled)?.ok_or_else(|| {
206                    Error::FiguredBass(format!(
207                        "{} is on no degree of {} {}",
208                        bass.name_with_octave(),
209                        self.scale.tonic().name(),
210                        self.mode
211                    ))
212                })?
213            }
214        };
215        let modifiers = notation.modifiers();
216        let mut names = Vec::with_capacity(notation.numbers().len() + 1);
217        for (number, modifier) in notation.numbers().iter().zip(modifiers) {
218            let Some(number) = number else {
219                continue;
220            };
221            let on = (degree as IntegerType + number - 1) % 7;
222            let sample = self.scale.pitch_at_degree(on)?;
223            names.push(modifier.modify_pitch_name(&sample.name())?);
224        }
225        names.push(bass.name());
226        names.reverse();
227        Ok(names)
228    }
229
230    /// Every note `bass` and `notation` stand for from the bass up to, but
231    /// not including, the octave above it: the closest the chord can be
232    /// voiced. music21's `getSamplePitches`.
233    ///
234    /// # Errors
235    ///
236    /// As [`FiguredBassScale::pitch_names`].
237    pub fn sample_pitches(&self, bass: &Pitch, notation: &Notation) -> Result<Vec<Pitch>> {
238        let top = bass.transpose(&DIMINISHED_OCTAVE)?;
239        self.pitches(bass, notation, Some(&top))
240    }
241
242    /// Every note `bass` and `notation` stand for, in every octave from the
243    /// bass up to `max_pitch` (`B5` unless given), both included, lowest
244    /// first: music21's `getPitches`.
245    ///
246    /// # Errors
247    ///
248    /// As [`FiguredBassScale::pitch_names`].
249    pub fn pitches(
250        &self,
251        bass: &Pitch,
252        notation: &Notation,
253        max_pitch: Option<&Pitch>,
254    ) -> Result<Vec<Pitch>> {
255        let max_pitch = max_pitch.unwrap_or(&DEFAULT_MAX_PITCH);
256        let names = self.pitch_names(bass, notation)?;
257        let mut pitches = Vec::new();
258        for name in &names {
259            for octave in 0..=max_pitch.implicit_octave() {
260                let pitch = Pitch::from_name(format!("{name}{octave}"))?;
261                if pitch.ps() >= bass.ps() && pitch.ps() <= max_pitch.ps() {
262                    pitches.push(pitch);
263                }
264            }
265        }
266        pitches.sort_by(|a, b| a.ps().total_cmp(&b.ps()));
267        Ok(pitches)
268    }
269}