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