Skip to main content

sim_lib_music_core/
arranger_render.rs

1use sim_kernel::{Result, Symbol};
2
3use crate::arranger::{
4    Arranger, ArrangerDiagnostic, ArrangerPlacement, ArrangerRender, PitchRemap,
5    PlacementTransform, PlayableRef, StretchPolicy, TracePolicy, music_err,
6};
7use crate::{
8    AtomRef, DiagnosticEvent, LaneId, MusicObject, NoteEvent, Pitch, PlayContext, PlayEvent, Time,
9    TimedNote, TraceEvent, time_to_tick,
10};
11
12#[derive(Clone, Debug)]
13struct ArrangedNote {
14    placement_id: Symbol,
15    lane_id: LaneId,
16    order: usize,
17    item: TimedNote,
18}
19
20struct NoteRender {
21    notes: Vec<ArrangedNote>,
22    diagnostics: Vec<ArrangerDiagnostic>,
23    traces: Vec<ArrangerTrace>,
24}
25
26struct ArrangerTrace {
27    at: Time,
28    step: u64,
29}
30
31impl Arranger {
32    /// Renders the arrangement into clipped, stable-ordered play events.
33    ///
34    /// Applies each placement's stretch, transforms, pitch remap, and filter,
35    /// clips events to the context range, and appends diagnostics and traces.
36    pub fn render_arrangement(&self, cx: &PlayContext) -> Result<ArrangerRender> {
37        let notes = self.render_notes();
38        let mut events = cx.upstream.clone();
39        for arranged in notes.notes {
40            let onset = time_to_tick(arranged.item.onset, cx.ppq).map_err(music_err)?;
41            let duration = time_to_tick(arranged.item.note.duration, cx.ppq).map_err(music_err)?;
42            let Some((time, duration)) = cx.range.clip_span(onset, duration) else {
43                continue;
44            };
45            events.push(PlayEvent::Note(NoteEvent {
46                lane_id: arranged.lane_id,
47                time,
48                duration,
49                pitch: arranged.item.note.pitch,
50                velocity: arranged.item.note.velocity,
51                channel: arranged.item.note.channel,
52            }));
53        }
54        for diagnostic in &notes.diagnostics {
55            let time = time_to_tick(diagnostic.at, cx.ppq).map_err(music_err)?;
56            events.push(PlayEvent::Diagnostic(DiagnosticEvent {
57                lane_id: LaneId::new("arranger-diagnostics"),
58                time,
59                message: diagnostic.message.clone(),
60            }));
61        }
62        for trace in notes.traces {
63            let time = time_to_tick(trace.at, cx.ppq).map_err(music_err)?;
64            events.push(PlayEvent::Trace(TraceEvent {
65                lane_id: LaneId::new("arranger-trace"),
66                time,
67                step: trace.step,
68            }));
69        }
70        crate::stable_event_order(&mut events);
71        Ok(ArrangerRender {
72            events,
73            diagnostics: notes.diagnostics,
74        })
75    }
76
77    /// Renders the arrangement and returns only its timed notes.
78    pub fn rendered_notes(&self) -> Vec<TimedNote> {
79        self.render_notes()
80            .notes
81            .into_iter()
82            .map(|note| note.item)
83            .collect()
84    }
85
86    /// Renders the arrangement and returns only the diagnostics it raises.
87    pub fn diagnostics(&self) -> Vec<ArrangerDiagnostic> {
88        self.render_notes().diagnostics
89    }
90
91    fn render_notes(&self) -> NoteRender {
92        let mut notes = Vec::new();
93        let mut diagnostics = Vec::new();
94        let mut traces = Vec::new();
95        for (order, placement) in self.placements.iter().enumerate() {
96            if placement.trace == TracePolicy::Full {
97                traces.push(ArrangerTrace {
98                    at: placement.at,
99                    step: order as u64,
100                });
101            }
102            let mut local = match &placement.playable {
103                PlayableRef::Inline(music) => notes_from_object(music.as_ref()),
104                PlayableRef::Symbol(symbol) => {
105                    push_diagnostic(
106                        &mut diagnostics,
107                        placement,
108                        format!("playable reference {symbol} is not resolved"),
109                    );
110                    continue;
111                }
112            };
113            if let Some(duration) = placement.duration {
114                local = clip_notes(local, duration);
115            }
116            apply_stretch(&mut local, placement, &mut diagnostics);
117            for transform in &placement.transform {
118                apply_transform(&mut local, transform, placement, &mut diagnostics);
119            }
120            apply_pitch_remap(
121                &mut local,
122                &placement.remap_pitch,
123                placement,
124                &mut diagnostics,
125            );
126            if !filter_notes(&mut local, placement, &mut diagnostics) {
127                continue;
128            }
129            for mut item in local {
130                item.onset += placement.at;
131                notes.push(ArrangedNote {
132                    placement_id: placement.id.clone(),
133                    lane_id: placement.lane.clone(),
134                    order,
135                    item,
136                });
137            }
138        }
139        stable_note_order(&mut notes);
140        NoteRender {
141            notes,
142            diagnostics,
143            traces,
144        }
145    }
146}
147
148fn apply_stretch(
149    notes: &mut [TimedNote],
150    placement: &ArrangerPlacement,
151    diagnostics: &mut Vec<ArrangerDiagnostic>,
152) {
153    let factor = match placement.stretch {
154        StretchPolicy::None => return,
155        StretchPolicy::TempoRatio(ratio) if ratio > Time::from_integer(0) => ratio.recip(),
156        StretchPolicy::TimeRatio(ratio) if ratio > Time::from_integer(0) => ratio,
157        StretchPolicy::TempoRatio(_) | StretchPolicy::TimeRatio(_) => {
158            push_diagnostic(diagnostics, placement, "stretch ratio must be positive");
159            return;
160        }
161        StretchPolicy::FitToDuration => {
162            let Some(target) = placement.duration else {
163                push_diagnostic(
164                    diagnostics,
165                    placement,
166                    "fit stretch needs a placement duration",
167                );
168                return;
169            };
170            let span = note_span(notes);
171            if target <= Time::from_integer(0) || span <= Time::from_integer(0) {
172                push_diagnostic(
173                    diagnostics,
174                    placement,
175                    "fit stretch needs positive source and target spans",
176                );
177                return;
178            }
179            target / span
180        }
181    };
182    for note in notes {
183        note.onset *= factor;
184        note.note.duration *= factor;
185    }
186}
187
188fn apply_transform(
189    notes: &mut [TimedNote],
190    transform: &PlacementTransform,
191    placement: &ArrangerPlacement,
192    diagnostics: &mut Vec<ArrangerDiagnostic>,
193) {
194    match transform {
195        PlacementTransform::TransposeSemitones(semitones) => {
196            for note in notes {
197                note.note.pitch = note.note.pitch.transpose(*semitones);
198            }
199        }
200        PlacementTransform::TransposeOctaves(octaves) => {
201            let semitones = i32::from(*octaves) * 12;
202            for note in notes {
203                note.note.pitch = note.note.pitch.transpose(semitones);
204            }
205        }
206        PlacementTransform::InvertAroundPitch(axis) => {
207            for note in notes {
208                note.note.pitch = note.note.pitch.invert(*axis);
209            }
210        }
211        PlacementTransform::InvertAroundPitchClass(axis) => {
212            for note in notes {
213                note.note.pitch = Pitch {
214                    class: note.note.pitch.class.invert(*axis),
215                    octave: note.note.pitch.octave,
216                };
217            }
218        }
219        PlacementTransform::Retrograde => {
220            let total = placement.duration.unwrap_or_else(|| note_span(notes));
221            if total <= Time::from_integer(0) {
222                push_diagnostic(diagnostics, placement, "retrograde needs a positive span");
223                return;
224            }
225            for note in notes {
226                note.onset = total - note.onset - note.note.duration;
227            }
228        }
229    }
230}
231
232fn apply_pitch_remap(
233    notes: &mut [TimedNote],
234    remap: &PitchRemap,
235    placement: &ArrangerPlacement,
236    diagnostics: &mut Vec<ArrangerDiagnostic>,
237) {
238    match remap {
239        PitchRemap::None => {}
240        PitchRemap::Chromatic(semitones) => {
241            for note in notes {
242                note.note.pitch = note.note.pitch.transpose(*semitones);
243            }
244        }
245        PitchRemap::PitchClass { from, to } => {
246            for note in notes {
247                if note.note.pitch.class == *from {
248                    note.note.pitch = Pitch {
249                        class: *to,
250                        octave: note.note.pitch.octave,
251                    };
252                }
253            }
254        }
255        PitchRemap::DrumKey(map) => {
256            for note in notes {
257                let Some(key) = note.note.pitch.to_midi() else {
258                    push_diagnostic(diagnostics, placement, "drum-key remap needs MIDI pitches");
259                    continue;
260                };
261                if let Some((_, target)) = map.iter().find(|(source, _)| *source == key) {
262                    note.note.pitch = Pitch::from_midi(*target);
263                }
264            }
265        }
266        PitchRemap::ScaleDegree(symbol)
267        | PitchRemap::ChordTone(symbol)
268        | PitchRemap::Tuning(symbol)
269        | PitchRemap::Vector(symbol)
270        | PitchRemap::Matrix(symbol)
271        | PitchRemap::Callable(symbol) => push_diagnostic(
272            diagnostics,
273            placement,
274            format!("pitch remap {symbol} needs a host resolver"),
275        ),
276    }
277}
278
279fn filter_notes(
280    notes: &mut Vec<TimedNote>,
281    placement: &ArrangerPlacement,
282    diagnostics: &mut Vec<ArrangerDiagnostic>,
283) -> bool {
284    let Some(filter) = &placement.filter else {
285        return true;
286    };
287    if filter.keep_lanes.is_empty() {
288        push_diagnostic(
289            diagnostics,
290            placement,
291            format!("filter {} evaluated as identity", filter.id),
292        );
293        return true;
294    }
295    if filter.keep_lanes.iter().any(|lane| lane == &placement.lane) {
296        true
297    } else {
298        notes.clear();
299        push_diagnostic(
300            diagnostics,
301            placement,
302            format!("filter {} removed lane {}", filter.id, placement.lane.0),
303        );
304        false
305    }
306}
307
308fn clip_notes(notes: Vec<TimedNote>, duration: Time) -> Vec<TimedNote> {
309    notes
310        .into_iter()
311        .filter_map(|mut item| {
312            let start = item.onset.max(Time::from_integer(0));
313            let end = (item.onset + item.note.duration).min(duration);
314            (start < end).then(|| {
315                item.onset = start;
316                item.note.duration = end - start;
317                item
318            })
319        })
320        .collect()
321}
322
323fn notes_from_object(object: &dyn MusicObject) -> Vec<TimedNote> {
324    let mut atoms = Vec::new();
325    object.voices(Time::from_integer(0), &mut atoms);
326    atoms
327        .into_iter()
328        .filter_map(|atom| match atom.atom {
329            AtomRef::Note(note) => Some(TimedNote {
330                onset: atom.onset,
331                note,
332            }),
333            AtomRef::Rest(_) | AtomRef::Phantom(_) => None,
334        })
335        .collect()
336}
337
338fn note_span(notes: &[TimedNote]) -> Time {
339    notes
340        .iter()
341        .map(|note| note.onset + note.note.duration)
342        .max()
343        .unwrap_or_else(|| Time::from_integer(0))
344}
345
346fn stable_note_order(notes: &mut [ArrangedNote]) {
347    notes.sort_by(|left, right| {
348        left.item
349            .onset
350            .cmp(&right.item.onset)
351            .then_with(|| left.lane_id.cmp(&right.lane_id))
352            .then_with(|| {
353                left.item
354                    .note
355                    .pitch
356                    .semitone()
357                    .cmp(&right.item.note.pitch.semitone())
358            })
359            .then_with(|| left.placement_id.cmp(&right.placement_id))
360            .then_with(|| left.order.cmp(&right.order))
361    });
362}
363
364fn push_diagnostic(
365    diagnostics: &mut Vec<ArrangerDiagnostic>,
366    placement: &ArrangerPlacement,
367    message: impl Into<String>,
368) {
369    diagnostics.push(ArrangerDiagnostic {
370        placement_id: placement.id.clone(),
371        at: placement.at,
372        message: message.into(),
373    });
374}