Skip to main content

sim_lib_music_core/
arranger.rs

1use sim_kernel::{Error, Result, Symbol};
2
3use crate::{
4    LaneDescriptor, LaneId, LaneKind, LaneTarget, Music, MusicError, Pitch, PitchClass, PlayEvent,
5    Time,
6};
7
8/// Arrangement of playable material laid out across lanes on a timeline.
9///
10/// Holds the ordered set of [ArrangerPlacement]s plus the lane ids the
11/// arrangement declares, and renders them into play events.
12#[derive(Clone, Debug)]
13pub struct Arranger {
14    /// Placements that make up the arrangement, in declaration order.
15    pub placements: Vec<ArrangerPlacement>,
16    /// Lane ids the arrangement declares.
17    pub lanes: Vec<LaneId>,
18}
19
20/// Single placement of playable material at a point on the arranger timeline.
21///
22/// Carries the source reference plus the per-placement transforms, stretch,
23/// pitch remap, filter, and trace policy applied during rendering.
24#[derive(Clone, Debug)]
25pub struct ArrangerPlacement {
26    /// Stable identifier used to attribute diagnostics and traces.
27    pub id: Symbol,
28    /// Playable material this placement renders.
29    pub playable: PlayableRef,
30    /// Onset of the placement on the arranger timeline.
31    pub at: Time,
32    /// Optional explicit duration used for clipping and fit-to-duration stretch.
33    pub duration: Option<Time>,
34    /// Lane the rendered notes are assigned to.
35    pub lane: LaneId,
36    /// Targets the lane drives.
37    pub targets: Vec<LaneTarget>,
38    /// Time-scaling policy applied to the placement.
39    pub stretch: StretchPolicy,
40    /// Ordered list of pitch and time transforms applied to the placement.
41    pub transform: Vec<PlacementTransform>,
42    /// Pitch remapping applied after transforms.
43    pub remap_pitch: PitchRemap,
44    /// Optional lane filter applied after pitch remapping.
45    pub filter: Option<FilterRef>,
46    /// Optional seed for deterministic placement behavior.
47    pub seed: Option<u64>,
48    /// Tracing verbosity for the placement.
49    pub trace: TracePolicy,
50}
51
52/// Reference to the playable material a placement renders.
53#[derive(Clone, Debug)]
54pub enum PlayableRef {
55    /// Inline music object owned by the placement.
56    Inline(Box<Music>),
57    /// Named reference resolved by the host at render time.
58    Symbol(Symbol),
59}
60
61/// Pitch or time transform applied to a placement's notes.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum PlacementTransform {
64    /// Transposes every pitch by a number of semitones.
65    TransposeSemitones(i32),
66    /// Transposes every pitch by a number of octaves.
67    TransposeOctaves(i16),
68    /// Inverts every pitch around a fixed pitch axis.
69    InvertAroundPitch(Pitch),
70    /// Inverts every pitch class around a fixed pitch-class axis.
71    InvertAroundPitchClass(PitchClass),
72    /// Reverses the placement in time.
73    Retrograde,
74}
75
76/// Time-scaling policy applied to a placement.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub enum StretchPolicy {
79    /// Leaves timing unchanged.
80    None,
81    /// Scales timing by the reciprocal of the given tempo ratio.
82    TempoRatio(Time),
83    /// Scales timing directly by the given time ratio.
84    TimeRatio(Time),
85    /// Scales the placement to fill its declared duration.
86    FitToDuration,
87}
88
89/// Pitch remapping applied after a placement's transforms.
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub enum PitchRemap {
92    /// Leaves pitches unchanged.
93    None,
94    /// Shifts every pitch by a fixed number of semitones.
95    Chromatic(i32),
96    /// Replaces one pitch class with another.
97    PitchClass {
98        /// Pitch class to match.
99        from: PitchClass,
100        /// Pitch class to substitute.
101        to: PitchClass,
102    },
103    /// Maps source MIDI keys to target keys for drum lanes.
104    DrumKey(Vec<(u8, u8)>),
105    /// Scale-degree remap resolved by a host-provided resolver.
106    ScaleDegree(Symbol),
107    /// Chord-tone remap resolved by a host-provided resolver.
108    ChordTone(Symbol),
109    /// Tuning remap resolved by a host-provided resolver.
110    Tuning(Symbol),
111    /// Vector remap resolved by a host-provided resolver.
112    Vector(Symbol),
113    /// Matrix remap resolved by a host-provided resolver.
114    Matrix(Symbol),
115    /// Callable remap resolved by a host-provided resolver.
116    Callable(Symbol),
117}
118
119/// Lane filter that keeps a placement only when its lane is retained.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct FilterRef {
122    /// Identifier reported in filter diagnostics.
123    pub id: Symbol,
124    /// Lanes the filter keeps; an empty list acts as identity.
125    pub keep_lanes: Vec<LaneId>,
126}
127
128/// Tracing verbosity for a placement during rendering.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum TracePolicy {
131    /// Emits no trace output.
132    Off,
133    /// Emits diagnostic-level trace output.
134    Diagnostics,
135    /// Emits full trace events.
136    Full,
137}
138
139/// Result of rendering an [Arranger]: play events plus diagnostics.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct ArrangerRender {
142    /// Stable-ordered play events produced by the arrangement.
143    pub events: Vec<PlayEvent>,
144    /// Diagnostics collected while rendering.
145    pub diagnostics: Vec<ArrangerDiagnostic>,
146}
147
148/// Diagnostic raised while rendering a placement.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct ArrangerDiagnostic {
151    /// Identifier of the placement that produced the diagnostic.
152    pub placement_id: Symbol,
153    /// Time on the arranger timeline the diagnostic refers to.
154    pub at: Time,
155    /// Human-readable diagnostic message.
156    pub message: String,
157}
158
159impl Arranger {
160    /// Builds an arranger from placements and lane ids, validating each placement.
161    pub fn new(placements: Vec<ArrangerPlacement>, lanes: Vec<LaneId>) -> Result<Self> {
162        let arranger = Self { placements, lanes };
163        arranger.validate()?;
164        Ok(arranger)
165    }
166
167    fn validate(&self) -> Result<()> {
168        for placement in &self.placements {
169            placement.validate()?;
170        }
171        Ok(())
172    }
173}
174
175impl ArrangerPlacement {
176    /// Builds a placement from a string id, playable, and onset using defaults.
177    pub fn new(id: impl Into<String>, playable: PlayableRef, at: Time) -> Result<Self> {
178        Self::with_symbol_id(Symbol::new(id.into()), playable, at)
179    }
180
181    /// Builds a placement from a symbol id, playable, and onset using defaults.
182    pub fn with_symbol_id(id: Symbol, playable: PlayableRef, at: Time) -> Result<Self> {
183        let placement = Self {
184            id,
185            playable,
186            at,
187            duration: None,
188            lane: LaneId::new("notes"),
189            targets: vec![LaneTarget::Instrument(Symbol::qualified(
190                "music/target",
191                "default",
192            ))],
193            stretch: StretchPolicy::None,
194            transform: Vec::new(),
195            remap_pitch: PitchRemap::None,
196            filter: None,
197            seed: None,
198            trace: TracePolicy::Off,
199        };
200        placement.validate()?;
201        Ok(placement)
202    }
203
204    /// Sets the placement's explicit duration, rejecting negative values.
205    pub fn with_duration(mut self, duration: Time) -> Result<Self> {
206        ensure_non_negative_kernel(duration, "arranger placement duration")?;
207        self.duration = Some(duration);
208        Ok(self)
209    }
210
211    /// Sets the lane the placement's notes are assigned to.
212    pub fn with_lane(mut self, lane: LaneId) -> Self {
213        self.lane = lane;
214        self
215    }
216
217    /// Replaces the placement's targets with a single target.
218    pub fn with_target(mut self, target: LaneTarget) -> Self {
219        self.targets = vec![target];
220        self
221    }
222
223    /// Replaces the placement's targets with the given list.
224    pub fn with_targets(mut self, targets: Vec<LaneTarget>) -> Self {
225        self.targets = targets;
226        self
227    }
228
229    /// Sets the placement's stretch policy.
230    pub fn with_stretch(mut self, stretch: StretchPolicy) -> Self {
231        self.stretch = stretch;
232        self
233    }
234
235    /// Replaces the placement's transform list.
236    pub fn with_transform(mut self, transform: Vec<PlacementTransform>) -> Self {
237        self.transform = transform;
238        self
239    }
240
241    /// Sets the placement's pitch remap.
242    pub fn with_pitch_remap(mut self, remap_pitch: PitchRemap) -> Self {
243        self.remap_pitch = remap_pitch;
244        self
245    }
246
247    /// Sets the placement's lane filter.
248    pub fn with_filter(mut self, filter: FilterRef) -> Self {
249        self.filter = Some(filter);
250        self
251    }
252
253    /// Sets the placement's deterministic seed.
254    pub fn with_seed(mut self, seed: u64) -> Self {
255        self.seed = Some(seed);
256        self
257    }
258
259    /// Sets the placement's trace policy.
260    pub fn with_trace(mut self, trace: TracePolicy) -> Self {
261        self.trace = trace;
262        self
263    }
264
265    pub(crate) fn validate(&self) -> Result<()> {
266        ensure_non_negative_kernel(self.at, "arranger placement onset")?;
267        if let Some(duration) = self.duration {
268            ensure_non_negative_kernel(duration, "arranger placement duration")?;
269        }
270        if self.targets.is_empty() {
271            return Err(Error::Eval(
272                "arranger placement must have at least one target".to_owned(),
273            ));
274        }
275        for target in &self.targets {
276            LaneDescriptor::new(self.lane.clone(), LaneKind::Note, target.clone(), 0)
277                .map_err(music_err)?;
278        }
279        Ok(())
280    }
281}
282
283impl PlayableRef {
284    /// Builds an inline reference that owns the given music object.
285    pub fn inline(music: Music) -> Self {
286        Self::Inline(Box::new(music))
287    }
288
289    /// Builds a symbolic reference resolved by the host at render time.
290    pub fn symbol(symbol: Symbol) -> Self {
291        Self::Symbol(symbol)
292    }
293}
294
295impl FilterRef {
296    /// Builds a filter from an identifier and the lanes it keeps.
297    pub fn new(id: Symbol, keep_lanes: Vec<LaneId>) -> Self {
298        Self { id, keep_lanes }
299    }
300}
301
302pub(crate) fn ensure_non_negative_kernel(value: Time, context: &str) -> Result<()> {
303    if value < Time::from_integer(0) {
304        Err(Error::Eval(format!("{context} cannot be negative")))
305    } else {
306        Ok(())
307    }
308}
309
310pub(crate) fn music_err(err: MusicError) -> Error {
311    Error::Eval(err.to_string())
312}