1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub enum Break {
33 Line,
35 Page,
37 Column,
39}
40
41impl Break {
42 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 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
70const NORMAL_PITCHES: &str = "9abcdefghjklmnopqrs";
73const LIQUESCENT_PITCHES: &str = ")ABCDEFGHJKLMNOPQRS";
75const E_FLAT_TOKENS: &str = "wx";
76const B_FLAT_TOKENS: &str = "iyz";
77
78const BELOW_LOWEST_LINE: IntegerType = 5;
80
81pub 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 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 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 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
232fn append(measure: &mut Stream, element: StreamElement) {
235 let at = measure.end_offset();
236 measure.insert_sorted(vec![StreamEvent::new(at, element)]);
237}
238
239fn 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
245pub 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
287fn 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 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 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 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 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}