use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use crate::model::{
BarlineKind, Fraction, Measure, MeasureBarline, MeasureId, MidiInstrumentModel, Part,
RepeatEndingCloseLocation, RepeatEndingCloseType, RepeatEndingPartModel, Score, TimedEventKind,
TimelineEventKind,
};
use super::{
BarlineLocation, EndingType, MeasureSequence, MusicXmlWriter, SequenceEvent,
attributes::initial_key_for_part, barline::EndingDisplay,
};
impl<'score> MusicXmlWriter<'score> {
pub(crate) fn write_credits(&mut self) {
for line in &self.score.metadata.post_tune_lyrics {
if line.text.trim().is_empty() {
continue;
}
self.xml.start("credit", &[("page", "1")]);
self.xml.text_element("credit-words", &line.text);
self.xml.end("credit");
}
}
pub(crate) fn write_metadata(&mut self) {
if let Some(title) = &self.score.metadata.title {
self.xml.start("work", &[]);
self.xml.text_element("work-title", &title.text);
self.xml.end("work");
}
if !self.score.metadata.composers.is_empty() {
self.xml.start("identification", &[]);
for composer in &self.score.metadata.composers {
self.xml
.text_element_attrs("creator", &[("type", "composer")], &composer.text);
}
self.xml.end("identification");
}
}
pub(crate) fn write_part_list(&mut self) {
self.xml.start("part-list", &[]);
for (index, part) in self.score.parts.iter().enumerate() {
let id = part_xml_id(part, index);
self.xml.start("score-part", &[("id", id.as_str())]);
self.xml
.text_element("part-name", part_name(part, self.score).as_str());
self.write_part_instruments(part, &id);
self.xml.end("score-part");
}
self.xml.end("part-list");
}
fn write_part_instruments(&mut self, part: &Part, part_id: &str) {
let fallback_name = part_name(part, self.score);
let instruments: Vec<(String, String, Option<MidiInstrumentModel>)> =
if part.instruments.is_empty() {
part.voices
.iter()
.filter_map(|voice| {
let midi = voice.midi_instrument?;
midi.has_content().then_some((voice, midi))
})
.enumerate()
.map(|(seq, (voice, midi))| {
let name = voice_instrument_name(voice).unwrap_or_else(|| {
midi.program.map_or_else(
|| fallback_name.clone(),
|program| gm_program_name(program).to_owned(),
)
});
(format!("{part_id}-I{}", seq + 1), name, Some(midi))
})
.collect()
} else {
part.instruments
.iter()
.filter(|instrument| !instrument.id.trim().is_empty())
.map(|instrument| {
let name = instrument
.name
.as_ref()
.map(|name| name.text.clone())
.or_else(|| {
instrument
.midi
.and_then(|midi| midi.program)
.map(|program| gm_program_name(program).to_owned())
})
.unwrap_or_else(|| fallback_name.clone());
(instrument.id.clone(), name, instrument.midi)
})
.collect()
};
if instruments.is_empty() {
return;
}
for (instrument_id, name, _) in &instruments {
self.xml
.start("score-instrument", &[("id", instrument_id.as_str())]);
self.xml.text_element("instrument-name", name);
self.xml.end("score-instrument");
}
for (instrument_id, _, midi) in &instruments {
let Some(midi) = midi.as_ref().filter(|midi| midi.has_content()) else {
continue;
};
self.xml
.start("midi-instrument", &[("id", instrument_id.as_str())]);
if let Some(channel) = midi.channel {
self.xml.text_element("midi-channel", &channel.to_string());
}
if let Some(program) = midi.program {
self.xml
.text_element("midi-program", &(u16::from(program) + 1).to_string());
}
if let Some(unpitched) = midi.midi_unpitched {
self.xml
.text_element("midi-unpitched", &unpitched.to_string());
}
if let Some(volume) = midi.volume_cc {
self.xml
.text_element("volume", &format!("{:.2}", f64::from(volume) / 1.27));
}
if let Some(pan) = midi.pan_cc {
self.xml.text_element("pan", &musicxml_pan_from_cc(pan));
}
self.xml.end("midi-instrument");
}
}
pub(crate) fn write_part(&mut self, part: &'score Part, part_index: usize) {
let id = part_xml_id(part, part_index);
self.active_key = initial_key_for_part(part)
.cloned()
.or_else(|| self.score.metadata.key.clone());
self.slur_numbers = Default::default();
self.lyric_hyphen_open.clear();
self.xml.start("part", &[("id", id.as_str())]);
let mut pending_left_repeat = false;
let measure_ids = part_measure_ids(part);
let voice_numbers = part_voice_numbers(part);
let overlay_voice_numbers = overlay_voice_numbers(part, &voice_numbers);
let ending_stops = ending_stop_schedule(part, &measure_ids);
for (measure_position, measure_id) in measure_ids.iter().enumerate() {
let measure_refs = part_measure_refs(part, *measure_id);
let number = measure_display_number(&measure_refs, *measure_id);
self.xml.start("measure", &[("number", number.as_str())]);
if measure_position == 0 {
self.write_attributes(part);
self.write_initial_directions(part_index == 0);
}
let left_barlines = unique_barlines(&measure_refs, true);
let endings = unique_endings(&measure_refs);
let has_left_repeat = pending_left_repeat
|| left_barlines
.iter()
.any(|barline| barline.kind == BarlineKind::RepeatStart);
pending_left_repeat = false;
if has_left_repeat && !endings.is_empty() {
self.write_ending_barline(
BarlineLocation::Left,
&endings,
EndingType::Start,
Some(BarlineKind::RepeatStart),
);
} else {
if has_left_repeat {
self.write_barline(BarlineLocation::Left, BarlineKind::RepeatStart, &[]);
}
if !endings.is_empty() {
self.write_ending_barline(
BarlineLocation::Left,
&endings,
EndingType::Start,
None,
);
}
}
let explicit_closes = unique_ending_closes(&measure_refs);
let explicit_left_closes = explicit_closes
.iter()
.filter(|close| close.location == RepeatEndingCloseLocation::Left)
.cloned()
.collect::<Vec<_>>();
if !explicit_left_closes.is_empty() {
self.write_ending_close_barlines(
BarlineLocation::Left,
&explicit_left_closes,
None,
);
}
if let Some(count) = unique_multiple_rest(&measure_refs) {
self.write_multiple_rest_measure_style(count);
}
let sequences =
measure_sequences(part, *measure_id, &voice_numbers, &overlay_voice_numbers);
for (sequence_index, sequence) in sequences.iter().enumerate() {
let cursor = self.write_sequence(sequence, part);
if let Some(next_sequence) = sequences.get(sequence_index + 1) {
let backup = next_sequence.musicxml_sequence_backup.unwrap_or(cursor);
if backup != Fraction::zero() {
self.write_backup(backup);
}
}
}
let explicit_right_closes = explicit_closes
.iter()
.filter(|close| close.location == RepeatEndingCloseLocation::Right)
.cloned()
.collect::<Vec<_>>();
let mut explicit_closes_pending =
(!explicit_right_closes.is_empty()).then_some(&explicit_right_closes);
let mut stop_endings = if explicit_closes_pending.is_some() {
None
} else {
ending_stops[measure_position].as_deref()
};
let right_barlines = unique_barlines(&measure_refs, false);
for barline in &right_barlines {
if let Some(closes) = explicit_closes_pending.take() {
self.write_ending_close_barlines(
BarlineLocation::Right,
closes,
Some(barline.kind),
);
} else if let Some(stops) = stop_endings.take() {
self.write_ending_barline(
BarlineLocation::Right,
stops,
EndingType::Stop,
Some(barline.kind),
);
} else {
self.write_barline(BarlineLocation::Right, barline.kind, &[]);
}
if barline.kind == BarlineKind::RepeatBoth {
pending_left_repeat = true;
}
}
if let Some(closes) = explicit_closes_pending.take() {
self.write_ending_close_barlines(BarlineLocation::Right, closes, None);
} else if let Some(stops) = stop_endings.take() {
self.write_ending_barline(BarlineLocation::Right, stops, EndingType::Stop, None);
}
if trailing_left_repeat_pending(&measure_refs) {
pending_left_repeat = true;
}
self.xml.end("measure");
}
self.xml.end("part");
}
fn write_ending_close_barlines(
&mut self,
location: BarlineLocation,
closes: &[EndingCloseDisplay],
repeat_kind: Option<BarlineKind>,
) {
let mut grouped: BTreeMap<EndingType, Vec<EndingDisplay>> = BTreeMap::new();
for close in closes {
grouped
.entry(close.kind)
.or_default()
.push(close.ending.clone());
}
let mut first = true;
for (kind, endings) in grouped {
self.write_ending_barline(
location,
&endings,
kind,
first.then_some(repeat_kind).flatten(),
);
first = false;
}
}
}
fn part_xml_id(part: &Part, index: usize) -> String {
let value = part.id.value.trim();
let fallback = format!("P{}", index + 1);
let raw = if value.is_empty() {
fallback.as_str()
} else {
value
};
let mut id = raw
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
ch
} else {
'_'
}
})
.collect::<String>();
if id
.chars()
.next()
.is_none_or(|ch| !ch.is_ascii_alphabetic() && ch != '_')
{
id.insert(0, 'P');
}
id
}
fn part_name(part: &Part, score: &Score) -> String {
part.name
.as_ref()
.or(score.metadata.title.as_ref())
.map(|line| line.text.clone())
.or_else(|| {
part.voices.iter().find_map(|voice| {
voice
.properties
.name
.as_ref()
.or(voice.properties.nm.as_ref())
.map(|line| line.text.clone())
})
})
.unwrap_or_else(|| "Music".to_owned())
}
fn musicxml_pan_from_cc(pan: u8) -> String {
if pan == 64 {
"0.00".to_owned()
} else {
format!("{:.2}", f64::from(pan) / 127.0 * 180.0 - 90.0)
}
}
fn voice_instrument_name(voice: &crate::model::Voice) -> Option<String> {
voice
.properties
.nm
.as_ref()
.or(voice.properties.name.as_ref())
.map(|line| line.text.clone())
}
fn gm_program_name(program: u8) -> &'static str {
GM_PROGRAM_NAMES
.get(program as usize)
.copied()
.unwrap_or(GM_PROGRAM_NAMES[0])
}
const GM_PROGRAM_NAMES: [&str; 128] = [
"acoustic_grand_piano",
"bright_acoustic_piano",
"electric_grand_piano",
"honkytonk_piano",
"electric_piano_1",
"electric_piano_2",
"harpsichord",
"clavinet",
"celesta",
"glockenspiel",
"music_box",
"vibraphone",
"marimba",
"xylophone",
"tubular_bells",
"dulcimer",
"drawbar_organ",
"percussive_organ",
"rock_organ",
"church_organ",
"reed_organ",
"accordion",
"harmonica",
"tango_accordion",
"acoustic_guitar_nylon",
"acoustic_guitar_steel",
"electric_guitar_jazz",
"electric_guitar_clean",
"electric_guitar_muted",
"overdriven_guitar",
"distortion_guitar",
"guitar_harmonics",
"acoustic_bass",
"electric_bass_finger",
"electric_bass_pick",
"fretless_bass",
"slap_bass_1",
"slap_bass_2",
"synth_bass_1",
"synth_bass_2",
"violin",
"viola",
"cello",
"contrabass",
"tremolo_strings",
"pizzicato_strings",
"orchestral_harp",
"timpani",
"string_ensemble_1",
"string_ensemble_2",
"synth_strings_1",
"synth_strings_2",
"choir_aahs",
"voice_oohs",
"synth_choir",
"orchestra_hit",
"trumpet",
"trombone",
"tuba",
"muted_trumpet",
"french_horn",
"brass_section",
"synth_brass_1",
"synth_brass_2",
"soprano_sax",
"alto_sax",
"tenor_sax",
"baritone_sax",
"oboe",
"english_horn",
"bassoon",
"clarinet",
"piccolo",
"flute",
"recorder",
"pan_flute",
"blown_bottle",
"shakuhachi",
"whistle",
"ocarina",
"lead_1_square",
"lead_2_sawtooth",
"lead_3_calliope",
"lead_4_chiff",
"lead_5_charang",
"lead_6_voice",
"lead_7_fifths",
"lead_8_bass__lead",
"pad_1_new_age",
"pad_2_warm",
"pad_3_polysynth",
"pad_4_choir",
"pad_5_bowed",
"pad_6_metallic",
"pad_7_halo",
"pad_8_sweep",
"fx_1_rain",
"fx_2_soundtrack",
"fx_3_crystal",
"fx_4_atmosphere",
"fx_5_brightness",
"fx_6_goblins",
"fx_7_echoes",
"fx_8_scifi",
"sitar",
"banjo",
"shamisen",
"koto",
"kalimba",
"bagpipe",
"fiddle",
"shanai",
"tinkle_bell",
"agogo",
"steel_drums",
"woodblock",
"taiko_drum",
"melodic_tom",
"synth_drum",
"reverse_cymbal",
"guitar_fret_noise",
"breath_noise",
"seashore",
"bird_tweet",
"telephone_ring",
"helicopter",
"applause",
"gunshot",
];
fn part_measure_ids(part: &Part) -> Vec<MeasureId> {
let mut ids = part
.voices
.iter()
.flat_map(|voice| voice.measures.iter().map(|measure| measure.id))
.collect::<Vec<_>>();
ids.sort_by_key(|id| (id.index, id.number));
ids.dedup();
ids
}
fn part_measure_refs(part: &Part, id: MeasureId) -> Vec<&Measure> {
part.voices
.iter()
.flat_map(|voice| &voice.measures)
.filter(|measure| measure.id == id)
.collect()
}
fn measure_display_number(measures: &[&Measure], id: MeasureId) -> String {
measures
.iter()
.find_map(|measure| measure.display_number.as_deref())
.filter(|number| !number.trim().is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| id.number.to_string())
}
fn measure_sequences<'score>(
part: &'score Part,
id: MeasureId,
voice_numbers: &[String],
overlay_voice_numbers: &BTreeMap<(usize, usize), String>,
) -> Vec<MeasureSequence<'score>> {
let mut sequences = Vec::new();
for (voice_index, voice) in part.voices.iter().enumerate() {
let voice_number = voice_numbers
.get(voice_index)
.cloned()
.unwrap_or_else(|| (voice_index + 1).to_string());
let slur_voice_key = voice.id.value.clone();
let events = voice
.events
.iter()
.filter(|event| event.measure == id)
.filter(|event| {
matches!(
event.kind,
TimedEventKind::Note(_)
| TimedEventKind::Chord(_)
| TimedEventKind::Rest(_)
| TimedEventKind::Spacer
| TimedEventKind::KeyChange(_)
| TimedEventKind::MeterChange(_)
| TimedEventKind::ClefChange(_)
| TimedEventKind::TempoChange(_)
| TimedEventKind::SectionLabel(_)
)
})
.map(SequenceEvent::Timed)
.collect::<Vec<_>>();
if events.iter().any(SequenceEvent::emits_musicxml) {
let measure = voice.measures.iter().find(|measure| measure.id == id);
sequences.push(MeasureSequence {
voice_number,
slur_voice_key: slur_voice_key.clone(),
staff: voice.staff,
expected_duration: measure.and_then(|measure| measure.expected_duration),
actual_duration: measure
.map(|measure| measure.actual_duration)
.unwrap_or_else(Fraction::zero),
unpitched: voice
.midi_instrument
.and_then(|midi| midi.midi_unpitched)
.is_some(),
musicxml_sequence_backup: events
.iter()
.find_map(|event| event.attachments().musicxml_sequence_backup),
events,
});
}
if let Some(measure) = voice.measures.iter().find(|measure| measure.id == id) {
for (overlay_index, overlay) in measure.overlays.iter().enumerate() {
let overlay_events = overlay
.events
.iter()
.filter(|event| {
matches!(
event.kind,
TimelineEventKind::Note { .. }
| TimelineEventKind::Rest { .. }
| TimelineEventKind::Spacer
)
})
.map(SequenceEvent::Overlay)
.collect::<Vec<_>>();
if !overlay_events.iter().any(SequenceEvent::emits_musicxml) {
continue;
}
sequences.push(MeasureSequence {
voice_number: overlay_voice_numbers
.get(&(voice_index, overlay_index))
.cloned()
.unwrap_or_else(|| (part.voices.len() + overlay_index + 1).to_string()),
slur_voice_key: slur_voice_key.clone(),
staff: voice.staff,
expected_duration: Some(overlay.expected_duration),
actual_duration: overlay.actual_duration,
unpitched: voice
.midi_instrument
.and_then(|midi| midi.midi_unpitched)
.is_some(),
musicxml_sequence_backup: overlay_events
.iter()
.find_map(|event| event.attachments().musicxml_sequence_backup),
events: overlay_events,
});
}
}
}
for sequence in &mut sequences {
sequence.events.sort_by(|left, right| {
compare_fraction(left.onset(), right.onset())
.then_with(|| left.source_start().cmp(&right.source_start()))
});
}
sequences
}
fn part_voice_numbers(part: &Part) -> Vec<String> {
let mut used = BTreeSet::new();
let mut next = 1u32;
part.voices
.iter()
.map(|voice| {
if let Some(number) = reader_style_voice_number(&part.id.value, &voice.id.value)
.filter(|number| !used.contains(number))
{
used.insert(number.clone());
return number;
}
let number = next_free_voice_number(&used, &mut next);
used.insert(number.clone());
number
})
.collect()
}
fn reader_style_voice_number(part_id: &str, voice_id: &str) -> Option<String> {
if voice_id == part_id {
return Some("1".to_owned());
}
let prefix = format!("{part_id}#");
let suffix = voice_id.strip_prefix(&prefix)?.trim();
(!suffix.is_empty()).then(|| suffix.to_owned())
}
fn next_free_voice_number(used: &BTreeSet<String>, next: &mut u32) -> String {
loop {
let number = next.to_string();
*next += 1;
if !used.contains(&number) {
return number;
}
}
}
fn overlay_voice_numbers(
part: &Part,
base_voice_numbers: &[String],
) -> BTreeMap<(usize, usize), String> {
let mut numbers = BTreeMap::new();
let mut used = base_voice_numbers.iter().cloned().collect::<BTreeSet<_>>();
let mut next = 1u32;
for (voice_index, voice) in part.voices.iter().enumerate() {
for measure in &voice.measures {
for (overlay_index, overlay) in measure.overlays.iter().enumerate() {
if overlay.events.is_empty() {
continue;
}
numbers
.entry((voice_index, overlay_index))
.or_insert_with(|| {
let number = next_free_voice_number(&used, &mut next);
used.insert(number.clone());
number
});
}
}
}
numbers
}
fn compare_fraction(left: Fraction, right: Fraction) -> Ordering {
(u64::from(left.numerator) * u64::from(right.denominator))
.cmp(&(u64::from(right.numerator) * u64::from(left.denominator)))
}
fn unique_barlines(measures: &[&Measure], left: bool) -> Vec<MeasureBarline> {
let mut barlines = measures
.iter()
.flat_map(|measure| measure.barlines.iter().map(|barline| (*measure, barline)))
.filter(|(measure, barline)| {
let leading = is_leading_barline(measure, barline);
matches!(
(left, leading, barline.kind),
(true, true, BarlineKind::RepeatStart)
| (
false,
false,
BarlineKind::Double
| BarlineKind::Final
| BarlineKind::Initial
| BarlineKind::RepeatEnd
| BarlineKind::RepeatBoth
| BarlineKind::Dotted
| BarlineKind::Dashed
| BarlineKind::Invisible
)
)
})
.map(|(_, barline)| *barline)
.collect::<Vec<_>>();
barlines.sort_by_key(|barline| (barline.span.start, barline.span.end, barline.kind as u8));
barlines.dedup_by_key(|barline| (barline.kind, barline.span));
barlines
}
fn is_leading_barline(measure: &Measure, barline: &MeasureBarline) -> bool {
measure.source_span.start == barline.span.start
}
fn trailing_left_repeat_pending(measures: &[&Measure]) -> bool {
measures.iter().any(|measure| {
measure.barlines.iter().any(|barline| {
barline.kind == BarlineKind::RepeatStart && !is_leading_barline(measure, barline)
})
})
}
fn ending_stop_schedule(part: &Part, measure_ids: &[MeasureId]) -> Vec<Option<Vec<EndingDisplay>>> {
let mut stops: Vec<Option<Vec<EndingDisplay>>> = vec![None; measure_ids.len()];
let mut open: Option<Vec<EndingDisplay>> = None;
for (position, measure_id) in measure_ids.iter().enumerate() {
let measure_refs = part_measure_refs(part, *measure_id);
if position > 0
&& open.is_some()
&& unique_barlines(&measure_refs, true)
.iter()
.any(|barline| barline.kind == BarlineKind::RepeatStart)
{
stops[position - 1] = open.take();
}
let starts = unique_endings(&measure_refs);
if !starts.is_empty() {
if let Some(open_endings) = open.take()
&& position > 0
{
stops[position - 1] = Some(open_endings);
}
open = Some(starts);
}
if open.is_some() && !unique_ending_closes(&measure_refs).is_empty() {
open.take();
continue;
}
if open.is_some()
&& (unique_barlines(&measure_refs, false)
.iter()
.any(|barline| stops_repeat_ending_barline(barline.kind))
|| trailing_left_repeat_pending(&measure_refs))
{
stops[position] = open.take();
}
}
stops
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct EndingCloseDisplay {
location: RepeatEndingCloseLocation,
kind: EndingType,
ending: EndingDisplay,
}
fn unique_ending_closes(measures: &[&Measure]) -> Vec<EndingCloseDisplay> {
let mut closes = measures
.iter()
.flat_map(|measure| &measure.repeat_ending_closes)
.map(|close| EndingCloseDisplay {
location: close.location,
kind: match close.close_type {
RepeatEndingCloseType::Stop => EndingType::Stop,
RepeatEndingCloseType::Discontinue => EndingType::Discontinue,
},
ending: ending_display_from_parts(&close.endings),
})
.collect::<Vec<_>>();
closes.sort();
closes.dedup();
closes
}
fn stops_repeat_ending_barline(kind: BarlineKind) -> bool {
matches!(
kind,
BarlineKind::Double
| BarlineKind::Final
| BarlineKind::RepeatEnd
| BarlineKind::RepeatBoth
| BarlineKind::Initial
)
}
fn ending_display(ending: &crate::model::RepeatEndingModel) -> EndingDisplay {
ending_display_from_parts(&ending.endings)
}
fn ending_display_from_parts(parts: &[RepeatEndingPartModel]) -> EndingDisplay {
let text = parts.iter().find_map(|part| match part {
crate::model::RepeatEndingPartModel::Text(text) => Some(text.clone()),
_ => None,
});
if let Some(text) = text {
return EndingDisplay {
number: "33".to_owned(),
text: Some(text),
};
}
let number = parts
.iter()
.map(|part| match part {
crate::model::RepeatEndingPartModel::Single(number) => number.to_string(),
crate::model::RepeatEndingPartModel::Range { start, end } => {
format!("{start}-{end}")
}
crate::model::RepeatEndingPartModel::Text(_) => unreachable!(),
})
.collect::<Vec<_>>()
.join(",");
EndingDisplay { number, text: None }
}
fn unique_endings(measures: &[&Measure]) -> Vec<EndingDisplay> {
let mut endings = measures
.iter()
.flat_map(|measure| &measure.repeat_endings)
.map(ending_display)
.collect::<Vec<_>>();
endings.sort();
endings.dedup();
endings
}
fn unique_multiple_rest(measures: &[&Measure]) -> Option<u32> {
measures
.iter()
.filter_map(|measure| measure.multiple_rest)
.find(|count| *count > 1)
}