Skip to main content

sim_lib_music_serial/
integral.rs

1//! Generic integral-serial parameter tracks with independent phasing and exhaustion.
2
3use std::any::Any;
4use std::collections::BTreeMap;
5use std::fmt::Debug;
6
7use sim_lib_music_core::{Articulation, Time};
8use sim_lib_serial_core::{AggregateRule, AlphabetId, SeriesTransform};
9use thiserror::Error;
10
11use crate::{ParameterAlphabet, ParameterError, ParameterSeries, ParameterValue};
12
13/// Exhaustion policy for one parameter track when a plan outlives the source series.
14#[derive(Copy, Clone, Debug, PartialEq, Eq)]
15pub enum Exhaustion {
16    /// Wrap around the source series indefinitely.
17    Cycle,
18    /// Stop emitting once the source series is consumed and shorten the projection.
19    Truncate,
20    /// Stop emitting once the source series is consumed but retain explicit omitted plan ordinals.
21    OneShot,
22}
23
24/// One emitted parameter value together with its source ordinal provenance.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct ParameterStep<T: ParameterValue> {
27    /// Zero-based position in the owning integral plan.
28    pub plan_ordinal: usize,
29    /// Zero-based source ordinal within the parameter series.
30    pub parameter_ordinal: usize,
31    /// Zero-based wrap count for cyclic reuse.
32    pub cycle: usize,
33    /// Typed parameter value emitted at this plan ordinal.
34    pub value: T,
35}
36
37/// Projection of one parameter track against a requested plan length.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct ParameterProjection<T: ParameterValue> {
40    name: String,
41    alphabet_id: AlphabetId,
42    source_len: usize,
43    phase: usize,
44    exhaustion: Exhaustion,
45    plan_len: usize,
46    steps: Vec<ParameterStep<T>>,
47    omitted_plan_ordinals: Vec<usize>,
48}
49
50impl<T: ParameterValue> ParameterProjection<T> {
51    /// Returns the stable parameter name.
52    pub fn name(&self) -> &str {
53        &self.name
54    }
55
56    /// Returns the retained source alphabet identity.
57    pub fn alphabet_id(&self) -> &AlphabetId {
58        &self.alphabet_id
59    }
60
61    /// Returns the source series length before phasing or exhaustion.
62    pub fn source_len(&self) -> usize {
63        self.source_len
64    }
65
66    /// Returns the phase offset applied before the first emitted value.
67    pub fn phase(&self) -> usize {
68        self.phase
69    }
70
71    /// Returns the exhaustion policy used by this projection.
72    pub const fn exhaustion(&self) -> Exhaustion {
73        self.exhaustion
74    }
75
76    /// Returns the requested target plan length.
77    pub fn plan_len(&self) -> usize {
78        self.plan_len
79    }
80
81    /// Returns the emitted steps with their parameter ordinal ledger.
82    pub fn steps(&self) -> &[ParameterStep<T>] {
83        &self.steps
84    }
85
86    /// Returns plan ordinals that intentionally produced no value under one-shot exhaustion.
87    pub fn omitted_plan_ordinals(&self) -> &[usize] {
88        &self.omitted_plan_ordinals
89    }
90}
91
92/// One generic named parameter track over an unchanged serial-core series.
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct ParameterTrack<T: ParameterValue> {
95    name: String,
96    series: ParameterSeries<T>,
97    phase: usize,
98    exhaustion: Exhaustion,
99}
100
101impl<T: ParameterValue> ParameterTrack<T> {
102    /// Constructs one exhaustive exactly-once track from a caller-declared value ladder.
103    pub fn try_new(
104        name: impl Into<String>,
105        values: Vec<T>,
106        exhaustion: Exhaustion,
107    ) -> Result<Self, IntegralError> {
108        let name = validate_parameter_name(name.into())?;
109        Ok(Self {
110            series: ParameterSeries::try_new(parameter_alphabet_id(&name), values)?,
111            name,
112            phase: 0,
113            exhaustion,
114        })
115    }
116
117    /// Constructs one track from a caller-declared aggregate rule.
118    pub fn try_new_with_rule(
119        name: impl Into<String>,
120        rule: AggregateRule,
121        values: Vec<T>,
122        exhaustion: Exhaustion,
123    ) -> Result<Self, IntegralError> {
124        let name = validate_parameter_name(name.into())?;
125        Ok(Self {
126            series: ParameterSeries::try_new_with_rule(parameter_alphabet_id(&name), rule, values)?,
127            name,
128            phase: 0,
129            exhaustion,
130        })
131    }
132
133    /// Returns this track with an explicit phase offset.
134    pub fn with_phase(mut self, phase: usize) -> Self {
135        self.phase = phase;
136        self
137    }
138
139    /// Returns the stable track name.
140    pub fn name(&self) -> &str {
141        &self.name
142    }
143
144    /// Returns the retained typed series.
145    pub fn series(&self) -> &ParameterSeries<T> {
146        &self.series
147    }
148
149    /// Returns the configured phase offset.
150    pub fn phase(&self) -> usize {
151        self.phase
152    }
153
154    /// Returns the configured exhaustion policy.
155    pub const fn exhaustion(&self) -> Exhaustion {
156        self.exhaustion
157    }
158
159    /// Applies a serial-core transform to this track without touching other tracks.
160    pub fn transformed(
161        &self,
162        transform: &SeriesTransform<ParameterAlphabet<T>>,
163    ) -> Result<Self, IntegralError> {
164        Ok(Self {
165            name: self.name.clone(),
166            series: self.series.apply(transform)?,
167            phase: self.phase,
168            exhaustion: self.exhaustion,
169        })
170    }
171
172    /// Projects this track against a requested integral-plan length.
173    pub fn project(&self, plan_len: usize) -> ParameterProjection<T> {
174        let source = self.series.order();
175        let source_len = source.len();
176        let mut steps = Vec::new();
177        let mut omitted_plan_ordinals = Vec::new();
178
179        if source_len == 0 {
180            return ParameterProjection {
181                name: self.name.clone(),
182                alphabet_id: self.series.alphabet().id().clone(),
183                source_len,
184                phase: self.phase,
185                exhaustion: self.exhaustion,
186                plan_len,
187                steps,
188                omitted_plan_ordinals,
189            };
190        }
191
192        let base_phase = match self.exhaustion {
193            Exhaustion::Cycle => self.phase % source_len,
194            Exhaustion::Truncate | Exhaustion::OneShot => self.phase,
195        };
196        for plan_ordinal in 0..plan_len {
197            let absolute = base_phase + plan_ordinal;
198            match self.exhaustion {
199                Exhaustion::Cycle => {
200                    let parameter_ordinal = absolute % source_len;
201                    let cycle = absolute / source_len;
202                    steps.push(ParameterStep {
203                        plan_ordinal,
204                        parameter_ordinal,
205                        cycle,
206                        value: source[parameter_ordinal].clone(),
207                    });
208                }
209                Exhaustion::Truncate => {
210                    if absolute >= source_len {
211                        break;
212                    }
213                    steps.push(ParameterStep {
214                        plan_ordinal,
215                        parameter_ordinal: absolute,
216                        cycle: 0,
217                        value: source[absolute].clone(),
218                    });
219                }
220                Exhaustion::OneShot => {
221                    if absolute >= source_len {
222                        omitted_plan_ordinals.push(plan_ordinal);
223                        continue;
224                    }
225                    steps.push(ParameterStep {
226                        plan_ordinal,
227                        parameter_ordinal: absolute,
228                        cycle: 0,
229                        value: source[absolute].clone(),
230                    });
231                }
232            }
233        }
234
235        ParameterProjection {
236            name: self.name.clone(),
237            alphabet_id: self.series.alphabet().id().clone(),
238            source_len,
239            phase: self.phase,
240            exhaustion: self.exhaustion,
241            plan_len,
242            steps,
243            omitted_plan_ordinals,
244        }
245    }
246}
247
248/// Source-ordinal ledger entry retained after one track is bound into a plan.
249#[derive(Clone, Debug, PartialEq, Eq)]
250pub struct ParameterOrdinalLedgerEntry {
251    /// Zero-based position in the integral plan.
252    pub plan_ordinal: usize,
253    /// Zero-based source ordinal inside the parameter track.
254    pub parameter_ordinal: usize,
255    /// Zero-based wrap count for cyclic reuse.
256    pub cycle: usize,
257}
258
259/// One typed bound track retained inside an [`IntegralPlan`].
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct BoundParameterTrack<T: ParameterValue> {
262    track: ParameterTrack<T>,
263    projection: ParameterProjection<T>,
264}
265
266impl<T: ParameterValue> BoundParameterTrack<T> {
267    /// Returns the original typed track.
268    pub fn track(&self) -> &ParameterTrack<T> {
269        &self.track
270    }
271
272    /// Returns the projected values and omitted ordinals for this plan.
273    pub fn projection(&self) -> &ParameterProjection<T> {
274        &self.projection
275    }
276
277    /// Returns the source-ordinal ledger retained by the track binding.
278    pub fn ordinal_ledger(&self) -> Vec<ParameterOrdinalLedgerEntry> {
279        self.projection
280            .steps()
281            .iter()
282            .map(|step| ParameterOrdinalLedgerEntry {
283                plan_ordinal: step.plan_ordinal,
284                parameter_ordinal: step.parameter_ordinal,
285                cycle: step.cycle,
286            })
287            .collect()
288    }
289}
290
291/// Type-erased view of one parameter track bound into an integral plan.
292pub trait ErasedParameterBinding: Debug + Send + Sync {
293    /// Returns the stable parameter name.
294    fn name(&self) -> &str;
295    /// Returns the source alphabet identity retained by this binding.
296    fn alphabet_id(&self) -> &AlphabetId;
297    /// Returns the configured phase offset.
298    fn phase(&self) -> usize;
299    /// Returns the configured exhaustion policy.
300    fn exhaustion(&self) -> Exhaustion;
301    /// Returns the source series length.
302    fn source_len(&self) -> usize;
303    /// Returns the requested plan length.
304    fn plan_len(&self) -> usize;
305    /// Returns the retained ordinal ledger for emitted values.
306    fn ordinal_ledger(&self) -> Vec<ParameterOrdinalLedgerEntry>;
307    /// Returns one debug rendering per emitted value in plan order.
308    fn debug_values(&self) -> Vec<String>;
309    /// Returns one debug rendering for every omitted one-shot plan ordinal.
310    fn omitted_plan_ordinals(&self) -> &[usize];
311    /// Returns a downcast hook for typed access.
312    fn as_any(&self) -> &dyn Any;
313}
314
315impl<T: ParameterValue + Send + Sync> ErasedParameterBinding for BoundParameterTrack<T> {
316    fn name(&self) -> &str {
317        self.track.name()
318    }
319
320    fn alphabet_id(&self) -> &AlphabetId {
321        self.projection.alphabet_id()
322    }
323
324    fn phase(&self) -> usize {
325        self.track.phase()
326    }
327
328    fn exhaustion(&self) -> Exhaustion {
329        self.track.exhaustion()
330    }
331
332    fn source_len(&self) -> usize {
333        self.projection.source_len()
334    }
335
336    fn plan_len(&self) -> usize {
337        self.projection.plan_len()
338    }
339
340    fn ordinal_ledger(&self) -> Vec<ParameterOrdinalLedgerEntry> {
341        BoundParameterTrack::ordinal_ledger(self)
342    }
343
344    fn debug_values(&self) -> Vec<String> {
345        self.projection
346            .steps()
347            .iter()
348            .map(|step| format!("{:?}", step.value))
349            .collect()
350    }
351
352    fn omitted_plan_ordinals(&self) -> &[usize] {
353        self.projection.omitted_plan_ordinals()
354    }
355
356    fn as_any(&self) -> &dyn Any {
357        self
358    }
359}
360
361/// Inspectable integral-serial plan with separately bound parameter tracks.
362#[derive(Debug)]
363pub struct IntegralPlan {
364    length: usize,
365    parameters: BTreeMap<String, Box<dyn ErasedParameterBinding>>,
366}
367
368impl IntegralPlan {
369    /// Constructs an empty plan that projects every bound track against `length`.
370    pub fn new(length: usize) -> Self {
371        Self {
372            length,
373            parameters: BTreeMap::new(),
374        }
375    }
376
377    /// Returns the target plan length used by every parameter binding.
378    pub fn length(&self) -> usize {
379        self.length
380    }
381
382    /// Binds one typed parameter track while preserving its independent ordinal ledger.
383    pub fn bind_parameter<T: ParameterValue + Send + Sync + 'static>(
384        &mut self,
385        track: ParameterTrack<T>,
386    ) -> Result<(), IntegralError> {
387        if self.parameters.contains_key(track.name()) {
388            return Err(IntegralError::DuplicateTrack(track.name().to_owned()));
389        }
390        let projection = track.project(self.length);
391        let name = track.name().to_owned();
392        self.parameters
393            .insert(name, Box::new(BoundParameterTrack { track, projection }));
394        Ok(())
395    }
396
397    /// Returns one type-erased bound parameter by stable name.
398    pub fn parameter(&self, name: &str) -> Option<&dyn ErasedParameterBinding> {
399        self.parameters.get(name).map(Box::as_ref)
400    }
401
402    /// Returns one typed bound parameter when `name` and `T` both match.
403    pub fn typed_parameter<T: ParameterValue>(
404        &self,
405        name: &str,
406    ) -> Option<&BoundParameterTrack<T>> {
407        self.parameters
408            .get(name)
409            .and_then(|binding| binding.as_any().downcast_ref::<BoundParameterTrack<T>>())
410    }
411
412    /// Returns the stable parameter names in sorted order.
413    pub fn parameter_names(&self) -> Vec<&str> {
414        self.parameters.keys().map(String::as_str).collect()
415    }
416}
417
418/// Track alias for exact duration values.
419pub type DurationTrack = ParameterTrack<Time>;
420/// Track alias for MIDI-style dynamic values.
421pub type DynamicsTrack = ParameterTrack<u8>;
422/// Track alias for MIDI-style register placement.
423pub type RegisterTrack = ParameterTrack<i8>;
424/// Track alias for articulation values from music-core.
425pub type ArticulationTrack = ParameterTrack<Articulation>;
426/// Track alias for caller-declared timbre labels.
427pub type TimbreTrack = ParameterTrack<String>;
428
429/// Failure while constructing or binding integral parameter tracks.
430#[derive(Clone, Debug, PartialEq, Eq, Error)]
431pub enum IntegralError {
432    /// The parameter name was empty or used invalid characters.
433    #[error("invalid parameter name {0:?}")]
434    InvalidParameterName(String),
435    /// The track tried to reuse a name already bound in the plan.
436    #[error("parameter track {0} is already bound")]
437    DuplicateTrack(String),
438    /// Constructing or transforming the generic parameter series failed.
439    #[error(transparent)]
440    Parameter(#[from] ParameterError),
441}
442
443fn validate_parameter_name(name: String) -> Result<String, IntegralError> {
444    if name.trim().is_empty() {
445        return Err(IntegralError::InvalidParameterName(name));
446    }
447    if name
448        .chars()
449        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
450    {
451        return Err(IntegralError::InvalidParameterName(name));
452    }
453    Ok(name)
454}
455
456fn parameter_alphabet_id(name: &str) -> String {
457    format!("parameter/{name}-v1")
458}