fn step_index(step: char) -> i32 {
match step.to_ascii_uppercase() {
'C' => 0,
'D' => 1,
'E' => 2,
'F' => 3,
'G' => 4,
'A' => 5,
'B' => 6,
_ => 0,
}
}
pub(crate) fn abs_step(step: char, octave: i8) -> i32 {
7 * (i32::from(octave) + 1) + step_index(step)
}
fn middle_abs_step(clef_text: Option<&str>) -> i32 {
match clef_family(clef_text) {
ClefFamily::Treble => abs_step('B', 4),
ClefFamily::Bass => abs_step('D', 3),
ClefFamily::Alto => abs_step('C', 4),
ClefFamily::Tenor => abs_step('A', 3),
}
}
fn top_line_abs_step(clef_text: Option<&str>) -> i32 {
match clef_family(clef_text) {
ClefFamily::Treble => abs_step('F', 5),
ClefFamily::Bass => abs_step('A', 3),
ClefFamily::Alto => abs_step('G', 4),
ClefFamily::Tenor => abs_step('F', 4),
}
}
enum ClefFamily {
Treble,
Bass,
Alto,
Tenor,
}
fn clef_family(clef_text: Option<&str>) -> ClefFamily {
let text = clef_text.unwrap_or("treble").trim().to_ascii_lowercase();
if text.starts_with("bass") || text.starts_with('f') {
ClefFamily::Bass
} else if text.starts_with("tenor") {
ClefFamily::Tenor
} else if text.starts_with("alto") || text.starts_with("c") {
ClefFamily::Alto
} else {
ClefFamily::Treble
}
}
pub(crate) fn middle_distance(clef_text: Option<&str>, step: char, octave: i8) -> i32 {
middle_abs_step(clef_text) - abs_step(step, octave)
}
pub(crate) fn auto_stem_up(distances: &[i32]) -> bool {
let mut sorted = distances.to_vec();
sorted.sort_unstable();
let mut left = 0usize;
let mut right = sorted.len().saturating_sub(1);
while left <= right {
let sum = sorted[left] + sorted[right];
if sum != 0 {
return sum > 0;
}
if left == right {
break;
}
left += 1;
right -= 1;
}
false
}
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);
}
match beam_distances {
Some(distances) => auto_stem_up(distances),
None => auto_stem_up(own_distances),
}
}
pub(crate) fn note_type_has_stem(note_type: &str) -> bool {
!matches!(note_type, "whole" | "breve" | "long" | "maxima")
}
pub(crate) fn rest_display(clef_text: Option<&str>, voice_slot: usize) -> (char, i8) {
const NATURAL_LINE: i32 = 2; let voice_offset = if voice_slot.is_multiple_of(2) { -1 } else { 1 };
let mut po = top_line_abs_step(clef_text) - 2 * (NATURAL_LINE + voice_offset) - 7;
po = po.clamp(0, 69);
let octave = (po / 7) as i8;
let step = ['C', 'D', 'E', 'F', 'G', 'A', 'B'][(po % 7) as usize];
(step, 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) | None => "above",
}
}
pub(crate) fn tie_orientation(stem_up: Option<bool>, multivoice: bool) -> &'static str {
if multivoice {
return match stem_up {
Some(false) => "under",
_ => "over",
};
}
match stem_up {
Some(true) => "under",
Some(false) | None => "over",
}
}
#[cfg(test)]
#[path = "stem_tests.rs"]
mod tests;