Skip to main content

music21_rs/
volpiano.rs

1//! Volpiano, the font chant is written in for the CANTUS database: music21's
2//! `volpiano`.
3//!
4//! A Volpiano string is a row of tokens, one character each: `1` and `2` a
5//! treble and a bass clef, a letter a note by where it sits on the staff (a
6//! capital one liquescent), `w`, `x`, `i`, `y` and `z` a flat on an E or a B
7//! from there on and their capitals a natural, `3` and `4` a barline and a
8//! double barline, one, two or three `7`s a line, page or column break, and
9//! hyphens the space between notes, one space or more ending a neume.
10//! [`from_volpiano`] reads one into a part and [`to_volpiano`] writes any
11//! stream as one.
12
13use std::collections::HashMap;
14
15use crate::bar::{Barline, BarlineType};
16use crate::clef::Clef;
17use crate::defaults::IntegerType;
18use crate::duration::Duration;
19use crate::error::{Error, Result};
20use crate::notation::{Notehead, StemDirection, Syllabic};
21use crate::note::Note;
22use crate::pitch::Pitch;
23use crate::pitch::accidental::Accidental;
24use crate::spanner::{Spanner, SpannerKind};
25use crate::stepname::StepName;
26use crate::stream::{Stream, StreamElement, StreamEvent, StreamKind};
27
28/// Where a manuscript breaks: music21's volpiano `LineBreak`, `PageBreak`
29/// and `ColumnBreak`, written as one, two and three `7`s.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub enum Break {
33    /// The line breaks here.
34    Line,
35    /// The page breaks here.
36    Page,
37    /// The column breaks here.
38    Column,
39}
40
41impl Break {
42    /// music21's class name for the break.
43    pub fn class_name(self) -> &'static str {
44        match self {
45            Self::Line => "LineBreak",
46            Self::Page => "PageBreak",
47            Self::Column => "ColumnBreak",
48        }
49    }
50
51    /// The break written as that many `7`s, where there is one.
52    fn of_count(count: usize) -> Option<Self> {
53        match count {
54            1 => Some(Self::Line),
55            2 => Some(Self::Page),
56            3 => Some(Self::Column),
57            _ => None,
58        }
59    }
60
61    fn token(self) -> &'static str {
62        match self {
63            Self::Line => "7",
64            Self::Page => "77",
65            Self::Column => "777",
66        }
67    }
68}
69
70/// The notes from two spaces below the lowest line up, each a step above the
71/// last.
72const NORMAL_PITCHES: &str = "9abcdefghjklmnopqrs";
73/// The same notes liquescent.
74const LIQUESCENT_PITCHES: &str = ")ABCDEFGHJKLMNOPQRS";
75const E_FLAT_TOKENS: &str = "wx";
76const B_FLAT_TOKENS: &str = "iyz";
77
78/// How many steps below the lowest line the first of the pitch tokens is.
79const BELOW_LOWEST_LINE: IntegerType = 5;
80
81/// Reads a Volpiano string into a part, as music21's `toPart` does.
82///
83/// Each note lasts a quarter, has no stem, and is liquescent with an `x`
84/// notehead where its token is a capital. A part opens in the treble clef
85/// until a clef token says otherwise; a flat token flats every E or B from
86/// there on, and its capital takes the flat away again. A barline closes
87/// the measure, every measure numbered nought. Two notes with nothing
88/// between them are a neume, a [`SpannerKind::Neume`] the part holds, and a
89/// neume takes in every note of its run. A break goes at the end of the
90/// measure being read. Tokens that are none of these are passed over.
91///
92/// ```
93/// use music21_rs::volpiano::from_volpiano;
94///
95/// let part = from_volpiano("1---c--d---fg---3")?;
96/// let names: Vec<String> = part
97///     .pitches()
98///     .iter()
99///     .map(|pitch| pitch.name_with_octave())
100///     .collect();
101/// assert_eq!(names, ["C4", "D4", "F4", "G4"]);
102/// assert_eq!(part.spanners().len(), 1);
103/// # Ok::<(), music21_rs::Error>(())
104/// ```
105///
106/// # Errors
107///
108/// A note further from its clef than a pitch can be spelled.
109pub fn from_volpiano(text: &str) -> Result<Stream> {
110    let mut measures: Vec<Stream> = Vec::new();
111    let mut measure = Stream::with_kind(StreamKind::Measure);
112    // Neumes, as the ordinals of their notes among every note read.
113    let mut neumes: Vec<Vec<usize>> = Vec::new();
114    let mut current_neume: Option<Vec<usize>> = None;
115    let mut waiting: Option<usize> = None;
116    let mut notes_read = 0;
117    let mut lowest_line = Clef::treble().lowest_line().unwrap_or(31);
118    let mut breaks = 0;
119    let mut b_is_flat = false;
120    let mut e_is_flat = false;
121
122    for token in text.chars() {
123        if token == '7' {
124            breaks += 1;
125            continue;
126        }
127        if let Some(kind) = Break::of_count(breaks) {
128            append(&mut measure, StreamElement::Break(kind));
129        }
130        breaks = 0;
131        if token == '-' || "1234".contains(token) {
132            waiting = None;
133            if let Some(neume) = current_neume.take() {
134                neumes.push(neume);
135            }
136            if token == '-' {
137                continue;
138            }
139        }
140        match token {
141            '1' | '2' => {
142                let clef = if token == '1' {
143                    Clef::treble()
144                } else {
145                    Clef::bass()
146                };
147                lowest_line = clef.lowest_line().unwrap_or(lowest_line);
148                append(&mut measure, StreamElement::Clef(clef));
149            }
150            '3' | '4' => {
151                let barline = if token == '4' {
152                    Barline::new(BarlineType::Double)
153                } else {
154                    Barline::new(BarlineType::Regular)
155                };
156                measure.set_right_barline(Some(barline));
157                measures.push(std::mem::replace(
158                    &mut measure,
159                    Stream::with_kind(StreamKind::Measure),
160                ));
161            }
162            _ => {
163                let normal = NORMAL_PITCHES.chars().position(|known| known == token);
164                let liquescent = LIQUESCENT_PITCHES.chars().position(|known| known == token);
165                if let Some(index) = normal.or(liquescent) {
166                    let mut pitch = Pitch::from_name("C4")?;
167                    pitch.set_diatonic_note_number(
168                        lowest_line + index as IntegerType - BELOW_LOWEST_LINE,
169                    )?;
170                    let step = pitch.step();
171                    if (step == StepName::B && b_is_flat) || (step == StepName::E && e_is_flat) {
172                        pitch.set_accidental(Accidental::new("flat")?);
173                    }
174                    let mut note = Note::from_pitch(pitch).with_duration(Duration::quarter());
175                    note.set_stem_direction(StemDirection::NoStem);
176                    if liquescent.is_some() {
177                        note.set_notehead(Notehead::X);
178                    }
179                    append(&mut measure, StreamElement::Note(note));
180                    let this = notes_read;
181                    notes_read += 1;
182                    // Notes with no hyphen between them are one neume.
183                    if let Some(neume) = current_neume.as_mut() {
184                        neume.push(this);
185                    } else if let Some(before) = waiting.take() {
186                        current_neume = Some(vec![before, this]);
187                    } else {
188                        waiting = Some(this);
189                    }
190                } else if let Some(lower) = flat_token(token) {
191                    let flat = !token.is_uppercase();
192                    if E_FLAT_TOKENS.contains(lower) {
193                        e_is_flat = flat;
194                    } else {
195                        b_is_flat = flat;
196                    }
197                }
198            }
199        }
200    }
201    if let Some(neume) = current_neume {
202        neumes.push(neume);
203    }
204    if let Some(kind) = Break::of_count(breaks) {
205        append(&mut measure, StreamElement::Break(kind));
206    }
207    if !measure.is_empty() {
208        measures.push(measure);
209    }
210
211    let mut part = Stream::with_kind(StreamKind::Part);
212    for measure in measures {
213        part.push(measure);
214    }
215    // Each note's place among the part's leaves.
216    let places: Vec<usize> = part
217        .leaves()
218        .iter()
219        .enumerate()
220        .filter(|(_, (_, element))| matches!(element, StreamElement::Note(_)))
221        .map(|(place, _)| place)
222        .collect();
223    for neume in neumes {
224        part.add_spanner(Spanner::new(
225            SpannerKind::Neume,
226            neume.into_iter().map(|ordinal| places[ordinal]).collect(),
227        ));
228    }
229    Ok(part)
230}
231
232/// music21's `append`: the element where the measure ends, sorted among
233/// what already stands there.
234fn append(measure: &mut Stream, element: StreamElement) {
235    let at = measure.end_offset();
236    measure.insert_sorted(vec![StreamEvent::new(at, element)]);
237}
238
239/// The flat token a token is, lower case, where it is one or its capital.
240fn flat_token(token: char) -> Option<char> {
241    let lower = token.to_ascii_lowercase();
242    (E_FLAT_TOKENS.contains(lower) || B_FLAT_TOKENS.contains(lower)).then_some(lower)
243}
244
245/// Writes a stream as Volpiano, as music21's `fromStream` does.
246///
247/// Every element is taken in turn, however deep: treble and bass clefs (and
248/// the treble and bass clefs an octave off) as `1` and `2`, any other clef
249/// changing only where the notes sit, barlines, breaks, and notes, a
250/// liquescent one being one with an `x` notehead. A note is followed by a
251/// hyphen, or by two where it has a lyric and three where that lyric says
252/// it ends a word, and by nothing where a neume carries on past it. A flat is written
253/// before the first flatted B or E, and a natural before the first one
254/// after that is not.
255///
256/// What Volpiano cannot write is left out, as music21 leaves it out: chords,
257/// rests, a note off the edge of what the tokens reach, and an accidental
258/// other than a flat on a B or an E (the note is written, with no hyphen
259/// after it).
260///
261/// ```
262/// use music21_rs::volpiano::{from_volpiano, to_volpiano};
263///
264/// let part = from_volpiano("1--c--d---f--d---ed--c")?;
265/// assert_eq!(to_volpiano(&part)?, "1---c-d-f-d-ed-c-");
266/// # Ok::<(), music21_rs::Error>(())
267/// ```
268///
269/// # Errors
270///
271/// A note under a clef that places no pitches.
272pub fn to_volpiano(stream: &Stream) -> Result<String> {
273    let mut inside = HashMap::new();
274    neumes_of(stream, 0, &mut inside);
275    let mut writer = Writer {
276        tokens: String::new(),
277        lowest_line: Clef::treble().lowest_line(),
278        b_is_flat: false,
279        e_is_flat: false,
280        position: 0,
281        inside,
282    };
283    writer.walk(stream)?;
284    Ok(writer.tokens)
285}
286
287/// For each leaf of `stream` in a neume, whether a neume carries on past it:
288/// music21 asks the first neume holding the note.
289fn neumes_of(stream: &Stream, base: usize, inside: &mut HashMap<usize, bool>) {
290    for spanner in stream.spanners() {
291        if spanner.kind() != SpannerKind::Neume {
292            continue;
293        }
294        let last = spanner.spanned().last().copied().flatten();
295        for place in spanner.spanned().iter().flatten() {
296            inside.entry(base + place).or_insert(Some(*place) != last);
297        }
298    }
299    let mut seen = base;
300    for event in stream.events() {
301        match event.element() {
302            StreamElement::Stream(inner) => {
303                neumes_of(inner, seen, inside);
304                seen += inner.leaves().len();
305            }
306            _ => seen += 1,
307        }
308    }
309}
310
311struct Writer {
312    tokens: String,
313    lowest_line: Option<IntegerType>,
314    b_is_flat: bool,
315    e_is_flat: bool,
316    /// The place in the stream's leaves of the element being written.
317    position: usize,
318    inside: HashMap<usize, bool>,
319}
320
321impl Writer {
322    fn walk(&mut self, stream: &Stream) -> Result<()> {
323        if stream.left_barline().is_some() {
324            self.barline(stream.left_barline());
325        }
326        for event in stream.events() {
327            match event.element() {
328                StreamElement::Stream(inner) => self.walk(inner)?,
329                element => {
330                    self.element(element)?;
331                    self.position += 1;
332                }
333            }
334        }
335        if stream.right_barline().is_some() {
336            self.barline(stream.right_barline());
337        }
338        Ok(())
339    }
340
341    fn barline(&mut self, barline: Option<&Barline>) {
342        let double = barline.is_some_and(|barline| {
343            matches!(barline.bar_type(), BarlineType::Double | BarlineType::Final)
344        });
345        self.tokens.push_str(if double { "---4" } else { "---3" });
346    }
347
348    fn pop_hyphens(&mut self) {
349        while self.tokens.ends_with('-') {
350            self.tokens.pop();
351        }
352    }
353
354    /// music21's `setAccFromPitch`: the flat or natural token for a B or an
355    /// E so far above the lowest line, where there is one.
356    fn accidental_at(&mut self, distance: IntegerType, natural: bool) {
357        let token = match distance {
358            -3 => 'y',
359            0 => 'w',
360            4 => 'i',
361            7 => 'x',
362            11 => 'z',
363            _ => return,
364        };
365        self.tokens.push(if natural {
366            token.to_ascii_uppercase()
367        } else {
368            token
369        });
370    }
371
372    fn element(&mut self, element: &StreamElement) -> Result<()> {
373        match element {
374            StreamElement::Clef(clef) => {
375                self.lowest_line = clef.lowest_line();
376                if clef.is_a("TrebleClef") {
377                    self.tokens.push_str("1---");
378                } else if clef.is_a("BassClef") {
379                    self.tokens.push_str("2---");
380                }
381            }
382            StreamElement::Barline(barline) => self.barline(Some(barline)),
383            StreamElement::Note(note) => self.note(note)?,
384            StreamElement::Break(kind) => {
385                self.pop_hyphens();
386                self.tokens.push_str(kind.token());
387                self.tokens.push_str("---");
388            }
389            _ => {}
390        }
391        Ok(())
392    }
393
394    fn note(&mut self, note: &Note) -> Result<()> {
395        let pitch = note.pitch();
396        let lowest_line = self.lowest_line.ok_or_else(|| {
397            Error::Stream("a note under a clef that places no pitches has no Volpiano".to_string())
398        })?;
399        let distance = pitch.diatonic_note_number() - lowest_line;
400        let Ok(index) = usize::try_from(distance + BELOW_LOWEST_LINE) else {
401            return Ok(());
402        };
403        let pitches = if note.notehead() == Notehead::X {
404            LIQUESCENT_PITCHES
405        } else {
406            NORMAL_PITCHES
407        };
408        let Some(token) = pitches.chars().nth(index) else {
409            return Ok(());
410        };
411        let step = pitch.step();
412        match pitch.written_accidental() {
413            Some(accidental) if accidental.alter() != 0.0 => {
414                if !matches!(step, StepName::B | StepName::E) || accidental.alter() != -1.0 {
415                    self.tokens.push(token);
416                    return Ok(());
417                }
418                if step == StepName::B && !self.b_is_flat {
419                    self.accidental_at(distance, false);
420                    self.b_is_flat = true;
421                } else if step == StepName::E && !self.e_is_flat {
422                    self.accidental_at(distance, false);
423                    self.e_is_flat = true;
424                }
425            }
426            _ if step == StepName::B && self.b_is_flat => {
427                self.accidental_at(distance, true);
428                self.b_is_flat = false;
429            }
430            _ if step == StepName::E && self.e_is_flat => {
431                self.accidental_at(distance, true);
432                self.e_is_flat = false;
433            }
434            _ => {}
435        }
436        self.tokens.push(token);
437        if self.inside.get(&self.position) == Some(&true) {
438            return Ok(());
439        }
440        match note.lyric().filter(|sung| !sung.is_empty()) {
441            None => self.tokens.push('-'),
442            Some(_) => {
443                // A lyric that says nothing of its place in a word ends none.
444                let ends = note.lyrics().first().is_some_and(|lyric| {
445                    matches!(
446                        lyric.explicit_syllabic(),
447                        Some(Syllabic::Single | Syllabic::End)
448                    )
449                });
450                self.tokens.push_str(if ends { "---" } else { "--" });
451            }
452        }
453        Ok(())
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    fn names(part: &Stream) -> Vec<String> {
462        part.pitches()
463            .iter()
464            .map(|pitch| pitch.name_with_octave())
465            .collect()
466    }
467
468    fn notes(part: &Stream) -> Vec<Note> {
469        part.leaves()
470            .into_iter()
471            .filter_map(|(_, element)| match element {
472                StreamElement::Note(note) => Some(note.clone()),
473                _ => None,
474            })
475            .collect()
476    }
477
478    #[test]
479    fn a_chant_is_read_and_written_back_as_music21_writes_it() {
480        let part = from_volpiano("1--c--d---f--d---ed--c--d---f---g--h--j---hgf--g--h---").unwrap();
481        assert_eq!(
482            to_volpiano(&part).unwrap(),
483            "1---c-d-f-d-ed-c-d-f-g-h-j-hgf-g-h-"
484        );
485    }
486
487    #[test]
488    fn a_clef_moves_the_notes_after_it() {
489        let part = from_volpiano("1---c--2---c").unwrap();
490        assert_eq!(names(&part), ["C4", "E2"]);
491    }
492
493    #[test]
494    fn flats_and_naturals_last_until_they_are_taken_back() {
495        let part = from_volpiano("1---e--we--e--We--e").unwrap();
496        assert_eq!(names(&part), ["E4", "Eb4", "Eb4", "E4", "E4"]);
497        assert_eq!(to_volpiano(&part).unwrap(), "1---e-we-e-We-e-");
498    }
499
500    #[test]
501    fn a_liquescent_note_has_an_x_notehead() {
502        let part = from_volpiano("1---e-E--").unwrap();
503        let notes = notes(&part);
504        assert_eq!(notes[0].notehead(), Notehead::Normal);
505        assert_eq!(notes[1].notehead(), Notehead::X);
506        assert_eq!(to_volpiano(&part).unwrap(), "1---e-E-");
507    }
508
509    #[test]
510    fn breaks_and_neumes_go_in_the_measure_being_read() {
511        let part = from_volpiano("1---e-3-ef-g-7-e-4-gh--j-77").unwrap();
512        let measures = part.measures();
513        assert_eq!(measures.len(), 3);
514        let breaks: Vec<Break> = measures[1]
515            .events()
516            .iter()
517            .filter_map(|event| match event.element() {
518                StreamElement::Break(kind) => Some(*kind),
519                _ => None,
520            })
521            .collect();
522        assert_eq!(breaks, [Break::Line]);
523        assert_eq!(part.spanners().len(), 2);
524        assert_eq!(
525            to_volpiano(&part).unwrap(),
526            "1---e----3ef-g7---e----4gh-j77---"
527        );
528    }
529
530    #[test]
531    fn a_neume_takes_every_note_of_its_run() {
532        let part = from_volpiano("1---cdef-g-").unwrap();
533        assert_eq!(part.spanners().len(), 1);
534        // The clef is the part's first leaf.
535        assert_eq!(
536            part.spanners()[0].spanned(),
537            [Some(1), Some(2), Some(3), Some(4)]
538        );
539        assert_eq!(to_volpiano(&part).unwrap(), "1---cdef-g-");
540    }
541
542    #[test]
543    fn a_neume_ends_at_a_barline_or_the_end() {
544        let part = from_volpiano("1---ef3-gh").unwrap();
545        let lengths: Vec<usize> = part
546            .spanners()
547            .iter()
548            .map(|neume| neume.spanned().len())
549            .collect();
550        assert_eq!(lengths, [2, 2]);
551        assert_eq!(to_volpiano(&part).unwrap(), "1---ef----3gh-");
552    }
553}