mod additive;
mod composition;
mod leading;
mod progression;
mod register;
use std::collections::BTreeSet;
use sim_lib_music_core::{
Articulation, Channel, ObjectId, Pitch, Staff, StaffNote, StaffVoice, Time,
};
use crate::TransformError;
pub use additive::*;
pub use composition::*;
pub use leading::*;
pub use progression::*;
pub use register::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MusicTransformChange {
Onset {
event_id: ObjectId,
before: Time,
after: Time,
},
Duration {
event_id: ObjectId,
before: Time,
after: Time,
},
Articulation {
event_id: ObjectId,
before: Articulation,
after: Articulation,
},
Pitch {
event_id: ObjectId,
before: Pitch,
after: Pitch,
},
Voice {
event_id: ObjectId,
before: ObjectId,
after: ObjectId,
},
CreatedVoice {
voice_id: ObjectId,
source_voice_id: ObjectId,
},
RepeatedIdentity {
source_note_id: ObjectId,
source_event_id: ObjectId,
repeated_note_id: ObjectId,
repeated_event_id: ObjectId,
occurrence: usize,
},
Removed {
note_id: ObjectId,
event_id: ObjectId,
reason: &'static str,
},
AddedVoice {
voice_id: ObjectId,
},
AddedNote {
voice_id: ObjectId,
note_id: ObjectId,
event_id: ObjectId,
},
RemovedVoice {
voice_id: ObjectId,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MusicTransform<T> {
pub value: T,
pub preserved: Vec<ObjectId>,
pub changes: Vec<MusicTransformChange>,
}
impl<T> MusicTransform<T> {
pub fn is_unchanged(&self) -> bool {
self.changes.is_empty()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct SustainSpan {
pub start: Time,
pub end: Time,
pub channel: Option<Channel>,
}
impl SustainSpan {
pub fn new(start: Time, end: Time, channel: Option<Channel>) -> Self {
Self {
start,
end,
channel,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DelayedNoteOrder {
Stable,
HighestFirst,
LowestFirst,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RhythmMask {
step: Time,
pattern: Vec<bool>,
}
impl RhythmMask {
pub fn new(step: Time, pattern: Vec<bool>) -> Result<Self, TransformError> {
if step <= Time::from_integer(0) {
return Err(TransformError::InvalidFactor);
}
if pattern.is_empty() {
return Err(TransformError::InvalidTransformOutput {
transform: "rhythm-mask",
reason: "pattern must not be empty",
});
}
Ok(Self { step, pattern })
}
pub fn step(&self) -> Time {
self.step
}
pub fn pattern(&self) -> &[bool] {
&self.pattern
}
fn keeps(&self, onset: Time) -> bool {
let slots = onset / self.step;
let slot = slots.numer().div_euclid(*slots.denom());
self.pattern[slot.rem_euclid(self.pattern.len() as i64) as usize]
}
}
pub fn sustain_staff(
staff: &Staff,
spans: &[SustainSpan],
) -> Result<MusicTransform<Staff>, TransformError> {
validate_spans(spans)?;
transform_notes(staff, |mut note, changes| {
let before = note.note.duration;
let mut end = note.end();
loop {
let prior_end = end;
for span in spans {
if span
.channel
.is_none_or(|channel| channel == note.note.channel)
&& end >= span.start
&& end < span.end
&& note.onset < span.end
{
end = span.end;
}
}
if end == prior_end {
break;
}
}
note.note.duration = end - note.onset;
if note.note.duration != before {
changes.push(MusicTransformChange::Duration {
event_id: note.event_id.clone(),
before,
after: note.note.duration,
});
}
note
})
}
pub fn slur_staff(staff: &Staff) -> Result<MusicTransform<Staff>, TransformError> {
let mut voices = staff.voices.clone();
let mut changes = Vec::new();
for voice in &mut voices {
voice.notes.sort_by(note_order);
for index in 0..voice.notes.len().saturating_sub(1) {
let next_onset = voice.notes[index + 1].onset;
let note = &mut voice.notes[index];
if note.end() < next_onset {
let before = note.note.duration;
note.note.duration = next_onset - note.onset;
changes.push(MusicTransformChange::Duration {
event_id: note.event_id.clone(),
before,
after: note.note.duration,
});
}
if note.note.articulation != Articulation::Legato {
let before = note.note.articulation;
note.note.articulation = Articulation::Legato;
changes.push(MusicTransformChange::Articulation {
event_id: note.event_id.clone(),
before,
after: Articulation::Legato,
});
}
}
}
finish(voices, changes)
}
pub fn expand_staff(staff: &Staff, factor: Time) -> Result<MusicTransform<Staff>, TransformError> {
if factor <= Time::from_integer(0) {
return Err(TransformError::InvalidFactor);
}
let mut voices = staff.voices.clone();
let mut changes = Vec::new();
for voice in &mut voices {
voice.duration *= factor;
for note in &mut voice.notes {
let onset = note.onset;
let duration = note.note.duration;
note.onset *= factor;
note.note.duration *= factor;
if note.onset != onset {
changes.push(MusicTransformChange::Onset {
event_id: note.event_id.clone(),
before: onset,
after: note.onset,
});
}
if note.note.duration != duration {
changes.push(MusicTransformChange::Duration {
event_id: note.event_id.clone(),
before: duration,
after: note.note.duration,
});
}
}
}
finish(voices, changes)
}
pub fn separate_delayed_notes(
staff: &Staff,
order: DelayedNoteOrder,
) -> Result<MusicTransform<Staff>, TransformError> {
let mut output = Vec::new();
let mut changes = Vec::new();
for voice in &staff.voices {
let mut notes = voice.notes.clone();
notes.sort_by(|left, right| delayed_order(left, right, order));
let mut lines = Vec::<StaffVoice>::new();
for mut note in notes {
let slot = lines.iter().position(|line| {
line.notes
.last()
.is_none_or(|last| last.end() <= note.onset)
});
let index = slot.unwrap_or(lines.len());
if index == lines.len() {
let id = if index == 0 {
voice.id.clone()
} else {
ObjectId::new(format!("{}/delayed-{index}", voice.id))
.expect("derived voice identity is non-empty")
};
if index > 0 {
changes.push(MusicTransformChange::CreatedVoice {
voice_id: id.clone(),
source_voice_id: voice.id.clone(),
});
}
lines.push(StaffVoice {
id,
name: if index == 0 {
voice.name.clone()
} else {
format!("{} delayed {}", voice.name, index + 1)
},
duration: voice.duration,
notes: Vec::new(),
});
}
let destination = lines[index].id.clone();
if note.voice_id != destination {
changes.push(MusicTransformChange::Voice {
event_id: note.event_id.clone(),
before: note.voice_id.clone(),
after: destination.clone(),
});
note.voice_id = destination;
}
lines[index].notes.push(note);
}
if lines.is_empty() {
lines.push(voice.clone());
}
output.extend(lines);
}
finish(output, changes)
}
fn transform_notes(
staff: &Staff,
mut f: impl FnMut(StaffNote, &mut Vec<MusicTransformChange>) -> StaffNote,
) -> Result<MusicTransform<Staff>, TransformError> {
let mut voices = staff.voices.clone();
let mut changes = Vec::new();
for voice in &mut voices {
voice.notes = voice
.notes
.drain(..)
.map(|note| f(note, &mut changes))
.collect();
if let Some(end) = voice.notes.iter().map(StaffNote::end).max() {
voice.duration = voice.duration.max(end);
}
}
finish(voices, changes)
}
fn finish(
voices: Vec<StaffVoice>,
changes: Vec<MusicTransformChange>,
) -> Result<MusicTransform<Staff>, TransformError> {
let staff = Staff::new(voices).map_err(TransformError::InvalidStaff)?;
let mut created = BTreeSet::new();
for change in &changes {
match change {
MusicTransformChange::CreatedVoice { voice_id, .. } => {
created.insert(voice_id);
}
MusicTransformChange::RepeatedIdentity {
repeated_note_id,
repeated_event_id,
..
} => {
created.insert(repeated_note_id);
created.insert(repeated_event_id);
}
MusicTransformChange::AddedVoice { voice_id } => {
created.insert(voice_id);
}
MusicTransformChange::AddedNote {
note_id, event_id, ..
} => {
created.insert(note_id);
created.insert(event_id);
}
_ => {}
}
}
Ok(MusicTransform {
preserved: staff
.object_ids()
.into_iter()
.filter(|id| !created.contains(id))
.collect(),
value: staff,
changes,
})
}
fn validate_spans(spans: &[SustainSpan]) -> Result<(), TransformError> {
if spans
.iter()
.any(|span| span.start < Time::from_integer(0) || span.end < span.start)
{
return Err(TransformError::InvalidTransformOutput {
transform: "sustain",
reason: "sustain spans must satisfy 0 <= start <= end",
});
}
Ok(())
}
fn note_order(left: &StaffNote, right: &StaffNote) -> std::cmp::Ordering {
left.onset
.cmp(&right.onset)
.then_with(|| left.note.pitch.cmp(&right.note.pitch))
.then_with(|| left.event_id.cmp(&right.event_id))
}
fn delayed_order(
left: &StaffNote,
right: &StaffNote,
order: DelayedNoteOrder,
) -> std::cmp::Ordering {
left.onset.cmp(&right.onset).then_with(|| {
let pitch = left.note.pitch.cmp(&right.note.pitch);
let pitch = match order {
DelayedNoteOrder::Stable | DelayedNoteOrder::LowestFirst => pitch,
DelayedNoteOrder::HighestFirst => pitch.reverse(),
};
pitch.then_with(|| left.event_id.cmp(&right.event_id))
})
}