sim_lib_music_transform/exact/
register.rs1use std::collections::BTreeMap;
2
3use sim_lib_music_core::{Pitch, Staff};
4
5use super::{MusicTransform, MusicTransformChange, finish, note_order, transform_notes};
6use crate::TransformError;
7
8#[derive(Copy, Clone, Debug, PartialEq, Eq)]
10pub enum RegisterTie {
11 Ascending,
13 Descending,
15}
16
17#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub struct RegisterRange {
20 pub low: Pitch,
22 pub high: Pitch,
24 pub tie: RegisterTie,
26}
27
28pub fn unwrap_register(
34 staff: &Staff,
35 range: RegisterRange,
36) -> Result<MusicTransform<Staff>, TransformError> {
37 let low = range.low.semitone();
38 let high = range.high.semitone();
39 if low > high {
40 return Err(TransformError::InvalidTransformOutput {
41 transform: "register-unwrap",
42 reason: "low pitch must not exceed high pitch",
43 });
44 }
45 let mut voices = staff.voices.clone();
46 let mut changes = Vec::new();
47 for voice in &mut voices {
48 voice.notes.sort_by(note_order);
49 let mut previous = None;
50 for note in &mut voice.notes {
51 let original = note.note.pitch;
52 let target = previous.unwrap_or_else(|| original.semitone());
53 let candidates = register_candidates(original, low, high);
54 let Some(semitone) = nearest_candidate(&candidates, target, range.tie) else {
55 return Err(TransformError::InvalidTransformOutput {
56 transform: "register-unwrap",
57 reason: "register contains no octave placement for a pitch class",
58 });
59 };
60 note.note.pitch = Pitch::from_semitone(semitone);
61 previous = Some(semitone);
62 if note.note.pitch != original {
63 changes.push(MusicTransformChange::Pitch {
64 event_id: note.event_id.clone(),
65 before: original,
66 after: note.note.pitch,
67 });
68 }
69 }
70 }
71 finish(voices, changes)
72}
73
74pub fn restore_register(
76 report: &MusicTransform<Staff>,
77) -> Result<MusicTransform<Staff>, TransformError> {
78 let originals = report
79 .changes
80 .iter()
81 .filter_map(|change| match change {
82 MusicTransformChange::Pitch {
83 event_id, before, ..
84 } => Some((event_id.clone(), *before)),
85 _ => None,
86 })
87 .collect::<BTreeMap<_, _>>();
88 transform_notes(&report.value, |mut note, changes| {
89 if let Some(original) = originals.get(¬e.event_id)
90 && note.note.pitch != *original
91 {
92 let before = note.note.pitch;
93 note.note.pitch = *original;
94 changes.push(MusicTransformChange::Pitch {
95 event_id: note.event_id.clone(),
96 before,
97 after: *original,
98 });
99 }
100 note
101 })
102}
103
104fn register_candidates(pitch: Pitch, low: i32, high: i32) -> Vec<i32> {
105 let class = i32::from(pitch.class.value());
106 let mut candidate = low + (class - low).rem_euclid(12);
107 let mut output = Vec::new();
108 while candidate <= high {
109 output.push(candidate);
110 candidate += 12;
111 }
112 output
113}
114
115fn nearest_candidate(candidates: &[i32], target: i32, tie: RegisterTie) -> Option<i32> {
116 candidates.iter().copied().min_by(|left, right| {
117 let left_distance = (*left - target).abs();
118 let right_distance = (*right - target).abs();
119 left_distance.cmp(&right_distance).then_with(|| match tie {
120 RegisterTie::Ascending => right.cmp(left),
121 RegisterTie::Descending => left.cmp(right),
122 })
123 })
124}