sim_lib_music_consonance/
patch.rs1use std::collections::BTreeSet;
2
3use sim_kernel::{ContentId, Datum, Symbol};
4use sim_lib_music_core::{Articulation, ObjectId, Staff, StaffNote, StaffVoice, Time};
5use sim_lib_music_transform::{
6 AdditiveStaffPatch, apply_additive_staff_patch, remove_additive_staff_patch,
7};
8use thiserror::Error;
9
10pub type ContentKey = ContentId;
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
15pub enum AdditionKind {
16 Note,
18 Ornament,
20 Chord,
22 Pedal,
24 Doubling,
26 Voice,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct NoteAddition {
33 pub note: StaffNote,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct OrnamentAddition {
40 pub anchor_event_id: ObjectId,
42 pub notes: Vec<StaffNote>,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct ChordAddition {
49 pub label: Option<String>,
51 pub notes: Vec<StaffNote>,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct PedalAddition {
58 pub label: Option<String>,
60 pub note: StaffNote,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct DoublingAddition {
67 pub source_event_id: ObjectId,
69 pub note: StaffNote,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct VoiceAddition {
76 pub voice: StaffVoice,
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum Addition {
83 Note(NoteAddition),
85 Ornament(OrnamentAddition),
87 Chord(ChordAddition),
89 Pedal(PedalAddition),
91 Doubling(DoublingAddition),
93 Voice(VoiceAddition),
95}
96
97impl Addition {
98 pub fn kind(&self) -> AdditionKind {
100 match self {
101 Self::Note(_) => AdditionKind::Note,
102 Self::Ornament(_) => AdditionKind::Ornament,
103 Self::Chord(_) => AdditionKind::Chord,
104 Self::Pedal(_) => AdditionKind::Pedal,
105 Self::Doubling(_) => AdditionKind::Doubling,
106 Self::Voice(_) => AdditionKind::Voice,
107 }
108 }
109
110 pub fn notes(&self) -> Box<dyn Iterator<Item = &StaffNote> + '_> {
112 match self {
113 Self::Note(value) => Box::new(std::iter::once(&value.note)),
114 Self::Ornament(value) => Box::new(value.notes.iter()),
115 Self::Chord(value) => Box::new(value.notes.iter()),
116 Self::Pedal(value) => Box::new(std::iter::once(&value.note)),
117 Self::Doubling(value) => Box::new(std::iter::once(&value.note)),
118 Self::Voice(value) => Box::new(value.voice.notes.iter()),
119 }
120 }
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct ConsonancePatch {
126 pub base: ContentKey,
128 pub additions: Vec<Addition>,
130}
131
132impl ConsonancePatch {
133 pub fn new(source: &Staff, additions: Vec<Addition>) -> Result<Self, PatchError> {
135 let patch = Self {
136 base: staff_content_key(source)?,
137 additions,
138 };
139 patch.compile(source)?;
140 Ok(patch)
141 }
142
143 pub(crate) fn compile(&self, source: &Staff) -> Result<AdditiveStaffPatch, PatchError> {
144 validate_additions(source, &self.additions)?;
145 let mut patch = AdditiveStaffPatch::default();
146 for addition in &self.additions {
147 match addition {
148 Addition::Voice(value) => patch.voices.push(value.voice.clone()),
149 _ => patch.notes.extend(addition.notes().cloned()),
150 }
151 }
152 apply_additive_staff_patch(source, &patch)
153 .map_err(|error| PatchError::InvalidAddition(error.to_string()))?;
154 Ok(patch)
155 }
156}
157
158pub fn apply_patch(source: &Staff, patch: &ConsonancePatch) -> Result<Staff, PatchError> {
160 require_base(source, &patch.base)?;
161 let additions = patch.compile(source)?;
162 apply_additive_staff_patch(source, &additions)
163 .map(|transform| transform.value)
164 .map_err(|error| PatchError::InvalidAddition(error.to_string()))
165}
166
167pub fn remove_patch(completed: &Staff, patch: &ConsonancePatch) -> Result<Staff, PatchError> {
169 let additions = compile_without_source(&patch.additions);
170 let source = remove_additive_staff_patch(completed, &additions)
171 .map(|transform| transform.value)
172 .map_err(|error| PatchError::InvalidInverse(error.to_string()))?;
173 require_base(&source, &patch.base)?;
174 Ok(source)
175}
176
177pub fn staff_content_key(staff: &Staff) -> Result<ContentKey, PatchError> {
179 staff_datum(staff)
180 .content_id()
181 .map_err(|error| PatchError::ContentIdentity(error.to_string()))
182}
183
184#[derive(Clone, Debug, Error, PartialEq, Eq)]
186pub enum PatchError {
187 #[error("consonance patch base does not match the supplied staff")]
189 BaseMismatch,
190 #[error("invalid consonance addition: {0}")]
192 InvalidAddition(String),
193 #[error("invalid consonance patch inverse: {0}")]
195 InvalidInverse(String),
196 #[error("staff content identity failed: {0}")]
198 ContentIdentity(String),
199}
200
201fn validate_additions(source: &Staff, additions: &[Addition]) -> Result<(), PatchError> {
202 let duration = source.duration();
203 let source_events = source
204 .notes()
205 .map(|note| (note.event_id.clone(), note))
206 .collect::<std::collections::BTreeMap<_, _>>();
207 for addition in additions {
208 validate_semantics(addition, &source_events)?;
209 for note in addition.notes() {
210 if note.note.duration <= Time::from_integer(0)
211 || note.onset < Time::from_integer(0)
212 || note.end() > duration
213 {
214 return invalid("added notes must have positive spans inside the source duration");
215 }
216 }
217 if let Addition::Voice(value) = addition
218 && value.voice.duration != duration
219 {
220 return invalid("added voices must retain the source staff duration");
221 }
222 }
223 Ok(())
224}
225
226fn validate_semantics(
227 addition: &Addition,
228 source_events: &std::collections::BTreeMap<ObjectId, &StaffNote>,
229) -> Result<(), PatchError> {
230 match addition {
231 Addition::Note(_) => {}
232 Addition::Ornament(value) => {
233 if !source_events.contains_key(&value.anchor_event_id) || value.notes.is_empty() {
234 return invalid("an ornament needs an existing anchor and at least one note");
235 }
236 }
237 Addition::Chord(value) => {
238 let Some(first) = value.notes.first() else {
239 return invalid("a chord addition must contain notes");
240 };
241 if value.notes.len() < 2
242 || value
243 .notes
244 .iter()
245 .any(|note| note.onset != first.onset || note.end() != first.end())
246 {
247 return invalid("a chord addition needs at least two notes with one exact span");
248 }
249 }
250 Addition::Pedal(_) => {}
251 Addition::Doubling(value) => {
252 let Some(source) = source_events.get(&value.source_event_id) else {
253 return invalid("a doubling must name an existing source event");
254 };
255 if value.note.onset != source.onset
256 || value.note.note.duration != source.note.duration
257 || value.note.note.pitch.class != source.note.pitch.class
258 {
259 return invalid("a doubling must retain source onset, duration, and pitch class");
260 }
261 }
262 Addition::Voice(value) if value.voice.notes.is_empty() => {
263 return invalid("an added voice must contain at least one note");
264 }
265 Addition::Voice(_) => {}
266 }
267 Ok(())
268}
269
270fn compile_without_source(additions: &[Addition]) -> AdditiveStaffPatch {
271 let mut patch = AdditiveStaffPatch::default();
272 for addition in additions {
273 match addition {
274 Addition::Voice(value) => patch.voices.push(value.voice.clone()),
275 _ => patch.notes.extend(addition.notes().cloned()),
276 }
277 }
278 patch
279}
280
281fn require_base(staff: &Staff, expected: &ContentKey) -> Result<(), PatchError> {
282 if staff_content_key(staff)? == *expected {
283 Ok(())
284 } else {
285 Err(PatchError::BaseMismatch)
286 }
287}
288
289fn staff_datum(staff: &Staff) -> Datum {
290 Datum::Node {
291 tag: Symbol::qualified("music/consonance", "staff-v1"),
292 fields: vec![
293 (Symbol::new("duration"), time_datum(staff.duration())),
294 (
295 Symbol::new("voices"),
296 Datum::Vector(staff.voices.iter().map(voice_datum).collect()),
297 ),
298 ],
299 }
300}
301
302fn voice_datum(voice: &StaffVoice) -> Datum {
303 Datum::Vector(vec![
304 Datum::String(voice.id.to_string()),
305 Datum::String(voice.name.clone()),
306 time_datum(voice.duration),
307 Datum::Vector(voice.notes.iter().map(note_datum).collect()),
308 ])
309}
310
311fn note_datum(note: &StaffNote) -> Datum {
312 Datum::Vector(vec![
313 Datum::String(note.voice_id.to_string()),
314 Datum::String(note.note_id.to_string()),
315 Datum::String(note.event_id.to_string()),
316 time_datum(note.onset),
317 time_datum(note.note.duration),
318 Datum::String(note.note.pitch.semitone().to_string()),
319 Datum::String(note.note.velocity.to_string()),
320 Datum::String(note.note.channel.0.to_string()),
321 Datum::String(articulation_name(note.note.articulation).to_owned()),
322 ])
323}
324
325fn time_datum(value: Time) -> Datum {
326 Datum::String(format!("{}/{}", value.numer(), value.denom()))
327}
328
329fn articulation_name(value: Articulation) -> &'static str {
330 match value {
331 Articulation::Normal => "normal",
332 Articulation::Staccato => "staccato",
333 Articulation::Legato => "legato",
334 Articulation::Tenuto => "tenuto",
335 Articulation::Accent => "accent",
336 Articulation::Marcato => "marcato",
337 }
338}
339
340fn invalid<T>(reason: impl Into<String>) -> Result<T, PatchError> {
341 Err(PatchError::InvalidAddition(reason.into()))
342}
343
344pub(crate) fn addition_ids(additions: &[Addition]) -> Vec<ObjectId> {
345 let mut ids = BTreeSet::new();
346 for addition in additions {
347 if let Addition::Voice(value) = addition {
348 ids.insert(value.voice.id.clone());
349 }
350 for note in addition.notes() {
351 ids.insert(note.note_id.clone());
352 ids.insert(note.event_id.clone());
353 }
354 }
355 ids.into_iter().collect()
356}