use crate::model::{
AlignedSymbolKind, AnnotationPlacementModel, EventAttachments, Part, TempoBeat, TempoBeatRole,
TempoModel, TextAttachment,
};
use super::notation::{DirectionSymbol, decoration_notation, symbol_direction};
use super::{MeasureSequence, MusicXmlWriter, unsupported_decoration_warning};
impl<'score> MusicXmlWriter<'score> {
pub(crate) fn write_initial_directions(&mut self, is_first_part: bool) {
if !is_first_part {
return;
}
if let Some(tempo_model) = &self.score.metadata.tempo_model {
self.write_tempo_direction(tempo_model);
} else if let Some(tempo) = &self.score.metadata.tempo
&& !tempo.text.trim().is_empty()
{
self.write_direction_words(&tempo.text, None, Some("1"), Some(1));
}
}
pub(crate) fn write_tempo_direction(&mut self, tempo: &TempoModel) {
let beat_unit = (tempo.beat_role == TempoBeatRole::PrintedMetronome)
.then(|| tempo.beat.and_then(beat_unit_model))
.flatten();
if tempo.beat.is_some()
&& tempo.beat_role == TempoBeatRole::PrintedMetronome
&& beat_unit.is_none()
{
if let Some(raw) = &self.score.metadata.tempo {
self.write_direction_words(&raw.text, None, Some("1"), Some(1));
}
return;
}
self.xml.start("direction", &[("placement", "above")]);
if let Some(text) = &tempo.text {
self.xml.start("direction-type", &[]);
self.xml.text_element("words", text);
self.xml.end("direction-type");
}
if let Some(unit) = &beat_unit {
self.xml.start("direction-type", &[]);
self.xml.start("metronome", &[]);
self.xml.text_element("beat-unit", unit.name);
if unit.dotted {
self.xml.empty("beat-unit-dot", &[]);
}
self.xml.text_element(
"per-minute",
&tempo.beat.expect("beat present").bpm.to_string(),
);
self.xml.end("metronome");
self.xml.end("direction-type");
}
if let Some(beat) = tempo.beat {
let sound_tempo = sound_tempo_qpm(beat);
self.xml
.empty("sound", &[("tempo", &format!("{sound_tempo:.2}"))]);
}
self.xml.end("direction");
}
pub(crate) fn write_rehearsal_direction(&mut self, label: &str) {
self.xml.start("direction", &[("placement", "above")]);
self.xml.start("direction-type", &[]);
self.xml
.text_element_attrs("rehearsal", &[("font-weight", "bold")], label);
self.xml.end("direction-type");
self.xml.end("direction");
}
pub(crate) fn write_harmony_and_directions(
&mut self,
attachments: &EventAttachments,
sequence: &MeasureSequence<'score>,
part: &Part,
) {
for symbol in &attachments.chord_symbols {
self.write_chord_symbol(symbol, sequence);
}
for symbol in attachments
.symbols
.iter()
.filter(|symbol| symbol.kind == AlignedSymbolKind::ChordSymbol)
{
self.write_plain_chord_symbol(&symbol.text, sequence);
}
for annotation in &attachments.annotations {
let text = annotation_text(annotation);
self.write_direction_words(
text,
annotation.placement,
Some(sequence.voice_number.as_str()),
Some(sequence.staff.value),
);
}
for symbol in attachments.symbols.iter().filter(|symbol| {
matches!(
symbol.kind,
AlignedSymbolKind::Annotation
| AlignedSymbolKind::Raw
| AlignedSymbolKind::Decoration
)
}) {
self.write_direction_words(
&symbol.text,
None,
Some(sequence.voice_number.as_str()),
Some(sequence.staff.value),
);
}
for decoration in &attachments.decorations {
if let Some(dynamic) = dynamic_decoration(decoration.name.as_str()) {
self.write_dynamic(dynamic, sequence, part);
} else if let Some(direction) = symbol_direction(decoration.name.as_str()) {
self.write_direction_type(direction, sequence, part);
} else if let Some(wedge) = wedge_decoration(decoration.name.as_str()) {
self.write_wedge(wedge, sequence, part);
} else if is_suppressed_decoration(decoration.name.as_str()) {
} else if decoration_notation(decoration).is_none() {
self.diagnostics
.push(unsupported_decoration_warning(decoration));
}
}
}
fn write_dynamic(
&mut self,
dynamic: &'static str,
sequence: &MeasureSequence<'score>,
part: &Part,
) {
self.xml.start("direction", &[("placement", "below")]);
self.xml.start("direction-type", &[]);
self.xml.start("dynamics", &[]);
self.xml.empty(dynamic, &[]);
self.xml.end("dynamics");
self.xml.end("direction-type");
self.xml.text_element("voice", &sequence.voice_number);
if part.staves.len() > 1 {
self.xml
.text_element("staff", &sequence.staff.value.to_string());
}
self.xml.end("direction");
}
fn write_direction_type(
&mut self,
direction: DirectionSymbol,
sequence: &MeasureSequence<'score>,
part: &Part,
) {
self.xml.start("direction", &[("placement", "above")]);
self.xml.start("direction-type", &[]);
match direction {
DirectionSymbol::Coda => self.xml.empty("coda", &[]),
DirectionSymbol::Segno => self.xml.empty("segno", &[]),
}
self.xml.end("direction-type");
self.xml.text_element("voice", &sequence.voice_number);
if part.staves.len() > 1 {
self.xml
.text_element("staff", &sequence.staff.value.to_string());
}
self.xml.end("direction");
}
fn write_wedge(
&mut self,
wedge: &'static str,
sequence: &MeasureSequence<'score>,
part: &Part,
) {
self.xml.start("direction", &[("placement", "below")]);
self.xml.start("direction-type", &[]);
self.xml.empty("wedge", &[("type", wedge)]);
self.xml.end("direction-type");
self.xml.text_element("voice", &sequence.voice_number);
if part.staves.len() > 1 {
self.xml
.text_element("staff", &sequence.staff.value.to_string());
}
self.xml.end("direction");
}
pub(crate) fn write_direction_words(
&mut self,
text: &str,
placement: Option<AnnotationPlacementModel>,
voice: Option<&str>,
staff: Option<u32>,
) {
let placement_attr = placement.map(placement_name);
let attrs = placement_attr.map(|placement| [("placement", placement)]);
let attrs_slice = attrs.as_ref().map_or(&[][..], |attrs| &attrs[..]);
self.xml.start("direction", attrs_slice);
self.xml.start("direction-type", &[]);
self.xml.text_element("words", text);
self.xml.end("direction-type");
if let Some(voice) = voice {
self.xml.text_element("voice", voice);
}
if let Some(staff) = staff
&& staff > 1
{
self.xml.text_element("staff", &staff.to_string());
}
self.xml.end("direction");
}
}
struct BeatUnit {
name: &'static str,
dotted: bool,
}
fn beat_unit_model(beat: TempoBeat) -> Option<BeatUnit> {
let name_for = |denominator: u32| -> Option<&'static str> {
match denominator {
1 => Some("whole"),
2 => Some("half"),
4 => Some("quarter"),
8 => Some("eighth"),
16 => Some("16th"),
32 => Some("32nd"),
64 => Some("64th"),
_ => None,
}
};
match beat.beat_numerator {
1 => name_for(beat.beat_denominator).map(|name| BeatUnit {
name,
dotted: false,
}),
3 if beat.beat_denominator.is_multiple_of(2) => {
name_for(beat.beat_denominator / 2).map(|name| BeatUnit { name, dotted: true })
}
_ => None,
}
}
fn sound_tempo_qpm(beat: TempoBeat) -> f64 {
(f64::from(beat.beat_numerator) / f64::from(beat.beat_denominator)) * 4.0 * f64::from(beat.bpm)
}
fn annotation_text(annotation: &TextAttachment) -> &str {
match annotation.placement {
Some(_) => annotation
.text
.strip_prefix(['^', '_', '<', '>', '@'])
.unwrap_or(&annotation.text),
None => &annotation.text,
}
}
fn placement_name(placement: AnnotationPlacementModel) -> &'static str {
match placement {
AnnotationPlacementModel::Above => "above",
AnnotationPlacementModel::Below => "below",
AnnotationPlacementModel::Left
| AnnotationPlacementModel::Right
| AnnotationPlacementModel::Free => "above",
}
}
fn wedge_decoration(name: &str) -> Option<&'static str> {
match name {
"crescendo(" | "<(" => Some("crescendo"),
"diminuendo(" | ">(" => Some("diminuendo"),
"crescendo)" | "<)" | "diminuendo)" | ">)" => Some("stop"),
_ => None,
}
}
fn dynamic_decoration(name: &str) -> Option<&'static str> {
match name {
"p" => Some("p"),
"pp" => Some("pp"),
"ppp" => Some("ppp"),
"pppp" => Some("pppp"),
"ppppp" => Some("ppppp"),
"pppppp" => Some("pppppp"),
"f" => Some("f"),
"ff" => Some("ff"),
"fff" => Some("fff"),
"ffff" => Some("ffff"),
"fffff" => Some("fffff"),
"ffffff" => Some("ffffff"),
"mp" => Some("mp"),
"mf" => Some("mf"),
"sf" => Some("sf"),
"sfp" => Some("sfp"),
"sfpp" => Some("sfpp"),
"fp" => Some("fp"),
"rf" => Some("rf"),
"rfz" => Some("rfz"),
"sfz" => Some("sfz"),
"sffz" => Some("sffz"),
"fz" => Some("fz"),
"n" => Some("n"),
"pf" => Some("pf"),
"sfzp" => Some("sfzp"),
_ => None,
}
}
fn is_suppressed_decoration(name: &str) -> bool {
matches!(name, "roll")
}