Skip to main content

music21/
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::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::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::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::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 ["TrebleClef", "Treble8vbClef", "Treble8vaClef"]
377                    .iter()
378                    .any(|class| clef.is_a(class))
379                {
380                    self.tokens.push_str("1---");
381                } else if ["BassClef", "Bass8vbClef", "Bass8vaClef"]
382                    .iter()
383                    .any(|class| clef.is_a(class))
384                {
385                    self.tokens.push_str("2---");
386                }
387            }
388            StreamElement::Barline(barline) => self.barline(Some(barline)),
389            StreamElement::Note(note) => self.note(note)?,
390            StreamElement::Break(kind) => {
391                self.pop_hyphens();
392                self.tokens.push_str(kind.token());
393                self.tokens.push_str("---");
394            }
395            _ => {}
396        }
397        Ok(())
398    }
399
400    fn note(&mut self, note: &Note) -> Result<()> {
401        let pitch = note.pitch();
402        let lowest_line = self.lowest_line.ok_or_else(|| {
403            Error::Stream("a note under a clef that places no pitches has no Volpiano".to_string())
404        })?;
405        let distance = pitch.diatonic_note_number() - lowest_line;
406        let Ok(index) = usize::try_from(distance + BELOW_LOWEST_LINE) else {
407            return Ok(());
408        };
409        let pitches = if note.notehead() == Notehead::X {
410            LIQUESCENT_PITCHES
411        } else {
412            NORMAL_PITCHES
413        };
414        let Some(token) = pitches.chars().nth(index) else {
415            return Ok(());
416        };
417        let step = pitch.step();
418        match pitch.written_accidental() {
419            Some(accidental) if accidental.alter() != 0.0 => {
420                if !matches!(step, StepName::B | StepName::E) || accidental.alter() != -1.0 {
421                    self.tokens.push(token);
422                    return Ok(());
423                }
424                if step == StepName::B && !self.b_is_flat {
425                    self.accidental_at(distance, false);
426                    self.b_is_flat = true;
427                } else if step == StepName::E && !self.e_is_flat {
428                    self.accidental_at(distance, false);
429                    self.e_is_flat = true;
430                }
431            }
432            _ if step == StepName::B && self.b_is_flat => {
433                self.accidental_at(distance, true);
434                self.b_is_flat = false;
435            }
436            _ if step == StepName::E && self.e_is_flat => {
437                self.accidental_at(distance, true);
438                self.e_is_flat = false;
439            }
440            _ => {}
441        }
442        self.tokens.push(token);
443        if self.inside.get(&self.position) == Some(&true) {
444            return Ok(());
445        }
446        match note.lyric().filter(|sung| !sung.is_empty()) {
447            None => self.tokens.push('-'),
448            Some(_) => {
449                // A lyric that says nothing of its place in a word ends none.
450                let ends = note.lyrics().first().is_some_and(|lyric| {
451                    matches!(
452                        lyric.explicit_syllabic(),
453                        Some(Syllabic::Single | Syllabic::End)
454                    )
455                });
456                self.tokens.push_str(if ends { "---" } else { "--" });
457            }
458        }
459        Ok(())
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    fn names(part: &Stream) -> Vec<String> {
468        part.pitches()
469            .iter()
470            .map(|pitch| pitch.name_with_octave())
471            .collect()
472    }
473
474    fn notes(part: &Stream) -> Vec<Note> {
475        part.leaves()
476            .into_iter()
477            .filter_map(|(_, element)| match element {
478                StreamElement::Note(note) => Some(note.clone()),
479                _ => None,
480            })
481            .collect()
482    }
483
484    #[test]
485    fn a_chant_is_read_and_written_back_as_music21_writes_it() {
486        let part = from_volpiano("1--c--d---f--d---ed--c--d---f---g--h--j---hgf--g--h---").unwrap();
487        assert_eq!(
488            to_volpiano(&part).unwrap(),
489            "1---c-d-f-d-ed-c-d-f-g-h-j-hgf-g-h-"
490        );
491    }
492
493    #[test]
494    fn a_clef_moves_the_notes_after_it() {
495        let part = from_volpiano("1---c--2---c").unwrap();
496        assert_eq!(names(&part), ["C4", "E2"]);
497    }
498
499    #[test]
500    fn flats_and_naturals_last_until_they_are_taken_back() {
501        let part = from_volpiano("1---e--we--e--We--e").unwrap();
502        assert_eq!(names(&part), ["E4", "Eb4", "Eb4", "E4", "E4"]);
503        assert_eq!(to_volpiano(&part).unwrap(), "1---e-we-e-We-e-");
504    }
505
506    #[test]
507    fn a_liquescent_note_has_an_x_notehead() {
508        let part = from_volpiano("1---e-E--").unwrap();
509        let notes = notes(&part);
510        assert_eq!(notes[0].notehead(), Notehead::Normal);
511        assert_eq!(notes[1].notehead(), Notehead::X);
512        assert_eq!(to_volpiano(&part).unwrap(), "1---e-E-");
513    }
514
515    #[test]
516    fn breaks_and_neumes_go_in_the_measure_being_read() {
517        let part = from_volpiano("1---e-3-ef-g-7-e-4-gh--j-77").unwrap();
518        let measures = part.measures();
519        assert_eq!(measures.len(), 3);
520        let breaks: Vec<Break> = measures[1]
521            .events()
522            .iter()
523            .filter_map(|event| match event.element() {
524                StreamElement::Break(kind) => Some(*kind),
525                _ => None,
526            })
527            .collect();
528        assert_eq!(breaks, [Break::Line]);
529        assert_eq!(part.spanners().len(), 2);
530        assert_eq!(
531            to_volpiano(&part).unwrap(),
532            "1---e----3ef-g7---e----4gh-j77---"
533        );
534    }
535
536    #[test]
537    fn a_neume_takes_every_note_of_its_run() {
538        let part = from_volpiano("1---cdef-g-").unwrap();
539        assert_eq!(part.spanners().len(), 1);
540        // The clef is the part's first leaf.
541        assert_eq!(
542            part.spanners()[0].spanned(),
543            [Some(1), Some(2), Some(3), Some(4)]
544        );
545        assert_eq!(to_volpiano(&part).unwrap(), "1---cdef-g-");
546    }
547
548    #[test]
549    fn a_neume_ends_at_a_barline_or_the_end() {
550        let part = from_volpiano("1---ef3-gh").unwrap();
551        let lengths: Vec<usize> = part
552            .spanners()
553            .iter()
554            .map(|neume| neume.spanned().len())
555            .collect();
556        assert_eq!(lengths, [2, 2]);
557        assert_eq!(to_volpiano(&part).unwrap(), "1---ef----3gh-");
558    }
559}