Skip to main content

sim_lib_view_daw/
arranger.rs

1//! Arranger object-roll Scene descriptors.
2
3use sim_kernel::{Expr, Symbol};
4use sim_lib_scene::{data_map, node, sym};
5use sim_value::build::{int, list, text, uint};
6
7/// Stable lens id for the arranger object-roll editor.
8pub const ARRANGER_OBJECT_ROLL_VIEW_ID: &str = "view:arranger-object-roll";
9
10/// Demo fixture name for the arranger object-roll editor.
11pub const ARRANGER_OBJECT_ROLL_DEMO_FIXTURE: &str = "arranger-object-roll";
12
13/// Editing actions exposed by the object-roll editor.
14pub const ARRANGER_OBJECT_ROLL_ACTIONS: &[&str] = &[
15    "set-at",
16    "set-duration",
17    "set-stretch",
18    "set-transform",
19    "set-remap-pitch",
20    "set-filter",
21    "set-target",
22    "set-seed",
23    "set-trace-policy",
24    "open-nested",
25    "freeze-to-piano-roll",
26    "freeze-to-midi",
27];
28
29/// One visible object-roll lane.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct ArrangerLane {
32    /// Stable lane id.
33    pub id: Symbol,
34    /// Display label.
35    pub label: String,
36    /// Placement cells in this lane.
37    pub placements: Vec<ArrangerObjectRollPlacement>,
38}
39
40/// One arranger placement cell shown by the object-roll editor.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct ArrangerObjectRollPlacement {
43    /// Stable placement id.
44    pub id: Symbol,
45    /// Display label.
46    pub label: String,
47    /// Lane that owns this placement.
48    pub lane: Symbol,
49    /// Playable object or reference.
50    pub playable: Symbol,
51    /// Start tick.
52    pub at: u64,
53    /// Duration in ticks.
54    pub duration: u64,
55    /// Stretch policy label.
56    pub stretch: String,
57    /// Transposition in semitones.
58    pub transpose: i32,
59    /// Inversion handle label.
60    pub invert: String,
61    /// Retrograde transform toggle.
62    pub retrograde: bool,
63    /// Pitch remap handle label.
64    pub remap_pitch: String,
65    /// Filter object.
66    pub filter: Symbol,
67    /// Target instrument, lane, or playable sink.
68    pub target: Symbol,
69    /// Deterministic seed for generative placements.
70    pub seed: u64,
71    /// Trace policy label.
72    pub trace_policy: String,
73    /// Whether this placement opens another arranger.
74    pub nested: bool,
75}
76
77/// Diagnostic class rendered by the object-roll editor.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum ArrangerDiagnosticKind {
80    /// A source event could not be represented in the current lane.
81    DroppedEvent,
82    /// A target does not provide a required capability.
83    MissingCapability,
84    /// A pitch remap cannot be applied.
85    ImpossibleRemap,
86    /// A placement range is clipped by the edit range.
87    ClippedRange,
88}
89
90impl ArrangerDiagnosticKind {
91    fn as_str(self) -> &'static str {
92        match self {
93            Self::DroppedEvent => "dropped-event",
94            Self::MissingCapability => "missing-capability",
95            Self::ImpossibleRemap => "impossible-remap",
96            Self::ClippedRange => "clipped-range",
97        }
98    }
99}
100
101/// One object-roll diagnostic.
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct ArrangerDiagnostic {
104    /// Related placement id.
105    pub placement: Symbol,
106    /// Diagnostic class.
107    pub diagnostic_kind: ArrangerDiagnosticKind,
108    /// Short display message.
109    pub message: String,
110}
111
112/// Complete arranger object-roll view.
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct ArrangerObjectRollView {
115    /// Intent target edited by arranger actions.
116    pub target: Symbol,
117    /// Arranger object being edited.
118    pub arranger: Symbol,
119    /// Visible lanes.
120    pub lanes: Vec<ArrangerLane>,
121    /// Diagnostics to display next to placements.
122    pub diagnostics: Vec<ArrangerDiagnostic>,
123}
124
125/// Render an arranger descriptor as a `scene/object-roll` node.
126pub fn arranger_object_roll_view(view: &ArrangerObjectRollView) -> Expr {
127    node(
128        "object-roll",
129        vec![
130            ("lens", sym(ARRANGER_OBJECT_ROLL_VIEW_ID)),
131            ("role", sym("arranger-object-roll")),
132            ("target", Expr::Symbol(view.target.clone())),
133            ("arranger", Expr::Symbol(view.arranger.clone())),
134            (
135                "actions",
136                list(
137                    ARRANGER_OBJECT_ROLL_ACTIONS
138                        .iter()
139                        .map(|action| text(*action))
140                        .collect(),
141                ),
142            ),
143            (
144                "lanes",
145                list(view.lanes.iter().map(arranger_lane_expr).collect()),
146            ),
147            (
148                "diagnostics",
149                list(
150                    view.diagnostics
151                        .iter()
152                        .map(arranger_diagnostic_expr)
153                        .collect(),
154                ),
155            ),
156        ],
157    )
158}
159
160/// Deterministic arranger object-roll fixture covering transform handles.
161pub fn arranger_object_roll_demo_view() -> ArrangerObjectRollView {
162    let melody_lane = Symbol::qualified("music/arranger-lane", "melody");
163    let nested_lane = Symbol::qualified("music/arranger-lane", "nested");
164    let automation_lane = Symbol::qualified("music/arranger-lane", "automation");
165    let motif = ArrangerObjectRollPlacement {
166        id: Symbol::qualified("music/arranger-placement", "motif"),
167        label: "Motif".to_owned(),
168        lane: melody_lane.clone(),
169        playable: Symbol::qualified("music/playable", "motif-roll"),
170        at: 0,
171        duration: 384,
172        stretch: "fit-to-duration".to_owned(),
173        transpose: 12,
174        invert: "pitch:C4".to_owned(),
175        retrograde: true,
176        remap_pitch: "scale:minor-pentatonic".to_owned(),
177        filter: Symbol::qualified("music/filter", "lead-only"),
178        target: Symbol::qualified("audio-synth/instrument", "dx7"),
179        seed: 9001,
180        trace_policy: "full".to_owned(),
181        nested: false,
182    };
183    let nested = ArrangerObjectRollPlacement {
184        id: Symbol::qualified("music/arranger-placement", "nested-arranger"),
185        label: "Nested arranger".to_owned(),
186        lane: nested_lane.clone(),
187        playable: Symbol::qualified("music/arranger", "bridge"),
188        at: 384,
189        duration: 384,
190        stretch: "tempo-ratio:3/2".to_owned(),
191        transpose: 0,
192        invert: "none".to_owned(),
193        retrograde: false,
194        remap_pitch: "vector:modal-axis".to_owned(),
195        filter: Symbol::qualified("music/filter", "none"),
196        target: Symbol::qualified("music/player-chain", "onscreen-keyboard"),
197        seed: 17,
198        trace_policy: "diagnostics".to_owned(),
199        nested: true,
200    };
201    let automation = ArrangerObjectRollPlacement {
202        id: Symbol::qualified("music/arranger-placement", "cutoff-sweep"),
203        label: "Cutoff sweep".to_owned(),
204        lane: automation_lane.clone(),
205        playable: Symbol::qualified("music/playable", "cutoff-curve"),
206        at: 768,
207        duration: 192,
208        stretch: "none".to_owned(),
209        transpose: 0,
210        invert: "none".to_owned(),
211        retrograde: false,
212        remap_pitch: "matrix:ps3300-map".to_owned(),
213        filter: Symbol::qualified("music/filter", "controls"),
214        target: Symbol::qualified("audio-synth/parameter", "cutoff"),
215        seed: 5,
216        trace_policy: "off".to_owned(),
217        nested: false,
218    };
219    ArrangerObjectRollView {
220        target: Symbol::qualified("music/arranger", "song-a"),
221        arranger: Symbol::qualified("music/arranger", "song-a"),
222        lanes: vec![
223            ArrangerLane {
224                id: melody_lane,
225                label: "Melody".to_owned(),
226                placements: vec![motif.clone()],
227            },
228            ArrangerLane {
229                id: nested_lane,
230                label: "Nested".to_owned(),
231                placements: vec![nested.clone()],
232            },
233            ArrangerLane {
234                id: automation_lane,
235                label: "Automation".to_owned(),
236                placements: vec![automation.clone()],
237            },
238        ],
239        diagnostics: vec![
240            ArrangerDiagnostic {
241                placement: motif.id.clone(),
242                diagnostic_kind: ArrangerDiagnosticKind::DroppedEvent,
243                message: "dropped control event".to_owned(),
244            },
245            ArrangerDiagnostic {
246                placement: motif.id,
247                diagnostic_kind: ArrangerDiagnosticKind::MissingCapability,
248                message: "target lacks pitch input".to_owned(),
249            },
250            ArrangerDiagnostic {
251                placement: nested.id,
252                diagnostic_kind: ArrangerDiagnosticKind::ImpossibleRemap,
253                message: "vector remap misses row".to_owned(),
254            },
255            ArrangerDiagnostic {
256                placement: automation.id,
257                diagnostic_kind: ArrangerDiagnosticKind::ClippedRange,
258                message: "placement clipped at loop end".to_owned(),
259            },
260        ],
261    }
262}
263
264/// Deterministic arranger object-roll demo scene.
265pub fn arranger_object_roll_demo_scene() -> Expr {
266    arranger_object_roll_view(&arranger_object_roll_demo_view())
267}
268
269fn arranger_lane_expr(lane: &ArrangerLane) -> Expr {
270    data_map(vec![
271        ("id", Expr::Symbol(lane.id.clone())),
272        ("label", text(lane.label.clone())),
273        (
274            "placements",
275            list(
276                lane.placements
277                    .iter()
278                    .map(arranger_placement_expr)
279                    .collect(),
280            ),
281        ),
282    ])
283}
284
285fn arranger_placement_expr(placement: &ArrangerObjectRollPlacement) -> Expr {
286    data_map(vec![
287        ("id", Expr::Symbol(placement.id.clone())),
288        ("label", text(placement.label.clone())),
289        ("lane", Expr::Symbol(placement.lane.clone())),
290        ("playable", Expr::Symbol(placement.playable.clone())),
291        ("at", uint(placement.at)),
292        ("duration", uint(placement.duration)),
293        ("stretch", text(placement.stretch.clone())),
294        ("transpose", int(i64::from(placement.transpose))),
295        ("invert", text(placement.invert.clone())),
296        ("retrograde", Expr::Bool(placement.retrograde)),
297        ("remap-pitch", text(placement.remap_pitch.clone())),
298        ("filter", Expr::Symbol(placement.filter.clone())),
299        ("target", Expr::Symbol(placement.target.clone())),
300        ("seed", uint(placement.seed)),
301        ("trace-policy", text(placement.trace_policy.clone())),
302        ("nested", Expr::Bool(placement.nested)),
303        (
304            "freeze-targets",
305            list(vec![text("piano-roll"), text("midi")]),
306        ),
307    ])
308}
309
310fn arranger_diagnostic_expr(diagnostic: &ArrangerDiagnostic) -> Expr {
311    data_map(vec![
312        ("placement", Expr::Symbol(diagnostic.placement.clone())),
313        ("diagnostic-kind", text(diagnostic.diagnostic_kind.as_str())),
314        ("message", text(diagnostic.message.clone())),
315    ])
316}