1pub mod harm;
12mod tables;
13
14use std::collections::BTreeMap;
15
16use crate::articulations::{Articulation, ArticulationKind};
17use crate::bar::{Barline, BarlineType, RepeatDirection};
18use crate::chord::Chord;
19use crate::clef::{Clef, ClefKind};
20use crate::defaults::{FloatType, IntegerType};
21use crate::duration::{Duration, DurationType, Grace, Tuplet, TupletType};
22use crate::dynamics::Dynamic;
23use crate::error::{Error, Result};
24use crate::expressions::{Expression, Fermata, Ornament, OrnamentKind};
25use crate::instrument::Instrument;
26use crate::key::{Key, KeySignature};
27use crate::makenotation::op_frac;
28use crate::metadata::{Metadata, is_contributor_unique_name};
29use crate::meter::TimeSignature;
30use crate::notation::{BeamDirection, BeamType, StemDirection, Tie, TieType};
31use crate::note::Note;
32use crate::pitch::{Accidental, PitchOptions};
33use crate::rest::Rest;
34use crate::roman::{Minor67Default, RomanNumeral};
35use crate::stream::{Stream, StreamElement, StreamEvent, StreamKind};
36use crate::tempo::MetronomeMark;
37
38use tables::{INSTRUMENT_CLASSES, INSTRUMENTS, REFERENCE_NAMES};
39
40fn humdrum_error(message: impl Into<String>) -> Error {
41 Error::Humdrum(message.into())
42}
43
44const SPINE_PATHS: [&str; 6] = ["*+", "*-", "*^", "*v", "*x", "*"];
46
47#[derive(Clone, Debug, Default)]
49struct Measure {
50 number: IntegerType,
51 suffix: Option<String>,
52 left: Option<Barline>,
53 right: Option<Barline>,
54}
55
56#[allow(clippy::large_enum_variant)]
60#[derive(Clone, Debug)]
61enum Thing {
62 Element(StreamElement),
63 Measure(Measure),
64 Tandem(String),
66 Word(String),
68}
69
70#[derive(Clone, Debug)]
74struct Placed {
75 offset: FloatType,
76 line: usize,
77 priority: usize,
79 voice: Option<usize>,
80 thing: Thing,
81}
82
83impl Placed {
84 fn length(&self) -> FloatType {
85 match &self.thing {
86 Thing::Element(element) => element.quarter_length(),
87 _ => 0.0,
88 }
89 }
90
91 fn class_order(&self) -> i32 {
92 match &self.thing {
93 Thing::Element(element) => element.class_sort_order(),
94 Thing::Measure(_) => -20,
95 _ => 0,
96 }
97 }
98}
99
100fn sort(things: &mut [Placed]) {
102 things.sort_by(|left, right| {
103 left.offset
104 .total_cmp(&right.offset)
105 .then(left.priority.cmp(&right.priority))
106 .then(left.class_order().cmp(&right.class_order()))
107 });
108}
109
110fn highest(things: &[Placed]) -> FloatType {
111 things
112 .iter()
113 .map(|thing| thing.offset + thing.length())
114 .fold(0.0, FloatType::max)
115}
116
117#[derive(Clone, Debug, Default)]
119struct Spine {
120 events: Vec<(String, usize)>,
121 parent: Option<usize>,
122 first_voice: bool,
123 splits: BTreeMap<usize, Vec<usize>>,
125 kind: String,
126 stream: Vec<Placed>,
127}
128
129fn type_of_number(number: u64) -> Option<DurationType> {
131 ((1..=2048).contains(&number) && number.is_power_of_two())
132 .then(|| {
133 DurationType::ALL
134 .into_iter()
135 .find(|kind| kind.quarter_length() == 4.0 / number as FloatType)
136 })
137 .flatten()
138}
139
140fn first_number(text: &str) -> Option<&str> {
141 let start = text.find(|c: char| c.is_ascii_digit())?;
142 let rest = &text[start..];
143 let end = rest
144 .find(|c: char| !c.is_ascii_digit())
145 .unwrap_or(rest.len());
146 Some(&rest[..end])
147}
148
149fn duration_of(contents: &str) -> Result<Duration> {
152 let dots = contents.matches('.').count() as u32;
153 let has_grace = contents.contains('q') || contents.contains('Q');
154 let number = first_number(contents);
155 let rational = contents.contains('%').then(|| {
156 let at = contents.find('%')?;
157 let before = &contents[..at];
158 let start = before
159 .rfind(|c: char| !c.is_ascii_digit())
160 .map_or(0, |index| index + 1);
161 let first = before[start..].parse::<FloatType>().ok()?;
162 let second = first_number(&contents[at + 1..])
163 .filter(|_| contents[at + 1..].starts_with(|c: char| c.is_ascii_digit()))?
164 .parse::<FloatType>()
165 .ok()?;
166 Some((first, second))
167 });
168 let mut duration = if let Some(Some((first, second))) = rational {
169 let plain = Duration::new(op_frac(4.0 * second / first))?;
170 match plain.type_and_dots() {
171 Some((kind, _)) if dots > 0 => Duration::from_type_with_dots(kind, dots),
172 _ => plain,
173 }
174 } else if let Some(number) = number {
175 let value: u64 = number
176 .parse()
177 .map_err(|_| humdrum_error(format!("Could not find a duration in {contents:?}")))?;
178 if value == 0 {
179 let kind = match number {
180 "000" => DurationType::Maxima,
181 "00" => DurationType::Longa,
182 _ => DurationType::Breve,
183 };
184 Duration::from_type_with_dots(kind, dots)
185 } else if let Some(kind) = type_of_number(value) {
186 Duration::from_type_with_dots(kind, dots)
187 } else {
188 let base = 1_u64 << value.ilog2();
191 let kind = type_of_number(base)
192 .ok_or_else(|| humdrum_error(format!("no note value for {value}")))?;
193 let divisor = gcd(value, base);
194 let mut tuplet =
195 Tuplet::new((value / divisor) as u32, (base / divisor) as u32, kind, 0);
196 if dots > 0 {
197 tuplet.set_duration_normal(Some((kind, dots)));
198 }
199 let mut duration = Duration::from_type(kind);
200 duration.append_tuplet(tuplet);
201 duration
202 }
203 } else if has_grace {
204 Duration::from_type(DurationType::Eighth)
205 } else {
206 return Err(humdrum_error(format!(
207 "Could not find a duration in {contents:?}"
208 )));
209 };
210 if has_grace {
211 duration = duration.grace_duration();
212 let mut grace = Grace::new();
213 if contents.matches('q').count() != 1 {
214 grace.set_slash(false);
215 }
216 duration.set_grace(Some(grace));
217 } else if contents.contains('P') {
218 duration = duration.grace_duration();
219 duration.set_grace(Some(Grace::appoggiatura()));
220 }
221 Ok(duration)
222}
223
224fn gcd(mut left: u64, mut right: u64) -> u64 {
225 while right != 0 {
226 (left, right) = (right, left % right);
227 }
228 left
229}
230
231struct Marks {
233 tie: Option<TieType>,
234 expressions: Vec<Expression>,
235 articulations: Vec<Articulation>,
236 stem: StemDirection,
237 beams: Vec<(BeamType, Option<BeamDirection>)>,
238}
239
240fn marks_of(contents: &str) -> Marks {
241 let has = |mark: char| contents.contains(mark);
242 let tie = if has('[') {
243 Some(TieType::Start)
244 } else if has(']') {
245 Some(TieType::Stop)
246 } else if has('_') {
247 Some(TieType::Continue)
248 } else {
249 None
250 };
251 let ornament = |kind: OrnamentKind| Expression::from(Ornament::of_kind(kind));
252 let mut expressions = Vec::new();
253 if has('t') {
254 expressions.push(ornament(OrnamentKind::HalfStepTrill));
255 } else if has('T') {
256 expressions.push(ornament(OrnamentKind::WholeStepTrill));
257 }
258 if has('w') {
259 expressions.push(ornament(OrnamentKind::HalfStepInvertedMordent));
260 } else if has('W') {
261 expressions.push(ornament(OrnamentKind::WholeStepInvertedMordent));
262 } else if has('m') {
263 expressions.push(ornament(OrnamentKind::HalfStepMordent));
264 } else if has('M') {
265 expressions.push(ornament(OrnamentKind::WholeStepMordent));
266 }
267 if has('S') {
268 expressions.push(ornament(OrnamentKind::Turn));
269 } else if has('$') {
270 expressions.push(ornament(OrnamentKind::InvertedTurn));
271 } else if has('R') {
272 expressions.push(ornament(OrnamentKind::Turn));
273 }
274 if has('O') {
275 expressions.push(ornament(OrnamentKind::Ornament));
276 }
277 let mut articulations = Vec::new();
278 let mut articulation = |class: &str| {
279 if let Some(kind) = ArticulationKind::from_class_name(class) {
280 articulations.push(Articulation::of_kind(kind));
281 }
282 };
283 if has('\'') {
284 articulation("Staccato");
285 }
286 if has('"') {
287 articulation("Pizzicato");
288 }
289 if has('`') {
290 articulation("Staccatissimo");
291 }
292 if has('~') {
293 articulation("Tenuto");
294 }
295 if has('^') {
296 articulation("Accent");
297 }
298 if has(';') {
299 expressions.push(Expression::Fermata(Fermata::new()));
300 }
301 if has('v') {
302 articulation("UpBow");
303 } else if has('u') {
304 articulation("DownBow");
305 }
306 let stem = if has('/') {
307 StemDirection::Up
308 } else if has('\\') {
309 StemDirection::Down
310 } else {
311 StemDirection::Unspecified
312 };
313 let mut beams = Vec::new();
314 for (mark, beam, direction) in [
315 ('L', BeamType::Start, None),
316 ('J', BeamType::Stop, None),
317 ('k', BeamType::PartialBeam, Some(BeamDirection::Left)),
318 ('K', BeamType::PartialBeam, Some(BeamDirection::Right)),
319 ] {
320 for _ in 0..contents.matches(mark).count() {
321 beams.push((beam, direction));
322 }
323 }
324 Marks {
325 tie,
326 expressions,
327 articulations,
328 stem,
329 beams,
330 }
331}
332
333fn note_of(contents: &str, given: Option<&Duration>) -> Result<StreamElement> {
335 let duration = match given {
338 Some(duration) => duration.clone(),
339 None => duration_of(contents).unwrap_or_else(|_| Duration::quarter()),
340 };
341 let marks = marks_of(contents);
342 if contents.contains('r') {
343 let mut rest = Rest::new(duration);
344 rest.set_tie(marks.tie.map(Tie::new));
345 *rest.expressions_mut() = marks.expressions;
346 *rest.articulations_mut() = marks.articulations;
347 return Ok(rest.into());
348 }
349 let letters = |c: char| matches!(c, 'a'..='g' | 'A'..='G');
350 let start = contents
351 .find(letters)
352 .ok_or_else(|| humdrum_error(format!("Could not parse {contents} for note information")))?;
353 let name: Vec<char> = contents[start..]
354 .chars()
355 .take_while(|c| letters(*c))
356 .collect();
357 let first = name[0];
358 let count = name.len() as IntegerType;
359 let octave = if first.is_ascii_lowercase() {
360 3 + count
361 } else {
362 4 - count
363 };
364 let mut pitch = PitchOptions::new()
365 .step(first.to_ascii_uppercase())
366 .octave(octave)
367 .build()?;
368 let sharps = contents.matches('#').count() as IntegerType;
369 let flats = contents.matches('-').count() as IntegerType;
370 if sharps > 0 {
371 pitch.set_written_accidental(Some(Accidental::new(sharps)?));
372 } else if flats > 0 {
373 pitch.set_written_accidental(Some(Accidental::new(-flats)?));
374 } else if contents.contains('n') {
375 pitch.set_written_accidental(Some(Accidental::new("n")?));
376 }
377 let mut note = Note::from_pitch(pitch).with_duration(duration);
378 note.set_tie(marks.tie.map(Tie::new));
379 *note.expressions_mut() = marks.expressions;
380 *note.articulations_mut() = marks.articulations;
381 note.set_stem_direction(marks.stem);
382 let mut beams = note.beams().clone();
383 for (beam, direction) in marks.beams {
384 beams.append(beam, direction);
385 }
386 note.set_beams(beams);
387 Ok(note.into())
388}
389
390fn measure_of(contents: &str, has_previous: bool) -> (Measure, Barline) {
393 let mut measure = Measure::default();
394 if let Some(number) = first_number(contents) {
395 measure.number = number.parse().unwrap_or(0);
396 let after = &contents[contents.find(number).unwrap_or(0) + number.len()..];
397 measure.suffix = after
398 .chars()
399 .next()
400 .filter(char::is_ascii_lowercase)
401 .map(String::from);
402 }
403 let bar_type = if contents.contains('-') {
404 Some(BarlineType::None)
405 } else if contents.contains('\'') {
406 Some(BarlineType::Short)
407 } else if contents.contains('`') {
408 Some(BarlineType::Tick)
409 } else if contents.contains("||") || contents.contains("==") {
410 Some(BarlineType::Double)
411 } else if contents.contains("!!") {
412 Some(BarlineType::HeavyHeavy)
413 } else if contents.contains("|!") {
414 Some(BarlineType::Final)
415 } else if contents.contains("!|") {
416 Some(BarlineType::HeavyLight)
417 } else if contents.contains('|') {
418 Some(BarlineType::Regular)
419 } else {
420 None
421 };
422 let chars: Vec<char> = contents.chars().collect();
423 let beside = |colon_first: bool| {
424 chars.windows(2).any(|pair| {
425 let (colon, bar) = if colon_first {
426 (pair[0], pair[1])
427 } else {
428 (pair[1], pair[0])
429 };
430 colon == ':' && matches!(bar, '|' | '!' | '=')
431 })
432 };
433 let both = contents.matches(':').count() > 1;
434 let end_repeat = both || beside(true);
435 let start_repeat = both || beside(false);
436 let make = |direction: Option<RepeatDirection>| {
437 let mut barline = match direction {
438 Some(direction) => Barline::repeat(direction, None),
439 None => Barline::new(BarlineType::Regular),
440 };
441 if let Some(bar_type) = bar_type {
442 barline.set_bar_type(bar_type);
443 }
444 barline
445 };
446 let right = make(end_repeat.then_some(RepeatDirection::End));
447 if has_previous {
448 if start_repeat {
449 measure.left = Some(make(Some(RepeatDirection::Start)));
450 }
451 } else {
452 measure.left = Some(if start_repeat {
453 make(Some(RepeatDirection::Start))
454 } else {
455 right.clone()
456 });
457 }
458 (measure, right)
459}
460
461fn tandem_of(tandem: &str) -> Result<Option<Thing>> {
464 let element = |element: StreamElement| Ok(Some(Thing::Element(element)));
465 if SPINE_PATHS.contains(&tandem) {
466 return Ok(None);
467 }
468 if let Some(kind) = tandem.strip_prefix("*clef") {
469 let clef = match kind {
470 "-" => Clef::of_kind(ClefKind::NoClef),
471 "X" => Clef::of_kind(ClefKind::PercussionClef),
472 "Gv2" | "Gv" => Clef::of_kind(ClefKind::Treble8vbClef),
473 "G^2" | "G^" => Clef::of_kind(ClefKind::Treble8vaClef),
474 "Fv4" | "Fv" => Clef::of_kind(ClefKind::Bass8vbClef),
475 other => Clef::from_string(other, 0)
476 .map_err(|_| humdrum_error(format!("Unknown clef type {tandem} found")))?,
477 };
478 return element(clef.into());
479 }
480 if let Some(text) = tandem.strip_prefix("*MM") {
481 let mark = match text.parse::<FloatType>() {
482 Ok(number) => MetronomeMark::new(number),
483 Err(_) => {
484 let text = text.strip_prefix('[').unwrap_or(text);
485 let text = text.trim_end();
486 MetronomeMark::from_text(text.strip_suffix(']').unwrap_or(text))
487 }
488 };
489 return element(mark.into());
490 }
491 if let Some(meter) = tandem.strip_prefix("*M") {
492 let digits = |text: &str| text.chars().take_while(char::is_ascii_digit).count();
493 let top = digits(meter);
494 let bottom = meter
495 .get(top + 1..)
496 .filter(|_| top > 0 && meter[top..].starts_with('/'))
497 .map(|rest| &rest[..digits(rest)])
498 .filter(|bottom| !bottom.is_empty())
499 .ok_or_else(|| humdrum_error(format!("Incorrect meter: {tandem} found")))?;
500 let numerator: u32 = meter[..top]
501 .parse()
502 .map_err(|_| humdrum_error(format!("Incorrect meter: {tandem} found")))?;
503 let signature = match bottom {
504 "0" => TimeSignature::new(numerator * 2, 1)?,
505 "00" => TimeSignature::new(numerator * 4, 1)?,
506 "000" => TimeSignature::new(numerator * 8, 1)?,
507 _ => TimeSignature::from_ratio_string(meter)?,
508 };
509 return element(signature.into());
510 }
511 let instrument = |table: &[(&str, &str)], code: &str| -> Option<Instrument> {
512 let class = table.iter().find(|(held, _)| *held == code)?.1;
513 Instrument::of_kind(class).ok()
514 };
515 if let Some(class) = tandem.strip_prefix("*IC") {
516 return Ok(Some(match instrument(INSTRUMENT_CLASSES, class) {
517 Some(instrument) => Thing::Element(instrument.into()),
518 None => Thing::Tandem(class.to_string()),
519 }));
520 }
521 if tandem.starts_with("*IG") || tandem.starts_with("*ITr") {
522 return Ok(Some(Thing::Tandem(tandem.to_string())));
523 }
524 if let Some(code) = tandem.strip_prefix("*I") {
525 return Ok(Some(match instrument(INSTRUMENTS, code) {
526 Some(instrument) => Thing::Element(instrument.into()),
527 None => Thing::Tandem(code.to_string()),
528 }));
529 }
530 if tandem.starts_with("*k") {
531 let mut sharps = tandem.matches('#').count() as IntegerType;
532 if sharps == 0 {
533 sharps = -(tandem.matches('-').count() as IntegerType);
534 }
535 return element(KeySignature::new(sharps).into());
536 }
537 if let Some(key) = tandem.strip_suffix(':') {
538 return element(StreamElement::Key(Key::from_tonic(&key[1..])?));
539 }
540 Ok(Some(Thing::Tandem(tandem.to_string())))
541}
542
543#[derive(Default)]
546struct KernState {
547 in_tuplet: bool,
548 last_note: Option<usize>,
549 beams: usize,
550 tuplet_length: FloatType,
551 wanted_length: FloatType,
552}
553
554fn with_duration(element: &mut StreamElement, change: impl FnOnce(&mut Duration)) {
555 let Some(mut duration) = element.duration().cloned() else {
556 return;
557 };
558 change(&mut duration);
559 match element {
560 StreamElement::Note(note) => note.set_duration(duration),
561 StreamElement::Chord(chord) => chord.set_duration(duration),
562 StreamElement::Rest(rest) => rest.set_duration(duration),
563 _ => {}
564 }
565}
566
567fn mark_first_tuplet(element: &mut StreamElement, kind: TupletType) {
568 with_duration(element, |duration| {
569 let mut tuplets = duration.tuplets();
570 if let Some(first) = tuplets.first_mut() {
571 first.set_tuplet_type(Some(kind));
572 }
573 duration.set_tuplets(tuplets);
574 });
575}
576
577impl KernState {
578 fn beam(&mut self, element: &mut StreamElement) {
581 let beams = match element {
582 StreamElement::Note(note) => note.beams().clone(),
583 StreamElement::Chord(chord) => chord.beams().clone(),
584 _ => return,
585 };
586 if self.beams != 0 && beams.is_empty() {
587 let mut carried = beams;
588 for _ in 0..self.beams {
589 carried.append(BeamType::Continue, None);
590 }
591 match element {
592 StreamElement::Note(note) => note.set_beams(carried),
593 StreamElement::Chord(chord) => chord.set_beams(carried),
594 _ => {}
595 }
596 } else if let Some(first) = beams.beams().first() {
597 if first.beam_type() == Some(BeamType::Stop) {
598 self.beams = 0;
599 } else {
600 self.beams += beams
601 .beams()
602 .iter()
603 .filter(|beam| beam.beam_type() != Some(BeamType::Stop))
604 .count();
605 }
606 }
607 }
608
609 fn tuplet(&mut self, stream: &mut [Placed], element: &mut StreamElement) {
612 let Some(duration) = element.duration().cloned() else {
613 return;
614 };
615 let tuplets = duration.tuplets();
616 if !self.in_tuplet && !tuplets.is_empty() {
617 self.in_tuplet = true;
618 self.wanted_length = tuplets[0].total_tuplet_length();
619 self.tuplet_length = duration.quarter_length();
620 mark_first_tuplet(element, TupletType::Start);
621 } else if self.in_tuplet && tuplets.is_empty() {
622 self.in_tuplet = false;
623 self.wanted_length = 0.0;
624 self.tuplet_length = 0.0;
625 if let Some(last) = self.last_note
626 && let Thing::Element(last) = &mut stream[last].thing
627 {
628 mark_first_tuplet(last, TupletType::Stop);
629 }
630 } else if self.in_tuplet {
631 self.tuplet_length = op_frac(self.tuplet_length + duration.quarter_length());
632 if self.tuplet_length >= self.wanted_length
633 && self.tuplet_length % self.wanted_length == 0.0
634 {
635 mark_first_tuplet(element, TupletType::Stop);
636 self.in_tuplet = false;
637 self.tuplet_length = 0.0;
638 self.wanted_length = 0.0;
639 }
640 }
641 }
642}
643
644fn chord_of(contents: &str) -> Result<StreamElement> {
646 let mut notes: Vec<Note> = Vec::new();
647 let mut first: Option<(Duration, String)> = None;
648 for token in contents.split_whitespace() {
649 let length: String = token
650 .chars()
651 .filter(|c| c.is_ascii_digit() || matches!(c, '.' | '%'))
652 .collect();
653 let bare: String = token
654 .chars()
655 .filter(|c| !(c.is_ascii_digit() || matches!(c, '.' | '%')))
656 .collect();
657 let element = match &first {
658 Some((duration, written)) if length.is_empty() || length == *written => {
659 note_of(&bare, Some(duration))?
660 }
661 _ => {
662 let element = note_of(token, None)?;
663 if first.is_none()
664 && let Some(duration) = element.duration()
665 {
666 first = Some((duration.clone(), length));
667 }
668 element
669 }
670 };
671 if let StreamElement::Note(note) = element {
672 notes.push(note);
673 }
674 }
675 let last_beams = notes
676 .last()
677 .map(|note| note.beams().clone())
678 .ok_or_else(|| humdrum_error("a chord with no notes in it"))?;
679 let duration = first.map(|(duration, _)| duration).unwrap_or_default();
680 let mut chord = Chord::new(notes)?.with_duration(duration);
681 chord.set_beams(last_beams);
682 Ok(chord.into())
683}
684
685fn append(stream: &mut Vec<Placed>, line: usize, priority: usize, thing: Thing) -> usize {
687 let offset = highest(stream);
688 stream.push(Placed {
689 offset,
690 line,
691 priority,
692 voice: None,
693 thing,
694 });
695 stream.len() - 1
696}
697
698fn parse_kern(events: &[(String, usize)]) -> Vec<Placed> {
700 let mut stream: Vec<Placed> = Vec::new();
701 let mut state = KernState::default();
702 let mut last_measure: Option<usize> = None;
704 for (contents, line) in events {
705 let made: Result<Option<Thing>> = (|| {
706 if contents == "." || contents.is_empty() {
707 return Ok(None);
708 }
709 if contents.starts_with('*') {
710 return tandem_of(contents);
711 }
712 if contents.starts_with('=') {
713 let (measure, right) = measure_of(contents, true);
716 if let Some(previous) = last_measure
717 && let Thing::Measure(previous) = &mut stream[previous].thing
718 {
719 previous.right = Some(right);
720 }
721 return Ok(Some(Thing::Measure(measure)));
722 }
723 if contents.starts_with('!') {
724 return Ok(None);
725 }
726 let mut element = if contents.contains(' ') {
727 chord_of(contents)?
728 } else {
729 note_of(contents, None)?
730 };
731 state.beam(&mut element);
732 state.tuplet(&mut stream, &mut element);
733 Ok(Some(Thing::Element(element)))
734 })();
735 let Ok(Some(thing)) = made else {
738 continue;
739 };
740 let is_measure = matches!(thing, Thing::Measure(_));
741 let sounds = matches!(
742 &thing,
743 Thing::Element(
744 StreamElement::Note(_) | StreamElement::Chord(_) | StreamElement::Rest(_)
745 )
746 );
747 let index = append(&mut stream, *line, *line, thing);
748 if is_measure {
749 last_measure = Some(index);
750 }
751 if sounds {
752 state.last_note = Some(index);
753 }
754 }
755 stream
756}
757
758fn parse_other(events: &[(String, usize)], dynamics: bool) -> Vec<Placed> {
761 let mut stream = Vec::new();
762 for (contents, line) in events {
763 let thing = if contents == "." || contents.is_empty() {
764 continue;
765 } else if contents.starts_with('*') {
766 if SPINE_PATHS.contains(&contents.as_str()) {
767 continue;
768 }
769 Thing::Tandem(contents.clone())
770 } else if contents.starts_with('=') || contents.starts_with('!') {
771 continue;
772 } else if dynamics {
773 if contents.starts_with('<') || contents.starts_with('>') {
774 continue;
775 }
776 Thing::Element(Dynamic::new(contents.as_str()).into())
777 } else {
778 Thing::Word(contents.clone())
779 };
780 append(&mut stream, *line, 0, thing);
781 }
782 stream
783}
784
785fn parse_harm(events: &[(String, usize)]) -> Result<Vec<Placed>> {
794 let mut stream = Vec::new();
795 let mut key = Key::from_tonic("C")?;
798 for (contents, line) in events {
799 if contents == "." || contents.is_empty() {
800 continue;
801 }
802 if contents.starts_with('*') {
803 if SPINE_PATHS.contains(&contents.as_str()) {
804 continue;
805 }
806 match contents.strip_suffix(':') {
807 Some(named) => key = Key::from_tonic(&named[1..])?,
808 None => {
811 append(&mut stream, *line, 0, Thing::Tandem(contents.clone()));
812 }
813 }
814 continue;
815 }
816 if contents.starts_with('=') || contents.starts_with('!') {
817 continue;
818 }
819 let Some(figure) = harm::convert_harm_to_roman(contents) else {
820 continue;
821 };
822 let numeral = RomanNumeral::with_minor_defaults(
823 &figure,
824 key.clone(),
825 Minor67Default::Flat,
826 Minor67Default::Sharp,
827 )?;
828 let chord = numeral.to_chord()?.with_numeral(numeral);
829 append(&mut stream, *line, 0, Thing::Element(chord.into()));
830 }
831 Ok(stream)
832}
833
834fn spines_of(lines: &[(usize, Vec<String>)], width: usize) -> Result<Vec<Spine>> {
837 let mut spines: Vec<Spine> = Vec::new();
838 let mut current: Vec<Option<usize>> = Vec::new();
839 for (line, cells) in lines {
840 if current.len() < width {
841 current.resize(width, None);
842 }
843 for (column, cell) in cells.iter().enumerate().take(width) {
844 let spine = match current[column] {
845 Some(spine) => spine,
846 None => {
847 spines.push(Spine::default());
848 current[column] = Some(spines.len() - 1);
849 spines.len() - 1
850 }
851 };
852 spines[spine].events.push((cell.clone(), *line));
853 }
854 if !cells
855 .iter()
856 .any(|cell| SPINE_PATHS.contains(&cell.as_str()))
857 {
858 continue;
859 }
860 let mut next: Vec<Option<usize>> = Vec::new();
861 let mut merging = false;
862 let mut merge_parent: Option<usize> = None;
863 let mut exchange: Option<usize> = None;
864 for (column, spine) in current.clone().into_iter().enumerate().take(width) {
865 let Some(cell) = cells.get(column) else {
866 if spine.is_some() {
867 next.push(spine);
868 }
869 continue;
870 };
871 let Some(spine) = spine else {
872 return Err(humdrum_error(format!(
873 "spine-path token at line {line} has no active spine"
874 )));
875 };
876 match cell.as_str() {
877 "*-" => {}
878 "*^" => {
879 let mut voices = Vec::new();
880 for first in [true, false] {
881 spines.push(Spine {
882 parent: Some(spine),
883 first_voice: first,
884 ..Spine::default()
885 });
886 voices.push(spines.len() - 1);
887 next.push(Some(spines.len() - 1));
888 }
889 spines[spine].splits.insert(*line, voices);
890 }
891 "*v" => {
892 if merging {
893 let parent = spines[spine].parent.or(merge_parent);
894 match parent {
895 Some(parent) => next.push(Some(parent)),
896 None => {
897 spines.push(Spine::default());
898 next.push(Some(spines.len() - 1));
899 }
900 }
901 merging = false;
902 merge_parent = None;
903 } else {
904 merging = true;
905 merge_parent = spines[spine].parent;
906 }
907 }
908 "*x" => match exchange.take() {
909 None => exchange = Some(spine),
910 Some(waiting) => {
911 next.push(Some(spine));
912 next.push(Some(waiting));
913 }
914 },
915 _ => next.push(Some(spine)),
916 }
917 }
918 if exchange.is_some() {
919 return Err(humdrum_error(format!(
920 "ProtoSpine found with unpaired exchange instruction at line {line}"
921 )));
922 }
923 current = next;
924 }
925 Ok(spines)
926}
927
928fn kind_of(spines: &[Spine], index: usize) -> Result<String> {
930 let mut at = Some(index);
931 while let Some(spine) = at {
932 if let Some(kind) = spines[spine]
933 .events
934 .iter()
935 .find_map(|(contents, _)| contents.strip_prefix("**"))
936 {
937 return Ok(kind.to_string());
938 }
939 at = spines[spine].parent;
940 }
941 Err(humdrum_error(format!(
942 "Could not determine spineType for spine with id {index}"
943 )))
944}
945
946fn insert_voices(spines: &mut [Spine], index: usize) {
949 let mut points: Vec<usize> = spines[index].splits.keys().copied().collect();
950 if points.is_empty() {
951 return;
952 }
953 let old = std::mem::take(&mut spines[index].stream);
954 let mut new: Vec<Placed> = Vec::new();
955 let insert = |new: &mut Vec<Placed>, spines: &[Spine], point: usize| {
956 let start = highest(new);
957 for (number, child) in spines[index].splits[&point].iter().enumerate() {
958 for thing in &spines[*child].stream {
959 if !spines[*child].first_voice && matches!(thing.thing, Thing::Measure(_)) {
960 continue;
961 }
962 let mut thing = thing.clone();
963 thing.offset += start;
964 thing.voice.get_or_insert(number + 1);
965 new.push(thing);
966 }
967 }
968 };
969 let mut last: Option<usize> = None;
970 for thing in old {
971 let mut at = 0;
974 while at < points.len() {
975 let point = points[at];
976 if last.is_some_and(|last| last > point) || thing.line < point {
977 at += 1;
978 continue;
979 }
980 insert(&mut new, spines, point);
981 points.remove(at);
982 at += 1;
983 }
984 last = Some(thing.line);
985 let offset = highest(&new);
986 new.push(Placed { offset, ..thing });
987 }
988 for point in points {
989 insert(&mut new, spines, point);
990 }
991 spines[index].stream = new;
992}
993
994struct Gathered {
996 measure: Measure,
997 offset: FloatType,
998 things: Vec<Placed>,
999}
1000
1001fn into_measures(mut stream: Vec<Placed>) -> (Vec<Gathered>, Vec<Placed>) {
1004 sort(&mut stream);
1005 let mut measures: Vec<Gathered> = Vec::new();
1006 let mut loose: Vec<Placed> = Vec::new();
1007 let mut current = Gathered {
1008 measure: Measure::default(),
1009 offset: 0.0,
1010 things: Vec::new(),
1011 };
1012 let mut number = 0;
1013 let mut has_one = false;
1014 for thing in stream {
1015 match thing.thing {
1016 Thing::Measure(measure) => {
1017 if number != 0 || !current.things.is_empty() {
1018 measures.push(current);
1019 }
1020 number = measure.number;
1021 has_one |= number == 1;
1022 current = Gathered {
1023 measure,
1024 offset: thing.offset,
1025 things: Vec::new(),
1026 };
1027 }
1028 _ => {
1029 if number != 0 || thing.length() != 0.0 {
1030 let offset = thing.offset - current.offset;
1031 current.things.push(Placed { offset, ..thing });
1032 } else {
1033 loose.push(thing);
1034 }
1035 }
1036 }
1037 }
1038 if !current.things.is_empty() {
1039 measures.push(current);
1040 }
1041 if let Some(first) = measures.first_mut() {
1042 if !has_one {
1043 first.measure.number = 1;
1044 }
1045 let (moved, kept): (Vec<Placed>, Vec<Placed>) = loose
1048 .into_iter()
1049 .partition(|thing| thing.offset == 0.0 && matches!(thing.thing, Thing::Element(_)));
1050 first.things.extend(moved);
1051 loose = kept;
1052 }
1053 (measures, loose)
1054}
1055
1056fn fix_tuplets(things: &mut [Placed]) {
1059 let tuplets = |thing: &Placed| match &thing.thing {
1060 Thing::Element(
1061 element @ (StreamElement::Note(_) | StreamElement::Chord(_) | StreamElement::Rest(_)),
1062 ) => Some(
1063 element
1064 .duration()
1065 .map(Duration::tuplets)
1066 .unwrap_or_default(),
1067 ),
1068 _ => None,
1069 };
1070 let mut groups: Vec<Vec<usize>> = Vec::new();
1071 let mut current: Vec<usize> = Vec::new();
1072 for (index, thing) in things.iter().enumerate() {
1073 let Some(held) = tuplets(thing) else {
1074 continue;
1075 };
1076 let Some(first) = held.first() else {
1077 if !current.is_empty() {
1078 groups.push(std::mem::take(&mut current));
1079 }
1080 continue;
1081 };
1082 current.push(index);
1083 if first.tuplet_type() == Some(TupletType::Stop) {
1084 groups.push(std::mem::take(&mut current));
1085 }
1086 }
1087 if !current.is_empty() {
1088 groups.push(current);
1089 }
1090 let ordinal = |kind: DurationType| DurationType::ALL.iter().position(|held| *held == kind);
1091 for group in groups {
1092 let Some(first) = tuplets(&things[group[0]]).and_then(|held| held.into_iter().next())
1093 else {
1094 continue;
1095 };
1096 let total = op_frac(first.total_tuplet_length());
1097 let mut length = 0.0;
1098 let mut smallest: Option<usize> = None;
1099 let mut largest: Option<usize> = None;
1100 for index in &group {
1101 length = op_frac(length + things[*index].length());
1102 let Some(kind) = tuplets(&things[*index])
1103 .and_then(|held| held.into_iter().next())
1104 .and_then(|tuplet| ordinal(tuplet.duration_type()))
1105 else {
1106 continue;
1107 };
1108 smallest = Some(smallest.map_or(kind, |held| held.max(kind)));
1109 largest = Some(largest.map_or(kind, |held| held.min(kind)));
1110 }
1111 if length == total || total == 0.0 {
1112 continue;
1113 }
1114 let excess = op_frac(length / total);
1115 let inverse = op_frac(1.0 / excess);
1116 let chosen = if excess.fract() == 0.0 {
1117 largest
1118 } else if inverse.fract() == 0.0 {
1119 smallest
1120 } else {
1121 None
1122 };
1123 let Some(kind) = chosen.map(|index| DurationType::ALL[index]) else {
1124 continue;
1125 };
1126 for index in group {
1127 if let Thing::Element(element) = &mut things[index].thing {
1128 with_duration(element, |duration| {
1129 let mut held = duration.tuplets();
1130 if let Some(first) = held.first_mut() {
1131 let normal = first.duration_normal().map_or(0, |(_, dots)| dots);
1132 let actual = first.duration_actual().map_or(0, |(_, dots)| dots);
1133 first.set_duration_normal(Some((kind, normal)));
1134 first.set_duration_actual(Some((kind, actual)));
1135 }
1136 duration.set_tuplets(held);
1137 });
1138 }
1139 }
1140 }
1141}
1142
1143fn measure_stream(gathered: Gathered) -> Stream {
1146 let mut things = gathered.things;
1147 sort(&mut things);
1148 fix_tuplets(&mut things);
1149 let lowest = things
1150 .iter()
1151 .find(|thing| thing.voice == Some(1))
1152 .map(|thing| thing.offset);
1153 let mut voices: BTreeMap<usize, Vec<Placed>> = BTreeMap::new();
1154 let mut own: Vec<Placed> = Vec::new();
1155 for thing in things {
1156 match (lowest, thing.voice) {
1157 (Some(lowest), Some(voice)) => voices.entry(voice).or_default().push(Placed {
1158 offset: thing.offset - lowest,
1159 ..thing
1160 }),
1161 _ => own.push(thing),
1162 }
1163 }
1164 let events = |things: Vec<Placed>| -> Vec<(FloatType, usize, i32, StreamElement)> {
1165 things
1166 .into_iter()
1167 .filter_map(|thing| match thing.thing {
1168 Thing::Element(element) => Some((
1169 thing.offset,
1170 thing.priority,
1171 element.class_sort_order(),
1172 element,
1173 )),
1174 _ => None,
1175 })
1176 .collect()
1177 };
1178 let mut held = events(own);
1179 for (_, things) in voices {
1180 let voice = Stream::with_kind(StreamKind::Voice).with_events(
1181 events(things)
1182 .into_iter()
1183 .map(|(offset, _, _, element)| StreamEvent::new(offset, element))
1184 .collect(),
1185 );
1186 held.push((lowest.unwrap_or(0.0), 0, 5, voice.into()));
1187 }
1188 held.sort_by(|left, right| {
1189 left.0
1190 .total_cmp(&right.0)
1191 .then(left.1.cmp(&right.1))
1192 .then(left.2.cmp(&right.2))
1193 });
1194 let mut measure = Stream::with_kind(StreamKind::Measure).with_events(
1195 held.into_iter()
1196 .map(|(offset, _, _, element)| StreamEvent::new(offset, element))
1197 .collect(),
1198 );
1199 measure.set_number(gathered.measure.number);
1200 measure.set_number_suffix(gathered.measure.suffix);
1201 measure.set_left_barline(gathered.measure.left);
1202 measure.set_right_barline(gathered.measure.right);
1203 measure
1204}
1205
1206fn staves_of(things: &[Placed]) -> Vec<IntegerType> {
1208 things
1209 .iter()
1210 .find_map(|thing| match &thing.thing {
1211 Thing::Tandem(tandem) => tandem.strip_prefix("*staff"),
1212 _ => None,
1213 })
1214 .map(|staves| {
1215 staves
1216 .split('/')
1217 .filter_map(|staff| staff.parse().ok())
1218 .collect()
1219 })
1220 .unwrap_or_default()
1221}
1222
1223fn reference(metadata: &mut Metadata, line: &str) -> Result<()> {
1226 let text = line.trim_start_matches('!');
1227 let (code, value) = text
1228 .split_once(':')
1229 .ok_or_else(|| humdrum_error(format!("a reference line with no code: {line}")))?;
1230 let code = code.trim().replace("@@", "@");
1231 let code = code.split('@').next().unwrap_or("");
1232 let value = value.trim();
1233 match REFERENCE_NAMES.iter().find(|(held, _)| *held == code) {
1234 Some((_, "")) => metadata.add_text(format!("humdrum:{code}"), value),
1235 Some((_, name)) if is_contributor_unique_name(name) => {
1236 metadata.add_contributor(*name, value);
1237 }
1238 Some((_, name)) => metadata.add_text(*name, value),
1239 None => metadata.add_text(code, value),
1240 }
1241 Ok(())
1242}
1243
1244fn score_of(lines: &[&str]) -> Result<Stream> {
1246 let mut metadata = Metadata::new();
1247 let mut rows: Vec<(usize, Vec<String>)> = Vec::new();
1248 let mut width = 0;
1249 for (index, line) in lines.iter().enumerate() {
1250 let line = line.trim_end();
1251 if line.is_empty() {
1252 continue;
1253 }
1254 if line.starts_with("!!!") {
1255 reference(&mut metadata, line)?;
1256 } else if line.starts_with("!!") {
1257 continue;
1258 } else {
1259 let cells: Vec<String> = line
1260 .split('\t')
1261 .enumerate()
1262 .filter(|(place, cell)| *place == 0 || !cell.is_empty())
1263 .map(|(_, cell)| cell.to_string())
1264 .collect();
1265 width = width.max(cells.len());
1266 rows.push((index + 1, cells));
1267 }
1268 }
1269 let mut spines = spines_of(&rows, width)?;
1270 for index in 0..spines.len() {
1271 spines[index].kind = kind_of(&spines, index)?;
1272 }
1273 for spine in &mut spines {
1274 spine.stream = match spine.kind.as_str() {
1275 "kern" => parse_kern(&spine.events),
1276 "dynam" => parse_other(&spine.events, true),
1277 "harm" => parse_harm(&spine.events)?,
1278 _ => parse_other(&spine.events, false),
1279 };
1280 }
1281 for index in 0..spines.len() {
1282 if spines[index].parent.is_none() {
1283 insert_voices(&mut spines, index);
1284 }
1285 }
1286
1287 let mut parts: Vec<(usize, Vec<Gathered>, Vec<Placed>)> = Vec::new();
1289 for (index, spine) in spines.iter().enumerate() {
1290 if spine.parent.is_none() && spine.kind == "kern" {
1291 let (measures, loose) = into_measures(spine.stream.clone());
1292 parts.push((index, measures, loose));
1293 }
1294 }
1295 let staff_of = |loose: &[Placed]| -> Option<IntegerType> {
1298 loose.iter().find_map(|thing| match &thing.thing {
1299 Thing::Tandem(tandem) => tandem
1300 .strip_prefix("*staff")
1301 .and_then(|staff| staff.parse().ok()),
1302 _ => None,
1303 })
1304 };
1305 for spine in spines
1306 .iter()
1307 .filter(|spine| spine.parent.is_none() && spine.kind != "kern")
1308 {
1309 let staves = staves_of(&spine.stream);
1310 let is_words = matches!(spine.kind.as_str(), "lyrics" | "text");
1311 let is_marks = matches!(spine.kind.as_str(), "dynam" | "harm");
1312 if !is_marks && !is_words {
1313 continue;
1314 }
1315 for staff in staves {
1316 let Some((_, measures, _)) = parts
1317 .iter_mut()
1318 .find(|(_, _, loose)| staff_of(loose) == Some(staff))
1319 else {
1320 if is_marks {
1321 return Err(humdrum_error(format!(
1322 "no staff {staff} for the {} spine",
1323 spine.kind
1324 )));
1325 }
1326 continue;
1327 };
1328 for thing in &spine.stream {
1329 for measure in measures.iter_mut() {
1330 let found = measure
1331 .things
1332 .iter()
1333 .position(|held| held.priority == thing.line);
1334 match (&thing.thing, found) {
1335 (
1336 Thing::Element(
1337 mark @ (StreamElement::Dynamic(_) | StreamElement::Chord(_)),
1338 ),
1339 Some(found),
1340 ) => {
1341 let offset = measure.things[found].offset;
1342 let priority = match mark {
1348 StreamElement::Chord(_) => thing.line,
1349 _ => 0,
1350 };
1351 measure.things.push(Placed {
1352 offset,
1353 line: thing.line,
1354 priority,
1355 voice: None,
1356 thing: Thing::Element(mark.clone()),
1357 });
1358 break;
1359 }
1360 (Thing::Word(word), Some(_)) => {
1361 for held in &mut measure.things {
1362 if held.priority != thing.line {
1363 continue;
1364 }
1365 match &mut held.thing {
1366 Thing::Element(StreamElement::Note(note)) => {
1367 note.set_lyric(Some(word))?;
1368 }
1369 Thing::Element(StreamElement::Rest(rest)) => {
1370 rest.set_lyric(Some(word));
1371 }
1372 Thing::Element(StreamElement::Chord(chord)) => {
1373 if let Some(singer) = chord.notes_mut().first_mut() {
1374 singer.set_lyric(Some(word))?;
1375 }
1376 }
1377 _ => {}
1378 }
1379 }
1380 }
1381 _ => {}
1382 }
1383 }
1384 }
1385 }
1386 }
1387
1388 let mut events = Vec::new();
1391 for (_, measures, _) in parts.into_iter().rev() {
1392 let mut held = Vec::new();
1393 let mut offset = 0.0;
1394 for gathered in measures {
1395 let measure = measure_stream(gathered);
1396 let length = measure.end_offset();
1397 held.push(StreamEvent::new(offset, measure));
1398 offset = op_frac(offset + length);
1399 }
1400 events.push(StreamEvent::new(
1401 0.0,
1402 Stream::with_kind(StreamKind::Part).with_events(held),
1403 ));
1404 }
1405 let mut score = Stream::with_kind(StreamKind::Score).with_events(events);
1406 score.set_metadata(Some(metadata));
1407 Ok(score)
1408}
1409
1410pub fn from_humdrum(text: &str) -> Result<Stream> {
1433 let lines: Vec<&str> = text.lines().collect();
1434 if lines.is_empty() {
1435 return Err(humdrum_error("Need a list of lines to parse!"));
1436 }
1437 let all = |line: &str, test: fn(&str) -> bool| {
1440 let line = line.trim_end();
1441 !line.is_empty() && line.split('\t').all(test)
1442 };
1443 let heading: fn(&str) -> bool = |cell| {
1444 cell.strip_prefix("**").is_some_and(|name| {
1445 !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1446 })
1447 };
1448 let starts: Vec<usize> = (0..lines.len())
1449 .filter(|index| all(lines[*index], heading))
1450 .collect();
1451 let mut ends: Vec<usize> = (0..lines.len())
1452 .filter(|index| all(lines[*index], |cell| cell == "*-"))
1453 .collect();
1454 if starts.len() < 2 {
1455 return score_of(&lines);
1456 }
1457 if ends.last().copied().unwrap_or(0) <= starts[starts.len() - 1] {
1458 ends.push(lines.len());
1459 }
1460 let mut pieces: Vec<&[&str]> = Vec::new();
1461 for (index, end) in ends.iter().enumerate() {
1462 let start = if index == 0 { 0 } else { ends[index - 1] + 1 };
1463 let stop = if index + 1 == ends.len() {
1464 lines.len()
1465 } else {
1466 (*end + 1).min(lines.len())
1467 };
1468 pieces.push(&lines[start.min(stop)..stop]);
1469 }
1470 if pieces.len() < 2 {
1471 return Err(humdrum_error(
1472 "Malformed Humdrum data: possibly multiple **tags without closing information.",
1473 ));
1474 }
1475 let mut events = Vec::new();
1476 let mut offset = 0.0;
1477 for (index, piece) in pieces.iter().enumerate() {
1478 let mut score = score_of(piece)?;
1479 let mut metadata = score.metadata().cloned().unwrap_or_default();
1480 metadata.set(
1481 "number",
1482 vec![crate::metadata::MetadataValue::new((index + 1).to_string())],
1483 );
1484 score.set_metadata(Some(metadata));
1485 let length = score.end_offset();
1486 events.push(StreamEvent::new(offset, score));
1487 offset = op_frac(offset + length);
1488 }
1489 Ok(Stream::with_kind(StreamKind::Opus).with_events(events))
1490}
1491
1492#[cfg(test)]
1493mod tests {
1494 use super::*;
1495
1496 #[test]
1497 fn a_length_is_a_number_and_its_dots() {
1498 assert_eq!(duration_of("4c").unwrap().quarter_length(), 1.0);
1499 assert_eq!(duration_of("8.d").unwrap().quarter_length(), 0.75);
1500 assert_eq!(duration_of("0r").unwrap().quarter_length(), 8.0);
1501 let triplet = duration_of("12e").unwrap();
1503 assert!((triplet.quarter_length() - 1.0 / 3.0).abs() < 1e-9);
1504 assert_eq!(triplet.tuplets()[0].actual(), 3);
1505 assert!(duration_of("qf").unwrap().is_grace());
1506 assert!(duration_of("c").is_err());
1507 }
1508
1509 #[test]
1510 fn letters_say_the_octave() {
1511 let name = |token: &str| match note_of(token, None).unwrap() {
1512 StreamElement::Note(note) => note.pitch().name_with_octave(),
1513 _ => "rest".to_string(),
1514 };
1515 assert_eq!(name("4c"), "C4");
1516 assert_eq!(name("4cc#"), "C#5");
1517 assert_eq!(name("4C"), "C3");
1518 assert_eq!(name("4BB-"), "Bb2");
1519 assert_eq!(name("4r"), "rest");
1520 }
1521
1522 #[test]
1523 fn a_barline_says_how_the_measure_before_ends() {
1524 let (measure, right) = measure_of("=12a:|!", true);
1525 assert_eq!(measure.number, 12);
1526 assert_eq!(measure.suffix.as_deref(), Some("a"));
1527 assert_eq!(right.repeat_direction(), Some(RepeatDirection::End));
1528 let (_, double) = measure_of("==", true);
1529 assert_eq!(double.bar_type(), BarlineType::Double);
1530 }
1531
1532 #[test]
1533 fn spines_are_parts_and_the_last_comes_first() {
1534 let score = from_humdrum(
1535 "**kern\t**kern\n*clefF4\t*clefG2\n*M2/4\t*M2/4\n=1\t=1\n2C\t4c\n.\t4d\n=2\t=2\n\
1536 2D\t2e\n==\t==\n*-\t*-\n",
1537 )
1538 .unwrap();
1539 let parts = score.parts();
1540 assert_eq!(parts.len(), 2);
1541 assert_eq!(parts[0].measures()[0].pitches().len(), 2);
1542 assert_eq!(parts[1].measures()[0].pitches()[0].name_with_octave(), "C3");
1543 }
1544
1545 #[test]
1546 fn a_split_spine_is_two_voices() {
1547 let score = from_humdrum("**kern\n*M2/4\n=1\n*^\n4c\t4e\n4d\t4f\n*v\t*v\n=2\n2g\n==\n*-\n")
1548 .unwrap();
1549 let part = score.parts()[0];
1550 let measures = part.measures();
1551 assert_eq!(measures[0].voices().len(), 2);
1552 assert!(measures[1].voices().is_empty());
1553 }
1554}