use crate::diagnostic::{Diagnostic, Severity, Span};
use crate::model::{
Accidental, AccidentalMark, AlignedLyric, AnnotationPlacementModel, BarlineKind, ChordEvent,
ChordMemberEvent, ClefChangeModel, DecorationAttachment, DecorationSourceKind,
EventAttachments, Fraction, GraceEvent, GraceEventKind, GraceGroupAttachment, GraceNoteEvent,
HarmonyKindText, KeyAccidentalModel, KeySignatureModel, LyricControl, Measure, MeasureBarline,
MeasureId, MeterModel, MidiInstrumentModel, MusicXmlInstrumentRef, MusicXmlPartInstrumentModel,
NoteEvent, Part, PartId, Pitch, RepeatEndingCloseLocation, RepeatEndingCloseModel,
RepeatEndingCloseType, RepeatEndingModel, RepeatEndingPartModel, RestEvent, RestVisibility,
Score, ScoreDirectiveModel, ScoreDirectiveTokenKindModel, ScoreDirectiveTokenModel,
ScoreMetadata, SlurAttachment, SlurRole, Staff, StaffId, TempoBeat, TempoBeatRole, TempoModel,
TextAttachment, TextLine, TieAttachment, TieRole, TimedEvent, TimedEventKind, TupletAttachment,
TupletRole, Voice, VoiceId, VoicePropertiesModel,
};
use crate::parse::ParseReport;
use roxmltree::{Document, Node, ParsingOptions};
use super::notation::tuplet_display_decoration_name;
const READER_SPAN: Span = Span { start: 0, end: 0 };
pub fn read_musicxml(xml: &str) -> ParseReport<Score> {
let options = ParsingOptions {
allow_dtd: true,
..ParsingOptions::default()
};
let document = match Document::parse_with_options(xml, options) {
Ok(document) => document,
Err(error) => {
let diagnostic = Diagnostic::new(
Severity::Error,
"musicxml.read.parse_error",
format!("MusicXML is not well-formed XML: {error}"),
READER_SPAN,
);
return ParseReport::new(empty_score(vec![diagnostic.clone()]), vec![diagnostic]);
}
};
let mut reader = Reader::default();
let score = reader.read_document(&document);
ParseReport::new(score, reader.diagnostics)
}
fn empty_score(diagnostics: Vec<Diagnostic>) -> Score {
Score::empty(READER_SPAN, diagnostics)
}
#[derive(Default)]
struct Reader {
diagnostics: Vec<Diagnostic>,
}
impl Reader {
fn warn(&mut self, code: &'static str, message: impl Into<String>) {
self.diagnostics.push(Diagnostic::new(
Severity::Warning,
code,
message,
READER_SPAN,
));
}
fn read_document(&mut self, document: &Document<'_>) -> Score {
let root = document.root_element();
if root.tag_name().name() != "score-partwise" {
self.warn(
"musicxml.read.unsupported_root",
format!(
"expected a <score-partwise> root, found <{}>; only partwise scores are read",
root.tag_name().name()
),
);
return empty_score(self.diagnostics.clone());
}
let mut score = empty_score(Vec::new());
score.divisions = self.read_divisions(root).unwrap_or(score.divisions);
self.read_metadata(root, &mut score.metadata);
self.read_header_key_meter(root, &mut score.metadata);
let part_list_result = self.read_part_list(root);
let header_key = score.metadata.key.clone();
let header_meter = score.metadata.meter.clone();
for (part_index, part_node) in children_named(root, "part").enumerate() {
let outcome = self.read_part(
part_node,
score.divisions,
&part_list_result.entries,
part_index == 0,
header_key.as_ref(),
header_meter.as_ref(),
);
if let Some(tempo) = outcome.header_tempo {
score.metadata.tempo_model.get_or_insert(tempo);
}
score.parts.push(outcome.part);
}
project_part_names_to_voice_properties(&mut score);
let part_score_blocks = part_score_blocks(&score);
let all_part_ids: Vec<&str> = score
.parts
.iter()
.map(|part| part.id.value.as_str())
.collect();
if let Some(directive) =
synthesize_score_directive(&part_list_result.groups, &all_part_ids, &part_score_blocks)
{
score.metadata.directives.push(directive);
}
score.diagnostics = self.diagnostics.clone();
score
}
fn read_divisions(&mut self, root: Node<'_, '_>) -> Option<u32> {
descendants_named(root, "divisions")
.next()
.and_then(|node| parse_u32(self, node, "divisions"))
.filter(|value| *value > 0)
}
fn read_header_key_meter(&mut self, root: Node<'_, '_>, metadata: &mut ScoreMetadata) {
let Some(attributes) = children_named(root, "part")
.next()
.and_then(|part| children_named(part, "measure").next())
.and_then(|measure| child_element(measure, "attributes"))
else {
return;
};
if let Some(key_node) = child_element(attributes, "key") {
metadata.key = Some(self.read_key(key_node));
}
if let Some(time_node) = child_element(attributes, "time") {
metadata.meter = self.read_meter(time_node);
}
}
fn read_key(&mut self, key_node: Node<'_, '_>) -> KeySignatureModel {
let fifths = child_text(key_node, "fifths")
.and_then(|text| self.parse_i8(text, "fifths"))
.unwrap_or(0);
let mut explicit_accidentals = Vec::new();
let mut pending_step: Option<char> = None;
let mut pending_alter: Option<i8> = None;
for child in element_children(key_node) {
match child.tag_name().name() {
"key-step" => {
pending_step = node_text(child).and_then(|text| text.chars().next());
pending_alter = None;
}
"key-alter" => {
pending_alter =
node_text(child).and_then(|text| self.parse_i8(text, "key-alter"));
}
"key-accidental" => {
if let Some(step) = pending_step {
let accidental = node_text(child)
.and_then(|name| self.accidental_from_name(name))
.or_else(|| pending_alter.and_then(Accidental::from_alter))
.unwrap_or(Accidental::Natural);
explicit_accidentals.push(KeyAccidentalModel {
step,
accidental,
source_span: READER_SPAN,
});
}
pending_step = None;
pending_alter = None;
}
_ => {}
}
}
KeySignatureModel {
display: String::new(),
fifths,
explicit_accidentals,
preserve_restatement: true,
source_span: READER_SPAN,
}
}
fn read_meter(&mut self, time_node: Node<'_, '_>) -> Option<MeterModel> {
let time_symbol = match time_node.attribute("symbol") {
Some("common") => Some("common".to_owned()),
Some("cut") => Some("cut".to_owned()),
_ => None,
};
let mut parts: Vec<String> = Vec::new();
let mut pending_beats: Option<String> = None;
for child in element_children(time_node) {
match child.tag_name().name() {
"beats" => pending_beats = node_text(child).map(str::to_owned),
"beat-type" => {
if let (Some(beats), Some(beat_type)) = (pending_beats.take(), node_text(child))
{
if beats.contains('+') {
parts.push(format!("({beats})/{beat_type}"));
} else {
parts.push(format!("{beats}/{beat_type}"));
}
}
}
_ => {}
}
}
let display = if parts.is_empty() {
match time_symbol.as_deref() {
Some("common") => "C".to_owned(),
Some("cut") => "C|".to_owned(),
_ => {
self.warn(
"musicxml.read.empty_time",
"<time> has no beats/beat-type pairs; meter not reconstructed",
);
return None;
}
}
} else {
let display = parts.join("+");
match (time_symbol.as_deref(), display.as_str()) {
(Some("common"), "4/4") => "C".to_owned(),
(Some("cut"), "2/2") => "C|".to_owned(),
_ => display,
}
};
Some(MeterModel {
display,
time_symbol,
duration: None,
free_meter: false,
preserve_restatement: true,
source_span: READER_SPAN,
})
}
fn read_clef(&mut self, attributes: Node<'_, '_>) -> Option<TextLine> {
let clef_node = child_element(attributes, "clef")?;
let sign = child_text(clef_node, "sign").unwrap_or("G");
let line = child_text(clef_node, "line").unwrap_or("2");
let octave_change = child_text(clef_node, "clef-octave-change")
.and_then(|text| self.parse_i8(text, "clef-octave-change"))
.unwrap_or(0);
clef_text_from(sign, line, octave_change).map(text_line)
}
fn read_staff_clefs(
&mut self,
attributes: Node<'_, '_>,
) -> std::collections::BTreeMap<u32, Option<TextLine>> {
let mut clefs = std::collections::BTreeMap::new();
for clef_node in children_named(attributes, "clef") {
let number = clef_node
.attribute("number")
.and_then(|raw| raw.trim().parse::<u32>().ok())
.unwrap_or(1);
let sign = child_text(clef_node, "sign").unwrap_or("G");
let line = child_text(clef_node, "line").unwrap_or("2");
let octave_change = child_text(clef_node, "clef-octave-change")
.and_then(|text| self.parse_i8(text, "clef-octave-change"))
.unwrap_or(0);
clefs.insert(
number,
clef_text_from(sign, line, octave_change).map(text_line),
);
}
clefs
}
fn read_transpose(&mut self, attributes: Node<'_, '_>) -> Option<i16> {
let transpose_node = child_element(attributes, "transpose")?;
let chromatic = child_text(transpose_node, "chromatic")?;
match chromatic.parse::<i16>() {
Ok(value) => Some(value),
Err(_) => {
self.warn(
"musicxml.read.invalid_chromatic",
format!("<chromatic> `{chromatic}` is not an i16; transpose ignored"),
);
None
}
}
}
fn parse_i8(&mut self, text: &str, label: &str) -> Option<i8> {
match text.trim().parse::<i8>() {
Ok(value) => Some(value),
Err(_) => {
self.warn(
"musicxml.read.invalid_integer",
format!("<{label}> `{}` is not a valid signed integer", text.trim()),
);
None
}
}
}
fn parse_alter(&mut self, text: &str, label: &str) -> Option<i8> {
let trimmed = text.trim();
let value = match trimmed.parse::<f64>() {
Ok(value) if value.is_finite() => value,
_ => {
self.warn(
"musicxml.read.invalid_alter",
format!("<{label}> `{trimmed}` is not a finite decimal alter; ignored"),
);
return None;
}
};
let rounded = value.round();
if (value - rounded).abs() > f64::EPSILON {
self.warn(
"musicxml.read.fractional_alter",
format!(
"<{label}> `{trimmed}` is a non-integer (microtonal) alter; \
rounded to {rounded} semitone(s) (croma has no sub-semitone model)"
),
);
}
Some(rounded.clamp(f64::from(i8::MIN), f64::from(i8::MAX)) as i8)
}
fn parse_u8(&mut self, text: &str, label: &str) -> Option<u8> {
match text.trim().parse::<u8>() {
Ok(value) => Some(value),
Err(_) => {
self.warn(
"musicxml.read.invalid_integer",
format!("<{label}> `{}` is not a valid 0-255 integer", text.trim()),
);
None
}
}
}
fn parse_u16(&mut self, text: &str, label: &str) -> Option<u16> {
match text.trim().parse::<u16>() {
Ok(value) => Some(value),
Err(_) => {
self.warn(
"musicxml.read.invalid_integer",
format!(
"<{label}> `{}` is not a valid non-negative integer",
text.trim()
),
);
None
}
}
}
fn read_metadata(&mut self, root: Node<'_, '_>, metadata: &mut ScoreMetadata) {
if let Some(title) = children_named(root, "work")
.next()
.and_then(|work| child_element(work, "work-title"))
{
metadata.title = Some(text_line(raw_text(title)));
}
if metadata.title.is_none()
&& let Some(title) = children_named(root, "movement-title").next()
{
metadata.title = Some(text_line(raw_text(title)));
}
if let Some(identification) = children_named(root, "identification").next() {
for creator in children_named(identification, "creator") {
if creator.attribute("type") == Some("composer") {
metadata.composers.push(text_line(raw_text(creator)));
}
}
}
for credit in children_named(root, "credit") {
if let Some(words) = child_element(credit, "credit-words") {
metadata.post_tune_lyrics.push(text_line(raw_text(words)));
}
}
}
fn read_part_list(&mut self, root: Node<'_, '_>) -> PartListResult {
let Some(part_list) = children_named(root, "part-list").next() else {
return PartListResult {
entries: Vec::new(),
groups: Vec::new(),
};
};
let mut entries: Vec<PartListEntry> = Vec::new();
let mut open_groups: Vec<(String, Option<String>, Vec<String>, usize)> = Vec::new();
let mut groups_started = 0usize;
let mut groups: Vec<PartGroupEntry> = Vec::new();
for child in element_children(part_list) {
match child.tag_name().name() {
"score-part" => {
let id = child.attribute("id").unwrap_or_default().to_owned();
let name = child_text(child, "part-name").map(str::to_owned);
let instruments = self.read_part_instruments(child);
for (_, _, ids, _) in &mut open_groups {
ids.push(id.clone());
}
entries.push(PartListEntry {
id,
name,
instruments,
});
}
"part-group" => {
let type_attr = child.attribute("type").unwrap_or_default();
let number = child.attribute("number").unwrap_or("1").to_owned();
match type_attr {
"start" => {
let symbol = child_text(child, "group-symbol").map(str::to_owned);
open_groups.push((number, symbol, Vec::new(), groups_started));
groups_started += 1;
}
"stop" => {
if let Some(pos) =
open_groups.iter().rposition(|(n, _, _, _)| n == &number)
{
let (_, symbol_opt, part_ids, start_order) =
open_groups.remove(pos);
let symbol = match symbol_opt.as_deref().unwrap_or_default() {
"brace" => '{',
"bracket" | "square" => '[',
_ => '\0', };
if !part_ids.is_empty() {
groups.push(PartGroupEntry {
symbol,
part_ids,
start_order,
});
}
}
}
_ => {}
}
}
_ => {}
}
}
for (number, _, _, _) in &open_groups {
self.warn(
"musicxml.read.unbalanced_part_group",
format!(
"<part-group number=\"{number}\" type=\"start\"> has no matching stop; \
the group is dropped"
),
);
}
PartListResult { entries, groups }
}
fn read_part_instruments(
&mut self,
score_part: Node<'_, '_>,
) -> Vec<MusicXmlPartInstrumentModel> {
let mut instruments: Vec<MusicXmlPartInstrumentModel> = Vec::new();
for node in children_named(score_part, "score-instrument") {
let Some(id) = node
.attribute("id")
.map(str::trim)
.filter(|id| !id.is_empty())
else {
continue;
};
let name = child_element(node, "instrument-name").map(|name| TextLine {
text: raw_text(name).to_owned(),
span: READER_SPAN,
});
instruments.push(MusicXmlPartInstrumentModel {
id: id.to_owned(),
name,
midi: None,
span: READER_SPAN,
});
}
for node in children_named(score_part, "midi-instrument") {
let Some(id) = node
.attribute("id")
.map(str::trim)
.filter(|id| !id.is_empty())
else {
continue;
};
let Some(midi) = self.read_midi_instrument(node) else {
continue;
};
if let Some(instrument) = instruments
.iter_mut()
.find(|instrument| instrument.id == id)
{
instrument.midi = Some(midi);
} else {
instruments.push(MusicXmlPartInstrumentModel {
id: id.to_owned(),
name: None,
midi: Some(midi),
span: READER_SPAN,
});
}
}
instruments
}
fn read_midi_instrument(&mut self, node: Node<'_, '_>) -> Option<MidiInstrumentModel> {
let channel =
child_text(node, "midi-channel").and_then(|text| self.parse_u8(text, "midi-channel"));
let program = child_text(node, "midi-program").and_then(|text| {
let raw = self.parse_u16(text, "midi-program")?;
match raw.checked_sub(1) {
Some(value) if value <= u16::from(u8::MAX) => u8::try_from(value).ok(),
_ => {
self.warn(
"musicxml.read.invalid_midi_program",
format!("<midi-program> `{raw}` is out of the 1-based GM range; ignored"),
);
None
}
}
});
let volume_cc = child_text(node, "volume")
.and_then(|text| self.cc_from_float(text, "volume", |v| v * 1.27));
let pan_cc = child_text(node, "pan")
.and_then(|text| self.cc_from_float(text, "pan", |p| (p + 90.0) * 127.0 / 180.0));
let midi_unpitched = child_text(node, "midi-unpitched").and_then(|text| {
let raw = self.parse_u16(text, "midi-unpitched")?;
if (1..=128).contains(&raw) {
u8::try_from(raw).ok()
} else {
self.warn(
"musicxml.read.invalid_midi_unpitched",
format!("<midi-unpitched> `{raw}` is outside the valid 1-128 range; ignored"),
);
None
}
});
let model = MidiInstrumentModel {
program,
channel,
volume_cc,
pan_cc,
midi_unpitched,
span: READER_SPAN,
};
model.has_content().then_some(model)
}
fn cc_from_float(&mut self, text: &str, label: &str, to_cc: impl Fn(f64) -> f64) -> Option<u8> {
let value: f64 = match text.trim().parse() {
Ok(value) => value,
Err(_) => {
self.warn(
"musicxml.read.invalid_float",
format!("<{label}> `{}` is not a number; ignored", text.trim()),
);
return None;
}
};
let cc = to_cc(value).round();
if (0.0..=127.0).contains(&cc) {
Some(cc as u8)
} else {
self.warn(
"musicxml.read.cc_out_of_range",
format!(
"<{label}> `{}` maps to CC {cc}, outside the valid 0-127 range; ignored",
text.trim()
),
);
None
}
}
fn read_part(
&mut self,
part_node: Node<'_, '_>,
divisions: u32,
part_list: &[PartListEntry],
capture_header_tempo: bool,
score_header_key: Option<&KeySignatureModel>,
score_header_meter: Option<&MeterModel>,
) -> PartOutcome {
let id = part_node.attribute("id").unwrap_or_default().to_owned();
let entry = part_list.iter().find(|entry| entry.id == id);
let name = entry
.and_then(|entry| entry.name.clone())
.filter(|name| !name.is_empty())
.map(text_line);
let mut voice_accumulators: Vec<VoiceAccumulator> = Vec::new();
let mut header_tempo: Option<TempoModel> = None;
let part_divisions = children_named(part_node, "measure")
.next()
.and_then(|measure| child_element(measure, "attributes"))
.and_then(|attributes| read_attributes_divisions(self, attributes))
.unwrap_or(divisions);
let staff_voices = staff_voice_map(part_node);
for (position, measure_node) in children_named(part_node, "measure").enumerate() {
let measure_id = self.read_measure_id(measure_node, position);
let is_part_first_measure = position == 0;
let capture_header = capture_header_tempo && is_part_first_measure;
let outcome = self.read_measure(
measure_node,
part_divisions,
measure_id,
capture_header,
is_part_first_measure,
&staff_voices,
);
if header_tempo.is_none() {
header_tempo = outcome.header_tempo;
}
const PRIMARY_VOICE: &str = "1";
let mut primary_measure: Option<VoiceMeasure> = None;
for (voice, voice_measure) in outcome.voices {
if voice == PRIMARY_VOICE {
primary_measure = Some(voice_measure);
continue;
}
let accumulator = voice_accumulator(&mut voice_accumulators, &voice);
accumulator.events.extend(voice_measure.events);
accumulator.measures.push(Measure {
id: measure_id,
display_number: outcome.measure.display_number.clone(),
source_span: READER_SPAN,
expected_duration: voice_measure.expected_duration,
actual_duration: voice_measure.actual_duration,
multiple_rest: None,
pickup: false,
complete: true,
barlines: Vec::new(),
repeat_endings: Vec::new(),
repeat_ending_closes: Vec::new(),
overlays: Vec::new(),
});
}
let (events, expected, actual) = match primary_measure {
Some(vm) => (vm.events, vm.expected_duration, vm.actual_duration),
None => (Vec::new(), None, Fraction::zero()),
};
let primary = voice_accumulator(&mut voice_accumulators, PRIMARY_VOICE);
primary.events.extend(events);
primary.measures.push(Measure {
expected_duration: expected,
actual_duration: actual,
..outcome.measure.clone()
});
}
let header_attributes = children_named(part_node, "measure")
.next()
.and_then(|measure| child_element(measure, "attributes"));
let initial_key = header_attributes
.and_then(|attributes| child_element(attributes, "key"))
.map(|key| self.read_key(key))
.filter(|key| !score_header_key.is_some_and(|header| key.structurally_matches(header)));
let initial_meter = header_attributes
.and_then(|attributes| child_element(attributes, "time"))
.and_then(|time| self.read_meter(time))
.filter(|meter| {
!score_header_meter.is_some_and(|header| meter.structurally_matches(header))
});
let mut initial_properties = VoicePropertiesModel::default();
let mut midi_transpose = None;
if let Some(attributes) = header_attributes {
initial_properties.clef = self.read_clef(attributes);
midi_transpose = self.read_transpose(attributes);
}
let staves_count = header_attributes.map(read_staves_count).unwrap_or(1);
let staff_clefs = header_attributes
.map(|attributes| self.read_staff_clefs(attributes))
.unwrap_or_default();
let voice_staffs = if staves_count > 1 {
voice_staff_map(part_node)
} else {
std::collections::BTreeMap::new()
};
voice_accumulators.sort_by_key(|accumulator| parse_voice_number(&accumulator.voice));
if voice_accumulators.is_empty() {
voice_accumulators.push(VoiceAccumulator {
voice: "1".to_owned(),
events: Vec::new(),
measures: Vec::new(),
});
}
let voices: Vec<Voice> = voice_accumulators
.into_iter()
.enumerate()
.map(|(index, accumulator)| {
let part_list_instrument = entry.and_then(|entry| entry.instruments.get(index));
let midi_instrument = part_list_instrument.and_then(|instrument| instrument.midi);
let instrument_name =
part_list_instrument.and_then(|instrument| instrument.name.clone());
let voice_staff = if staves_count > 1 {
voice_staffs
.get(&accumulator.voice)
.copied()
.unwrap_or(1)
.clamp(1, staves_count)
} else {
1
};
let (mut init_props, transpose) = if staves_count > 1 {
let props = VoicePropertiesModel {
clef: staff_clefs.get(&voice_staff).cloned().flatten(),
..VoicePropertiesModel::default()
};
(props, if index == 0 { midi_transpose } else { None })
} else if index == 0 {
(initial_properties.clone(), midi_transpose)
} else {
(VoicePropertiesModel::default(), None)
};
if let Some(instrument_name) = instrument_name {
init_props.nm.get_or_insert(instrument_name);
}
let voice_id = VoiceId {
value: voice_id_value(&id, &accumulator.voice, index),
span: READER_SPAN,
};
Voice {
id: voice_id,
staff: StaffId {
value: voice_staff,
span: READER_SPAN,
},
initial_key: (index == 0).then(|| initial_key.clone()).flatten(),
initial_meter: (index == 0).then(|| initial_meter.clone()).flatten(),
initial_properties: init_props.clone(),
properties: init_props,
measures: accumulator.measures,
events: accumulator.events,
midi_instrument,
midi_transpose: transpose,
source_span: READER_SPAN,
}
})
.collect();
let staves = build_staves(staves_count, &voices);
PartOutcome {
part: Part {
id: PartId {
value: id,
span: READER_SPAN,
},
name,
instruments: entry
.map(|entry| entry.instruments.clone())
.unwrap_or_default(),
staves,
voices,
source_span: READER_SPAN,
},
header_tempo,
}
}
fn read_measure_id(&mut self, measure_node: Node<'_, '_>, position: usize) -> MeasureId {
let number = measure_node
.attribute("number")
.and_then(|raw| raw.trim().parse::<u32>().ok())
.unwrap_or_else(|| u32::try_from(position + 1).unwrap_or(u32::MAX));
MeasureId {
index: u32::try_from(position).unwrap_or(u32::MAX),
number,
}
}
fn read_measure(
&mut self,
measure_node: Node<'_, '_>,
divisions: u32,
measure_id: MeasureId,
capture_header_tempo: bool,
is_part_first_measure: bool,
staff_voices: &std::collections::BTreeMap<u32, String>,
) -> MeasureOutcome {
let mut divisions = divisions;
let mut voices: Vec<(String, VoiceMeasureState)> = Vec::new();
let mut current_voice: String = "1".to_owned();
let mut pending_sequence_backup: Option<Fraction> = None;
let mut cursor = Fraction::zero();
let mut header_tempo: Option<TempoModel> = None;
let mut seen_note = false;
let mut header_attributes_consumed = !is_part_first_measure;
let mut barlines: Vec<MeasureBarline> = Vec::new();
let mut repeat_endings: Vec<RepeatEndingModel> = Vec::new();
let mut repeat_ending_closes: Vec<RepeatEndingCloseModel> = Vec::new();
let mut next_right_span_start: usize = 1;
let mut multiple_rest: Option<u32> = None;
let display_number = measure_node
.attribute("number")
.map(str::trim)
.filter(|number| !number.is_empty())
.map(ToOwned::to_owned);
for child in element_children(measure_node) {
match child.tag_name().name() {
"note" => {
let voice = note_voice_number(child).unwrap_or_else(|| current_voice.clone());
current_voice = voice.clone();
let state = voice_state_at(&mut voices, &voice, cursor);
state.cursor_restore_base = None;
if child_element(child, "grace").is_some() {
let builder = state
.grace_builder
.get_or_insert_with(GraceGroupBuilder::default);
self.push_grace_note(builder, child);
seen_note = true;
continue;
}
if let Some(builder) = state.grace_builder.take() {
state.pending_graces.push(builder.finish());
}
let Some(parsed) = self.read_note(child, divisions) else {
continue;
};
seen_note = true;
if parsed.measure_rest {
state.measure_rest_duration = Some(parsed.duration);
}
let mut attachments = self.read_note_attachments(
child,
&mut state.open_tuplets,
&mut state.events,
);
let sequence_starts_without_advancing = !state
.events
.iter()
.any(|event| abc_event_advances_cursor(&event.kind));
if sequence_starts_without_advancing
&& attachments.musicxml_sequence_backup.is_none()
{
attachments.musicxml_sequence_backup = pending_sequence_backup.take();
} else {
pending_sequence_backup = None;
}
if parsed.chord_member {
self.fold_chord_member(
&mut state.events,
state.last_main_event,
parsed,
attachments,
&mut state.next_chord_span_start,
);
continue;
}
prepend_attachments(&mut attachments, std::mem::take(&mut state.pending));
state.pending_insert_index = None;
attachments
.grace_groups
.extend(std::mem::take(&mut state.pending_graces));
state.last_main_event = Some(state.events.len());
state.events.push(TimedEvent {
measure: measure_id,
onset: cursor,
duration: parsed.duration,
source: READER_SPAN,
kind: parsed.kind,
attachments,
});
cursor = cursor.saturating_add(parsed.duration);
state.cursor = cursor;
state.max_cursor = max_fraction(state.max_cursor, state.cursor);
}
"harmony" => {
if let Some(symbol) = self.read_harmony(child) {
let state = voice_state_at(&mut voices, ¤t_voice, cursor);
state.mark_pending_position();
state.pending.chord_symbols.push(symbol);
}
}
"direction" => {
let mut tempo_to_apply = None;
let mut event_attachments = None;
match self.read_direction(child) {
ParsedDirection::Tempo(tempo) => tempo_to_apply = Some(tempo),
ParsedDirection::TempoAndEvent(tempo, attachments) => {
tempo_to_apply = Some(tempo);
event_attachments = Some(attachments);
}
ParsedDirection::SectionLabel(label) => {
seen_note = true;
let state = voice_state_at(&mut voices, ¤t_voice, cursor);
state.events.push(TimedEvent {
measure: measure_id,
onset: cursor,
duration: Fraction::zero(),
source: READER_SPAN,
kind: TimedEventKind::SectionLabel(label),
attachments: std::mem::take(&mut state.pending),
});
state.pending_insert_index = None;
continue;
}
ParsedDirection::Event(attachments) => {
event_attachments = Some(attachments)
}
ParsedDirection::Ignored => {}
}
if let Some(tempo) = tempo_to_apply {
if capture_header_tempo && !seen_note && header_tempo.is_none() {
header_tempo = Some(tempo);
} else {
let state = voice_state_at(&mut voices, ¤t_voice, cursor);
state.events.push(TimedEvent {
measure: measure_id,
onset: cursor,
duration: Fraction::zero(),
source: READER_SPAN,
kind: TimedEventKind::TempoChange(tempo),
attachments: std::mem::take(&mut state.pending),
});
state.pending_insert_index = None;
}
}
if let Some(attachments) = event_attachments {
let voice =
direction_voice_number(child).unwrap_or_else(|| current_voice.clone());
current_voice = voice.clone();
let state = voice_state_at(&mut voices, &voice, cursor);
let mut attachments = *attachments;
if let Some(builder) = state.grace_builder.as_mut()
&& attachments_are_decorations_only(&attachments)
{
let decorations = std::mem::take(&mut attachments.decorations);
if let Some(decorations) =
builder.append_decorations_to_last_note(decorations)
{
attachments.decorations = decorations;
}
}
if !attachments.is_empty() {
state.mark_pending_position();
state.pending.extend(attachments);
}
}
}
"forward" => {
if let Some(duration) = self.read_duration(child, divisions) {
let from = cursor;
cursor = cursor.saturating_add(duration);
let state = voice_state(&mut voices, ¤t_voice);
mark_clef_cursor_restore_for_abc(
&mut state.events,
from,
cursor,
&mut state.cursor_restore_base,
);
state.max_cursor = max_fraction(state.max_cursor, cursor);
}
}
"backup" => {
if let Some(duration) = self.read_duration(child, divisions) {
let state = voice_state(&mut voices, ¤t_voice);
state.cursor_restore_base = Some(cursor);
pending_sequence_backup = cursor.less_than(duration).then_some(duration);
cursor = subtract_fraction(cursor, duration);
}
}
"barline" => {
self.read_barline(
child,
&mut barlines,
&mut repeat_endings,
&mut repeat_ending_closes,
&mut next_right_span_start,
);
seen_note = true;
}
"attributes" => {
if let Some(value) = read_attributes_divisions(self, child) {
divisions = value;
}
if let Some(count) = read_multiple_rest(child) {
multiple_rest = Some(count);
}
if !header_attributes_consumed {
header_attributes_consumed = true;
} else {
voice_state_at(&mut voices, ¤t_voice, cursor);
self.read_mid_measure_attributes(
child,
measure_id,
cursor,
&mut voices,
¤t_voice,
staff_voices,
);
seen_note = true;
}
}
_ => {}
}
}
let mut voice_measures: Vec<(String, VoiceMeasure)> = Vec::new();
for (voice, mut state) in voices {
if let Some(builder) = state.grace_builder.take() {
let group = builder.finish();
match state.last_main_event.and_then(|index| state.events.get_mut(index)) {
Some(event) => event.attachments.after_grace_groups.push(group),
None => self.warn(
"musicxml.read.orphan_grace",
"a trailing <grace> run has no preceding note to bind as an after-grace; dropped",
),
}
}
if !state.pending_graces.is_empty() {
let groups = std::mem::take(&mut state.pending_graces);
match state
.last_main_event
.and_then(|index| state.events.get_mut(index))
{
Some(event) => {
let mut merged = groups;
merged.append(&mut event.attachments.grace_groups);
event.attachments.grace_groups = merged;
}
None => self.warn(
"musicxml.read.orphan_before_grace",
"a <grace> run had no following note to bind as a before-grace group; dropped",
),
}
}
if !state.pending_is_empty() {
let spacer = TimedEvent {
measure: measure_id,
onset: state.cursor,
duration: Fraction::zero(),
source: READER_SPAN,
kind: TimedEventKind::Spacer,
attachments: std::mem::take(&mut state.pending),
};
match state.pending_insert_index {
Some(index) if index <= state.events.len() => {
state.events.insert(index, spacer);
}
_ => state.events.push(spacer),
}
}
let (expected_duration, actual_duration) = match state.measure_rest_duration {
Some(duration) => (Some(duration), duration),
None => (None, state.max_cursor),
};
voice_measures.push((
voice,
VoiceMeasure {
events: state.events,
expected_duration,
actual_duration,
},
));
}
voice_measures.sort_by_key(|(voice, _)| parse_voice_number(voice));
MeasureOutcome {
voices: voice_measures,
header_tempo,
measure: Measure {
id: measure_id,
display_number,
source_span: READER_SPAN,
expected_duration: None,
actual_duration: Fraction::zero(),
multiple_rest,
pickup: false,
complete: true,
barlines,
repeat_endings,
repeat_ending_closes,
overlays: Vec::new(),
},
}
}
fn read_mid_measure_attributes(
&mut self,
attributes: Node<'_, '_>,
measure_id: MeasureId,
cursor: Fraction,
voices: &mut Vec<(String, VoiceMeasureState)>,
current_voice: &str,
staff_voices: &std::collections::BTreeMap<u32, String>,
) {
for child in element_children(attributes) {
let kind = match child.tag_name().name() {
"key" => Some(TimedEventKind::KeyChange(self.read_key(child))),
"time" => self.read_meter(child).map(TimedEventKind::MeterChange),
"clef" => Some(TimedEventKind::ClefChange(self.read_clef_change(child))),
"divisions" => None,
"measure-style" => None,
other => {
self.warn(
"musicxml.read.unsupported_mid_measure_attribute",
format!(
"<attributes> child <{other}> mid-measure has no Score event inverse; skipped"
),
);
None
}
};
if let Some(kind) = kind {
let voice = clef_staff_voice(&kind, child, staff_voices).unwrap_or(current_voice);
let state = voice_state_at(voices, voice, cursor);
state.events.push(TimedEvent {
measure: measure_id,
onset: cursor,
duration: Fraction::zero(),
source: READER_SPAN,
kind,
attachments: EventAttachments::default(),
});
}
}
}
fn read_clef_change(&mut self, clef_node: Node<'_, '_>) -> ClefChangeModel {
let sign = child_text(clef_node, "sign").unwrap_or("G");
let line = child_text(clef_node, "line").unwrap_or("2");
let octave_change = child_text(clef_node, "clef-octave-change")
.and_then(|text| self.parse_i8(text, "clef-octave-change"))
.unwrap_or(0);
let text = clef_text_from(sign, line, octave_change).unwrap_or_else(|| "treble".to_owned());
ClefChangeModel {
clef: text_line(text),
source_span: READER_SPAN,
musicxml_cursor_pre_backup: None,
musicxml_cursor_back: None,
}
}
fn read_barline(
&mut self,
barline: Node<'_, '_>,
barlines: &mut Vec<MeasureBarline>,
repeat_endings: &mut Vec<RepeatEndingModel>,
repeat_ending_closes: &mut Vec<RepeatEndingCloseModel>,
next_right_span_start: &mut usize,
) {
let is_left = barline.attribute("location") == Some("left");
let bar_style = child_text(barline, "bar-style");
let repeat_direction = child_element(barline, "repeat")
.and_then(|repeat| repeat.attribute("direction"))
.map(str::to_owned);
for ending in children_named(barline, "ending") {
match ending.attribute("type") {
Some("start") => {
if let Some(model) = self.read_ending(ending) {
repeat_endings.push(model);
}
}
Some("stop") | Some("discontinue") => {
if let Some(model) = self.read_ending(ending) {
let close_type = match ending.attribute("type") {
Some("discontinue") => RepeatEndingCloseType::Discontinue,
_ => RepeatEndingCloseType::Stop,
};
repeat_ending_closes.push(RepeatEndingCloseModel {
span: model.span,
close_type,
location: if is_left {
RepeatEndingCloseLocation::Left
} else {
RepeatEndingCloseLocation::Right
},
endings: model.endings,
});
}
}
_ => {}
}
}
if bar_style.is_none() && repeat_direction.is_none() {
return;
}
let Some(kind) = barline_kind_from(bar_style, repeat_direction.as_deref(), is_left) else {
self.warn(
"musicxml.read.unsupported_barline",
format!(
"<barline location={:?}> bar-style {:?} / repeat {:?} has no BarlineKind inverse; ignored",
barline.attribute("location").unwrap_or(""),
bar_style.unwrap_or(""),
repeat_direction.as_deref().unwrap_or(""),
),
);
return;
};
let span = if is_left {
READER_SPAN
} else {
let start = *next_right_span_start;
*next_right_span_start = next_right_span_start.saturating_add(1);
Span { start, end: start }
};
barlines.push(MeasureBarline { kind, span });
}
fn read_ending(&mut self, ending: Node<'_, '_>) -> Option<RepeatEndingModel> {
if let Some(text) = node_text(ending) {
return Some(RepeatEndingModel {
span: READER_SPAN,
endings: vec![RepeatEndingPartModel::Text(text.to_owned())],
});
}
let number = match ending.attribute("number") {
Some(number) => number.trim(),
None => {
self.warn(
"musicxml.read.ending_without_number",
"<ending type=\"start\"> has no number and no text; not reconstructed",
);
return None;
}
};
let mut parts = Vec::new();
for token in number.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
if let Some((start, end)) = token.split_once('-') {
match (start.trim().parse::<u32>(), end.trim().parse::<u32>()) {
(Ok(start), Ok(end)) => {
parts.push(RepeatEndingPartModel::Range { start, end });
}
_ => {
self.warn(
"musicxml.read.invalid_ending_range",
format!("<ending number> range `{token}` is not `start-end`; skipped"),
);
}
}
} else {
match token.parse::<u32>() {
Ok(value) => parts.push(RepeatEndingPartModel::Single(value)),
Err(_) => self.warn(
"musicxml.read.invalid_ending_number",
format!("<ending number> token `{token}` is not a u32; skipped"),
),
}
}
}
if parts.is_empty() {
self.warn(
"musicxml.read.empty_ending_number",
format!("<ending number=\"{number}\"> yielded no pass numbers; not reconstructed"),
);
return None;
}
Some(RepeatEndingModel {
span: READER_SPAN,
endings: parts,
})
}
fn read_note(&mut self, note_node: Node<'_, '_>, divisions: u32) -> Option<ParsedNote> {
let chord_member = child_element(note_node, "chord").is_some();
if child_element(note_node, "grace").is_some() {
return None;
}
let duration = self.read_duration(note_node, divisions).unwrap_or_else(|| {
self.warn(
"musicxml.read.note_missing_duration",
"<note> has no <duration>; treated as zero-length",
);
Fraction::zero()
});
if let Some(rest_node) = child_element(note_node, "rest") {
let visibility = if note_node.attribute("print-object") == Some("no") {
RestVisibility::Invisible
} else {
RestVisibility::Visible
};
let measure_rest = rest_node.attribute("measure") == Some("yes");
return Some(ParsedNote {
kind: TimedEventKind::Rest(RestEvent { visibility }),
duration,
chord_member: false,
measure_rest,
});
}
let pitch = self.read_pitch(note_node)?;
let written_accidental = child_text(note_node, "accidental")
.and_then(|name| self.accidental_from_name(name))
.map(|kind| AccidentalMark {
kind,
explicit: true,
courtesy: false,
source: READER_SPAN,
});
Some(ParsedNote {
kind: TimedEventKind::Note(NoteEvent {
pitch,
written_accidental,
chord_member,
}),
duration,
chord_member,
measure_rest: false,
})
}
fn push_grace_note(&mut self, builder: &mut GraceGroupBuilder, note_node: Node<'_, '_>) {
if builder.slash.is_none()
&& let Some(grace) = child_element(note_node, "grace")
&& grace.attribute("slash") == Some("yes")
{
builder.slash = Some(READER_SPAN);
}
let is_chord_member = child_element(note_node, "chord").is_some();
let slurs = self.read_grace_slurs(note_node);
if child_element(note_node, "rest").is_some() {
let visibility = if note_node.attribute("print-object") == Some("no") {
RestVisibility::Invisible
} else {
RestVisibility::Visible
};
builder.events.push(GraceEvent {
source_span: READER_SPAN,
kind: GraceEventKind::Rest(RestEvent { visibility }),
slurs,
});
return;
}
let Some(grace_note) = self.read_grace_note(note_node) else {
return;
};
if is_chord_member {
if let Some(previous) = builder.events.last_mut() {
match &mut previous.kind {
GraceEventKind::Note(first) => {
previous.kind = GraceEventKind::Chord(vec![first.clone(), grace_note]);
}
GraceEventKind::Chord(members) => members.push(grace_note),
GraceEventKind::Rest(_) => {
self.warn(
"musicxml.read.grace_chord_on_rest",
"a grace <chord/> follows a grace rest; treated as a separate note",
);
builder.events.push(GraceEvent {
source_span: READER_SPAN,
kind: GraceEventKind::Note(grace_note),
slurs,
});
}
}
return;
}
}
builder.events.push(GraceEvent {
source_span: READER_SPAN,
kind: GraceEventKind::Note(grace_note),
slurs,
});
}
fn read_grace_note(&mut self, note_node: Node<'_, '_>) -> Option<GraceNoteEvent> {
let pitch = self.read_pitch(note_node)?;
let written_accidental = child_text(note_node, "accidental")
.and_then(|name| self.accidental_from_name(name))
.map(|kind| AccidentalMark {
kind,
explicit: true,
courtesy: false,
source: READER_SPAN,
});
let display_duration = self.read_note_type_fraction(note_node);
let mut decorations = Vec::new();
if let Some(notations) = child_element(note_node, "notations") {
self.read_decorations(notations, &mut decorations);
}
Some(GraceNoteEvent {
pitch,
written_accidental,
decorations,
length_multiplier: display_duration,
})
}
fn read_grace_slurs(&mut self, note_node: Node<'_, '_>) -> Vec<SlurAttachment> {
let Some(notations) = child_element(note_node, "notations") else {
return Vec::new();
};
children_named(notations, "slur")
.filter_map(|slur| self.read_slur(slur))
.collect()
}
fn read_note_type_fraction(&mut self, note_node: Node<'_, '_>) -> Fraction {
let base = child_text(note_node, "type")
.and_then(note_type_fraction)
.unwrap_or_else(|| {
self.warn(
"musicxml.read.grace_missing_type",
"a grace <note> has no recognised <type>; assuming eighth",
);
Fraction::new(1, 8)
});
let dots = children_named(note_node, "dot").count();
base.dotted(dots)
}
fn fold_chord_member(
&mut self,
events: &mut [TimedEvent],
last_main_event: Option<usize>,
parsed: ParsedNote,
attachments: EventAttachments,
next_chord_span_start: &mut usize,
) {
let member_pitch = match parsed.kind {
TimedEventKind::Note(note) => note,
_ => {
self.warn(
"musicxml.read.chord_member_not_note",
"a <chord/> member is not a pitched note; ignored",
);
return;
}
};
let member = ChordMemberEvent {
pitch: member_pitch.pitch,
duration: parsed.duration,
written_accidental: member_pitch.written_accidental,
source_span: READER_SPAN,
attachments,
};
let Some(event) = last_main_event.and_then(|index| events.get_mut(index)) else {
self.warn(
"musicxml.read.chord_member_without_head",
"a <chord/> member has no preceding note to attach to; ignored",
);
return;
};
match &mut event.kind {
TimedEventKind::Note(first) => {
let span_start = *next_chord_span_start;
*next_chord_span_start = next_chord_span_start.saturating_add(1);
let source_span = Span {
start: span_start,
end: span_start,
};
let first_member = ChordMemberEvent {
pitch: first.pitch,
duration: event.duration,
written_accidental: first.written_accidental,
source_span: READER_SPAN,
attachments: event.attachments.clone(),
};
event.kind = TimedEventKind::Chord(ChordEvent {
members: vec![first_member, member],
source_span,
});
}
TimedEventKind::Chord(chord) => chord.members.push(member),
_ => self.warn(
"musicxml.read.chord_member_without_head",
"a <chord/> member follows a non-note event; ignored",
),
}
}
fn read_note_attachments(
&mut self,
note_node: Node<'_, '_>,
open_tuplets: &mut OpenTuplets,
events: &mut [TimedEvent],
) -> EventAttachments {
let mut attachments = EventAttachments {
instrument: self.read_note_instrument_ref(note_node),
..EventAttachments::default()
};
let notations = child_element(note_node, "notations");
if let Some(notations) = notations {
for tied in children_named(notations, "tied") {
if let Some(tie) = self.read_tie(tied) {
attachments.ties.push(tie);
}
}
}
if attachments.ties.is_empty() {
for tie in children_named(note_node, "tie") {
if let Some(tie) = self.read_tie(tie) {
attachments.ties.push(tie);
}
}
}
if let Some(notations) = notations {
for slur in children_named(notations, "slur") {
if let Some(slur) = self.read_slur(slur) {
attachments.slurs.push(slur);
}
}
}
let time_modification = self.read_time_modification_ratio(note_node);
let tuplet_elements: Vec<(TupletRole, u32)> = notations
.map(|notations| {
children_named(notations, "tuplet")
.filter_map(|tuplet| self.read_tuplet_marker(tuplet))
.collect()
})
.unwrap_or_default();
attachments.tuplets =
open_tuplets.resolve(self, &tuplet_elements, time_modification, events);
if let Some(notations) = notations {
self.read_tuplet_display_carriers(notations, &mut attachments.decorations);
self.read_decorations(notations, &mut attachments.decorations);
}
self.read_lyrics(note_node, &mut attachments.lyrics);
attachments
}
fn read_note_instrument_ref(
&mut self,
note_node: Node<'_, '_>,
) -> Option<MusicXmlInstrumentRef> {
children_named(note_node, "instrument").find_map(|instrument| {
let id = instrument.attribute("id")?.trim();
(!id.is_empty()).then(|| MusicXmlInstrumentRef {
id: id.to_owned(),
span: READER_SPAN,
})
})
}
fn read_direction(&mut self, direction: Node<'_, '_>) -> ParsedDirection {
let tempo = self.read_tempo_direction(direction);
let mut attachments = EventAttachments::default();
let placement = direction.attribute("placement");
for direction_type in children_named(direction, "direction-type") {
for element in element_children(direction_type) {
match element.tag_name().name() {
"words" => {
if tempo.is_some() {
continue;
}
match demoted_chord_symbol_from_words(element, placement) {
Some(symbol) => attachments.chord_symbols.push(symbol),
None => attachments
.annotations
.push(annotation_from_words(element, placement)),
}
}
"metronome" if tempo.is_some() => {}
"dynamics" => {
for dynamic in element_children(element) {
match dynamic_decoration_name(dynamic.tag_name().name()) {
Some(name) => {
attachments
.decorations
.push(named_decoration_with_placement(name, placement));
}
None => self.warn(
"musicxml.read.unsupported_dynamic",
format!(
"<dynamics> child <{}> has no ABC decoration inverse; skipped",
dynamic.tag_name().name()
),
),
}
}
}
"wedge" => {
if let Some(name) = wedge_decoration_name(element.attribute("type")) {
attachments
.decorations
.push(named_decoration_with_placement(name, placement));
} else {
self.warn(
"musicxml.read.unsupported_wedge",
format!(
"<wedge type={:?}> has no ABC decoration inverse; skipped",
element.attribute("type").unwrap_or("")
),
);
}
}
"coda" => attachments
.decorations
.push(named_decoration_with_placement("coda", placement)),
"segno" => attachments
.decorations
.push(named_decoration_with_placement("segno", placement)),
"rehearsal" => {
return ParsedDirection::SectionLabel(raw_text(element).to_owned());
}
other => self.warn(
"musicxml.read.unsupported_direction_type",
format!("<direction-type> child <{other}> is not reconstructed; skipped"),
),
}
}
}
if attachments.annotations.is_empty()
&& attachments.decorations.is_empty()
&& attachments.chord_symbols.is_empty()
{
tempo.map_or(ParsedDirection::Ignored, ParsedDirection::Tempo)
} else if let Some(tempo) = tempo {
ParsedDirection::TempoAndEvent(tempo, Box::new(attachments))
} else {
ParsedDirection::Event(Box::new(attachments))
}
}
fn read_tempo_direction(&mut self, direction: Node<'_, '_>) -> Option<TempoModel> {
if child_element(direction, "voice").is_some() {
return None;
}
let words = || tempo_words(direction);
let sound_beat =
child_element(direction, "sound").and_then(|sound| self.read_sound_tempo_beat(sound));
if let Some(metronome) = descendants_named(direction, "metronome").next() {
if let Some(beat) = self.read_tempo_beat(metronome) {
return Some(TempoModel {
text: words(),
beat: Some(beat),
beat_role: TempoBeatRole::PrintedMetronome,
source_span: READER_SPAN,
});
}
if let Some(beat) = sound_beat {
return Some(TempoModel {
text: words(),
beat: Some(beat),
beat_role: TempoBeatRole::PlaybackSoundOnly,
source_span: READER_SPAN,
});
}
return None;
}
if child_element(direction, "sound").is_some() {
let text = words();
let beat = sound_beat;
if beat.is_none() && text.is_none() {
return None;
}
return Some(TempoModel {
text,
beat,
beat_role: TempoBeatRole::PlaybackSoundOnly,
source_span: READER_SPAN,
});
}
if let Some(text) = words().filter(|text| !text.trim().is_empty()) {
return Some(TempoModel {
text: Some(text),
beat: None,
beat_role: TempoBeatRole::PrintedMetronome,
source_span: READER_SPAN,
});
}
None
}
fn read_tempo_beat(&mut self, metronome: Node<'_, '_>) -> Option<TempoBeat> {
let unit = child_text(metronome, "beat-unit")?;
let base_denominator: u32 = match unit {
"whole" => 1,
"half" => 2,
"quarter" => 4,
"eighth" => 8,
"16th" => 16,
"32nd" => 32,
"64th" => 64,
other => {
self.warn(
"musicxml.read.unknown_beat_unit",
format!("<beat-unit> `{other}` is not a recognised note type; tempo ignored"),
);
return None;
}
};
let dotted = child_element(metronome, "beat-unit-dot").is_some();
let (beat_numerator, beat_denominator) = if dotted {
(3, base_denominator.saturating_mul(2))
} else {
(1, base_denominator)
};
let bpm = match child_text(metronome, "per-minute")
.and_then(|text| self.parse_tempo_bpm(text, "per-minute"))
{
Some(bpm) => bpm,
_ => {
self.warn(
"musicxml.read.invalid_per_minute",
"<per-minute> is missing or not a non-negative finite number; tempo ignored",
);
return None;
}
};
Some(TempoBeat {
beat_numerator,
beat_denominator,
bpm,
})
}
fn read_sound_tempo_beat(&mut self, sound: Node<'_, '_>) -> Option<TempoBeat> {
let bpm = self.parse_tempo_bpm(sound.attribute("tempo")?, "sound tempo")?;
Some(TempoBeat {
beat_numerator: 1,
beat_denominator: 4,
bpm,
})
}
fn parse_tempo_bpm(&mut self, text: &str, label: &str) -> Option<u32> {
let trimmed = text.trim();
let value = match trimmed.parse::<f64>() {
Ok(value) if value.is_finite() && value >= 0.0 => value,
_ => {
self.warn(
"musicxml.read.invalid_tempo",
format!("<{label}> `{trimmed}` is not a non-negative finite number"),
);
return None;
}
};
let rounded = value.round();
if rounded > f64::from(u32::MAX) {
self.warn(
"musicxml.read.invalid_tempo",
format!("<{label}> `{trimmed}` is too large for ABC tempo BPM"),
);
return None;
}
if (value - rounded).abs() > f64::EPSILON {
self.warn(
"musicxml.read.fractional_tempo",
format!(
"<{label}> `{trimmed}` has fractional BPM; rounded to {rounded} for ABC Q:"
),
);
}
Some(rounded as u32)
}
fn read_harmony(&mut self, harmony: Node<'_, '_>) -> Option<TextAttachment> {
let kind_text = child_element(harmony, "kind").and_then(|kind| kind.attribute("text"));
let (text, musicxml_harmony_text) = match kind_text {
Some(text)
if starts_with_abc_chord_root(text) || child_element(harmony, "root").is_none() =>
{
(text.to_owned(), HarmonyKindText::AbcNative)
}
Some(text) => (
self.synthesise_chord_symbol(harmony)?,
HarmonyKindText::Text(text.to_owned()),
),
None => (
self.synthesise_chord_symbol(harmony)?,
HarmonyKindText::Textless,
),
};
Some(TextAttachment {
text,
span: READER_SPAN,
placement: None,
musicxml_harmony_text,
})
}
fn synthesise_chord_symbol(&mut self, harmony: Node<'_, '_>) -> Option<String> {
let Some(root_node) = child_element(harmony, "root") else {
self.warn(
"musicxml.read.harmony_without_root",
"textless <harmony> has no <root>; chord symbol not reconstructed",
);
return None;
};
let Some(root_step) = child_text(root_node, "root-step").and_then(first_upper_letter)
else {
self.warn(
"musicxml.read.harmony_without_root",
"textless <harmony> <root> has no <root-step> letter; chord symbol not reconstructed",
);
return None;
};
let root_alter = child_text(root_node, "root-alter")
.and_then(|text| self.parse_alter(text, "root-alter"))
.unwrap_or(0);
let mut symbol = String::new();
symbol.push(root_step);
symbol.push_str(accidental_suffix(root_alter));
let Some(kind_node) = child_element(harmony, "kind") else {
self.warn(
"musicxml.read.harmony_unmodellable_kind",
"textless <harmony> has no <kind>; chord symbol not reconstructed",
);
return None;
};
let kind_value = node_text(kind_node).unwrap_or("");
match chord_kind_suffix(kind_value) {
Some(suffix) => symbol.push_str(suffix),
None => {
if !kind_value.is_empty() {
self.warn(
"musicxml.read.harmony_unknown_kind",
format!(
"textless <harmony> <kind> `{kind_value}` is not a known quality; \
emitting the bare root (the kind@text carrier preserves the label)"
),
);
}
}
}
for degree in element_children(harmony).filter(|n| n.tag_name().name() == "degree") {
self.append_chord_degree(&mut symbol, degree);
}
if let Some(bass_node) = child_element(harmony, "bass")
&& let Some(bass_step) = child_text(bass_node, "bass-step").and_then(first_upper_letter)
{
let bass_alter = child_text(bass_node, "bass-alter")
.and_then(|text| self.parse_alter(text, "bass-alter"))
.unwrap_or(0);
symbol.push('/');
symbol.push(bass_step);
symbol.push_str(accidental_suffix(bass_alter));
}
Some(symbol)
}
fn append_chord_degree(&mut self, symbol: &mut String, degree: Node<'_, '_>) {
let degree_type = child_text(degree, "degree-type").unwrap_or("add");
if degree_type == "subtract" {
return;
}
let Some(value) = child_text(degree, "degree-value") else {
return;
};
let alter = child_text(degree, "degree-alter")
.and_then(|text| self.parse_alter(text, "degree-alter"))
.unwrap_or(0);
symbol.push_str(degree_accidental(alter));
symbol.push_str(value.trim());
}
fn read_lyrics(&mut self, note_node: Node<'_, '_>, out: &mut Vec<AlignedLyric>) {
for lyric in children_named(note_node, "lyric") {
let verse = lyric
.attribute("number")
.and_then(|raw| raw.trim().parse::<u32>().ok())
.unwrap_or(1);
if verse > MAX_LYRIC_VERSE {
self.warn(
"musicxml.read.lyric_number_out_of_range",
format!(
"<lyric number=\"{verse}\"> is above the supported {MAX_LYRIC_VERSE} verses; not reconstructed"
),
);
continue;
}
let text_node = child_element(lyric, "text");
if text_node.is_none() && child_element(lyric, "extend").is_some() {
out.push(AlignedLyric {
verse,
text: String::new(),
span: READER_SPAN,
control: LyricControl::Extender,
same_note_extend: false,
});
continue;
}
let Some(text_node) = text_node else {
self.warn(
"musicxml.read.lyric_without_text",
"<lyric> has neither <text> nor <extend>; not reconstructed",
);
continue;
};
out.push(AlignedLyric {
verse,
text: raw_text(text_node).to_owned(),
span: READER_SPAN,
control: LyricControl::Syllable,
same_note_extend: child_element(lyric, "extend").is_some(),
});
if matches!(
child_text(lyric, "syllabic"),
Some("begin") | Some("middle")
) {
out.push(AlignedLyric {
verse,
text: "-".to_owned(),
span: READER_SPAN,
control: LyricControl::Hyphen,
same_note_extend: false,
});
}
}
}
fn read_tie(&mut self, node: Node<'_, '_>) -> Option<TieAttachment> {
let role = match node.attribute("type") {
Some("start") => TieRole::Start,
Some("stop") => TieRole::Stop,
other => {
self.warn(
"musicxml.read.unknown_tie_type",
format!(
"<{}> type `{}` is neither start nor stop; ignored",
node.tag_name().name(),
other.unwrap_or("")
),
);
return None;
}
};
let pair_id = node
.attribute("number")
.and_then(|raw| raw.trim().parse::<u32>().ok())
.unwrap_or(1);
Some(TieAttachment {
pair_id,
role,
span: READER_SPAN,
dotted: node.attribute("line-type") == Some("dotted"),
})
}
fn read_slur(&mut self, node: Node<'_, '_>) -> Option<SlurAttachment> {
let role = match node.attribute("type") {
Some("start") => SlurRole::Start,
Some("stop") => SlurRole::Stop,
other => {
self.warn(
"musicxml.read.unsupported_slur_type",
format!(
"<slur> type `{}` is not start/stop; ignored",
other.unwrap_or("")
),
);
return None;
}
};
let pair_id = node
.attribute("number")
.and_then(|raw| raw.trim().parse::<u32>().ok())
.unwrap_or(1);
Some(SlurAttachment {
pair_id,
role,
span: READER_SPAN,
dotted: node.attribute("line-type") == Some("dotted"),
})
}
fn read_tuplet_marker(&mut self, node: Node<'_, '_>) -> Option<(TupletRole, u32)> {
let role = match node.attribute("type") {
Some("start") => TupletRole::Start,
Some("stop") => TupletRole::Stop,
other => {
self.warn(
"musicxml.read.unknown_tuplet_type",
format!(
"<tuplet> type `{}` is not start/stop; ignored",
other.unwrap_or("")
),
);
return None;
}
};
let number = node
.attribute("number")
.and_then(|raw| raw.trim().parse::<u32>().ok())
.unwrap_or(1);
Some((role, number))
}
fn read_tuplet_display_carriers(
&mut self,
notations: Node<'_, '_>,
out: &mut Vec<DecorationAttachment>,
) {
for tuplet in children_named(notations, "tuplet") {
if tuplet.attribute("type") != Some("start") {
continue;
}
let actual = child_element(tuplet, "tuplet-actual")
.and_then(|element| self.read_tuplet_display_detail(element));
let normal = child_element(tuplet, "tuplet-normal")
.and_then(|element| self.read_tuplet_display_detail(element));
let bracket = tuplet.attribute("bracket");
if bracket.is_none() && actual.is_none() && normal.is_none() {
continue;
}
let name = match tuplet_display_decoration_name(bracket, actual, normal) {
Some(name) => name,
None => {
self.warn(
"musicxml.read.unsupported_tuplet_display",
"<tuplet> display metadata has no ABC carrier inverse; skipped",
);
continue;
}
};
out.push(named_decoration(&name));
}
}
fn read_tuplet_display_detail<'a>(
&mut self,
element: Node<'a, '_>,
) -> Option<(&'a str, &'a str, usize)> {
let number = child_element(element, "tuplet-number").and_then(node_text);
let note_type = child_element(element, "tuplet-type").and_then(node_text);
let dots = children_named(element, "tuplet-dot").count();
match (number, note_type) {
(Some(number), Some(note_type)) => Some((number, note_type, dots)),
_ => {
self.warn(
"musicxml.read.unsupported_tuplet_display",
format!(
"<{}> is missing tuplet-number or tuplet-type; skipped",
element.tag_name().name()
),
);
None
}
}
}
fn read_time_modification_ratio(&mut self, note_node: Node<'_, '_>) -> Option<(u32, u32)> {
let node = child_element(note_node, "time-modification")?;
let actual = child_text(node, "actual-notes").and_then(|t| t.parse::<u32>().ok());
let normal = child_text(node, "normal-notes").and_then(|t| t.parse::<u32>().ok());
match (actual, normal) {
(Some(actual), Some(normal)) if actual > 0 && normal > 0 => Some((actual, normal)),
_ => {
self.warn(
"musicxml.read.invalid_time_modification",
"<time-modification> lacks positive actual-notes/normal-notes; ignored",
);
None
}
}
}
fn read_decorations(&mut self, notations: Node<'_, '_>, out: &mut Vec<DecorationAttachment>) {
for child in element_children(notations) {
match child.tag_name().name() {
"ornaments" | "technical" | "articulations" => {
for element in element_children(child) {
self.push_decoration(element, out);
}
}
"fermata" => {
let name = match child.attribute("type") {
Some("inverted") => "invertedfermata",
_ => "fermata",
};
out.push(named_decoration(name));
}
"arpeggiate" => out.push(named_decoration("arpeggio")),
"glissando" | "slide" => self.push_direct_spanner(child, out),
_ => {}
}
}
}
fn push_decoration(&mut self, element: Node<'_, '_>, out: &mut Vec<DecorationAttachment>) {
let tag = element.tag_name().name();
if tag == "wavy-line" {
let wavy_type = element.attribute("type").unwrap_or("start");
let number = element.attribute("number").unwrap_or("1");
if matches!(wavy_type, "start" | "stop" | "continue")
&& !number.is_empty()
&& number.bytes().all(|byte| byte.is_ascii_digit())
{
out.push(named_decoration(&format!(
"musicxml-wavy-line-{wavy_type}-{number}"
)));
return;
}
self.warn(
"musicxml.read.unsupported_notation",
format!("<wavy-line type=\"{wavy_type}\" number=\"{number}\"> has no ABC decoration inverse; skipped"),
);
return;
}
if tag == "fingering" {
if let Some(text) = node_text(element)
&& matches!(text, "0" | "1" | "2" | "3" | "4" | "5")
{
out.push(named_decoration(text));
return;
}
if let Some(text) = node_text(element) {
out.push(named_decoration(&format!(
"musicxml-tech-fingering-hex-{}",
crate::hex::encode_hex_utf8(text)
)));
} else {
self.warn(
"musicxml.read.unsupported_fingering",
"<fingering> lacks text; skipped",
);
}
return;
}
if matches!(tag, "string" | "fret") {
if let Some(text) = node_text(element)
&& !text.is_empty()
&& text.bytes().all(|byte| byte.is_ascii_digit())
{
out.push(named_decoration(&format!("musicxml-tech-{tag}-{text}")));
return;
}
self.warn(
"musicxml.read.unsupported_notation",
format!("<{tag}> lacks decimal text; skipped"),
);
return;
}
if tag == "tremolo" {
let tremolo_type = element.attribute("type").unwrap_or("single");
let Some(marks) = node_text(element) else {
self.warn(
"musicxml.read.unsupported_notation",
"<tremolo> lacks a mark count; skipped",
);
return;
};
if matches!(tremolo_type, "single" | "start" | "stop")
&& matches!(marks, "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8")
{
let mut name = format!("musicxml-tremolo-{tremolo_type}-{marks}");
if matches!(tremolo_type, "start" | "stop")
&& let Some(note_node) =
element.ancestors().find(|node| node.has_tag_name("note"))
&& let Some((actual, normal)) = tremolo_note_time_modification(note_node)
{
name.push_str(&format!("-tm-{actual}-{normal}"));
}
out.push(named_decoration(&name));
return;
}
self.warn(
"musicxml.read.unsupported_notation",
format!("<tremolo type=\"{tremolo_type}\">{marks}</tremolo> has no ABC decoration inverse; skipped"),
);
return;
}
match decoration_for_notation_element(tag) {
Some(name) => out.push(named_decoration(name)),
None => self.warn(
"musicxml.read.unsupported_notation",
format!("<{tag}> has no ABC decoration inverse; skipped"),
),
}
}
fn push_direct_spanner(&mut self, element: Node<'_, '_>, out: &mut Vec<DecorationAttachment>) {
let tag = element.tag_name().name();
let spanner_type = element.attribute("type").unwrap_or("start");
let line_type = element.attribute("line-type").unwrap_or("solid");
let number = element.attribute("number").unwrap_or("1");
if matches!(spanner_type, "start" | "stop")
&& matches!(line_type, "solid" | "dashed" | "dotted" | "wavy")
&& !number.is_empty()
&& number.bytes().all(|byte| byte.is_ascii_digit())
{
let text = node_text(element)
.map(|text| format!("-hex-{}", crate::hex::encode_hex_utf8(text)))
.unwrap_or_default();
out.push(named_decoration(&format!(
"musicxml-{tag}-{spanner_type}-{line_type}-{number}{text}"
)));
return;
}
self.warn(
"musicxml.read.unsupported_notation",
format!(
"<{tag} type=\"{spanner_type}\" line-type=\"{line_type}\" number=\"{number}\"> has no ABC decoration inverse; skipped"
),
);
}
fn accidental_from_name(&mut self, name: &str) -> Option<Accidental> {
match name {
"flat-flat" => Some(Accidental::DoubleFlat),
"flat" => Some(Accidental::Flat),
"natural" => Some(Accidental::Natural),
"sharp" => Some(Accidental::Sharp),
"double-sharp" => Some(Accidental::DoubleSharp),
other => {
self.warn(
"musicxml.read.unknown_accidental",
format!("<accidental> `{other}` is not a recognised MusicXML accidental"),
);
None
}
}
}
fn read_pitch(&mut self, note_node: Node<'_, '_>) -> Option<Pitch> {
if let Some(pitch_node) = child_element(note_node, "pitch") {
return self.read_pitched_note(pitch_node);
}
if let Some(unpitched_node) = child_element(note_node, "unpitched") {
return self.read_unpitched_note(unpitched_node);
}
None
}
fn read_pitched_note(&mut self, pitch_node: Node<'_, '_>) -> Option<Pitch> {
let step = child_text(pitch_node, "step")
.and_then(|text| text.trim().chars().next())
.or_else(|| {
self.warn(
"musicxml.read.pitch_missing_step",
"<pitch> has no usable <step>; note skipped",
);
None
})?;
let octave = child_text(pitch_node, "octave")
.and_then(|text| text.trim().parse::<i8>().ok())
.or_else(|| {
self.warn(
"musicxml.read.pitch_missing_octave",
"<pitch> has no usable <octave>; note skipped",
);
None
})?;
let alter = child_text(pitch_node, "alter")
.and_then(|text| self.parse_alter(text, "alter"))
.unwrap_or(0);
Some(Pitch {
step,
alter,
octave,
spelling_source: READER_SPAN,
})
}
fn read_unpitched_note(&mut self, unpitched_node: Node<'_, '_>) -> Option<Pitch> {
let step = child_text(unpitched_node, "display-step")
.and_then(|text| text.trim().chars().next())
.or_else(|| {
self.warn(
"musicxml.read.unpitched_missing_display_step",
"<unpitched> has no usable <display-step>; note skipped",
);
None
})?;
let octave = child_text(unpitched_node, "display-octave")
.and_then(|text| text.trim().parse::<i8>().ok())
.or_else(|| {
self.warn(
"musicxml.read.unpitched_missing_display_octave",
"<unpitched> has no usable <display-octave>; note skipped",
);
None
})?;
Some(Pitch {
step,
alter: 0,
octave,
spelling_source: READER_SPAN,
})
}
fn read_duration(&mut self, node: Node<'_, '_>, divisions: u32) -> Option<Fraction> {
let raw = child_text(node, "duration")?;
let value = match raw.trim().parse::<u32>() {
Ok(value) => value,
Err(_) => {
self.warn(
"musicxml.read.invalid_duration",
format!("<duration> `{}` is not a non-negative integer", raw.trim()),
);
return None;
}
};
let denominator = divisions.max(1).saturating_mul(4);
Some(Fraction::new(value, denominator))
}
}
struct PartListEntry {
id: String,
name: Option<String>,
instruments: Vec<MusicXmlPartInstrumentModel>,
}
const MAX_LYRIC_VERSE: u32 = 100;
struct PartGroupEntry {
symbol: char,
part_ids: Vec<String>,
start_order: usize,
}
struct PartListResult {
entries: Vec<PartListEntry>,
groups: Vec<PartGroupEntry>,
}
struct VoiceAccumulator {
voice: String,
events: Vec<TimedEvent>,
measures: Vec<Measure>,
}
fn voice_accumulator<'a>(
accumulators: &'a mut Vec<VoiceAccumulator>,
voice: &str,
) -> &'a mut VoiceAccumulator {
let index = match accumulators
.iter()
.position(|accumulator| accumulator.voice == voice)
{
Some(index) => index,
None => {
accumulators.push(VoiceAccumulator {
voice: voice.to_owned(),
events: Vec::new(),
measures: Vec::new(),
});
accumulators.len() - 1
}
};
&mut accumulators[index]
}
fn voice_id_value(part_id: &str, voice: &str, index: usize) -> String {
if index == 0 {
part_id.to_owned()
} else {
format!("{part_id}#{}", voice.trim())
}
}
fn project_part_names_to_voice_properties(score: &mut Score) {
let single_part = score.parts.len() == 1;
let title = score.metadata.title.as_ref().map(|title| title.text.trim());
for part in &mut score.parts {
let Some(name) = part
.name
.clone()
.filter(|line| !line.text.trim().is_empty())
else {
continue;
};
if single_part && title.is_some_and(|title| title == name.text.trim()) {
continue;
}
let Some(voice) = part.voices.first_mut() else {
continue;
};
if voice.initial_properties.name.is_none() {
voice.initial_properties.name = Some(name.clone());
}
if voice.properties.name.is_none() {
voice.properties.name = Some(name);
}
}
}
struct PartScoreBlock {
part_id: String,
text: String,
multi_voice: bool,
}
fn part_score_blocks(score: &Score) -> Vec<PartScoreBlock> {
score
.parts
.iter()
.map(|part| {
let voice_ids = part
.voices
.iter()
.map(|voice| voice.id.value.as_str())
.collect::<Vec<_>>();
let text = if part.staves.len() > 1 {
let staff_groups = part
.staves
.iter()
.filter_map(|staff| match staff.voices.as_slice() {
[] => None,
[id] => Some(id.value.clone()),
ids => Some(format!(
"({})",
ids.iter()
.map(|id| id.value.as_str())
.collect::<Vec<_>>()
.join(" ")
)),
})
.collect::<Vec<_>>();
format!("{{{}}}", staff_groups.join(" "))
} else {
match voice_ids.as_slice() {
[] => part.id.value.clone(),
[id] => (*id).to_owned(),
ids => format!("({})", ids.join(" ")),
}
};
PartScoreBlock {
part_id: part.id.value.clone(),
text,
multi_voice: voice_ids.len() > 1,
}
})
.collect()
}
fn part_score_block_text(part_id: &str, part_score_blocks: &[PartScoreBlock]) -> String {
part_score_blocks
.iter()
.find(|block| block.part_id == part_id)
.map(|block| block.text.clone())
.unwrap_or_else(|| part_id.to_owned())
}
fn synthesize_score_directive(
groups: &[PartGroupEntry],
all_part_ids: &[&str],
part_score_blocks: &[PartScoreBlock],
) -> Option<ScoreDirectiveModel> {
let has_multi_voice_part = part_score_blocks.iter().any(|block| block.multi_voice);
if groups.is_empty() && !has_multi_voice_part {
return None;
}
let text = build_score_text(groups, all_part_ids, part_score_blocks);
if text.is_empty() {
return None;
}
let tokens = parse_score_tokens(&text);
Some(ScoreDirectiveModel {
span: READER_SPAN,
value: TextLine {
text,
span: READER_SPAN,
},
tokens,
})
}
fn build_score_text(
groups: &[PartGroupEntry],
all_part_ids: &[&str],
part_score_blocks: &[PartScoreBlock],
) -> String {
let parents = group_parents(groups);
let children_of = |parent: Option<usize>| -> Vec<usize> {
(0..groups.len())
.filter(|&i| !groups[i].part_ids.is_empty() && parents[i] == parent)
.collect()
};
let top_level = children_of(None);
let mut result_tokens: Vec<String> = Vec::new();
let mut skip_ids: std::collections::HashSet<&str> = Default::default();
for &id in all_part_ids {
if skip_ids.contains(id) {
continue;
}
let starting = top_level
.iter()
.copied()
.filter(|&i| groups[i].part_ids.first().is_some_and(|first| first == id))
.max_by_key(|&i| groups[i].part_ids.len());
if let Some(index) = starting {
result_tokens.push(render_group_tree(
index,
groups,
&parents,
part_score_blocks,
));
for consumed in &groups[index].part_ids {
skip_ids.insert(consumed.as_str());
}
} else {
result_tokens.push(part_score_block_text(id, part_score_blocks));
}
}
result_tokens.join(" ")
}
fn group_parents(groups: &[PartGroupEntry]) -> Vec<Option<usize>> {
let contains = |outer: &PartGroupEntry, inner: &PartGroupEntry| {
inner.part_ids.iter().all(|id| outer.part_ids.contains(id))
};
(0..groups.len())
.map(|i| {
let group = &groups[i];
if group.part_ids.is_empty() {
return None;
}
(0..groups.len())
.filter(|&j| {
let candidate = &groups[j];
j != i
&& contains(candidate, group)
&& (candidate.part_ids.len() > group.part_ids.len()
|| candidate.start_order < group.start_order)
})
.min_by_key(|&j| {
(
groups[j].part_ids.len(),
std::cmp::Reverse(groups[j].start_order),
)
})
})
.collect()
}
fn render_group_tree(
index: usize,
groups: &[PartGroupEntry],
parents: &[Option<usize>],
part_score_blocks: &[PartScoreBlock],
) -> String {
let group = &groups[index];
let children: Vec<usize> = (0..groups.len())
.filter(|&i| !groups[i].part_ids.is_empty() && parents[i] == Some(index))
.collect();
let mut tokens: Vec<String> = Vec::new();
let mut skip_remaining: usize = 0;
for id in &group.part_ids {
if skip_remaining > 0 {
skip_remaining -= 1;
continue;
}
let child = children
.iter()
.copied()
.filter(|&i| groups[i].part_ids.first() == Some(id))
.max_by_key(|&i| groups[i].part_ids.len());
if let Some(child) = child {
skip_remaining = groups[child].part_ids.len().saturating_sub(1);
tokens.push(render_group_tree(child, groups, parents, part_score_blocks));
} else {
tokens.push(part_score_block_text(id, part_score_blocks));
}
}
let inner = tokens.join(" ");
let open = open_char(group.symbol);
let close = close_char(group.symbol);
if open == '\0' {
inner
} else {
format!("{open}{inner}{close}")
}
}
fn open_char(symbol: char) -> char {
match symbol {
'[' => '[',
'{' => '{',
_ => '\0',
}
}
fn close_char(symbol: char) -> char {
match symbol {
'[' => ']',
'{' => '}',
_ => '\0',
}
}
fn parse_score_tokens(text: &str) -> Vec<ScoreDirectiveTokenModel> {
let mut tokens = Vec::new();
let mut chars = text.char_indices().peekable();
while let Some((_, ch)) = chars.next() {
if ch.is_whitespace() {
continue;
}
let kind = match ch {
'(' | '[' | '{' => ScoreDirectiveTokenKindModel::GroupStart(ch),
')' | ']' | '}' => ScoreDirectiveTokenKindModel::GroupEnd(ch),
'|' => ScoreDirectiveTokenKindModel::StaffSeparator,
',' => ScoreDirectiveTokenKindModel::MeasureSeparator,
'*' => ScoreDirectiveTokenKindModel::FloatingVoiceMarker,
_ => {
let mut id = String::new();
id.push(ch);
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch.is_whitespace()
|| matches!(next_ch, '(' | ')' | '[' | ']' | '{' | '}' | '|')
{
break;
}
id.push(next_ch);
chars.next();
}
ScoreDirectiveTokenKindModel::Voice(id)
}
};
tokens.push(ScoreDirectiveTokenModel {
span: READER_SPAN,
kind,
});
}
tokens
}
struct PartOutcome {
part: Part,
header_tempo: Option<TempoModel>,
}
struct MeasureOutcome {
voices: Vec<(String, VoiceMeasure)>,
measure: Measure,
header_tempo: Option<TempoModel>,
}
struct VoiceMeasure {
events: Vec<TimedEvent>,
expected_duration: Option<Fraction>,
actual_duration: Fraction,
}
struct VoiceMeasureState {
events: Vec<TimedEvent>,
cursor: Fraction,
max_cursor: Fraction,
cursor_restore_base: Option<Fraction>,
measure_rest_duration: Option<Fraction>,
pending: EventAttachments,
pending_insert_index: Option<usize>,
open_tuplets: OpenTuplets,
grace_builder: Option<GraceGroupBuilder>,
pending_graces: Vec<GraceGroupAttachment>,
last_main_event: Option<usize>,
next_chord_span_start: usize,
}
impl Default for VoiceMeasureState {
fn default() -> Self {
Self {
events: Vec::new(),
cursor: Fraction::zero(),
max_cursor: Fraction::zero(),
cursor_restore_base: None,
measure_rest_duration: None,
pending: EventAttachments::default(),
pending_insert_index: None,
open_tuplets: OpenTuplets::default(),
grace_builder: None,
pending_graces: Vec::new(),
last_main_event: None,
next_chord_span_start: 1_000_000,
}
}
}
impl VoiceMeasureState {
fn pending_is_empty(&self) -> bool {
self.pending.chord_symbols.is_empty()
&& self.pending.annotations.is_empty()
&& self.pending.decorations.is_empty()
}
fn mark_pending_position(&mut self) {
if self.pending_is_empty() {
self.pending_insert_index = Some(self.events.len());
}
}
}
enum ParsedDirection {
Tempo(TempoModel),
TempoAndEvent(TempoModel, Box<EventAttachments>),
Event(Box<EventAttachments>),
SectionLabel(String),
Ignored,
}
struct ParsedNote {
kind: TimedEventKind,
duration: Fraction,
chord_member: bool,
measure_rest: bool,
}
fn attachments_are_decorations_only(attachments: &EventAttachments) -> bool {
if attachments.decorations.is_empty() {
return false;
}
let mut remainder = attachments.clone();
remainder.decorations.clear();
remainder.is_empty()
}
#[derive(Default)]
struct GraceGroupBuilder {
slash: Option<Span>,
events: Vec<GraceEvent>,
}
impl GraceGroupBuilder {
fn append_decorations_to_last_note(
&mut self,
decorations: Vec<DecorationAttachment>,
) -> Option<Vec<DecorationAttachment>> {
match self.events.last_mut().map(|event| &mut event.kind) {
Some(GraceEventKind::Note(note)) => {
note.decorations.extend(decorations);
None
}
Some(GraceEventKind::Chord(members)) => match members.last_mut() {
Some(note) => {
note.decorations.extend(decorations);
None
}
None => Some(decorations),
},
Some(GraceEventKind::Rest(_)) | None => Some(decorations),
}
}
fn finish(mut self) -> GraceGroupAttachment {
let note_count = u32::try_from(self.events.len()).unwrap_or(u32::MAX);
let base_unit = super::grace::grace_base_unit(note_count);
for event in &mut self.events {
match &mut event.kind {
GraceEventKind::Note(note) => {
note.length_multiplier = divide_fraction(note.length_multiplier, base_unit);
}
GraceEventKind::Chord(members) => {
for member in members {
member.length_multiplier =
divide_fraction(member.length_multiplier, base_unit);
}
}
GraceEventKind::Rest(_) => {}
}
}
GraceGroupAttachment {
span: READER_SPAN,
slash: self.slash,
note_count,
events: self.events,
slurs: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy)]
struct OpenTuplet {
pair_id: u32,
number: u32,
actual_notes: u32,
normal_notes: u32,
}
#[derive(Default)]
struct OpenTuplets {
open: Vec<OpenTuplet>,
next_pair_id: u32,
}
impl OpenTuplets {
fn resolve(
&mut self,
reader: &mut Reader,
markers: &[(TupletRole, u32)],
time_modification: Option<(u32, u32)>,
events: &mut [TimedEvent],
) -> Vec<TupletAttachment> {
let mut out = Vec::new();
let has_start = markers.iter().any(|(role, _)| *role == TupletRole::Start);
let has_stop = markers.iter().any(|(role, _)| *role == TupletRole::Stop);
if !has_start && !has_stop {
if let Some((tm_actual, tm_normal)) = time_modification
&& !self.open.is_empty()
{
if self.open.len() == 1 {
let outer = &self.open[0];
if outer.actual_notes != tm_actual || outer.normal_notes != tm_normal {
let composite_actual = outer.actual_notes;
let composite_normal = outer.normal_notes;
let outer_pair_id = outer.pair_id;
patch_tuplet_ratio(events, outer_pair_id, tm_actual, tm_normal);
patch_inner_tuplet_ratio(
events,
outer_pair_id,
composite_actual,
composite_normal,
tm_actual,
tm_normal,
);
self.open[0].actual_notes = tm_actual;
self.open[0].normal_notes = tm_normal;
}
}
for open in &self.open {
out.push(TupletAttachment {
pair_id: open.pair_id,
actual_notes: open.actual_notes,
normal_notes: open.normal_notes,
role: TupletRole::Continue,
span: READER_SPAN,
});
}
}
return out;
}
for (role, number) in markers {
match role {
TupletRole::Start => {
let (actual_notes, normal_notes) = time_modification.unwrap_or_else(|| {
reader.warn(
"musicxml.read.tuplet_without_time_modification",
"<tuplet type=\"start\"> has no <time-modification>; treating as a \
display-only bracket (1:1, no time compression)",
);
(1, 1)
});
let pair_id = self.next_pair_id;
self.next_pair_id = self.next_pair_id.saturating_add(1);
self.open.push(OpenTuplet {
pair_id,
number: *number,
actual_notes,
normal_notes,
});
out.push(TupletAttachment {
pair_id,
actual_notes,
normal_notes,
role: TupletRole::Start,
span: READER_SPAN,
});
}
TupletRole::Stop => {
let index = self
.open
.iter()
.rposition(|open| open.number == *number)
.or_else(|| self.open.len().checked_sub(1));
let closed = index.map(|index| self.open.remove(index));
let closed = match closed {
Some(open) => open,
None => {
reader.warn(
"musicxml.read.tuplet_stop_without_start",
"<tuplet type=\"stop\"> has no open tuplet to close; ignored",
);
continue;
}
};
out.push(TupletAttachment {
pair_id: closed.pair_id,
actual_notes: closed.actual_notes,
normal_notes: closed.normal_notes,
role: TupletRole::Stop,
span: READER_SPAN,
});
}
TupletRole::Continue => {}
}
}
if has_stop && !has_start && time_modification.is_some() {
for open in &self.open {
out.push(TupletAttachment {
pair_id: open.pair_id,
actual_notes: open.actual_notes,
normal_notes: open.normal_notes,
role: TupletRole::Continue,
span: READER_SPAN,
});
}
}
out
}
}
fn patch_tuplet_ratio(
events: &mut [TimedEvent],
target_pair_id: u32,
actual_notes: u32,
normal_notes: u32,
) {
for event in events.iter_mut().rev() {
for tuplet in event.attachments.tuplets.iter_mut() {
if tuplet.pair_id == target_pair_id {
tuplet.actual_notes = actual_notes;
tuplet.normal_notes = normal_notes;
}
}
}
}
fn patch_inner_tuplet_ratio(
events: &mut [TimedEvent],
outer_pair_id: u32,
composite_actual: u32,
composite_normal: u32,
outer_actual: u32,
outer_normal: u32,
) {
let num = u64::from(composite_actual) * u64::from(outer_normal);
let den = u64::from(composite_normal) * u64::from(outer_actual);
if den == 0 {
return;
}
let g = crate::model::gcd_u64(num, den);
let inner_actual = num / g;
let inner_normal = den / g;
let (Ok(inner_actual), Ok(inner_normal)) =
(u32::try_from(inner_actual), u32::try_from(inner_normal))
else {
return;
};
for event in events.iter_mut().rev() {
for tuplet in event.attachments.tuplets.iter_mut() {
if tuplet.pair_id != outer_pair_id
&& tuplet.actual_notes == composite_actual
&& tuplet.normal_notes == composite_normal
{
tuplet.actual_notes = inner_actual;
tuplet.normal_notes = inner_normal;
}
}
}
}
fn divide_fraction(left: Fraction, right: Fraction) -> Fraction {
if right.numerator == 0 {
return Fraction::zero();
}
Fraction::new(
left.numerator.saturating_mul(right.denominator),
left.denominator.saturating_mul(right.numerator),
)
}
fn note_type_fraction(name: &str) -> Option<Fraction> {
Some(match name.trim() {
"maxima" => Fraction {
numerator: 8,
denominator: 1,
},
"long" => Fraction {
numerator: 4,
denominator: 1,
},
"breve" => Fraction {
numerator: 2,
denominator: 1,
},
"whole" => Fraction {
numerator: 1,
denominator: 1,
},
"half" => Fraction {
numerator: 1,
denominator: 2,
},
"quarter" => Fraction {
numerator: 1,
denominator: 4,
},
"eighth" => Fraction {
numerator: 1,
denominator: 8,
},
"16th" => Fraction {
numerator: 1,
denominator: 16,
},
"32nd" => Fraction {
numerator: 1,
denominator: 32,
},
"64th" => Fraction {
numerator: 1,
denominator: 64,
},
"128th" => Fraction {
numerator: 1,
denominator: 128,
},
_ => return None,
})
}
fn named_decoration(name: &str) -> DecorationAttachment {
DecorationAttachment {
name: name.to_owned(),
span: READER_SPAN,
source_kind: DecorationSourceKind::Named,
placement: None,
}
}
fn named_decoration_with_placement(name: &str, placement: Option<&str>) -> DecorationAttachment {
let mut decoration = named_decoration(name);
decoration.placement = direction_placement_model(placement);
decoration
}
fn direction_placement_model(placement: Option<&str>) -> Option<AnnotationPlacementModel> {
match placement {
Some("above") => Some(AnnotationPlacementModel::Above),
Some("below") => Some(AnnotationPlacementModel::Below),
_ => None,
}
}
fn decoration_for_notation_element(element: &str) -> Option<&'static str> {
Some(match element {
"staccato" => "staccato",
"accent" => "accent",
"tenuto" => "tenuto",
"staccatissimo" => "wedge",
"strong-accent" => "marcato",
"breath-mark" => "breath",
"scoop" => "slide",
"caesura" => "caesura",
"detached-legato" => "detached-legato",
"falloff" => "falloff",
"doit" => "doit",
"trill-mark" => "trill",
"mordent" => "mordent",
"inverted-mordent" => "uppermordent",
"turn" => "turn",
"inverted-turn" => "invertedturn",
"up-bow" => "upbow",
"down-bow" => "downbow",
"open-string" => "open",
"thumb-position" => "thumb",
"snap-pizzicato" => "snap",
"stopped" => "plus",
_ => return None,
})
}
fn annotation_from_words(words: Node<'_, '_>, placement: Option<&str>) -> TextAttachment {
let text = raw_text(words);
let (placement_model, prefix) = match placement {
Some("below") => (Some(AnnotationPlacementModel::Below), "_"),
Some(_) => (Some(AnnotationPlacementModel::Above), "^"),
None => (None, ""),
};
TextAttachment {
text: format!("{prefix}{text}"),
span: READER_SPAN,
placement: placement_model,
musicxml_harmony_text: HarmonyKindText::AbcNative,
}
}
fn demoted_chord_symbol_from_words(
words: Node<'_, '_>,
placement: Option<&str>,
) -> Option<TextAttachment> {
if placement.is_some() {
return None;
}
let text = raw_text(words);
if text != text.trim() {
return None;
}
Some(TextAttachment {
text: text.to_owned(),
span: READER_SPAN,
placement: None,
musicxml_harmony_text: HarmonyKindText::AbcNative,
})
}
fn dynamic_decoration_name(element: &str) -> Option<&'static str> {
Some(match element {
"p" => "p",
"pp" => "pp",
"ppp" => "ppp",
"pppp" => "pppp",
"ppppp" => "ppppp",
"pppppp" => "pppppp",
"f" => "f",
"ff" => "ff",
"fff" => "fff",
"ffff" => "ffff",
"fffff" => "fffff",
"ffffff" => "ffffff",
"mp" => "mp",
"mf" => "mf",
"sf" => "sf",
"sfp" => "sfp",
"sfpp" => "sfpp",
"fp" => "fp",
"rf" => "rf",
"rfz" => "rfz",
"sfz" => "sfz",
"sffz" => "sffz",
"fz" => "fz",
"n" => "n",
"pf" => "pf",
"sfzp" => "sfzp",
_ => return None,
})
}
fn wedge_decoration_name(wedge_type: Option<&str>) -> Option<&'static str> {
Some(match wedge_type {
Some("crescendo") => "crescendo(",
Some("diminuendo") => "diminuendo(",
Some("stop") => "crescendo)",
_ => return None,
})
}
fn prepend_attachments(target: &mut EventAttachments, head: EventAttachments) {
if head.chord_symbols.is_empty() && head.annotations.is_empty() && head.decorations.is_empty() {
return;
}
let mut chord_symbols = head.chord_symbols;
chord_symbols.append(&mut target.chord_symbols);
target.chord_symbols = chord_symbols;
let mut annotations = head.annotations;
annotations.append(&mut target.annotations);
target.annotations = annotations;
let mut decorations = head.decorations;
decorations.append(&mut target.decorations);
target.decorations = decorations;
}
fn text_line(text: impl Into<String>) -> TextLine {
TextLine {
text: text.into(),
span: READER_SPAN,
}
}
fn max_fraction(left: Fraction, right: Fraction) -> Fraction {
if left.less_than(right) { right } else { left }
}
fn subtract_fraction(left: Fraction, right: Fraction) -> Fraction {
let numerator = u64::from(left.numerator)
.saturating_mul(u64::from(right.denominator))
.saturating_sub(u64::from(right.numerator).saturating_mul(u64::from(left.denominator)));
let denominator = u64::from(left.denominator).saturating_mul(u64::from(right.denominator));
Fraction::new(
u32::try_from(numerator).unwrap_or(u32::MAX),
u32::try_from(denominator).unwrap_or(u32::MAX),
)
}
fn barline_kind_from(
bar_style: Option<&str>,
repeat_direction: Option<&str>,
is_left: bool,
) -> Option<BarlineKind> {
Some(match (bar_style, repeat_direction, is_left) {
(Some("heavy-light"), Some("forward"), true) => BarlineKind::RepeatStart,
(Some("light-heavy"), Some("backward"), _) => BarlineKind::RepeatEnd,
(Some("light-light"), None, _) => BarlineKind::Double,
(Some("light-heavy"), None, _) => BarlineKind::Final,
(Some("heavy-light"), None, _) => BarlineKind::Initial,
(Some("dotted"), None, _) => BarlineKind::Dotted,
(Some("dashed"), None, _) => BarlineKind::Dashed,
(Some("none"), None, _) => BarlineKind::Invisible,
_ => return None,
})
}
fn clef_text_from(sign: &str, line: &str, octave_change: i8) -> Option<String> {
let base = match (sign.trim(), line.trim()) {
("F", "4") => "bass",
("C", "3") => "alto",
("C", "4") => "tenor",
("percussion", _) => "perc",
_ => "treble",
};
let suffix = match octave_change {
-2 => "-15",
-1 => "-8",
1 => "+8",
2 => "+15",
_ => "",
};
if base == "treble" && suffix.is_empty() {
return None;
}
Some(format!("{base}{suffix}"))
}
fn chord_kind_suffix(kind: &str) -> Option<&'static str> {
Some(match kind.trim() {
"major" => "",
"minor" => "m",
"augmented" => "+",
"diminished" => "dim",
"dominant" | "dominant-seventh" => "7",
"major-seventh" => "maj7",
"minor-seventh" => "m7",
"diminished-seventh" => "dim7",
"augmented-seventh" => "aug7",
"half-diminished" | "half-diminished-seventh" => "m7b5",
"major-sixth" => "6",
"minor-sixth" => "m6",
"dominant-ninth" => "9",
"major-ninth" => "maj9",
"minor-ninth" => "m9",
"dominant-11th" => "11",
"major-11th" => "maj11",
"minor-11th" => "m11",
"dominant-13th" => "13",
"major-13th" => "maj13",
"minor-13th" => "m13",
"suspended-fourth" => "sus4",
"suspended-second" => "sus2",
"power" => "5",
_ => return None,
})
}
fn accidental_suffix(alter: i8) -> &'static str {
match alter {
2 => "##",
1 => "#",
-1 => "b",
-2 => "bb",
_ => "",
}
}
fn degree_accidental(alter: i8) -> &'static str {
match alter {
a if a > 0 => "#",
a if a < 0 => "b",
_ => "",
}
}
fn first_upper_letter(text: &str) -> Option<char> {
text.trim()
.chars()
.next()
.map(|ch| ch.to_ascii_uppercase())
.filter(char::is_ascii_uppercase)
}
fn element_children<'a, 'input>(node: Node<'a, 'input>) -> impl Iterator<Item = Node<'a, 'input>> {
node.children().filter(Node::is_element)
}
fn children_named<'a, 'input>(
node: Node<'a, 'input>,
name: &'a str,
) -> impl Iterator<Item = Node<'a, 'input>> {
node.children()
.filter(move |child| child.is_element() && child.tag_name().name() == name)
}
fn descendants_named<'a, 'input>(
node: Node<'a, 'input>,
name: &'a str,
) -> impl Iterator<Item = Node<'a, 'input>> {
node.descendants()
.filter(move |child| child.is_element() && child.tag_name().name() == name)
}
fn child_element<'a, 'input>(node: Node<'a, 'input>, name: &str) -> Option<Node<'a, 'input>> {
node.children()
.find(|child| child.is_element() && child.tag_name().name() == name)
}
fn tremolo_note_time_modification(note_node: Node<'_, '_>) -> Option<(u32, u32)> {
let node = child_element(note_node, "time-modification")?;
let actual = child_text(node, "actual-notes")?.parse::<u32>().ok()?;
let normal = child_text(node, "normal-notes")?.parse::<u32>().ok()?;
(actual > 0 && normal > 0).then_some((actual, normal))
}
fn node_text<'a>(node: Node<'a, '_>) -> Option<&'a str> {
node.text().map(str::trim).filter(|text| !text.is_empty())
}
fn raw_text<'a>(node: Node<'a, '_>) -> &'a str {
node.text().unwrap_or("")
}
fn child_text<'a>(node: Node<'a, '_>, name: &str) -> Option<&'a str> {
child_element(node, name).and_then(node_text)
}
fn build_staves(count: u32, voices: &[Voice]) -> Vec<Staff> {
(1..=count.max(1))
.map(|staff_number| Staff {
id: StaffId {
value: staff_number,
span: READER_SPAN,
},
voices: voices
.iter()
.filter(|voice| voice.staff.value == staff_number)
.map(|voice| voice.id.clone())
.collect(),
source_span: READER_SPAN,
})
.collect()
}
fn read_staves_count(attributes: Node<'_, '_>) -> u32 {
child_text(attributes, "staves")
.and_then(|text| text.trim().parse::<u32>().ok())
.filter(|count| *count >= 1)
.unwrap_or(1)
}
fn staff_voice_map(part_node: Node<'_, '_>) -> std::collections::BTreeMap<u32, String> {
let staves = children_named(part_node, "measure")
.next()
.and_then(|measure| child_element(measure, "attributes"))
.map(read_staves_count)
.unwrap_or(1);
if staves <= 1 {
return std::collections::BTreeMap::new();
}
let mut voices: Vec<(String, u32)> = voice_staff_map(part_node).into_iter().collect();
voices.sort_by_key(|(voice, _)| parse_voice_number(voice));
let mut map = std::collections::BTreeMap::new();
for (voice, staff) in voices {
map.entry(staff).or_insert(voice);
}
map
}
fn voice_staff_map(part_node: Node<'_, '_>) -> std::collections::BTreeMap<String, u32> {
let mut map = std::collections::BTreeMap::new();
for measure in children_named(part_node, "measure") {
for note in children_named(measure, "note") {
let voice = child_text(note, "voice").unwrap_or("1").to_owned();
let staff = child_text(note, "staff")
.and_then(|text| text.trim().parse::<u32>().ok())
.unwrap_or(1);
map.entry(voice).or_insert(staff);
}
}
map
}
fn read_attributes_divisions(reader: &mut Reader, attributes: Node<'_, '_>) -> Option<u32> {
child_element(attributes, "divisions")
.and_then(|node| parse_u32(reader, node, "divisions"))
.filter(|value| *value > 0)
}
fn parse_u32(reader: &mut Reader, node: Node<'_, '_>, label: &str) -> Option<u32> {
let text = node_text(node)?;
match text.parse::<u32>() {
Ok(value) => Some(value),
Err(_) => {
reader.warn(
"musicxml.read.invalid_integer",
format!("<{label}> `{text}` is not a non-negative integer"),
);
None
}
}
}
fn note_voice_number(note_node: Node<'_, '_>) -> Option<String> {
child_text(note_node, "voice").map(str::to_owned)
}
fn direction_voice_number(direction: Node<'_, '_>) -> Option<String> {
child_text(direction, "voice").map(str::to_owned)
}
fn tempo_words(direction: Node<'_, '_>) -> Option<String> {
let words: Vec<String> = children_named(direction, "direction-type")
.flat_map(|dt| children_named(dt, "words"))
.filter_map(|word| {
let text = raw_text(word)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
(!text.is_empty()).then_some(text)
})
.collect();
(!words.is_empty()).then_some(words.join(" "))
}
fn starts_with_abc_chord_root(text: &str) -> bool {
matches!(text.trim_start().chars().next(), Some('A'..='G'))
}
fn parse_voice_number(voice: &str) -> u32 {
voice.trim().parse::<u32>().unwrap_or(u32::MAX)
}
fn voice_state<'a>(
voices: &'a mut Vec<(String, VoiceMeasureState)>,
voice: &str,
) -> &'a mut VoiceMeasureState {
let index = match voices.iter().position(|(name, _)| name == voice) {
Some(index) => index,
None => {
voices.push((voice.to_owned(), VoiceMeasureState::default()));
voices.len() - 1
}
};
&mut voices[index].1
}
fn voice_state_at<'a>(
voices: &'a mut Vec<(String, VoiceMeasureState)>,
voice: &str,
cursor: Fraction,
) -> &'a mut VoiceMeasureState {
let state = voice_state(voices, voice);
state.cursor = cursor;
state
}
fn clef_staff_voice<'a>(
kind: &TimedEventKind,
clef_node: Node<'_, '_>,
staff_voices: &'a std::collections::BTreeMap<u32, String>,
) -> Option<&'a str> {
if !matches!(kind, TimedEventKind::ClefChange(_)) {
return None;
}
let staff = clef_node.attribute("number")?.trim().parse::<u32>().ok()?;
staff_voices.get(&staff).map(String::as_str)
}
fn read_multiple_rest(attributes: Node<'_, '_>) -> Option<u32> {
let measure_style = child_element(attributes, "measure-style")?;
let count = child_text(measure_style, "multiple-rest")?
.trim()
.parse::<u32>()
.ok()?;
(count > 1).then_some(count)
}
#[cfg(feature = "musicxml-reader")]
pub fn complete_score_for_abc(score: &mut Score) {
if let Some(key) = score.metadata.key.as_mut()
&& key.display.is_empty()
{
key.display = key_display_for_abc(key);
}
let header_key = score.metadata.key.clone();
move_header_playback_tempo_to_first_voice(score);
for part in &mut score.parts {
for voice in &mut part.voices {
if let Some(key) = voice.initial_key.as_mut()
&& key.display.is_empty()
{
key.display = key_display_for_abc(key);
}
renumber_voice_tuplet_pair_ids(voice);
reproject_grace_slurs_for_abc(voice);
reproject_zero_duration_prefixes_for_abc(voice);
preserve_voice_onset_gaps_for_abc(voice);
synthesize_voice_barline_events(voice);
for event in &mut voice.events {
if let TimedEventKind::KeyChange(key) = &mut event.kind
&& key.display.is_empty()
{
key.display = key_display_for_abc(key);
}
}
let voice_initial_key = voice.initial_key.clone().or_else(|| header_key.clone());
complete_pitch_accidentals_for_abc(voice, voice_initial_key.as_ref());
}
}
}
#[cfg(feature = "musicxml-reader")]
fn complete_pitch_accidentals_for_abc(voice: &mut Voice, header_key: Option<&KeySignatureModel>) {
let mut current_key = header_key.cloned();
let mut current_measure: Option<u32> = None;
let mut measure_accidentals: std::collections::BTreeMap<(char, i8), i8> =
std::collections::BTreeMap::new();
let shift = crate::lower::voice::voice_octave_shift(&voice.properties);
for event in &mut voice.events {
if current_measure != Some(event.measure.index) {
current_measure = Some(event.measure.index);
measure_accidentals.clear();
}
let abc_pitch_anchor = matches!(
event.kind,
TimedEventKind::Note(_) | TimedEventKind::Chord(_) | TimedEventKind::Rest(_)
);
if abc_pitch_anchor {
complete_grace_group_accidentals_for_abc(
&mut event.attachments.grace_groups,
current_key.as_ref(),
&mut measure_accidentals,
);
}
match &mut event.kind {
TimedEventKind::Note(note) => complete_note_accidental_for_abc(
note.pitch,
note.pitch.octave.saturating_sub(shift),
&mut note.written_accidental,
current_key.as_ref(),
&mut measure_accidentals,
),
TimedEventKind::Chord(chord) => {
for member in &mut chord.members {
complete_note_accidental_for_abc(
member.pitch,
member.pitch.octave.saturating_sub(shift),
&mut member.written_accidental,
current_key.as_ref(),
&mut measure_accidentals,
);
}
}
TimedEventKind::KeyChange(key) => {
current_key = Some(key.clone());
}
_ => {}
}
if abc_pitch_anchor {
complete_grace_group_accidentals_for_abc(
&mut event.attachments.after_grace_groups,
current_key.as_ref(),
&mut measure_accidentals,
);
}
}
}
#[cfg(feature = "musicxml-reader")]
fn complete_grace_group_accidentals_for_abc(
groups: &mut [GraceGroupAttachment],
current_key: Option<&KeySignatureModel>,
measure_accidentals: &mut std::collections::BTreeMap<(char, i8), i8>,
) {
for group in groups {
for grace in &mut group.events {
match &mut grace.kind {
GraceEventKind::Note(note) => complete_note_accidental_for_abc(
note.pitch,
note.pitch.octave,
&mut note.written_accidental,
current_key,
measure_accidentals,
),
GraceEventKind::Chord(notes) => {
for note in notes {
complete_note_accidental_for_abc(
note.pitch,
note.pitch.octave,
&mut note.written_accidental,
current_key,
measure_accidentals,
);
}
}
GraceEventKind::Rest(_) => {}
}
}
}
}
#[cfg(feature = "musicxml-reader")]
fn complete_note_accidental_for_abc(
pitch: Pitch,
ledger_octave: i8,
written_accidental: &mut Option<AccidentalMark>,
current_key: Option<&KeySignatureModel>,
measure_accidentals: &mut std::collections::BTreeMap<(char, i8), i8>,
) {
let step = pitch.step.to_ascii_uppercase();
let ledger_key = (step, ledger_octave);
if written_accidental.is_none() {
let implied_alter = measure_accidentals
.get(&ledger_key)
.copied()
.unwrap_or_else(|| key_signature_alter(current_key, step));
if implied_alter != pitch.alter
&& let Some(kind) = Accidental::from_alter(pitch.alter)
{
*written_accidental = Some(AccidentalMark {
kind,
explicit: true,
courtesy: false,
source: READER_SPAN,
});
}
}
if written_accidental.is_some() {
measure_accidentals.insert(ledger_key, pitch.alter);
}
}
#[cfg(feature = "musicxml-reader")]
fn key_signature_alter(key: Option<&KeySignatureModel>, step: char) -> i8 {
let Some(key) = key else {
return 0;
};
if let Some(explicit) = key
.explicit_accidentals
.iter()
.rev()
.find(|accidental| accidental.step.eq_ignore_ascii_case(&step))
{
return explicit.accidental.alter();
}
use crate::model::{FLAT_ORDER as FLATS, SHARP_ORDER as SHARPS};
if key.fifths > 0 {
let count = usize::from(key.fifths.unsigned_abs()).min(SHARPS.len());
if SHARPS[..count].contains(&step) {
return 1;
}
} else if key.fifths < 0 {
let count = usize::from(key.fifths.unsigned_abs()).min(FLATS.len());
if FLATS[..count].contains(&step) {
return -1;
}
}
0
}
#[cfg(feature = "musicxml-reader")]
fn move_header_playback_tempo_to_first_voice(score: &mut Score) {
let Some(tempo) = score.metadata.tempo_model.as_ref() else {
return;
};
if tempo.beat_role != TempoBeatRole::PlaybackSoundOnly || tempo.beat.is_none() {
return;
}
let Some(voice) = score
.parts
.first_mut()
.and_then(|part| part.voices.first_mut())
else {
return;
};
let Some(measure) = voice
.measures
.first()
.map(|measure| measure.id)
.or_else(|| voice.events.first().map(|event| event.measure))
else {
return;
};
let tempo = score
.metadata
.tempo_model
.take()
.expect("tempo_model checked above");
voice.events.insert(
0,
TimedEvent {
measure,
onset: Fraction::zero(),
duration: Fraction::zero(),
source: tempo.source_span,
kind: TimedEventKind::TempoChange(tempo),
attachments: EventAttachments::default(),
},
);
}
#[cfg(feature = "musicxml-reader")]
fn reproject_zero_duration_prefixes_for_abc(voice: &mut Voice) {
let mut moves: Vec<EventAttachments> = (0..voice.events.len())
.map(|_| EventAttachments::default())
.collect();
for index in 0..voice.events.len() {
if !zero_duration_direction_event_carries_prefixes(&voice.events[index]) {
continue;
}
let measure = voice.events[index].measure.index;
let Some(target) = next_abc_prefix_anchor(voice, index + 1, measure) else {
continue;
};
let moved = take_abc_prefix_attachments(&mut voice.events[index].attachments);
moves[target].extend(moved);
}
for (index, moved) in moves.into_iter().enumerate() {
if !abc_prefix_attachments_are_empty(&moved) {
prepend_attachments(&mut voice.events[index].attachments, moved);
}
}
}
#[cfg(feature = "musicxml-reader")]
fn zero_duration_direction_event_carries_prefixes(event: &TimedEvent) -> bool {
matches!(
event.kind,
TimedEventKind::TempoChange(_) | TimedEventKind::SectionLabel(_)
) && !abc_prefix_attachments_are_empty(&event.attachments)
}
#[cfg(feature = "musicxml-reader")]
fn next_abc_prefix_anchor(voice: &Voice, start: usize, measure: u32) -> Option<usize> {
for index in start..voice.events.len() {
let event = &voice.events[index];
if event.measure.index != measure {
break;
}
if matches!(
event.kind,
TimedEventKind::Note(_) | TimedEventKind::Rest(_) | TimedEventKind::Chord(_)
) {
return Some(index);
}
}
None
}
#[cfg(feature = "musicxml-reader")]
fn abc_prefix_attachments_are_empty(attachments: &EventAttachments) -> bool {
attachments.chord_symbols.is_empty()
&& attachments.annotations.is_empty()
&& attachments.decorations.is_empty()
}
#[cfg(feature = "musicxml-reader")]
fn take_abc_prefix_attachments(attachments: &mut EventAttachments) -> EventAttachments {
EventAttachments {
chord_symbols: std::mem::take(&mut attachments.chord_symbols),
annotations: std::mem::take(&mut attachments.annotations),
decorations: std::mem::take(&mut attachments.decorations),
..EventAttachments::default()
}
}
#[cfg(feature = "musicxml-reader")]
fn preserve_voice_onset_gaps_for_abc(voice: &mut Voice) {
let mut events = std::mem::take(&mut voice.events).into_iter().peekable();
let mut rebuilt = Vec::new();
for measure in &voice.measures {
let mut cursor = Fraction::zero();
while events
.peek()
.is_some_and(|event| event.measure.index == measure.id.index)
{
let event = events.next().expect("peeked event must exist");
if abc_event_has_position(&event.kind) && cursor.less_than(event.onset) {
let duration = subtract_fraction(event.onset, cursor);
push_musicxml_forward_carrier(&mut rebuilt, event.measure, cursor, duration);
cursor = event.onset;
}
if abc_event_advances_cursor(&event.kind) {
cursor = event.onset.saturating_add(event.duration);
} else if abc_event_has_position(&event.kind) {
cursor = max_fraction(cursor, event.onset);
}
rebuilt.push(event);
}
if cursor.less_than(measure.actual_duration) {
let duration = subtract_fraction(measure.actual_duration, cursor);
push_musicxml_forward_carrier(&mut rebuilt, measure.id, cursor, duration);
}
}
for event in events {
rebuilt.push(event);
}
voice.events = rebuilt;
}
#[cfg(feature = "musicxml-reader")]
fn push_musicxml_forward_carrier(
rebuilt: &mut Vec<TimedEvent>,
measure: MeasureId,
onset: Fraction,
duration: Fraction,
) {
if duration == Fraction::zero() {
return;
}
rebuilt.push(TimedEvent {
measure,
onset,
duration,
source: READER_SPAN,
kind: TimedEventKind::Rest(RestEvent {
visibility: RestVisibility::Invisible,
}),
attachments: EventAttachments {
musicxml_forward: true,
..EventAttachments::default()
},
});
}
#[cfg(feature = "musicxml-reader")]
fn mark_clef_cursor_restore_for_abc(
events: &mut [TimedEvent],
from: Fraction,
to: Fraction,
cursor_restore_base: &mut Option<Fraction>,
) {
if !from.less_than(to) {
return;
}
let base = cursor_restore_base.unwrap_or(to);
let pre_backup = subtract_fraction(base, from);
let cursor_back = subtract_fraction(to, from);
for event in events.iter_mut().rev() {
if abc_event_advances_cursor(&event.kind) {
break;
}
if event.onset != from {
continue;
}
if let TimedEventKind::ClefChange(clef) = &mut event.kind
&& clef.musicxml_cursor_back.is_none()
{
clef.musicxml_cursor_pre_backup = Some(pre_backup);
clef.musicxml_cursor_back = Some(cursor_back);
*cursor_restore_base = Some(to);
break;
}
}
}
#[cfg(feature = "musicxml-reader")]
fn abc_event_has_position(kind: &TimedEventKind) -> bool {
matches!(
kind,
TimedEventKind::Note(_)
| TimedEventKind::Chord(_)
| TimedEventKind::Rest(_)
| TimedEventKind::Spacer
| TimedEventKind::KeyChange(_)
| TimedEventKind::MeterChange(_)
| TimedEventKind::ClefChange(_)
| TimedEventKind::TempoChange(_)
| TimedEventKind::SectionLabel(_)
)
}
#[cfg(feature = "musicxml-reader")]
fn abc_event_advances_cursor(kind: &TimedEventKind) -> bool {
matches!(
kind,
TimedEventKind::Note(_) | TimedEventKind::Chord(_) | TimedEventKind::Rest(_)
)
}
#[cfg(feature = "musicxml-reader")]
fn reproject_grace_slurs_for_abc(voice: &mut Voice) {
for event in &mut voice.events {
for group in event
.attachments
.grace_groups
.iter_mut()
.chain(&mut event.attachments.after_grace_groups)
{
reproject_one_grace_group_slurs(group);
}
}
}
#[cfg(feature = "musicxml-reader")]
fn reproject_one_grace_group_slurs(group: &mut GraceGroupAttachment) {
use std::collections::HashSet;
let internal_stop_ids: HashSet<u32> = group
.events
.iter()
.flat_map(|grace| &grace.slurs)
.filter(|slur| slur.role == SlurRole::Stop)
.map(|slur| slur.pair_id)
.collect();
let internal_start_ids: HashSet<u32> = group
.events
.iter()
.flat_map(|grace| &grace.slurs)
.filter(|slur| slur.role == SlurRole::Start)
.map(|slur| slur.pair_id)
.collect();
let mut has_fully_internal_stop = false;
for grace in &mut group.events {
let mut kept = Vec::with_capacity(grace.slurs.len());
for slur in grace.slurs.drain(..) {
if slur.role == SlurRole::Start && !internal_stop_ids.contains(&slur.pair_id) {
group.slurs.push(slur);
} else {
if slur.role == SlurRole::Stop && internal_start_ids.contains(&slur.pair_id) {
has_fully_internal_stop = true;
}
kept.push(slur);
}
}
grace.slurs = kept;
}
if has_fully_internal_stop && group.span.end == 0 {
group.span = Span { start: 0, end: 1 };
}
}
#[cfg(feature = "musicxml-reader")]
fn renumber_voice_tuplet_pair_ids(voice: &mut Voice) {
use std::collections::HashMap;
let mut remap: HashMap<(u32, u32), u32> = HashMap::new();
let mut next_id: u32 = 0;
fn renumber(
tuplets: &mut [TupletAttachment],
measure_index: u32,
remap: &mut HashMap<(u32, u32), u32>,
next_id: &mut u32,
) {
for tuplet in tuplets {
tuplet.pair_id = *remap
.entry((measure_index, tuplet.pair_id))
.or_insert_with(|| {
let id = *next_id;
*next_id = next_id.saturating_add(1);
id
});
}
}
for event in &mut voice.events {
let measure_index = event.measure.index;
renumber(
&mut event.attachments.tuplets,
measure_index,
&mut remap,
&mut next_id,
);
if let TimedEventKind::Chord(chord) = &mut event.kind {
for member in &mut chord.members {
renumber(
&mut member.attachments.tuplets,
measure_index,
&mut remap,
&mut next_id,
);
}
}
}
}
#[cfg(feature = "musicxml-reader")]
fn synthesize_voice_barline_events(voice: &mut Voice) {
use std::collections::BTreeMap;
let mut events_by_measure: BTreeMap<u32, Vec<TimedEvent>> = BTreeMap::new();
for event in std::mem::take(&mut voice.events) {
events_by_measure
.entry(event.measure.index)
.or_default()
.push(event);
}
let mut rebuilt: Vec<TimedEvent> = Vec::new();
let mut emitted_measures: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
let count = voice.measures.len();
for (position, measure) in voice.measures.iter().enumerate() {
let index = measure.id.index;
emitted_measures.insert(index);
let next_has_leading_barline = voice
.measures
.get(position + 1)
.is_some_and(|next| next.barlines.iter().any(is_leading_reader_barline));
let synthesize_trailing_regular = position + 1 < count
&& !measure.barlines.iter().any(is_trailing_reader_barline)
&& !next_has_leading_barline;
emit_measure_with_barlines(
measure,
measure.id,
events_by_measure.remove(&index).unwrap_or_default(),
synthesize_trailing_regular,
&mut rebuilt,
);
}
for (index, events) in events_by_measure {
if emitted_measures.contains(&index) {
continue;
}
rebuilt.extend(events);
}
voice.events = rebuilt;
}
#[cfg(feature = "musicxml-reader")]
fn emit_measure_with_barlines(
measure: &Measure,
measure_id: MeasureId,
content: Vec<TimedEvent>,
synthesize_trailing_regular: bool,
out: &mut Vec<TimedEvent>,
) {
for barline in &measure.barlines {
if is_leading_reader_barline(barline) {
out.push(barline_event(*barline, measure_id));
}
}
for ending in &measure.repeat_endings {
out.push(TimedEvent {
measure: measure_id,
onset: Fraction::zero(),
duration: Fraction::zero(),
source: ending.span,
kind: TimedEventKind::RepeatEnding(ending.clone()),
attachments: EventAttachments::default(),
});
}
let content_anchors_measure = content.iter().any(|event| {
matches!(
event.kind,
TimedEventKind::Note(_)
| TimedEventKind::Chord(_)
| TimedEventKind::Rest(_)
| TimedEventKind::Spacer
)
});
out.extend(content);
if !content_anchors_measure {
out.push(TimedEvent {
measure: measure_id,
onset: Fraction::zero(),
duration: Fraction::zero(),
source: READER_SPAN,
kind: TimedEventKind::Spacer,
attachments: EventAttachments::default(),
});
}
for close in &measure.repeat_ending_closes {
out.push(TimedEvent {
measure: measure_id,
onset: Fraction::zero(),
duration: Fraction::zero(),
source: close.span,
kind: TimedEventKind::RepeatEndingClose(close.clone()),
attachments: EventAttachments::default(),
});
}
for barline in &measure.barlines {
if is_trailing_reader_barline(barline) {
out.push(barline_event(*barline, measure_id));
}
}
if synthesize_trailing_regular {
out.push(barline_event(
MeasureBarline {
kind: BarlineKind::Regular,
span: Span { start: 1, end: 1 },
},
measure_id,
));
}
}
#[cfg(feature = "musicxml-reader")]
fn is_trailing_reader_barline(barline: &MeasureBarline) -> bool {
!is_leading_reader_barline(barline)
}
#[cfg(feature = "musicxml-reader")]
fn is_leading_reader_barline(barline: &MeasureBarline) -> bool {
barline.span.start == READER_SPAN.start
}
#[cfg(feature = "musicxml-reader")]
fn barline_event(barline: MeasureBarline, measure_id: MeasureId) -> TimedEvent {
TimedEvent {
measure: measure_id,
onset: Fraction::zero(),
duration: Fraction::zero(),
source: barline.span,
kind: TimedEventKind::Barline(barline),
attachments: EventAttachments::default(),
}
}
#[cfg(feature = "musicxml-reader")]
fn key_display_for_abc(key: &KeySignatureModel) -> String {
let tonic = canonical_major_key_display(key.fifths);
if key.explicit_accidentals.is_empty() {
return tonic;
}
let mut display = tonic;
for accidental in &key.explicit_accidentals {
display.push(' ');
display.push_str(accidental.accidental.abc_sign());
display.push(accidental.step.to_ascii_lowercase());
}
display
}
#[cfg(feature = "musicxml-reader")]
fn canonical_major_key_display(fifths: i8) -> String {
let name = match fifths {
-7 => "Cb",
-6 => "Gb",
-5 => "Db",
-4 => "Ab",
-3 => "Eb",
-2 => "Bb",
-1 => "F",
0 => "C",
1 => "G",
2 => "D",
3 => "A",
4 => "E",
5 => "B",
6 => "F#",
7 => "C#",
_ => "C",
};
name.to_owned()
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;