sim-lib-music-serial 0.1.1

Immutable serial plans with stable row and event identity, explicit provenance, and validated partial temporal order.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
//! Generic integral-serial parameter tracks with independent phasing and exhaustion.

use std::any::Any;
use std::collections::BTreeMap;
use std::fmt::Debug;

use sim_lib_music_core::{Articulation, Time};
use sim_lib_serial_core::{AggregateRule, AlphabetId, SeriesTransform};
use thiserror::Error;

use crate::{ParameterAlphabet, ParameterError, ParameterSeries, ParameterValue};

/// Exhaustion policy for one parameter track when a plan outlives the source series.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Exhaustion {
    /// Wrap around the source series indefinitely.
    Cycle,
    /// Stop emitting once the source series is consumed and shorten the projection.
    Truncate,
    /// Stop emitting once the source series is consumed but retain explicit omitted plan ordinals.
    OneShot,
}

/// One emitted parameter value together with its source ordinal provenance.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParameterStep<T: ParameterValue> {
    /// Zero-based position in the owning integral plan.
    pub plan_ordinal: usize,
    /// Zero-based source ordinal within the parameter series.
    pub parameter_ordinal: usize,
    /// Zero-based wrap count for cyclic reuse.
    pub cycle: usize,
    /// Typed parameter value emitted at this plan ordinal.
    pub value: T,
}

/// Projection of one parameter track against a requested plan length.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParameterProjection<T: ParameterValue> {
    name: String,
    alphabet_id: AlphabetId,
    source_len: usize,
    phase: usize,
    exhaustion: Exhaustion,
    plan_len: usize,
    steps: Vec<ParameterStep<T>>,
    omitted_plan_ordinals: Vec<usize>,
}

impl<T: ParameterValue> ParameterProjection<T> {
    /// Returns the stable parameter name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the retained source alphabet identity.
    pub fn alphabet_id(&self) -> &AlphabetId {
        &self.alphabet_id
    }

    /// Returns the source series length before phasing or exhaustion.
    pub fn source_len(&self) -> usize {
        self.source_len
    }

    /// Returns the phase offset applied before the first emitted value.
    pub fn phase(&self) -> usize {
        self.phase
    }

    /// Returns the exhaustion policy used by this projection.
    pub const fn exhaustion(&self) -> Exhaustion {
        self.exhaustion
    }

    /// Returns the requested target plan length.
    pub fn plan_len(&self) -> usize {
        self.plan_len
    }

    /// Returns the emitted steps with their parameter ordinal ledger.
    pub fn steps(&self) -> &[ParameterStep<T>] {
        &self.steps
    }

    /// Returns plan ordinals that intentionally produced no value under one-shot exhaustion.
    pub fn omitted_plan_ordinals(&self) -> &[usize] {
        &self.omitted_plan_ordinals
    }
}

/// One generic named parameter track over an unchanged serial-core series.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParameterTrack<T: ParameterValue> {
    name: String,
    series: ParameterSeries<T>,
    phase: usize,
    exhaustion: Exhaustion,
}

impl<T: ParameterValue> ParameterTrack<T> {
    /// Constructs one exhaustive exactly-once track from a caller-declared value ladder.
    pub fn try_new(
        name: impl Into<String>,
        values: Vec<T>,
        exhaustion: Exhaustion,
    ) -> Result<Self, IntegralError> {
        let name = validate_parameter_name(name.into())?;
        Ok(Self {
            series: ParameterSeries::try_new(parameter_alphabet_id(&name), values)?,
            name,
            phase: 0,
            exhaustion,
        })
    }

    /// Constructs one track from a caller-declared aggregate rule.
    pub fn try_new_with_rule(
        name: impl Into<String>,
        rule: AggregateRule,
        values: Vec<T>,
        exhaustion: Exhaustion,
    ) -> Result<Self, IntegralError> {
        let name = validate_parameter_name(name.into())?;
        Ok(Self {
            series: ParameterSeries::try_new_with_rule(parameter_alphabet_id(&name), rule, values)?,
            name,
            phase: 0,
            exhaustion,
        })
    }

    /// Returns this track with an explicit phase offset.
    pub fn with_phase(mut self, phase: usize) -> Self {
        self.phase = phase;
        self
    }

    /// Returns the stable track name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the retained typed series.
    pub fn series(&self) -> &ParameterSeries<T> {
        &self.series
    }

    /// Returns the configured phase offset.
    pub fn phase(&self) -> usize {
        self.phase
    }

    /// Returns the configured exhaustion policy.
    pub const fn exhaustion(&self) -> Exhaustion {
        self.exhaustion
    }

    /// Applies a serial-core transform to this track without touching other tracks.
    pub fn transformed(
        &self,
        transform: &SeriesTransform<ParameterAlphabet<T>>,
    ) -> Result<Self, IntegralError> {
        Ok(Self {
            name: self.name.clone(),
            series: self.series.apply(transform)?,
            phase: self.phase,
            exhaustion: self.exhaustion,
        })
    }

    /// Projects this track against a requested integral-plan length.
    pub fn project(&self, plan_len: usize) -> ParameterProjection<T> {
        let source = self.series.order();
        let source_len = source.len();
        let mut steps = Vec::new();
        let mut omitted_plan_ordinals = Vec::new();

        if source_len == 0 {
            return ParameterProjection {
                name: self.name.clone(),
                alphabet_id: self.series.alphabet().id().clone(),
                source_len,
                phase: self.phase,
                exhaustion: self.exhaustion,
                plan_len,
                steps,
                omitted_plan_ordinals,
            };
        }

        let base_phase = match self.exhaustion {
            Exhaustion::Cycle => self.phase % source_len,
            Exhaustion::Truncate | Exhaustion::OneShot => self.phase,
        };
        for plan_ordinal in 0..plan_len {
            let absolute = base_phase + plan_ordinal;
            match self.exhaustion {
                Exhaustion::Cycle => {
                    let parameter_ordinal = absolute % source_len;
                    let cycle = absolute / source_len;
                    steps.push(ParameterStep {
                        plan_ordinal,
                        parameter_ordinal,
                        cycle,
                        value: source[parameter_ordinal].clone(),
                    });
                }
                Exhaustion::Truncate => {
                    if absolute >= source_len {
                        break;
                    }
                    steps.push(ParameterStep {
                        plan_ordinal,
                        parameter_ordinal: absolute,
                        cycle: 0,
                        value: source[absolute].clone(),
                    });
                }
                Exhaustion::OneShot => {
                    if absolute >= source_len {
                        omitted_plan_ordinals.push(plan_ordinal);
                        continue;
                    }
                    steps.push(ParameterStep {
                        plan_ordinal,
                        parameter_ordinal: absolute,
                        cycle: 0,
                        value: source[absolute].clone(),
                    });
                }
            }
        }

        ParameterProjection {
            name: self.name.clone(),
            alphabet_id: self.series.alphabet().id().clone(),
            source_len,
            phase: self.phase,
            exhaustion: self.exhaustion,
            plan_len,
            steps,
            omitted_plan_ordinals,
        }
    }
}

/// Source-ordinal ledger entry retained after one track is bound into a plan.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParameterOrdinalLedgerEntry {
    /// Zero-based position in the integral plan.
    pub plan_ordinal: usize,
    /// Zero-based source ordinal inside the parameter track.
    pub parameter_ordinal: usize,
    /// Zero-based wrap count for cyclic reuse.
    pub cycle: usize,
}

/// One typed bound track retained inside an [`IntegralPlan`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoundParameterTrack<T: ParameterValue> {
    track: ParameterTrack<T>,
    projection: ParameterProjection<T>,
}

impl<T: ParameterValue> BoundParameterTrack<T> {
    /// Returns the original typed track.
    pub fn track(&self) -> &ParameterTrack<T> {
        &self.track
    }

    /// Returns the projected values and omitted ordinals for this plan.
    pub fn projection(&self) -> &ParameterProjection<T> {
        &self.projection
    }

    /// Returns the source-ordinal ledger retained by the track binding.
    pub fn ordinal_ledger(&self) -> Vec<ParameterOrdinalLedgerEntry> {
        self.projection
            .steps()
            .iter()
            .map(|step| ParameterOrdinalLedgerEntry {
                plan_ordinal: step.plan_ordinal,
                parameter_ordinal: step.parameter_ordinal,
                cycle: step.cycle,
            })
            .collect()
    }
}

/// Type-erased view of one parameter track bound into an integral plan.
pub trait ErasedParameterBinding: Debug + Send + Sync {
    /// Returns the stable parameter name.
    fn name(&self) -> &str;
    /// Returns the source alphabet identity retained by this binding.
    fn alphabet_id(&self) -> &AlphabetId;
    /// Returns the configured phase offset.
    fn phase(&self) -> usize;
    /// Returns the configured exhaustion policy.
    fn exhaustion(&self) -> Exhaustion;
    /// Returns the source series length.
    fn source_len(&self) -> usize;
    /// Returns the requested plan length.
    fn plan_len(&self) -> usize;
    /// Returns the retained ordinal ledger for emitted values.
    fn ordinal_ledger(&self) -> Vec<ParameterOrdinalLedgerEntry>;
    /// Returns one debug rendering per emitted value in plan order.
    fn debug_values(&self) -> Vec<String>;
    /// Returns one debug rendering for every omitted one-shot plan ordinal.
    fn omitted_plan_ordinals(&self) -> &[usize];
    /// Returns a downcast hook for typed access.
    fn as_any(&self) -> &dyn Any;
}

impl<T: ParameterValue + Send + Sync> ErasedParameterBinding for BoundParameterTrack<T> {
    fn name(&self) -> &str {
        self.track.name()
    }

    fn alphabet_id(&self) -> &AlphabetId {
        self.projection.alphabet_id()
    }

    fn phase(&self) -> usize {
        self.track.phase()
    }

    fn exhaustion(&self) -> Exhaustion {
        self.track.exhaustion()
    }

    fn source_len(&self) -> usize {
        self.projection.source_len()
    }

    fn plan_len(&self) -> usize {
        self.projection.plan_len()
    }

    fn ordinal_ledger(&self) -> Vec<ParameterOrdinalLedgerEntry> {
        BoundParameterTrack::ordinal_ledger(self)
    }

    fn debug_values(&self) -> Vec<String> {
        self.projection
            .steps()
            .iter()
            .map(|step| format!("{:?}", step.value))
            .collect()
    }

    fn omitted_plan_ordinals(&self) -> &[usize] {
        self.projection.omitted_plan_ordinals()
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

/// Inspectable integral-serial plan with separately bound parameter tracks.
#[derive(Debug)]
pub struct IntegralPlan {
    length: usize,
    parameters: BTreeMap<String, Box<dyn ErasedParameterBinding>>,
}

impl IntegralPlan {
    /// Constructs an empty plan that projects every bound track against `length`.
    pub fn new(length: usize) -> Self {
        Self {
            length,
            parameters: BTreeMap::new(),
        }
    }

    /// Returns the target plan length used by every parameter binding.
    pub fn length(&self) -> usize {
        self.length
    }

    /// Binds one typed parameter track while preserving its independent ordinal ledger.
    pub fn bind_parameter<T: ParameterValue + Send + Sync + 'static>(
        &mut self,
        track: ParameterTrack<T>,
    ) -> Result<(), IntegralError> {
        if self.parameters.contains_key(track.name()) {
            return Err(IntegralError::DuplicateTrack(track.name().to_owned()));
        }
        let projection = track.project(self.length);
        let name = track.name().to_owned();
        self.parameters
            .insert(name, Box::new(BoundParameterTrack { track, projection }));
        Ok(())
    }

    /// Returns one type-erased bound parameter by stable name.
    pub fn parameter(&self, name: &str) -> Option<&dyn ErasedParameterBinding> {
        self.parameters.get(name).map(Box::as_ref)
    }

    /// Returns one typed bound parameter when `name` and `T` both match.
    pub fn typed_parameter<T: ParameterValue>(
        &self,
        name: &str,
    ) -> Option<&BoundParameterTrack<T>> {
        self.parameters
            .get(name)
            .and_then(|binding| binding.as_any().downcast_ref::<BoundParameterTrack<T>>())
    }

    /// Returns the stable parameter names in sorted order.
    pub fn parameter_names(&self) -> Vec<&str> {
        self.parameters.keys().map(String::as_str).collect()
    }
}

/// Track alias for exact duration values.
pub type DurationTrack = ParameterTrack<Time>;
/// Track alias for MIDI-style dynamic values.
pub type DynamicsTrack = ParameterTrack<u8>;
/// Track alias for MIDI-style register placement.
pub type RegisterTrack = ParameterTrack<i8>;
/// Track alias for articulation values from music-core.
pub type ArticulationTrack = ParameterTrack<Articulation>;
/// Track alias for caller-declared timbre labels.
pub type TimbreTrack = ParameterTrack<String>;

/// Failure while constructing or binding integral parameter tracks.
#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum IntegralError {
    /// The parameter name was empty or used invalid characters.
    #[error("invalid parameter name {0:?}")]
    InvalidParameterName(String),
    /// The track tried to reuse a name already bound in the plan.
    #[error("parameter track {0} is already bound")]
    DuplicateTrack(String),
    /// Constructing or transforming the generic parameter series failed.
    #[error(transparent)]
    Parameter(#[from] ParameterError),
}

fn validate_parameter_name(name: String) -> Result<String, IntegralError> {
    if name.trim().is_empty() {
        return Err(IntegralError::InvalidParameterName(name));
    }
    if name
        .chars()
        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
    {
        return Err(IntegralError::InvalidParameterName(name));
    }
    Ok(name)
}

fn parameter_alphabet_id(name: &str) -> String {
    format!("parameter/{name}-v1")
}