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 ["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 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 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}