Skip to main content

sim_lib_music_serial/
chromatic.rs

1//! Built-in strict chromatic realizer registered through the open registry.
2
3use std::cmp::Reverse;
4use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
5
6use sim_lib_music_core::{Note, Pitch, Time};
7
8use crate::{
9    EvidenceId, InvariantLedger, InvariantLedgerEntry, InvariantStatus, RealizationContext,
10    RealizedSerialEvent, RealizedSerialNote, RealizedSerialOrigin, RealizerId, SerialEventId,
11    SerialPlan, SerialRealization, SerialRealizer, StrictEventSpec, StrictRealizationError,
12    TiePolicy,
13};
14
15/// Stable id of the built-in strict chromatic realizer.
16pub fn strict_chromatic_realizer_id() -> RealizerId {
17    RealizerId::new("realizer/strict-chromatic").expect("built-in realizer id is valid")
18}
19
20/// Built-in strict chromatic serial realizer.
21#[derive(Clone, Debug)]
22pub struct ChromaticSerialRealizer {
23    id: RealizerId,
24}
25
26impl Default for ChromaticSerialRealizer {
27    fn default() -> Self {
28        Self {
29            id: strict_chromatic_realizer_id(),
30        }
31    }
32}
33
34impl SerialRealizer for ChromaticSerialRealizer {
35    fn id(&self) -> &RealizerId {
36        &self.id
37    }
38
39    fn realize(
40        &self,
41        plan: &SerialPlan,
42        context: &RealizationContext,
43    ) -> Result<SerialRealization, StrictRealizationError> {
44        realize_chromatic_with_id(self.id(), plan, context)
45    }
46}
47
48#[derive(Clone, Debug)]
49struct RealizedEventState {
50    event: RealizedSerialEvent,
51    note_indexes: Vec<usize>,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
55struct UnitKey {
56    row_id: String,
57    ordinal: usize,
58    label: String,
59}
60
61#[derive(Clone, Debug)]
62struct EventUnit {
63    members: Vec<SerialEventId>,
64    key: UnitKey,
65}
66
67pub(crate) fn realize_chromatic_with_id(
68    realizer_id: &RealizerId,
69    plan: &SerialPlan,
70    context: &RealizationContext,
71) -> Result<SerialRealization, StrictRealizationError> {
72    for event_id in plan.events().keys() {
73        let Some(spec) = context.specs.get(event_id) else {
74            return Err(StrictRealizationError::MissingSpec(event_id.clone()));
75        };
76        if spec.duration <= Time::from_integer(0) {
77            return Err(StrictRealizationError::NonPositiveDuration(
78                event_id.clone(),
79            ));
80        }
81    }
82
83    let units = build_units(plan);
84    let unit_index = units
85        .iter()
86        .enumerate()
87        .flat_map(|(index, unit)| unit.members.iter().cloned().map(move |id| (id, index)))
88        .collect::<BTreeMap<_, _>>();
89    let order = topo_units(plan, &units, &unit_index);
90
91    let mut notes = Vec::<RealizedSerialNote>::new();
92    let mut events = BTreeMap::<SerialEventId, RealizedEventState>::new();
93    let mut cursor = Time::from_integer(0);
94    for unit_idx in order {
95        let unit = &units[unit_idx];
96        let unit_onset = cursor;
97        let mut unit_duration = Time::from_integer(0);
98        for event_id in &unit.members {
99            let planned = plan.event(event_id).expect("unit event must exist");
100            let spec = context
101                .specs
102                .get(event_id)
103                .expect("validated event specs must exist");
104            unit_duration = unit_duration.max(spec.duration);
105            let mut note_indexes = Vec::new();
106            if matches!(spec.sound, crate::EventSound::Notes) {
107                let displacements = event_displacements(spec, planned.ordinals.len(), event_id)?;
108                for (note_index, (ordinal, displacement)) in planned
109                    .ordinals
110                    .iter()
111                    .cloned()
112                    .zip(displacements)
113                    .enumerate()
114                {
115                    let row_form = plan
116                        .row(&ordinal.row_id)
117                        .expect("validated ordinal row must exist");
118                    let pitch_class = row_form.classes()[ordinal.ordinal];
119                    let midi = 12
120                        * (i16::from(spec.pitch_layout.register) + i16::from(displacement) + 1)
121                        + i16::from(pitch_class.value());
122                    if !(0..=127).contains(&midi) {
123                        return Err(StrictRealizationError::MidiOutOfRange {
124                            event_id: event_id.clone(),
125                            midi,
126                        });
127                    }
128                    let note = Note::new(
129                        spec.duration,
130                        Pitch::from_midi(midi as u8),
131                        spec.velocity,
132                        spec.channel,
133                        spec.articulation,
134                    )
135                    .map_err(|error| StrictRealizationError::MusicCore(error.to_string()))?;
136                    note_indexes.push(notes.len());
137                    notes.push(RealizedSerialNote {
138                        event_id: event_id.clone(),
139                        voice: planned.voice.clone(),
140                        note_index,
141                        onset: unit_onset,
142                        note,
143                        origin: RealizedSerialOrigin {
144                            realizer_id: realizer_id.clone(),
145                            licenses: planned.licenses.clone(),
146                            ordinals: planned.ordinals.clone(),
147                            source_ordinal: ordinal.clone(),
148                            row_forms: planned
149                                .ordinals
150                                .iter()
151                                .map(|item| {
152                                    (
153                                        item.row_id.clone(),
154                                        plan.row(&item.row_id)
155                                            .expect("validated row must exist")
156                                            .clone(),
157                                    )
158                                })
159                                .collect(),
160                        },
161                    });
162                }
163            }
164            events.insert(
165                event_id.clone(),
166                RealizedEventState {
167                    event: RealizedSerialEvent {
168                        event_id: event_id.clone(),
169                        onset: unit_onset,
170                        duration: spec.duration,
171                        is_rest: matches!(spec.sound, crate::EventSound::Rest),
172                        ties_into_next: matches!(spec.tie, TiePolicy::IntoNext),
173                    },
174                    note_indexes,
175                },
176            );
177        }
178        cursor += match context.simultaneous_policy {
179            crate::SimultaneousRenderPolicy::PreserveOnset => unit_duration,
180        };
181    }
182
183    apply_ties(plan, context, &mut events, &mut notes)?;
184
185    let realized_events = events
186        .into_values()
187        .map(|state| state.event)
188        .collect::<Vec<_>>();
189    let evidence_ids = vec![
190        EvidenceId::new("evidence/strict-specs").expect("evidence id"),
191        EvidenceId::new("evidence/typed-origin").expect("evidence id"),
192    ];
193    let ledger = InvariantLedger::new(vec![
194        InvariantLedgerEntry::new(
195            realizer_id.clone(),
196            "serial ordinal order remains identical to the planned order",
197            "chromatic realization kept the planned event and ordinal traversal order intact",
198            InvariantStatus::Preserved,
199            vec![EvidenceId::new("evidence/strict-ordinal-order").expect("evidence id")],
200            None,
201        )
202        .with_invariant_id("serial/ordinal-order"),
203        InvariantLedgerEntry::new(
204            realizer_id.clone(),
205            "the chromatic aggregate remains unchanged under strict realization",
206            "strict chromatic realization preserved every source pitch class exactly",
207            InvariantStatus::Preserved,
208            vec![EvidenceId::new("evidence/strict-chromatic-aggregate").expect("evidence id")],
209            None,
210        )
211        .with_invariant_id("serial/chromatic-aggregate"),
212        InvariantLedgerEntry::new(
213            realizer_id.clone(),
214            "every realized note retains typed serial provenance and explicit strict event specs",
215            format!(
216                "realized {} events and {} sounding notes through {}",
217                realized_events.len(),
218                notes.len(),
219                realizer_id
220            ),
221            InvariantStatus::Preserved,
222            evidence_ids,
223            None,
224        ),
225    ]);
226
227    Ok(SerialRealization::new(
228        plan.clone(),
229        realized_events,
230        notes,
231        ledger,
232    ))
233}
234
235fn event_displacements(
236    spec: &StrictEventSpec,
237    ordinals: usize,
238    event_id: &SerialEventId,
239) -> Result<Vec<i8>, StrictRealizationError> {
240    match spec.pitch_layout.octave_displacements.len() {
241        0 => Ok(vec![0; ordinals]),
242        1 => Ok(vec![spec.pitch_layout.octave_displacements[0]; ordinals]),
243        len if len == ordinals => Ok(spec.pitch_layout.octave_displacements.clone()),
244        len => Err(StrictRealizationError::OctaveDisplacementMismatch {
245            event_id: event_id.clone(),
246            ordinals,
247            displacements: len,
248        }),
249    }
250}
251
252fn build_units(plan: &SerialPlan) -> Vec<EventUnit> {
253    let grouped = plan
254        .simultaneous_groups()
255        .into_iter()
256        .map(|(group, events)| {
257            let mut members = events
258                .iter()
259                .map(|event| event.id.clone())
260                .collect::<Vec<_>>();
261            members.sort();
262            let label = format!("group/{group}");
263            (group, event_unit(plan, label, members))
264        })
265        .collect::<BTreeMap<_, _>>();
266    let mut units = grouped.into_values().collect::<Vec<_>>();
267    let grouped_ids = units
268        .iter()
269        .flat_map(|unit| unit.members.iter().cloned())
270        .collect::<BTreeSet<_>>();
271    for event in plan.events().values() {
272        if !grouped_ids.contains(&event.id) {
273            units.push(event_unit(
274                plan,
275                event.id.as_str().to_owned(),
276                vec![event.id.clone()],
277            ));
278        }
279    }
280    units.sort_by(|left, right| left.key.cmp(&right.key));
281    units
282}
283
284fn event_unit(plan: &SerialPlan, label: String, members: Vec<SerialEventId>) -> EventUnit {
285    let key = members
286        .iter()
287        .filter_map(|event_id| plan.event(event_id))
288        .flat_map(|event| event.ordinals.iter())
289        .min_by(|left, right| {
290            left.row_id
291                .cmp(&right.row_id)
292                .then_with(|| left.ordinal.cmp(&right.ordinal))
293        })
294        .map(|ordinal| UnitKey {
295            row_id: ordinal.row_id.as_str().to_owned(),
296            ordinal: ordinal.ordinal,
297            label: label.clone(),
298        })
299        .unwrap_or(UnitKey {
300            row_id: String::new(),
301            ordinal: 0,
302            label: label.clone(),
303        });
304    EventUnit { members, key }
305}
306
307fn topo_units(
308    plan: &SerialPlan,
309    units: &[EventUnit],
310    unit_index: &BTreeMap<SerialEventId, usize>,
311) -> Vec<usize> {
312    let mut indegree = vec![0usize; units.len()];
313    let mut outgoing = vec![BTreeSet::<usize>::new(); units.len()];
314    for (before, after) in plan.precedence().edges() {
315        let before_idx = unit_index[before];
316        let after_idx = unit_index[after];
317        if before_idx != after_idx && outgoing[before_idx].insert(after_idx) {
318            indegree[after_idx] += 1;
319        }
320    }
321    let mut heap = BinaryHeap::<Reverse<(UnitKey, usize)>>::new();
322    for (index, unit) in units.iter().enumerate() {
323        if indegree[index] == 0 {
324            heap.push(Reverse((unit.key.clone(), index)));
325        }
326    }
327    let mut order = Vec::with_capacity(units.len());
328    while let Some(Reverse((_, index))) = heap.pop() {
329        order.push(index);
330        for &target in &outgoing[index] {
331            indegree[target] -= 1;
332            if indegree[target] == 0 {
333                heap.push(Reverse((units[target].key.clone(), target)));
334            }
335        }
336    }
337    order
338}
339
340fn apply_ties(
341    plan: &SerialPlan,
342    context: &RealizationContext,
343    events: &mut BTreeMap<SerialEventId, RealizedEventState>,
344    notes: &mut Vec<RealizedSerialNote>,
345) -> Result<(), StrictRealizationError> {
346    let mut by_voice = plan
347        .events()
348        .values()
349        .map(|event| event.voice.clone())
350        .collect::<BTreeSet<_>>()
351        .into_iter()
352        .map(|voice| {
353            let mut ids = events
354                .values()
355                .filter(|state| {
356                    plan.event(&state.event.event_id)
357                        .is_some_and(|event| event.voice == voice)
358                })
359                .map(|state| state.event.event_id.clone())
360                .collect::<Vec<_>>();
361            ids.sort_by(|left, right| {
362                let left_state = &events[left];
363                let right_state = &events[right];
364                left_state
365                    .event
366                    .onset
367                    .cmp(&right_state.event.onset)
368                    .then_with(|| left.cmp(right))
369            });
370            (voice, ids)
371        })
372        .collect::<BTreeMap<_, _>>();
373
374    for event_ids in by_voice.values_mut() {
375        let mut index = 0usize;
376        while index < event_ids.len() {
377            let current_id = event_ids[index].clone();
378            let spec = context
379                .specs
380                .get(&current_id)
381                .expect("specs validated up front");
382            if !matches!(spec.tie, TiePolicy::IntoNext) {
383                index += 1;
384                continue;
385            }
386            let Some(next_id) = event_ids.get(index + 1).cloned() else {
387                return Err(StrictRealizationError::MissingTieTarget(current_id));
388            };
389            let current_indexes = events[&current_id].note_indexes.clone();
390            let next_indexes = events[&next_id].note_indexes.clone();
391            if current_indexes.len() != next_indexes.len() {
392                return Err(StrictRealizationError::InvalidTieTarget {
393                    source_event: current_id,
394                    target_event: next_id,
395                    reason: "pitch multiplicity differs",
396                });
397            }
398            if current_indexes.is_empty() {
399                return Err(StrictRealizationError::InvalidTieTarget {
400                    source_event: current_id,
401                    target_event: next_id,
402                    reason: "rests cannot tie",
403                });
404            }
405            let current_pitches = current_indexes
406                .iter()
407                .map(|&note_index| notes[note_index].note.pitch)
408                .collect::<Vec<_>>();
409            let next_pitches = next_indexes
410                .iter()
411                .map(|&note_index| notes[note_index].note.pitch)
412                .collect::<Vec<_>>();
413            if current_pitches != next_pitches {
414                return Err(StrictRealizationError::InvalidTieTarget {
415                    source_event: current_id,
416                    target_event: next_id,
417                    reason: "tied pitches differ",
418                });
419            }
420            let extension = events[&next_id].event.duration;
421            for &note_index in &current_indexes {
422                let note = &mut notes[note_index];
423                note.note.duration += extension;
424            }
425            for &note_index in next_indexes.iter().rev() {
426                notes.remove(note_index);
427                for state in events.values_mut() {
428                    for index in &mut state.note_indexes {
429                        if *index > note_index {
430                            *index -= 1;
431                        }
432                    }
433                }
434            }
435            events
436                .get_mut(&next_id)
437                .expect("event must exist")
438                .note_indexes
439                .clear();
440            events
441                .get_mut(&next_id)
442                .expect("event must exist")
443                .event
444                .is_rest = true;
445            index += 2;
446        }
447    }
448    Ok(())
449}