Skip to main content

music21_rs/roman/
realize.rs

1//! Sounding a numeral: the figured-bass column read over the key's
2//! scale from the degree the inversion names, then respelled to the
3//! quality the numeral asks for.
4
5use super::*;
6
7impl RomanNumeral {
8    /// The collection the figure's degrees are read off.
9    pub(super) fn reading(&self) -> Result<Reading> {
10        if let Some(scale) = &self.scale {
11            return Ok(Reading::Scale(scale.clone()));
12        }
13        let key = self.effective_key()?;
14        // Every augmented sixth is read in the parallel minor, whatever key
15        // it is written in: its sixth degree is the flat one.
16        if matches!(self.kind, RomanKind::AugmentedSixth(_)) && key.mode() != "minor" {
17            return Ok(Reading::Key(Key::from_tonic_mode(
18                &key.tonic_pitch().name(),
19                Some("minor"),
20            )?));
21        }
22        Ok(Reading::Key(key))
23    }
24
25    /// The chord the numeral stands for, spelled where its key sounds.
26    ///
27    /// This is music21's `_updatePitches`, and it is a figured-bass reading
28    /// rather than a stack of intervals: the bass is the scale degree the
29    /// inversion figure puts there, every number of the column is that many
30    /// scale steps above it, and only then is the result respelled to the
31    /// quality the numeral's case and symbols asked for. Reading it off the
32    /// scale is what lets a numeral mean something in a mode, and what makes
33    /// `V7b5` alter one note rather than name a different chord.
34    pub fn to_chord(&self) -> Result<Chord> {
35        let reading = self.reading()?;
36        let numbers = self.figures.numbers();
37        let implies_root = FIGURES_IMPLYING_ROOT.contains(&numbers.as_slice());
38        let bass_degree = self.bass_scale_degree(&numbers, implies_root)?;
39
40        let mut pitches = vec![reading.pitch_at(bass_degree)?];
41        for figure in self.figures.figures().iter().rev() {
42            let Some(number) = figure.number() else {
43                continue;
44            };
45            let degree = bass_degree + number - 1;
46            let mut pitch = figure.modifier().modify(&reading.pitch_at(degree)?)?;
47            let below = pitches.last().map_or(0.0, Pitch::ps);
48            if pitch.ps() < below {
49                pitch.set_octave(Some(pitch.octave().unwrap_or(4) + 1));
50            }
51            pitches.push(pitch);
52        }
53
54        // The alteration in front of the numeral moves the chord without
55        // renaming it — music21 transposes by an augmented unison — but it
56        // leaves the notes above the fifth alone, since a `bVII9` is a chord
57        // on a flattened seventh degree and not a flattened ninth.
58        if self.accidental != 0 {
59            let untouched = upper_extension_indices(&pitches)?;
60            for (index, pitch) in pitches.iter_mut().enumerate() {
61                if untouched.contains(&index) {
62                    continue;
63                }
64                let alter = pitch.accidental().alter() + FloatType::from(self.accidental);
65                pitch.set_accidental(Some(Accidental::new(alter)?));
66            }
67        }
68
69        // A column that says nothing about a root is a stack over its bass.
70        let root = if implies_root {
71            None
72        } else {
73            Some(pitches[0].clone())
74        };
75
76        self.match_accidentals_to_quality(&mut pitches, root.as_ref())?;
77        self.correct_bracketed_pitches(&mut pitches, root.as_ref())?;
78
79        // A note left out or put in must not move the root, so the root is
80        // read while the chord is still whole and recorded from there.
81        let altered = !self.figures.omitted.is_empty() || !self.figures.added.is_empty();
82        let recorded = match &root {
83            Some(root) => Some(root.clone()),
84            None if altered => Chord::new(pitches.as_slice())?.root().cloned(),
85            None => None,
86        };
87
88        self.omit_steps(&mut pitches, recorded.as_ref())?;
89        self.add_steps(&mut pitches, &reading)?;
90
91        let mut chord = Chord::new(pitches.as_slice())?;
92        chord.set_root(recorded);
93        Ok(chord)
94    }
95
96    /// The scale degree the inversion figure puts in the bass.
97    pub(super) fn bass_scale_degree(
98        &self,
99        numbers: &[u8],
100        implies_root: bool,
101    ) -> Result<IntegerType> {
102        if !implies_root {
103            return Ok(IntegerType::from(self.degree));
104        }
105        bass_scale_degree_from_notation_in(self.degree, numbers, self.reading()?.cardinality())
106            .map(IntegerType::from)
107    }
108
109    /// music21's `_matchAccidentalsToQuality`, over the chord being built:
110    /// an accidental written on a figure is left where it was put, which is
111    /// what keeps the flat of `V7b5`.
112    pub(super) fn match_accidentals_to_quality(
113        &self,
114        pitches: &mut [Pitch],
115        root: Option<&Pitch>,
116    ) -> Result<()> {
117        let written: Vec<u8> = [3u8, 5, 7]
118            .into_iter()
119            .filter(|step| self.figures.alters(*step))
120            .collect();
121        match_pitches_to_quality(pitches, root, self.implied_quality, &written)
122    }
123
124    /// music21's `_correctBracketedPitches`: an alteration written in square
125    /// brackets moves a chord step without changing which step it is.
126    pub(super) fn correct_bracketed_pitches(
127        &self,
128        pitches: &mut [Pitch],
129        root: Option<&Pitch>,
130    ) -> Result<()> {
131        for (alter, step) in &self.figures.bracketed {
132            let Some(index) = chord_step_index(pitches, root, *step)? else {
133                continue;
134            };
135            let moved = pitches[index].accidental().alter() + FloatType::from(*alter);
136            pitches[index].set_accidental(Some(Accidental::new(moved)?));
137        }
138        Ok(())
139    }
140
141    /// music21's omitted steps: a `[no3]` drops every note of that step.
142    pub(super) fn omit_steps(&self, pitches: &mut Vec<Pitch>, root: Option<&Pitch>) -> Result<()> {
143        if self.figures.omitted.is_empty() {
144            return Ok(());
145        }
146        let mut dropped = Vec::new();
147        for step in &self.figures.omitted {
148            if let Some(index) = chord_step_index(pitches, root, *step)? {
149                dropped.push(pitches[index].name());
150            }
151        }
152        pitches.retain(|pitch| !dropped.contains(&pitch.name()));
153        Ok(())
154    }
155
156    /// music21's added steps: an `[add4]` puts in the note that many scale
157    /// steps above the *root*, at or above the bass.
158    pub(super) fn add_steps(&self, pitches: &mut Vec<Pitch>, reading: &Reading) -> Result<()> {
159        if self.figures.added.is_empty() {
160            return Ok(());
161        }
162        let bass = pitches.first().map_or(0.0, Pitch::ps);
163        for (alter, step) in &self.figures.added {
164            let degree = IntegerType::from(self.degree) + IntegerType::from(*step) - 1;
165            let mut added = reading.pitch_at(degree)?;
166            let moved = added.accidental().alter() + FloatType::from(*alter);
167            added.set_accidental(Some(Accidental::new(moved)?));
168            while added.ps() < bass {
169                added.set_octave(Some(added.octave().unwrap_or(4) + 1));
170            }
171            // An added note spelled onto the bass belongs above it, not under
172            // it: `IV[add#7]` in C would otherwise put `E#` in the bass.
173            if added.ps() == bass
174                && pitches
175                    .first()
176                    .is_some_and(|low| added.diatonic_note_number() < low.diatonic_note_number())
177            {
178                added.set_octave(Some(added.octave().unwrap_or(4) + 1));
179            }
180            if !pitches
181                .iter()
182                .any(|pitch| pitch.name_with_octave() == added.name_with_octave())
183            {
184                pitches.push(added);
185            }
186        }
187        // Two notes may sound alike and still be written apart, and the one
188        // written lower is the one written first.
189        pitches.sort_by(|left, right| {
190            left.ps()
191                .partial_cmp(&right.ps())
192                .unwrap_or(std::cmp::Ordering::Equal)
193                .then(
194                    left.diatonic_note_number()
195                        .cmp(&right.diatonic_note_number()),
196                )
197        });
198        Ok(())
199    }
200}
201
202/// Respells the third, fifth and seventh of a chord to the quality asked for.
203///
204/// This is music21's `_matchAccidentalsToQuality`. The letters come from
205/// wherever the notes came from — a scale, usually — and the quality decides
206/// only the accidentals, so a minor reading of `C E G` gives `C E- G` and
207/// keeps the letters it was handed. Chord steps listed in `written` are left
208/// alone, which is how an accidental somebody wrote survives the correction.
209pub fn match_pitches_to_quality(
210    pitches: &mut [Pitch],
211    root: Option<&Pitch>,
212    quality: ImpliedQuality,
213    written: &[u8],
214) -> Result<()> {
215    let correct = quality.correct_semitones();
216    for (step, want) in [3u8, 5, 7].into_iter().zip(correct.iter().copied()) {
217        if written.contains(&step) {
218            continue;
219        }
220        let Some(index) = chord_step_index(pitches, root, step)? else {
221            continue;
222        };
223        let have = step_semitones(pitches, root, index)?;
224        if have == IntegerType::from(want) {
225            continue;
226        }
227        correct_faulty_pitch(&mut pitches[index], IntegerType::from(want) - have)?;
228    }
229
230    // A seventh does not have to match the scale: an `i7` read in a major key
231    // would otherwise take the major seventh the scale spells.
232    if correct.len() == 2
233        && quality == ImpliedQuality::Minor
234        && !written.contains(&7)
235        && let Some(index) = chord_step_index(pitches, root, 7)?
236        && step_semitones(pitches, root, index)? == 11
237    {
238        correct_faulty_pitch(&mut pitches[index], -1)?;
239    }
240    Ok(())
241}
242
243/// What a roman numeral counts its degrees against.
244///
245/// Usually a key, which has seven of them; but music21 reads a numeral over
246/// any concrete scale, and an octatonic one has eight.
247pub(super) enum Reading {
248    Key(Key),
249    Scale(crate::scale::Scale),
250}
251
252impl Reading {
253    /// How many degrees there are before the collection repeats.
254    fn cardinality(&self) -> u8 {
255        match self {
256            Self::Key(_) => 7,
257            Self::Scale(scale) => scale.degree_count() as u8,
258        }
259    }
260
261    /// The pitch a degree spells, folded into the octave the collection's
262    /// tonic stands in.
263    fn pitch_at(&self, degree: IntegerType) -> Result<Pitch> {
264        match self {
265            Self::Key(key) => degree_pitch(key, degree),
266            Self::Scale(scale) => {
267                let count = IntegerType::from(self.cardinality());
268                let wrapped = (degree - 1).rem_euclid(count) + 1;
269                scale.pitch_at_degree(wrapped)
270            }
271        }
272    }
273}
274
275/// The pitch a scale degree spells, folded into the octave the scale's tonic
276/// stands in — which is what music21's `pitchFromDegree` does, so a ninth
277/// comes back as the second and the caller lifts it.
278pub(super) fn degree_pitch(key: &Key, degree: IntegerType) -> Result<Pitch> {
279    let wrapped = (degree - 1).rem_euclid(7) + 1;
280    key.pitch_from_degree(wrapped as usize)
281}
282
283/// The natural note at a diatonic note number, where 22 is middle C.
284pub(super) fn natural_at_diatonic_number(number: IntegerType) -> Result<Pitch> {
285    const LETTERS: [char; 7] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
286    let letter = LETTERS[((number - 1).rem_euclid(7)) as usize];
287    Pitch::builder()
288        .step(letter)
289        .octave((number - 1).div_euclid(7))
290        .build()
291}
292
293/// Which of a chord's notes are the seventh and the extensions above it.
294///
295/// music21 leaves these alone when it moves a chord by the alteration in
296/// front of its numeral, since the alteration is written against the root.
297pub(super) fn upper_extension_indices(pitches: &[Pitch]) -> Result<Vec<usize>> {
298    let chord = Chord::new(pitches)?;
299    let Some(root) = chord.root().cloned() else {
300        return Ok(Vec::new());
301    };
302    let mut indices = Vec::new();
303    for step in [7u8, 2, 4, 6] {
304        if let Some(index) = chord_step_index(pitches, Some(&root), step)? {
305            indices.push(index);
306        }
307    }
308    Ok(indices)
309}
310
311/// Where a chord step stands among a set of pitches, counting from the root
312/// the chord infers when none was recorded.
313pub(super) fn chord_step_index(
314    pitches: &[Pitch],
315    root: Option<&Pitch>,
316    step: u8,
317) -> Result<Option<usize>> {
318    let inferred;
319    let root = match root {
320        Some(root) => root,
321        None => {
322            let chord = Chord::new(pitches)?;
323            let Some(found) = chord.root().cloned() else {
324                return Ok(None);
325            };
326            inferred = found;
327            &inferred
328        }
329    };
330    let wanted = IntegerType::from(step);
331    Ok(pitches.iter().position(|pitch| {
332        (pitch.diatonic_note_number() - root.diatonic_note_number()).rem_euclid(7) + 1 == wanted
333    }))
334}
335
336/// How many semitones a pitch stands above the root, within the octave.
337pub(super) fn step_semitones(
338    pitches: &[Pitch],
339    root: Option<&Pitch>,
340    index: usize,
341) -> Result<IntegerType> {
342    let inferred;
343    let root = match root {
344        Some(root) => root,
345        None => {
346            let chord = Chord::new(pitches)?;
347            let Some(found) = chord.root().cloned() else {
348                return Ok(0);
349            };
350            inferred = found;
351            &inferred
352        }
353    };
354    let distance = (pitches[index].ps() - root.ps()).round() as IntegerType;
355    Ok(distance.rem_euclid(12))
356}
357
358/// music21's `correctFaultyPitch`: moves a note by the semitones it is out
359/// by, reading a correction of half an octave or more the short way round.
360pub(super) fn correct_faulty_pitch(pitch: &mut Pitch, correction: IntegerType) -> Result<()> {
361    let folded = fold_correction(correction) + pitch.accidental().alter() as IntegerType;
362    let alter = fold_correction(folded);
363    pitch.set_accidental(Some(Accidental::new(FloatType::from(alter))?));
364    Ok(())
365}
366
367/// Half an octave or more in either direction is the same note the other way.
368pub(super) fn fold_correction(semitones: IntegerType) -> IntegerType {
369    if semitones >= 6 {
370        semitones - 12
371    } else if semitones <= -6 {
372        semitones + 12
373    } else {
374        semitones
375    }
376}