Skip to main content

sim_lib_music_serial/
realization.rs

1//! Strict serial-plan realization output and failures.
2
3use 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/// Complete serial provenance for one realized sounding note.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct RealizedSerialOrigin {
17    /// Stable realizer identity that produced this note.
18    pub realizer_id: RealizerId,
19    /// Structural readings that license the realized note.
20    pub licenses: Vec<StructuralLicense>,
21    /// Every structural ordinal cited by the planned event.
22    pub ordinals: Vec<OrdinalRef>,
23    /// The specific ordinal realized by this note.
24    pub source_ordinal: OrdinalRef,
25    /// Row forms keyed by the row instances referenced by `ordinals`.
26    pub row_forms: BTreeMap<crate::RowInstanceId, RowForm>,
27}
28
29/// One realized sounding note with stable plan/event provenance.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct RealizedSerialNote {
32    /// Stable planned event identity.
33    pub event_id: SerialEventId,
34    /// Stable voice identity chosen for the note.
35    pub voice: VoiceId,
36    /// Stable ordinal occurrence within the event's rendered note list.
37    pub note_index: usize,
38    /// Exact absolute onset in whole-note units.
39    pub onset: Time,
40    /// Sounding note payload.
41    pub note: Note,
42    /// Serial provenance retained for this note.
43    pub origin: RealizedSerialOrigin,
44}
45
46/// One realized event span, which may sound notes or occupy time as a rest.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct RealizedSerialEvent {
49    /// Stable planned event identity.
50    pub event_id: SerialEventId,
51    /// Exact onset assigned during realization.
52    pub onset: Time,
53    /// Exact occupied duration, whether or not the event sounds notes.
54    pub duration: Time,
55    /// Whether this event occupies silence rather than sounding notes.
56    pub is_rest: bool,
57    /// Whether this event tied into the following same-voice event.
58    pub ties_into_next: bool,
59}
60
61/// Realized serial notes plus exact event spans, retaining the source plan unchanged.
62#[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    /// Builds one exact realization from the preserved plan, event spans, and notes.
73    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    /// Builds one exact realization and attaches an optional adaptation report.
83    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    /// Returns the equality-identical structural source plan.
113    pub fn plan(&self) -> &SerialPlan {
114        &self.plan
115    }
116
117    /// Returns the exact realized event spans in canonical time order.
118    pub fn events(&self) -> &[RealizedSerialEvent] {
119        &self.events
120    }
121
122    /// Returns the realized sounding notes in canonical order.
123    pub fn notes(&self) -> &[RealizedSerialNote] {
124        &self.notes
125    }
126
127    /// Returns the sounding pitches in canonical note order.
128    pub fn sounding_pitches(&self) -> Vec<Pitch> {
129        self.notes.iter().map(|note| note.note.pitch).collect()
130    }
131
132    /// Returns the realizer invariant ledger recorded for this realization.
133    pub fn ledger(&self) -> &InvariantLedger<RealizerId> {
134        &self.ledger
135    }
136
137    /// Returns the optional serial-spine report attached by an adaptive realizer.
138    pub fn spine_report(&self) -> Option<&SerialSpineReport> {
139        self.spine_report.as_ref()
140    }
141}
142
143/// Failure while realizing or rendering a strict serial plan.
144#[derive(Clone, Debug, PartialEq, Eq, Error)]
145pub enum StrictRealizationError {
146    /// A registry lookup named a missing realizer id.
147    #[error("serial realizer {0} is not registered")]
148    UnknownRealizer(RealizerId),
149    /// One plan event lacked an explicit realization spec.
150    #[error("plan event {0} is missing a strict realization spec")]
151    MissingSpec(SerialEventId),
152    /// One event spec named an impossible register or octave displacement result.
153    #[error("event {event_id} realizes MIDI pitch {midi}, outside 0..=127")]
154    MidiOutOfRange {
155        /// Affected event.
156        event_id: SerialEventId,
157        /// Rejected MIDI note number.
158        midi: i16,
159    },
160    /// The caller supplied a non-positive duration.
161    #[error("event {0} must have a strictly positive duration")]
162    NonPositiveDuration(SerialEventId),
163    /// The octave-displacement vector does not match the event's cardinality.
164    #[error("event {event_id} has {ordinals} ordinals but {displacements} octave displacements")]
165    OctaveDisplacementMismatch {
166        /// Affected event.
167        event_id: SerialEventId,
168        /// Event ordinal count.
169        ordinals: usize,
170        /// Supplied displacement count.
171        displacements: usize,
172    },
173    /// A tie requested a following same-voice event that did not exist.
174    #[error("event {0} ties into the next event, but no later same-voice event exists")]
175    MissingTieTarget(SerialEventId),
176    /// A tie target did not realize the same pitch multiplicity.
177    #[error("event {source_event} cannot tie into {target_event}: {reason}")]
178    InvalidTieTarget {
179        /// Source event requesting the tie.
180        source_event: SerialEventId,
181        /// Target event.
182        target_event: SerialEventId,
183        /// Human-readable mismatch reason.
184        reason: &'static str,
185    },
186    /// Rendering through canonical music-core score conversion failed.
187    #[error("music-core rendering failed: {0}")]
188    MusicCore(String),
189    /// A modal or adapted realizer requires a scale context.
190    #[error("serial realizer {0} requires a modal scale in the realization context")]
191    MissingModalScale(RealizerId),
192    /// One adaptation pitch-map operation failed.
193    #[error("pitch-map adaptation failed: {0}")]
194    PitchMap(String),
195}