use crate::model::{DecorationAttachment, EventAttachments, SlurRole, TieRole, TupletRole};
use super::engrave::stem;
use super::{
MeasureSequence, MusicXmlWriter, TimeModification, TupletNumbers,
unsupported_duration_diagnostics,
};
impl<'score> MusicXmlWriter<'score> {
pub(crate) fn write_notations(
&mut self,
attachments: &EventAttachments,
time_modification: Option<TimeModification>,
tuplet_numbers: &TupletNumbers,
sequence: &MeasureSequence<'score>,
stem_up: Option<bool>,
) {
let has_tied = !attachments.ties.is_empty();
let has_slurs = !attachments.slurs.is_empty();
let notation_kinds = attachments
.decorations
.iter()
.filter_map(decoration_notation)
.collect::<Vec<_>>();
let has_tuplets = attachments
.tuplets
.iter()
.any(|tuplet| matches!(tuplet.role, TupletRole::Start | TupletRole::Stop));
let has_notation_decorations = notation_kinds.iter().any(NotationKind::emits_own_notation);
if !(has_tied || has_slurs || has_tuplets || has_notation_decorations) {
return;
}
let multivoice = sequence.staff_multivoice;
let voice_slot = sequence.staff_voice_slot;
self.xml.start("notations", &[]);
for tie in &attachments.ties {
let number = tie.pair_id.to_string();
let mut attrs = vec![
(
"type",
match tie.role {
TieRole::Start => "start",
TieRole::Stop => "stop",
},
),
("number", number.as_str()),
];
if tie.dotted {
attrs.push(("line-type", "dotted"));
}
if self.options.tie_orientation {
attrs.push(("orientation", stem::tie_orientation(stem_up, multivoice)));
}
self.xml.empty("tied", &attrs);
}
for slur in &attachments.slurs {
let number = self
.slur_numbers
.number_for(&sequence.slur_voice_key, slur.pair_id, slur.role)
.to_string();
let mut attrs = vec![
(
"type",
match slur.role {
SlurRole::Start => "start",
SlurRole::Stop => "stop",
},
),
("number", number.as_str()),
];
if slur.dotted {
attrs.push(("line-type", "dotted"));
}
if self.options.slur_placement {
attrs.push((
"placement",
stem::slur_placement(stem_up, multivoice, voice_slot),
));
}
self.xml.empty("slur", &attrs);
}
let mut tuplet_displays = notation_kinds.iter().filter_map(|kind| {
if let NotationKind::TupletDisplay(display) = kind {
Some(display)
} else {
None
}
});
for tuplet in &attachments.tuplets {
let Some(tuplet_type) = (match tuplet.role {
TupletRole::Start => Some("start"),
TupletRole::Stop => Some("stop"),
TupletRole::Continue => None,
}) else {
continue;
};
let number = tuplet_numbers.number_for(tuplet.pair_id).to_string();
let display = (tuplet_type == "start")
.then(|| tuplet_displays.next())
.flatten();
let default_bracket = (tuplet_type == "start" && display.is_none())
.then(|| self.engraving_tuplet_bracket(tuplet.pair_id))
.flatten();
self.write_tuplet_notation(tuplet_type, number.as_str(), display, default_bracket);
}
if has_notation_decorations {
let notation_kinds = notation_kinds
.iter()
.filter(|kind| kind.emits_own_notation())
.cloned()
.collect::<Vec<_>>();
let kinds = |want: fn(&NotationKind) -> Option<&'static str>| {
notation_kinds.iter().filter_map(want).collect::<Vec<_>>()
};
for kind in ¬ation_kinds {
if let NotationKind::Spanner {
element,
spanner_type,
line_type,
number,
text,
} = kind
{
let attrs = [
("type", *spanner_type),
("number", number.as_str()),
("line-type", *line_type),
];
if text.is_empty() {
self.xml.empty(element, &attrs);
} else {
self.xml.text_element_attrs(element, &attrs, text);
}
}
}
let ornaments = notation_kinds
.iter()
.filter(|kind| {
matches!(
kind,
NotationKind::Ornament(_)
| NotationKind::Tremolo { .. }
| NotationKind::WavyLine { .. }
)
})
.collect::<Vec<_>>();
if !ornaments.is_empty() {
self.xml.start("ornaments", &[]);
for kind in ornaments {
match kind {
NotationKind::Ornament(name) => self.xml.empty(name, &[]),
NotationKind::WavyLine { wavy_type, number } => self
.xml
.empty("wavy-line", &[("type", wavy_type), ("number", number)]),
NotationKind::Tremolo {
tremolo_type,
marks,
} => {
self.xml
.text_element_attrs("tremolo", &[("type", tremolo_type)], marks)
}
_ => unreachable!("ornaments list only contains ornament notations"),
}
}
self.xml.end("ornaments");
}
let articulations = kinds(|kind| match kind {
NotationKind::Articulation(name) => Some(name),
_ => None,
});
let technical = notation_kinds
.iter()
.filter(|kind| {
matches!(
kind,
NotationKind::Technical(_) | NotationKind::TechnicalText { .. }
)
})
.collect::<Vec<_>>();
if !technical.is_empty() {
self.xml.start("technical", &[]);
for kind in technical {
match kind {
NotationKind::Technical(name) => self.xml.empty(name, &[]),
NotationKind::TechnicalText { name, text } => {
self.xml.text_element(name, text);
}
_ => unreachable!("technical list only contains technical notations"),
}
}
self.xml.end("technical");
}
if !articulations.is_empty() {
self.xml.start("articulations", &[]);
for name in articulations {
self.xml.empty(name, &[]);
}
self.xml.end("articulations");
}
for kind in ¬ation_kinds {
if let NotationKind::Fermata(fermata_type) = kind {
self.xml.empty("fermata", &[("type", fermata_type)]);
}
}
for kind in notation_kinds {
if matches!(kind, NotationKind::Arpeggiate) {
self.xml.empty("arpeggiate", &[]);
}
}
}
if time_modification.is_none() {
self.diagnostics
.extend(unsupported_duration_diagnostics(attachments));
}
self.xml.end("notations");
}
fn engraving_tuplet_bracket(&self, pair_id: u32) -> Option<&'static str> {
if self.options.tuplet_display != crate::options::TupletDisplay::EngravingDefault {
return None;
}
Some(
if self.tuplet_brackets.get(&pair_id).copied().unwrap_or(true) {
"yes"
} else {
"no"
},
)
}
fn write_tuplet_notation(
&mut self,
tuplet_type: &'static str,
number: &str,
display: Option<&TupletDisplay>,
default_bracket: Option<&'static str>,
) {
let mut attrs = vec![("type", tuplet_type)];
if let Some(display) = display {
if let Some(bracket) = display.bracket {
attrs.push(("bracket", bracket));
}
if let (Some(actual), Some(normal)) = (&display.actual, &display.normal) {
self.xml.start("tuplet", &attrs);
self.write_tuplet_display_detail("tuplet-actual", actual);
self.write_tuplet_display_detail("tuplet-normal", normal);
self.xml.end("tuplet");
} else {
self.xml.empty("tuplet", &attrs);
}
} else {
attrs.push(("number", number));
if let Some(bracket) = default_bracket {
attrs.push(("bracket", bracket));
}
self.xml.empty("tuplet", &attrs);
}
}
fn write_tuplet_display_detail(&mut self, element: &'static str, detail: &TupletDisplayDetail) {
self.xml.start(element, &[]);
self.xml.text_element("tuplet-number", &detail.number);
self.xml.text_element("tuplet-type", detail.note_type);
for _ in 0..detail.dots {
self.xml.empty("tuplet-dot", &[]);
}
self.xml.end(element);
}
pub(crate) fn write_time_modification(&mut self, time_modification: TimeModification) {
self.xml.start("time-modification", &[]);
self.xml
.text_element("actual-notes", &time_modification.actual_notes.to_string());
self.xml
.text_element("normal-notes", &time_modification.normal_notes.to_string());
self.xml.end("time-modification");
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NotationKind {
Ornament(&'static str),
Articulation(&'static str),
Technical(&'static str),
TechnicalText { name: &'static str, text: String },
Fermata(&'static str),
Tremolo {
tremolo_type: &'static str,
marks: &'static str,
},
TupletDisplay(TupletDisplay),
Spanner {
element: &'static str,
spanner_type: &'static str,
line_type: &'static str,
number: String,
text: String,
},
WavyLine {
wavy_type: &'static str,
number: String,
},
Arpeggiate,
}
impl NotationKind {
fn emits_own_notation(&self) -> bool {
!matches!(self, NotationKind::TupletDisplay(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TupletDisplay {
bracket: Option<&'static str>,
actual: Option<TupletDisplayDetail>,
normal: Option<TupletDisplayDetail>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TupletDisplayDetail {
number: String,
note_type: &'static str,
dots: u8,
}
pub(crate) fn decoration_notation(decoration: &DecorationAttachment) -> Option<NotationKind> {
Some(match decoration.name.as_str() {
"." | "staccato" => NotationKind::Articulation("staccato"),
">" | "accent" | "emphasis" => NotationKind::Articulation("accent"),
"tenuto" => NotationKind::Articulation("tenuto"),
"wedge" => NotationKind::Articulation("staccatissimo"),
"marcato" => NotationKind::Articulation("strong-accent"),
"breath" => NotationKind::Articulation("breath-mark"),
"caesura" => NotationKind::Articulation("caesura"),
"detached-legato" => NotationKind::Articulation("detached-legato"),
"falloff" => NotationKind::Articulation("falloff"),
"doit" => NotationKind::Articulation("doit"),
"fermata" => NotationKind::Fermata("upright"),
"invertedfermata" => NotationKind::Fermata("inverted"),
"trill" => NotationKind::Ornament("trill-mark"),
"mordent" | "lowermordent" => NotationKind::Ornament("mordent"),
"uppermordent" | "pralltriller" => NotationKind::Ornament("inverted-mordent"),
"turn" => NotationKind::Ornament("turn"),
"invertedturn" => NotationKind::Ornament("inverted-turn"),
"upbow" => NotationKind::Technical("up-bow"),
"downbow" => NotationKind::Technical("down-bow"),
"open" => NotationKind::Technical("open-string"),
"thumb" => NotationKind::Technical("thumb-position"),
"snap" => NotationKind::Technical("snap-pizzicato"),
"+" | "plus" => NotationKind::Technical("stopped"),
"0" => NotationKind::TechnicalText {
name: "fingering",
text: "0".to_owned(),
},
"1" => NotationKind::TechnicalText {
name: "fingering",
text: "1".to_owned(),
},
"2" => NotationKind::TechnicalText {
name: "fingering",
text: "2".to_owned(),
},
"3" => NotationKind::TechnicalText {
name: "fingering",
text: "3".to_owned(),
},
"4" => NotationKind::TechnicalText {
name: "fingering",
text: "4".to_owned(),
},
"5" => NotationKind::TechnicalText {
name: "fingering",
text: "5".to_owned(),
},
"arpeggio" => NotationKind::Arpeggiate,
"slide" => NotationKind::Articulation("scoop"),
_ => {
return tremolo_notation(decoration.name.as_str())
.or_else(|| technical_value_notation(decoration.name.as_str()))
.or_else(|| spanner_notation(decoration.name.as_str()))
.or_else(|| tuplet_display_notation(decoration.name.as_str()));
}
})
}
#[cfg(feature = "musicxml-reader")]
pub(crate) fn tuplet_display_decoration_name(
bracket: Option<&str>,
actual: Option<(&str, &str, usize)>,
normal: Option<(&str, &str, usize)>,
) -> Option<String> {
let bracket = match bracket {
Some(value) => bracket_attr(value)?,
None => "none",
};
let mut name = format!("musicxml-tuplet-detail-v1-b{bracket}");
match (actual, normal) {
(
Some((actual_number, actual_type, actual_dots)),
Some((normal_number, normal_type, normal_dots)),
) => {
let actual_type = note_type_attr(actual_type)?;
let normal_type = note_type_attr(normal_type)?;
if positive_decimal_text(actual_number).is_none()
|| positive_decimal_text(normal_number).is_none()
{
return None;
}
name.push_str(&format!(
"-a{actual_number}-{actual_type}-{actual_dots}-n{normal_number}-{normal_type}-{normal_dots}"
));
Some(name)
}
(None, None) => (bracket != "none").then_some(name),
_ => None,
}
}
fn tuplet_display_notation(name: &str) -> Option<NotationKind> {
let rest = name.strip_prefix("musicxml-tuplet-detail-v1-b")?;
let (bracket, detail) = match rest.split_once("-a") {
Some((bracket, rest)) => {
let (actual, normal) = rest.split_once("-n")?;
(
bracket,
Some((
tuplet_display_detail(actual)?,
tuplet_display_detail(normal)?,
)),
)
}
None => (rest, None),
};
if bracket.contains('-') {
return None;
}
let bracket = match bracket {
"none" => None,
value => Some(bracket_attr(value)?),
};
let (actual, normal) = detail
.map(|(actual, normal)| (Some(actual), Some(normal)))
.unwrap_or((None, None));
if bracket.is_none() && actual.is_none() {
return None;
}
Some(NotationKind::TupletDisplay(TupletDisplay {
bracket,
actual,
normal,
}))
}
fn tuplet_display_detail(text: &str) -> Option<TupletDisplayDetail> {
let mut parts = text.split('-');
let number = positive_decimal_text(parts.next()?)?.to_owned();
let note_type = note_type_attr(parts.next()?)?;
let dots = parts.next()?.parse::<u8>().ok()?;
if parts.next().is_some() {
return None;
}
Some(TupletDisplayDetail {
number,
note_type,
dots,
})
}
fn bracket_attr(value: &str) -> Option<&'static str> {
match value {
"yes" => Some("yes"),
"no" => Some("no"),
_ => None,
}
}
fn note_type_attr(value: &str) -> Option<&'static str> {
match value {
"1024th" => Some("1024th"),
"512th" => Some("512th"),
"256th" => Some("256th"),
"128th" => Some("128th"),
"64th" => Some("64th"),
"32nd" => Some("32nd"),
"16th" => Some("16th"),
"eighth" => Some("eighth"),
"quarter" => Some("quarter"),
"half" => Some("half"),
"whole" => Some("whole"),
"breve" => Some("breve"),
"long" => Some("long"),
"maxima" => Some("maxima"),
_ => None,
}
}
fn spanner_notation(name: &str) -> Option<NotationKind> {
if let Some(rest) = name.strip_prefix("musicxml-glissando-") {
return direct_spanner_notation("glissando", rest);
}
if let Some(rest) = name.strip_prefix("musicxml-slide-") {
return direct_spanner_notation("slide", rest);
}
let rest = name.strip_prefix("musicxml-wavy-line-")?;
let (wavy_type, number) = rest.split_once('-')?;
if number.contains('-') {
return None;
}
Some(NotationKind::WavyLine {
wavy_type: wavy_type_attr(wavy_type)?,
number: decimal_text(number)?.to_owned(),
})
}
fn direct_spanner_notation(element: &'static str, rest: &str) -> Option<NotationKind> {
let (head, text) = match rest.split_once("-hex-") {
Some((head, hex)) => (head, decode_hex_utf8(hex)?),
None => (rest, String::new()),
};
let mut parts = head.split('-');
let spanner_type = direct_spanner_type_attr(parts.next()?)?;
let line_type = line_type_attr(parts.next()?)?;
let number = decimal_text(parts.next()?)?.to_owned();
if parts.next().is_some() {
return None;
}
Some(NotationKind::Spanner {
element,
spanner_type,
line_type,
number,
text,
})
}
fn direct_spanner_type_attr(value: &str) -> Option<&'static str> {
match value {
"start" => Some("start"),
"stop" => Some("stop"),
_ => None,
}
}
fn wavy_type_attr(value: &str) -> Option<&'static str> {
match value {
"start" => Some("start"),
"stop" => Some("stop"),
"continue" => Some("continue"),
_ => None,
}
}
fn line_type_attr(value: &str) -> Option<&'static str> {
match value {
"solid" => Some("solid"),
"dashed" => Some("dashed"),
"dotted" => Some("dotted"),
"wavy" => Some("wavy"),
_ => None,
}
}
fn decimal_text(text: &str) -> Option<&str> {
(!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())).then_some(text)
}
fn positive_decimal_text(text: &str) -> Option<&str> {
decimal_text(text).filter(|text| text.parse::<u32>().is_ok_and(|value| value > 0))
}
fn technical_value_notation(name: &str) -> Option<NotationKind> {
if let Some(text) = name.strip_prefix("musicxml-tech-string-") {
return decimal_technical_text("string", text);
}
if let Some(text) = name.strip_prefix("musicxml-tech-fret-") {
return decimal_technical_text("fret", text);
}
let hex = name.strip_prefix("musicxml-tech-fingering-hex-")?;
let text = decode_hex_utf8(hex)?;
(!text.is_empty()).then_some(NotationKind::TechnicalText {
name: "fingering",
text,
})
}
fn decimal_technical_text(name: &'static str, text: &str) -> Option<NotationKind> {
decimal_text(text).map(|text| NotationKind::TechnicalText {
name,
text: text.to_owned(),
})
}
fn decode_hex_utf8(hex: &str) -> Option<String> {
crate::hex::decode_hex_utf8(hex).filter(|text| !text.is_empty())
}
fn tremolo_notation(name: &str) -> Option<NotationKind> {
let rest = name.strip_prefix("musicxml-tremolo-")?;
let mut parts = rest.split('-');
let tremolo_type = match parts.next()? {
"single" => "single",
"start" => "start",
"stop" => "stop",
_ => return None,
};
let marks = match parts.next()? {
"0" => "0",
"1" => "1",
"2" => "2",
"3" => "3",
"4" => "4",
"5" => "5",
"6" => "6",
"7" => "7",
"8" => "8",
_ => return None,
};
Some(NotationKind::Tremolo {
tremolo_type,
marks,
})
}
pub(crate) fn tremolo_time_modification(name: &str) -> Option<TimeModification> {
let rest = name.strip_prefix("musicxml-tremolo-")?;
let mut parts = rest.split('-');
parts.next()?; parts.next()?; if parts.next()? != "tm" {
return None;
}
let actual_notes = parts.next()?.parse::<u32>().ok()?;
let normal_notes = parts.next()?.parse::<u32>().ok()?;
if parts.next().is_some() || actual_notes == 0 || normal_notes == 0 {
return None;
}
Some(TimeModification {
actual_notes,
normal_notes,
})
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum DirectionSymbol {
Coda,
Segno,
}
pub(crate) fn symbol_direction(name: &str) -> Option<DirectionSymbol> {
match name.to_ascii_lowercase().as_str() {
"coda" => Some(DirectionSymbol::Coda),
"segno" => Some(DirectionSymbol::Segno),
_ => None,
}
}