1use 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
15pub fn strict_chromatic_realizer_id() -> RealizerId {
17 RealizerId::new("realizer/strict-chromatic").expect("built-in realizer id is valid")
18}
19
20#[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(¤t_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[¤t_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(|¬e_index| notes[note_index].note.pitch)
408 .collect::<Vec<_>>();
409 let next_pitches = next_indexes
410 .iter()
411 .map(|¬e_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 ¬e_index in ¤t_indexes {
422 let note = &mut notes[note_index];
423 note.note.duration += extension;
424 }
425 for ¬e_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}