use crate::model::{DecorationAttachment, EventAttachments, SlurRole, TieRole, TupletRole};
use super::{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,
slur_voice_key: &str,
) {
let has_tied = !attachments.ties.is_empty();
let has_slurs = !attachments.slurs.is_empty();
let has_tuplets = attachments
.tuplets
.iter()
.any(|tuplet| matches!(tuplet.role, TupletRole::Start | TupletRole::Stop));
let has_notation_decorations = attachments
.decorations
.iter()
.any(|decoration| decoration_notation(decoration).is_some());
if !(has_tied || has_slurs || has_tuplets || has_notation_decorations) {
return;
}
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"));
}
self.xml.empty("tied", &attrs);
}
for slur in &attachments.slurs {
let number = self
.slur_numbers
.number_for(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"));
}
self.xml.empty("slur", &attrs);
}
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();
self.xml.empty(
"tuplet",
&[("type", tuplet_type), ("number", number.as_str())],
);
}
if has_notation_decorations {
let notation_kinds = attachments
.decorations
.iter()
.filter_map(decoration_notation)
.collect::<Vec<_>>();
let kinds = |want: fn(NotationKind) -> Option<&'static str>| {
notation_kinds
.iter()
.copied()
.filter_map(want)
.collect::<Vec<_>>()
};
let ornaments = kinds(|kind| match kind {
NotationKind::Ornament(name) => Some(name),
_ => None,
});
if !ornaments.is_empty() {
self.xml.start("ornaments", &[]);
for name in ornaments {
self.xml.empty(name, &[]);
}
self.xml.end("ornaments");
}
let technical = notation_kinds
.iter()
.copied()
.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");
}
let articulations = kinds(|kind| match kind {
NotationKind::Articulation(name) => Some(name),
_ => None,
});
if !articulations.is_empty() {
self.xml.start("articulations", &[]);
for name in articulations {
self.xml.empty(name, &[]);
}
self.xml.end("articulations");
}
for kind in notation_kinds.iter().copied() {
if let NotationKind::Fermata(fermata_type) = kind {
self.xml.empty("fermata", &[("type", fermata_type)]);
}
}
for kind in notation_kinds {
if kind == NotationKind::Arpeggiate {
self.xml.empty("arpeggiate", &[]);
}
}
}
if time_modification.is_none() {
self.diagnostics
.extend(unsupported_duration_diagnostics(attachments));
}
self.xml.end("notations");
}
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, Copy, PartialEq, Eq)]
pub(crate) enum NotationKind {
Ornament(&'static str),
Articulation(&'static str),
Technical(&'static str),
TechnicalText {
name: &'static str,
text: &'static str,
},
Fermata(&'static str),
Arpeggiate,
}
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"),
"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",
},
"1" => NotationKind::TechnicalText {
name: "fingering",
text: "1",
},
"2" => NotationKind::TechnicalText {
name: "fingering",
text: "2",
},
"3" => NotationKind::TechnicalText {
name: "fingering",
text: "3",
},
"4" => NotationKind::TechnicalText {
name: "fingering",
text: "4",
},
"5" => NotationKind::TechnicalText {
name: "fingering",
text: "5",
},
"arpeggio" => NotationKind::Arpeggiate,
"slide" => NotationKind::Articulation("scoop"),
_ => return None,
})
}
#[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,
}
}