use std::collections::BTreeMap;
use sim_lib_music_core::{Pitch, Staff};
use super::{MusicTransform, MusicTransformChange, finish, note_order, transform_notes};
use crate::TransformError;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RegisterTie {
Ascending,
Descending,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct RegisterRange {
pub low: Pitch,
pub high: Pitch,
pub tie: RegisterTie,
}
pub fn unwrap_register(
staff: &Staff,
range: RegisterRange,
) -> Result<MusicTransform<Staff>, TransformError> {
let low = range.low.semitone();
let high = range.high.semitone();
if low > high {
return Err(TransformError::InvalidTransformOutput {
transform: "register-unwrap",
reason: "low pitch must not exceed high pitch",
});
}
let mut voices = staff.voices.clone();
let mut changes = Vec::new();
for voice in &mut voices {
voice.notes.sort_by(note_order);
let mut previous = None;
for note in &mut voice.notes {
let original = note.note.pitch;
let target = previous.unwrap_or_else(|| original.semitone());
let candidates = register_candidates(original, low, high);
let Some(semitone) = nearest_candidate(&candidates, target, range.tie) else {
return Err(TransformError::InvalidTransformOutput {
transform: "register-unwrap",
reason: "register contains no octave placement for a pitch class",
});
};
note.note.pitch = Pitch::from_semitone(semitone);
previous = Some(semitone);
if note.note.pitch != original {
changes.push(MusicTransformChange::Pitch {
event_id: note.event_id.clone(),
before: original,
after: note.note.pitch,
});
}
}
}
finish(voices, changes)
}
pub fn restore_register(
report: &MusicTransform<Staff>,
) -> Result<MusicTransform<Staff>, TransformError> {
let originals = report
.changes
.iter()
.filter_map(|change| match change {
MusicTransformChange::Pitch {
event_id, before, ..
} => Some((event_id.clone(), *before)),
_ => None,
})
.collect::<BTreeMap<_, _>>();
transform_notes(&report.value, |mut note, changes| {
if let Some(original) = originals.get(¬e.event_id)
&& note.note.pitch != *original
{
let before = note.note.pitch;
note.note.pitch = *original;
changes.push(MusicTransformChange::Pitch {
event_id: note.event_id.clone(),
before,
after: *original,
});
}
note
})
}
fn register_candidates(pitch: Pitch, low: i32, high: i32) -> Vec<i32> {
let class = i32::from(pitch.class.value());
let mut candidate = low + (class - low).rem_euclid(12);
let mut output = Vec::new();
while candidate <= high {
output.push(candidate);
candidate += 12;
}
output
}
fn nearest_candidate(candidates: &[i32], target: i32, tie: RegisterTie) -> Option<i32> {
candidates.iter().copied().min_by(|left, right| {
let left_distance = (*left - target).abs();
let right_distance = (*right - target).abs();
left_distance.cmp(&right_distance).then_with(|| match tie {
RegisterTie::Ascending => right.cmp(left),
RegisterTie::Descending => left.cmp(right),
})
})
}