use crate::musicxml::attributes::clef_model;
const TREBLE_MIDDLE: i32 = 41;
const STEP_LETTERS: [char; 7] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
pub(crate) fn abs_step(step: char, octave: i8) -> i32 {
let index = STEP_LETTERS
.iter()
.position(|&letter| letter == step.to_ascii_uppercase())
.unwrap_or(0);
7 * (i32::from(octave) + 1) + index as i32
}
fn middle_step_for(sign: &str, line: &str) -> i32 {
let (reference, standard_line) = match sign {
"G" => (39, 2),
"F" => (31, 4),
"C" => (35, 3),
_ => return TREBLE_MIDDLE,
};
let line = match line {
"1" => 1,
"2" => 2,
"3" => 3,
"4" => 4,
"5" => 5,
_ => standard_line,
};
reference + 2 * (3 - line)
}
fn middle_step(clef_text: Option<&str>) -> i32 {
let model = clef_model(clef_text);
middle_step_for(model.sign, model.line)
}
pub(crate) fn middle_distance(clef_text: Option<&str>, step: char, octave: i8) -> i32 {
middle_step(clef_text) - abs_step(step, octave)
}
pub(crate) fn note_type_has_stem(note_type: &str) -> bool {
matches!(
note_type,
"half"
| "quarter"
| "eighth"
| "16th"
| "32nd"
| "64th"
| "128th"
| "256th"
| "512th"
| "1024th"
)
}
pub(crate) fn stem_up(
own_distances: &[i32],
beam_distances: Option<&[i32]>,
multivoice: bool,
voice_slot: usize,
) -> bool {
if multivoice {
return voice_slot.is_multiple_of(2);
}
let distances = match beam_distances {
Some(group) if !group.is_empty() => group,
_ => own_distances,
};
let (Some(lowest), Some(highest)) = (
distances.iter().map(|&d| i64::from(d)).max(),
distances.iter().map(|&d| i64::from(d)).min(),
) else {
return false;
};
let below = lowest.max(0);
let above = (-highest).max(0);
if below != above {
return below > above;
}
let below_count = distances.iter().filter(|&&d| d > 0).count();
let above_count = distances.iter().filter(|&&d| d < 0).count();
below_count > above_count
}
pub(crate) fn rest_display(clef_text: Option<&str>, voice_slot: usize) -> (char, i8) {
let middle = middle_step(clef_text);
let target = if voice_slot.is_multiple_of(2) {
middle + 4
} else {
middle - 4
};
let octave = target.div_euclid(7) - 1;
let index = usize::try_from(target.rem_euclid(7)).unwrap_or(0);
let letter = STEP_LETTERS.get(index).copied().unwrap_or('C');
let octave = i8::try_from(octave).unwrap_or(if octave < 0 { i8::MIN } else { i8::MAX });
(letter, octave)
}
pub(crate) fn slur_placement(
stem_up: Option<bool>,
multivoice: bool,
voice_slot: usize,
) -> &'static str {
if multivoice {
return if voice_slot.is_multiple_of(2) {
"above"
} else {
"below"
};
}
match stem_up {
Some(true) => "below",
Some(false) => "above",
None => "above",
}
}
pub(crate) fn tie_orientation(stem_up: Option<bool>, multivoice: bool) -> &'static str {
match (multivoice, stem_up) {
(true, Some(false)) => "under",
(true, _) => "over",
(false, Some(true)) => "under",
(false, _) => "over",
}
}
#[cfg(test)]
#[path = "stem_tests.rs"]
mod tests;