Skip to main content

sim_lib_music_serial/
realizer.rs

1//! Open serial realizer identities, context, and registry-facing traits.
2
3use std::any::Any;
4use std::collections::BTreeMap;
5use std::fmt::{Display, Formatter};
6use std::sync::Arc;
7
8use sim_lib_music_core::{Articulation, Channel, Time};
9use sim_lib_pitch_dissonance::ContextualSonanceOptions;
10use sim_lib_pitch_scale::{PlayerScale, Scale};
11use sim_lib_sound_tuning::Tuning;
12
13use crate::{ErasedParameterBinding, SerialEventId, SerialPlan, SerialRealization, VoiceId};
14
15fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
16    let value = value.into();
17    if value.trim().is_empty() {
18        return Err(format!("{kind} cannot be empty"));
19    }
20    if value
21        .chars()
22        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
23    {
24        return Err(format!(
25            "{kind} must use ASCII letters, digits, /, -, _, or ."
26        ));
27    }
28    Ok(value)
29}
30
31/// Stable identity for one registered serial realizer.
32#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct RealizerId(String);
34
35impl RealizerId {
36    /// Creates a validated stable identifier.
37    pub fn new(value: impl Into<String>) -> Result<Self, String> {
38        Ok(Self(validate_id("realizer-id", value)?))
39    }
40
41    /// Returns the stable wire text.
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45}
46
47impl Display for RealizerId {
48    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
49        formatter.write_str(&self.0)
50    }
51}
52
53/// How one planned event should sound.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum EventSound {
56    /// Sound one note per ordinal.
57    Notes,
58    /// Occupy time silently.
59    Rest,
60}
61
62/// How octave placement is derived for one event's ordinals.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct StrictPitchLayout {
65    /// MIDI-style octave register, where `4` means the `C4..B4` octave.
66    pub register: i8,
67    /// Per-ordinal octave displacements relative to `register`.
68    pub octave_displacements: Vec<i8>,
69}
70
71impl StrictPitchLayout {
72    /// Places every ordinal in the same octave register.
73    pub fn in_register(register: i8) -> Self {
74        Self {
75            register,
76            octave_displacements: Vec::new(),
77        }
78    }
79}
80
81/// Explicit tie behavior for one event.
82#[derive(Copy, Clone, Debug, PartialEq, Eq)]
83pub enum TiePolicy {
84    /// Re-articulate normally.
85    None,
86    /// Sustain the same pitches into the next same-voice event and suppress its attack.
87    IntoNext,
88}
89
90/// How explicit simultaneous groups should be rendered.
91#[derive(Copy, Clone, Debug, PartialEq, Eq)]
92pub enum SimultaneousRenderPolicy {
93    /// Keep every simultaneous group at one exact onset and advance by the longest member.
94    PreserveOnset,
95}
96
97/// Explicit realization choices for one planned event.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct StrictEventSpec {
100    /// Sounding or silent behavior.
101    pub sound: EventSound,
102    /// Pitch placement policy.
103    pub pitch_layout: StrictPitchLayout,
104    /// Exact occupied duration.
105    pub duration: Time,
106    /// MIDI velocity.
107    pub velocity: u8,
108    /// MIDI channel.
109    pub channel: Channel,
110    /// Articulation to apply when the event sounds.
111    pub articulation: Articulation,
112    /// Explicit tie behavior.
113    pub tie: TiePolicy,
114}
115
116impl StrictEventSpec {
117    /// Constructs a sounding note spec with explicit pitch/timing/dynamic choices.
118    pub fn notes(
119        register: i8,
120        duration: Time,
121        velocity: u8,
122        channel: Channel,
123        articulation: Articulation,
124    ) -> Self {
125        Self {
126            sound: EventSound::Notes,
127            pitch_layout: StrictPitchLayout::in_register(register),
128            duration,
129            velocity,
130            channel,
131            articulation,
132            tie: TiePolicy::None,
133        }
134    }
135
136    /// Constructs a silent span with explicit timing.
137    pub fn rest(duration: Time) -> Self {
138        Self {
139            sound: EventSound::Rest,
140            pitch_layout: StrictPitchLayout::in_register(4),
141            duration,
142            velocity: 0,
143            channel: Channel::new(0).expect("MIDI channel zero is valid"),
144            articulation: Articulation::Normal,
145            tie: TiePolicy::None,
146        }
147    }
148}
149
150/// Inclusive register policy for one voice.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct RegisterBounds {
153    /// Lowest allowed octave register.
154    pub lowest: i8,
155    /// Highest allowed octave register.
156    pub highest: i8,
157}
158
159/// Generic voice-level realization policy retained in open context data.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct VoiceBounds {
162    /// Maximum simultaneous note count allowed for the voice, if any.
163    pub max_notes_per_event: Option<usize>,
164}
165
166/// One type-erased auxiliary service available to realizers.
167pub trait RealizationService: Any + Send + Sync {
168    /// Returns a downcast hook for typed access.
169    fn as_any(&self) -> &dyn Any;
170}
171
172impl<T: Any + Send + Sync> RealizationService for T {
173    fn as_any(&self) -> &dyn Any {
174        self
175    }
176}
177
178/// Open service bag passed through realization context data.
179#[derive(Clone, Default)]
180pub struct RealizationServices {
181    entries: BTreeMap<String, Arc<dyn RealizationService>>,
182}
183
184impl RealizationServices {
185    /// Creates an empty service bag.
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Registers or replaces one named service.
191    pub fn insert(
192        &mut self,
193        name: impl Into<String>,
194        service: Arc<dyn RealizationService>,
195    ) -> Option<Arc<dyn RealizationService>> {
196        self.entries.insert(name.into(), service)
197    }
198
199    /// Returns one typed service by stable name.
200    pub fn get<T: Any + Send + Sync>(&self, name: &str) -> Option<&T> {
201        self.entries
202            .get(name)
203            .and_then(|service| service.as_ref().as_any().downcast_ref::<T>())
204    }
205
206    /// Returns the stable service names in sorted order.
207    pub fn names(&self) -> Vec<&str> {
208        self.entries.keys().map(String::as_str).collect()
209    }
210}
211
212impl std::fmt::Debug for RealizationServices {
213    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
214        formatter
215            .debug_struct("RealizationServices")
216            .field("names", &self.names())
217            .finish()
218    }
219}
220
221impl PartialEq for RealizationServices {
222    fn eq(&self, other: &Self) -> bool {
223        self.names() == other.names()
224    }
225}
226
227impl Eq for RealizationServices {}
228
229/// Complete explicit choices and open services required to realize one serial plan.
230#[derive(Clone)]
231pub struct RealizationContext {
232    /// One explicit strict spec per planned event.
233    pub specs: BTreeMap<SerialEventId, StrictEventSpec>,
234    /// Policy for explicit simultaneous groups.
235    pub simultaneous_policy: SimultaneousRenderPolicy,
236    /// Optional target scale retained for realizers that need scale awareness.
237    pub scale: Option<Scale>,
238    /// Optional caller-defined performance scale used by modal spine realizers.
239    pub modal_scale: Option<PlayerScale>,
240    /// Optional target tuning retained for pitch/frequency-aware realizers.
241    pub tuning: Option<Arc<dyn Tuning>>,
242    /// Optional contextual-sonance policy used by adaptation reports.
243    pub contextual_sonance: Option<ContextualSonanceOptions>,
244    /// Optional per-voice register policy.
245    pub register_bounds: BTreeMap<VoiceId, RegisterBounds>,
246    /// Optional per-voice realization limits.
247    pub voice_bounds: BTreeMap<VoiceId, VoiceBounds>,
248    /// Optional typed parameter tracks exposed by stable name.
249    pub parameter_tracks: BTreeMap<String, Arc<dyn ErasedParameterBinding>>,
250    /// Open auxiliary services keyed by stable name.
251    pub services: RealizationServices,
252}
253
254impl RealizationContext {
255    /// Builds a context from explicit specs using the default simultaneous policy.
256    pub fn new(specs: BTreeMap<SerialEventId, StrictEventSpec>) -> Self {
257        Self {
258            specs,
259            simultaneous_policy: SimultaneousRenderPolicy::PreserveOnset,
260            scale: None,
261            modal_scale: None,
262            tuning: None,
263            contextual_sonance: None,
264            register_bounds: BTreeMap::new(),
265            voice_bounds: BTreeMap::new(),
266            parameter_tracks: BTreeMap::new(),
267            services: RealizationServices::default(),
268        }
269    }
270}
271
272impl std::fmt::Debug for RealizationContext {
273    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
274        formatter
275            .debug_struct("RealizationContext")
276            .field("specs", &self.specs)
277            .field("simultaneous_policy", &self.simultaneous_policy)
278            .field("scale", &self.scale)
279            .field("modal_scale", &self.modal_scale)
280            .field("tuning", &self.tuning.as_ref().map(|tuning| tuning.name()))
281            .field("contextual_sonance", &self.contextual_sonance)
282            .field("register_bounds", &self.register_bounds)
283            .field("voice_bounds", &self.voice_bounds)
284            .field(
285                "parameter_tracks",
286                &self.parameter_tracks.keys().collect::<Vec<_>>(),
287            )
288            .field("services", &self.services)
289            .finish()
290    }
291}
292
293impl RealizationContext {
294    /// Returns the effective modal scale for open serial adaptation, if any.
295    pub fn effective_modal_scale(&self) -> Option<PlayerScale> {
296        self.modal_scale
297            .clone()
298            .or_else(|| self.scale.map(PlayerScale::from_scale))
299    }
300}
301
302/// Backwards-compatible alias for the former strict-only context name.
303pub type StrictRealizationContext = RealizationContext;
304
305/// Open serial realizer component registered by stable id.
306pub trait SerialRealizer: Send + Sync {
307    /// Returns the stable realizer identity.
308    fn id(&self) -> &RealizerId;
309
310    /// Realizes one immutable serial plan using the supplied context.
311    fn realize(
312        &self,
313        plan: &SerialPlan,
314        context: &RealizationContext,
315    ) -> Result<SerialRealization, crate::StrictRealizationError>;
316}