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 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 ¬es.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 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 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}