1mod additive;
4mod composition;
5mod leading;
6mod progression;
7mod register;
8
9use std::collections::BTreeSet;
10
11use sim_lib_music_core::{
12 Articulation, Channel, ObjectId, Pitch, Staff, StaffNote, StaffVoice, Time,
13};
14
15use crate::TransformError;
16
17pub use additive::*;
18pub use composition::*;
19pub use leading::*;
20pub use progression::*;
21pub use register::*;
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum MusicTransformChange {
26 Onset {
28 event_id: ObjectId,
30 before: Time,
32 after: Time,
34 },
35 Duration {
37 event_id: ObjectId,
39 before: Time,
41 after: Time,
43 },
44 Articulation {
46 event_id: ObjectId,
48 before: Articulation,
50 after: Articulation,
52 },
53 Pitch {
55 event_id: ObjectId,
57 before: Pitch,
59 after: Pitch,
61 },
62 Voice {
64 event_id: ObjectId,
66 before: ObjectId,
68 after: ObjectId,
70 },
71 CreatedVoice {
73 voice_id: ObjectId,
75 source_voice_id: ObjectId,
77 },
78 RepeatedIdentity {
80 source_note_id: ObjectId,
82 source_event_id: ObjectId,
84 repeated_note_id: ObjectId,
86 repeated_event_id: ObjectId,
88 occurrence: usize,
90 },
91 Removed {
93 note_id: ObjectId,
95 event_id: ObjectId,
97 reason: &'static str,
99 },
100 AddedVoice {
102 voice_id: ObjectId,
104 },
105 AddedNote {
107 voice_id: ObjectId,
109 note_id: ObjectId,
111 event_id: ObjectId,
113 },
114 RemovedVoice {
116 voice_id: ObjectId,
118 },
119}
120
121#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct MusicTransform<T> {
124 pub value: T,
126 pub preserved: Vec<ObjectId>,
128 pub changes: Vec<MusicTransformChange>,
130}
131
132impl<T> MusicTransform<T> {
133 pub fn is_unchanged(&self) -> bool {
135 self.changes.is_empty()
136 }
137}
138
139#[derive(Copy, Clone, Debug, PartialEq, Eq)]
141pub struct SustainSpan {
142 pub start: Time,
144 pub end: Time,
146 pub channel: Option<Channel>,
148}
149
150impl SustainSpan {
151 pub fn new(start: Time, end: Time, channel: Option<Channel>) -> Self {
153 Self {
154 start,
155 end,
156 channel,
157 }
158 }
159}
160
161#[derive(Copy, Clone, Debug, PartialEq, Eq)]
163pub enum DelayedNoteOrder {
164 Stable,
166 HighestFirst,
168 LowestFirst,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct RhythmMask {
175 step: Time,
176 pattern: Vec<bool>,
177}
178
179impl RhythmMask {
180 pub fn new(step: Time, pattern: Vec<bool>) -> Result<Self, TransformError> {
182 if step <= Time::from_integer(0) {
183 return Err(TransformError::InvalidFactor);
184 }
185 if pattern.is_empty() {
186 return Err(TransformError::InvalidTransformOutput {
187 transform: "rhythm-mask",
188 reason: "pattern must not be empty",
189 });
190 }
191 Ok(Self { step, pattern })
192 }
193
194 pub fn step(&self) -> Time {
196 self.step
197 }
198
199 pub fn pattern(&self) -> &[bool] {
201 &self.pattern
202 }
203
204 fn keeps(&self, onset: Time) -> bool {
205 let slots = onset / self.step;
206 let slot = slots.numer().div_euclid(*slots.denom());
207 self.pattern[slot.rem_euclid(self.pattern.len() as i64) as usize]
208 }
209}
210
211pub fn sustain_staff(
218 staff: &Staff,
219 spans: &[SustainSpan],
220) -> Result<MusicTransform<Staff>, TransformError> {
221 validate_spans(spans)?;
222 transform_notes(staff, |mut note, changes| {
223 let before = note.note.duration;
224 let mut end = note.end();
225 loop {
226 let prior_end = end;
227 for span in spans {
228 if span
229 .channel
230 .is_none_or(|channel| channel == note.note.channel)
231 && end >= span.start
232 && end < span.end
233 && note.onset < span.end
234 {
235 end = span.end;
236 }
237 }
238 if end == prior_end {
239 break;
240 }
241 }
242 note.note.duration = end - note.onset;
243 if note.note.duration != before {
244 changes.push(MusicTransformChange::Duration {
245 event_id: note.event_id.clone(),
246 before,
247 after: note.note.duration,
248 });
249 }
250 note
251 })
252}
253
254pub fn slur_staff(staff: &Staff) -> Result<MusicTransform<Staff>, TransformError> {
259 let mut voices = staff.voices.clone();
260 let mut changes = Vec::new();
261 for voice in &mut voices {
262 voice.notes.sort_by(note_order);
263 for index in 0..voice.notes.len().saturating_sub(1) {
264 let next_onset = voice.notes[index + 1].onset;
265 let note = &mut voice.notes[index];
266 if note.end() < next_onset {
267 let before = note.note.duration;
268 note.note.duration = next_onset - note.onset;
269 changes.push(MusicTransformChange::Duration {
270 event_id: note.event_id.clone(),
271 before,
272 after: note.note.duration,
273 });
274 }
275 if note.note.articulation != Articulation::Legato {
276 let before = note.note.articulation;
277 note.note.articulation = Articulation::Legato;
278 changes.push(MusicTransformChange::Articulation {
279 event_id: note.event_id.clone(),
280 before,
281 after: Articulation::Legato,
282 });
283 }
284 }
285 }
286 finish(voices, changes)
287}
288
289pub fn expand_staff(staff: &Staff, factor: Time) -> Result<MusicTransform<Staff>, TransformError> {
291 if factor <= Time::from_integer(0) {
292 return Err(TransformError::InvalidFactor);
293 }
294 let mut voices = staff.voices.clone();
295 let mut changes = Vec::new();
296 for voice in &mut voices {
297 voice.duration *= factor;
298 for note in &mut voice.notes {
299 let onset = note.onset;
300 let duration = note.note.duration;
301 note.onset *= factor;
302 note.note.duration *= factor;
303 if note.onset != onset {
304 changes.push(MusicTransformChange::Onset {
305 event_id: note.event_id.clone(),
306 before: onset,
307 after: note.onset,
308 });
309 }
310 if note.note.duration != duration {
311 changes.push(MusicTransformChange::Duration {
312 event_id: note.event_id.clone(),
313 before: duration,
314 after: note.note.duration,
315 });
316 }
317 }
318 }
319 finish(voices, changes)
320}
321
322pub fn separate_delayed_notes(
328 staff: &Staff,
329 order: DelayedNoteOrder,
330) -> Result<MusicTransform<Staff>, TransformError> {
331 let mut output = Vec::new();
332 let mut changes = Vec::new();
333 for voice in &staff.voices {
334 let mut notes = voice.notes.clone();
335 notes.sort_by(|left, right| delayed_order(left, right, order));
336 let mut lines = Vec::<StaffVoice>::new();
337 for mut note in notes {
338 let slot = lines.iter().position(|line| {
339 line.notes
340 .last()
341 .is_none_or(|last| last.end() <= note.onset)
342 });
343 let index = slot.unwrap_or(lines.len());
344 if index == lines.len() {
345 let id = if index == 0 {
346 voice.id.clone()
347 } else {
348 ObjectId::new(format!("{}/delayed-{index}", voice.id))
349 .expect("derived voice identity is non-empty")
350 };
351 if index > 0 {
352 changes.push(MusicTransformChange::CreatedVoice {
353 voice_id: id.clone(),
354 source_voice_id: voice.id.clone(),
355 });
356 }
357 lines.push(StaffVoice {
358 id,
359 name: if index == 0 {
360 voice.name.clone()
361 } else {
362 format!("{} delayed {}", voice.name, index + 1)
363 },
364 duration: voice.duration,
365 notes: Vec::new(),
366 });
367 }
368 let destination = lines[index].id.clone();
369 if note.voice_id != destination {
370 changes.push(MusicTransformChange::Voice {
371 event_id: note.event_id.clone(),
372 before: note.voice_id.clone(),
373 after: destination.clone(),
374 });
375 note.voice_id = destination;
376 }
377 lines[index].notes.push(note);
378 }
379 if lines.is_empty() {
380 lines.push(voice.clone());
381 }
382 output.extend(lines);
383 }
384 finish(output, changes)
385}
386
387fn transform_notes(
388 staff: &Staff,
389 mut f: impl FnMut(StaffNote, &mut Vec<MusicTransformChange>) -> StaffNote,
390) -> Result<MusicTransform<Staff>, TransformError> {
391 let mut voices = staff.voices.clone();
392 let mut changes = Vec::new();
393 for voice in &mut voices {
394 voice.notes = voice
395 .notes
396 .drain(..)
397 .map(|note| f(note, &mut changes))
398 .collect();
399 if let Some(end) = voice.notes.iter().map(StaffNote::end).max() {
400 voice.duration = voice.duration.max(end);
401 }
402 }
403 finish(voices, changes)
404}
405
406fn finish(
407 voices: Vec<StaffVoice>,
408 changes: Vec<MusicTransformChange>,
409) -> Result<MusicTransform<Staff>, TransformError> {
410 let staff = Staff::new(voices).map_err(TransformError::InvalidStaff)?;
411 let mut created = BTreeSet::new();
412 for change in &changes {
413 match change {
414 MusicTransformChange::CreatedVoice { voice_id, .. } => {
415 created.insert(voice_id);
416 }
417 MusicTransformChange::RepeatedIdentity {
418 repeated_note_id,
419 repeated_event_id,
420 ..
421 } => {
422 created.insert(repeated_note_id);
423 created.insert(repeated_event_id);
424 }
425 MusicTransformChange::AddedVoice { voice_id } => {
426 created.insert(voice_id);
427 }
428 MusicTransformChange::AddedNote {
429 note_id, event_id, ..
430 } => {
431 created.insert(note_id);
432 created.insert(event_id);
433 }
434 _ => {}
435 }
436 }
437 Ok(MusicTransform {
438 preserved: staff
439 .object_ids()
440 .into_iter()
441 .filter(|id| !created.contains(id))
442 .collect(),
443 value: staff,
444 changes,
445 })
446}
447
448fn validate_spans(spans: &[SustainSpan]) -> Result<(), TransformError> {
449 if spans
450 .iter()
451 .any(|span| span.start < Time::from_integer(0) || span.end < span.start)
452 {
453 return Err(TransformError::InvalidTransformOutput {
454 transform: "sustain",
455 reason: "sustain spans must satisfy 0 <= start <= end",
456 });
457 }
458 Ok(())
459}
460
461fn note_order(left: &StaffNote, right: &StaffNote) -> std::cmp::Ordering {
462 left.onset
463 .cmp(&right.onset)
464 .then_with(|| left.note.pitch.cmp(&right.note.pitch))
465 .then_with(|| left.event_id.cmp(&right.event_id))
466}
467
468fn delayed_order(
469 left: &StaffNote,
470 right: &StaffNote,
471 order: DelayedNoteOrder,
472) -> std::cmp::Ordering {
473 left.onset.cmp(&right.onset).then_with(|| {
474 let pitch = left.note.pitch.cmp(&right.note.pitch);
475 let pitch = match order {
476 DelayedNoteOrder::Stable | DelayedNoteOrder::LowestFirst => pitch,
477 DelayedNoteOrder::HighestFirst => pitch.reverse(),
478 };
479 pitch.then_with(|| left.event_id.cmp(&right.event_id))
480 })
481}