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}