1use std::collections::BTreeMap;
4
5use sim_lib_music_core::{Note, Pitch, Time};
6use sim_lib_pitch_serial::RowForm;
7use thiserror::Error;
8
9use crate::{
10 InvariantLedger, OrdinalRef, RealizerId, SerialEventId, SerialPlan, SerialSpineReport,
11 StructuralLicense, VoiceId,
12};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct RealizedSerialOrigin {
17 pub realizer_id: RealizerId,
19 pub licenses: Vec<StructuralLicense>,
21 pub ordinals: Vec<OrdinalRef>,
23 pub source_ordinal: OrdinalRef,
25 pub row_forms: BTreeMap<crate::RowInstanceId, RowForm>,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct RealizedSerialNote {
32 pub event_id: SerialEventId,
34 pub voice: VoiceId,
36 pub note_index: usize,
38 pub onset: Time,
40 pub note: Note,
42 pub origin: RealizedSerialOrigin,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct RealizedSerialEvent {
49 pub event_id: SerialEventId,
51 pub onset: Time,
53 pub duration: Time,
55 pub is_rest: bool,
57 pub ties_into_next: bool,
59}
60
61#[derive(Clone, Debug, PartialEq)]
63pub struct SerialRealization {
64 plan: SerialPlan,
65 events: Vec<RealizedSerialEvent>,
66 notes: Vec<RealizedSerialNote>,
67 ledger: InvariantLedger<RealizerId>,
68 spine_report: Option<SerialSpineReport>,
69}
70
71impl SerialRealization {
72 pub fn new(
74 plan: SerialPlan,
75 events: Vec<RealizedSerialEvent>,
76 notes: Vec<RealizedSerialNote>,
77 ledger: InvariantLedger<RealizerId>,
78 ) -> Self {
79 Self::new_with_spine(plan, events, notes, ledger, None)
80 }
81
82 pub fn new_with_spine(
84 plan: SerialPlan,
85 mut events: Vec<RealizedSerialEvent>,
86 mut notes: Vec<RealizedSerialNote>,
87 ledger: InvariantLedger<RealizerId>,
88 spine_report: Option<SerialSpineReport>,
89 ) -> Self {
90 events.sort_by(|left, right| {
91 left.onset
92 .cmp(&right.onset)
93 .then_with(|| left.event_id.cmp(&right.event_id))
94 });
95 notes.sort_by(|left, right| {
96 left.onset
97 .cmp(&right.onset)
98 .then_with(|| left.voice.cmp(&right.voice))
99 .then_with(|| left.note.pitch.cmp(&right.note.pitch))
100 .then_with(|| left.event_id.cmp(&right.event_id))
101 .then_with(|| left.note_index.cmp(&right.note_index))
102 });
103 Self {
104 plan,
105 events,
106 notes,
107 ledger,
108 spine_report,
109 }
110 }
111
112 pub fn plan(&self) -> &SerialPlan {
114 &self.plan
115 }
116
117 pub fn events(&self) -> &[RealizedSerialEvent] {
119 &self.events
120 }
121
122 pub fn notes(&self) -> &[RealizedSerialNote] {
124 &self.notes
125 }
126
127 pub fn sounding_pitches(&self) -> Vec<Pitch> {
129 self.notes.iter().map(|note| note.note.pitch).collect()
130 }
131
132 pub fn ledger(&self) -> &InvariantLedger<RealizerId> {
134 &self.ledger
135 }
136
137 pub fn spine_report(&self) -> Option<&SerialSpineReport> {
139 self.spine_report.as_ref()
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq, Error)]
145pub enum StrictRealizationError {
146 #[error("serial realizer {0} is not registered")]
148 UnknownRealizer(RealizerId),
149 #[error("plan event {0} is missing a strict realization spec")]
151 MissingSpec(SerialEventId),
152 #[error("event {event_id} realizes MIDI pitch {midi}, outside 0..=127")]
154 MidiOutOfRange {
155 event_id: SerialEventId,
157 midi: i16,
159 },
160 #[error("event {0} must have a strictly positive duration")]
162 NonPositiveDuration(SerialEventId),
163 #[error("event {event_id} has {ordinals} ordinals but {displacements} octave displacements")]
165 OctaveDisplacementMismatch {
166 event_id: SerialEventId,
168 ordinals: usize,
170 displacements: usize,
172 },
173 #[error("event {0} ties into the next event, but no later same-voice event exists")]
175 MissingTieTarget(SerialEventId),
176 #[error("event {source_event} cannot tie into {target_event}: {reason}")]
178 InvalidTieTarget {
179 source_event: SerialEventId,
181 target_event: SerialEventId,
183 reason: &'static str,
185 },
186 #[error("music-core rendering failed: {0}")]
188 MusicCore(String),
189 #[error("serial realizer {0} requires a modal scale in the realization context")]
191 MissingModalScale(RealizerId),
192 #[error("pitch-map adaptation failed: {0}")]
194 PitchMap(String),
195}