Skip to main content

sim_lib_view_daw/
specialized.rs

1//! Specialized component views for synth editors.
2//!
3//! These scenes are stable fixtures for component classes that need more than a
4//! generic parameter panel: envelopes, algorithm routes, response plots, fixed
5//! banks, step grids, polyphony activity, scopes, and SysEx comparison.
6
7use sim_kernel::{Expr, Symbol};
8use sim_lib_scene::{data_map, node, sym};
9
10/// Envelope curve editor view id.
11pub const ENVELOPE_CURVE_VIEW_ID: &str = "view:component-envelope-curve";
12/// Algorithm routing view id.
13pub const ALGORITHM_ROUTING_VIEW_ID: &str = "view:component-algorithm-routing";
14/// Wiring diagram view id.
15pub const WIRING_DIAGRAM_VIEW_ID: &str = "view:component-wiring-diagram";
16/// Filter response view id.
17pub const FILTER_RESPONSE_VIEW_ID: &str = "view:component-filter-response";
18/// Resonator response view id.
19pub const RESONATOR_RESPONSE_VIEW_ID: &str = "view:component-resonator-response";
20/// Fixed filter bank view id.
21pub const FIXED_FILTER_BANK_VIEW_ID: &str = "view:component-fixed-filter-bank";
22/// Sequencer step-grid view id.
23pub const SEQUENCER_STEP_GRID_VIEW_ID: &str = "view:component-sequencer-step-grid";
24/// Polyphony activity map view id.
25pub const POLYPHONY_ACTIVITY_VIEW_ID: &str = "view:component-polyphony-activity";
26/// Scope and spectrum monitor view id.
27pub const SCOPE_SPECTRUM_VIEW_ID: &str = "view:component-scope-spectrum";
28/// SysEx comparison view id.
29pub const SYSEX_COMPARISON_VIEW_ID: &str = "view:component-sysex-comparison";
30
31/// Stable ids for all specialized component views.
32pub const SPECIALIZED_COMPONENT_VIEW_IDS: [&str; 10] = [
33    ENVELOPE_CURVE_VIEW_ID,
34    ALGORITHM_ROUTING_VIEW_ID,
35    WIRING_DIAGRAM_VIEW_ID,
36    FILTER_RESPONSE_VIEW_ID,
37    RESONATOR_RESPONSE_VIEW_ID,
38    FIXED_FILTER_BANK_VIEW_ID,
39    SEQUENCER_STEP_GRID_VIEW_ID,
40    POLYPHONY_ACTIVITY_VIEW_ID,
41    SCOPE_SPECTRUM_VIEW_ID,
42    SYSEX_COMPARISON_VIEW_ID,
43];
44
45/// A component-to-view declaration used by editor routing.
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct SpecializedComponentView {
48    /// The stable view id.
49    pub view_id: &'static str,
50    /// The component id in the audio-synth component namespace.
51    pub component: &'static str,
52}
53
54/// Components that declare specialized views.
55pub const SPECIALIZED_DECLARING_COMPONENTS: [SpecializedComponentView; 12] = [
56    SpecializedComponentView {
57        view_id: ENVELOPE_CURVE_VIEW_ID,
58        component: "dx7-operator-eg",
59    },
60    SpecializedComponentView {
61        view_id: ENVELOPE_CURVE_VIEW_ID,
62        component: "r700-envelope",
63    },
64    SpecializedComponentView {
65        view_id: ALGORITHM_ROUTING_VIEW_ID,
66        component: "dx7-voice",
67    },
68    SpecializedComponentView {
69        view_id: WIRING_DIAGRAM_VIEW_ID,
70        component: "dx7-voice",
71    },
72    SpecializedComponentView {
73        view_id: FILTER_RESPONSE_VIEW_ID,
74        component: "r700-vcf",
75    },
76    SpecializedComponentView {
77        view_id: FILTER_RESPONSE_VIEW_ID,
78        component: "m55-ladder-filter",
79    },
80    SpecializedComponentView {
81        view_id: RESONATOR_RESPONSE_VIEW_ID,
82        component: "ps3300-resonator",
83    },
84    SpecializedComponentView {
85        view_id: FIXED_FILTER_BANK_VIEW_ID,
86        component: "m55-fixed-filter-bank",
87    },
88    SpecializedComponentView {
89        view_id: SEQUENCER_STEP_GRID_VIEW_ID,
90        component: "r700-sequencer",
91    },
92    SpecializedComponentView {
93        view_id: POLYPHONY_ACTIVITY_VIEW_ID,
94        component: "ps3300-poly-array",
95    },
96    SpecializedComponentView {
97        view_id: SCOPE_SPECTRUM_VIEW_ID,
98        component: "scope-spectrum-monitor",
99    },
100    SpecializedComponentView {
101        view_id: SYSEX_COMPARISON_VIEW_ID,
102        component: "dx7-sysex-patch",
103    },
104];
105
106/// Envelope curve fixture name.
107pub const ENVELOPE_CURVE_FIXTURE: &str = "envelope-curve";
108/// Algorithm routing fixture name.
109pub const ALGORITHM_ROUTING_FIXTURE: &str = "algorithm-routing";
110/// Filter response fixture name.
111pub const FILTER_RESPONSE_FIXTURE: &str = "filter-response";
112/// Resonator response fixture name.
113pub const RESONATOR_RESPONSE_FIXTURE: &str = "resonator-response";
114/// Fixed filter bank fixture name.
115pub const FIXED_FILTER_BANK_FIXTURE: &str = "fixed-filter-bank";
116/// Sequencer step-grid fixture name.
117pub const SEQUENCER_STEP_GRID_FIXTURE: &str = "sequencer-step-grid";
118/// Polyphony activity fixture name.
119pub const POLYPHONY_ACTIVITY_FIXTURE: &str = "polyphony-activity";
120/// Scope and spectrum monitor fixture name.
121pub const SCOPE_SPECTRUM_FIXTURE: &str = "scope-spectrum";
122/// SysEx comparison fixture name.
123pub const SYSEX_COMPARISON_FIXTURE: &str = "sysex-comparison";
124
125const SPECIALIZED_FIXTURE_NAMES: [&str; 9] = [
126    ENVELOPE_CURVE_FIXTURE,
127    ALGORITHM_ROUTING_FIXTURE,
128    FILTER_RESPONSE_FIXTURE,
129    RESONATOR_RESPONSE_FIXTURE,
130    FIXED_FILTER_BANK_FIXTURE,
131    SEQUENCER_STEP_GRID_FIXTURE,
132    POLYPHONY_ACTIVITY_FIXTURE,
133    SCOPE_SPECTRUM_FIXTURE,
134    SYSEX_COMPARISON_FIXTURE,
135];
136
137/// Return every stable specialized view id.
138pub fn specialized_view_ids() -> &'static [&'static str] {
139    &SPECIALIZED_COMPONENT_VIEW_IDS
140}
141
142/// Return every component-to-view declaration.
143pub fn specialized_declaring_components() -> &'static [SpecializedComponentView] {
144    &SPECIALIZED_DECLARING_COMPONENTS
145}
146
147/// Return the fixture names covered by snapshot tests.
148pub fn specialized_fixture_names() -> [&'static str; 9] {
149    SPECIALIZED_FIXTURE_NAMES
150}
151
152/// Build a deterministic specialized view snapshot by fixture name.
153pub fn specialized_snapshot(name: &str) -> Option<Expr> {
154    match name {
155        ENVELOPE_CURVE_FIXTURE => Some(envelope_curve_view()),
156        ALGORITHM_ROUTING_FIXTURE => Some(algorithm_routing_view()),
157        FILTER_RESPONSE_FIXTURE => Some(filter_response_view()),
158        RESONATOR_RESPONSE_FIXTURE => Some(resonator_response_view()),
159        FIXED_FILTER_BANK_FIXTURE => Some(fixed_filter_bank_view()),
160        SEQUENCER_STEP_GRID_FIXTURE => Some(sequencer_step_grid_view()),
161        POLYPHONY_ACTIVITY_FIXTURE => Some(polyphony_activity_view()),
162        SCOPE_SPECTRUM_FIXTURE => Some(scope_spectrum_view()),
163        SYSEX_COMPARISON_FIXTURE => Some(sysex_comparison_view()),
164        _ => None,
165    }
166}
167
168fn envelope_curve_view() -> Expr {
169    let curve = vec![
170        (0.0, 0.0),
171        (0.04, 1.0),
172        (0.18, 0.72),
173        (0.82, 0.48),
174        (1.0, 0.0),
175    ];
176    view_root(
177        ENVELOPE_CURVE_VIEW_ID,
178        "envelope-curve-editor",
179        vec![
180            response_plot(
181                ENVELOPE_CURVE_VIEW_ID,
182                "envelope-curve",
183                &[("level", curve)],
184            ),
185            stage_table(&[
186                ("attack", 0.04, 1.0),
187                ("decay", 0.14, 0.72),
188                ("sustain", 0.64, 0.48),
189                ("release", 0.18, 0.0),
190            ]),
191            node(
192                "button",
193                vec![
194                    ("role", sym("envelope-point-editor")),
195                    ("action", sym("edit-envelope")),
196                    ("label", Expr::String("Edit point".to_owned())),
197                ],
198            ),
199        ],
200    )
201}
202
203fn algorithm_routing_view() -> Expr {
204    let routing = node(
205        "matrix",
206        vec![
207            ("lens", id(ALGORITHM_ROUTING_VIEW_ID)),
208            ("role", sym("algorithm-routing-matrix")),
209            (
210                "rows",
211                numeric_rows(&[
212                    vec![0.0, 0.0, 0.8, 0.0, 0.0, 1.0],
213                    vec![0.0, 0.0, 0.0, 0.5, 1.0, 0.0],
214                    vec![0.0, 0.7, 0.0, 1.0, 0.0, 0.0],
215                    vec![0.6, 0.0, 0.0, 0.0, 0.0, 0.0],
216                    vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
217                    vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
218                ]),
219            ),
220            (
221                "row-labels",
222                string_list(&["op1", "op2", "op3", "op4", "op5", "op6"]),
223            ),
224            (
225                "col-labels",
226                string_list(&["op1", "op2", "op3", "op4", "op5", "out"]),
227            ),
228            ("editable", Expr::Bool(true)),
229        ],
230    );
231    let wiring = node(
232        "graph",
233        vec![
234            ("lens", id(WIRING_DIAGRAM_VIEW_ID)),
235            ("role", sym("wiring-diagram")),
236            (
237                "nodes",
238                Expr::List(
239                    ["op1", "op2", "op3", "op4", "op5", "op6", "out"]
240                        .iter()
241                        .map(|name| graph_node(name))
242                        .collect(),
243                ),
244            ),
245            (
246                "edges",
247                Expr::List(vec![
248                    graph_edge("op6", "op1"),
249                    graph_edge("op5", "op2"),
250                    graph_edge("op4", "op3"),
251                    graph_edge("op3", "out"),
252                    graph_edge("op1", "out"),
253                ]),
254            ),
255        ],
256    );
257    view_root(
258        ALGORITHM_ROUTING_VIEW_ID,
259        "algorithm-routing-view",
260        vec![routing, wiring],
261    )
262}
263
264fn filter_response_view() -> Expr {
265    view_root(
266        FILTER_RESPONSE_VIEW_ID,
267        "filter-response-view",
268        vec![
269            sweep_slider("cutoff", 80.0, 12_000.0, 1_800.0),
270            sweep_slider("resonance", 0.0, 1.0, 0.62),
271            response_plot(
272                FILTER_RESPONSE_VIEW_ID,
273                "filter-response-plot",
274                &[
275                    (
276                        "low-pass",
277                        vec![
278                            (20.0, -0.2),
279                            (100.0, -0.1),
280                            (1_000.0, -1.5),
281                            (3_000.0, -8.0),
282                            (10_000.0, -32.0),
283                        ],
284                    ),
285                    (
286                        "resonant",
287                        vec![
288                            (20.0, -0.4),
289                            (100.0, -0.2),
290                            (1_000.0, 2.4),
291                            (3_000.0, -5.5),
292                            (10_000.0, -30.0),
293                        ],
294                    ),
295                ],
296            ),
297        ],
298    )
299}
300
301fn resonator_response_view() -> Expr {
302    view_root(
303        RESONATOR_RESPONSE_VIEW_ID,
304        "resonator-response-view",
305        vec![
306            sweep_slider("frequency", 110.0, 7_040.0, 880.0),
307            sweep_slider("depth", 0.0, 1.0, 0.48),
308            response_plot(
309                RESONATOR_RESPONSE_VIEW_ID,
310                "resonator-response-plot",
311                &[
312                    (
313                        "bank-a",
314                        vec![
315                            (110.0, -12.0),
316                            (220.0, -4.0),
317                            (440.0, 0.0),
318                            (880.0, 3.2),
319                            (1_760.0, -5.0),
320                        ],
321                    ),
322                    (
323                        "bank-b",
324                        vec![
325                            (110.0, -16.0),
326                            (220.0, -7.0),
327                            (440.0, -1.0),
328                            (880.0, 2.5),
329                            (1_760.0, 0.2),
330                        ],
331                    ),
332                ],
333            ),
334        ],
335    )
336}
337
338fn fixed_filter_bank_view() -> Expr {
339    view_root(
340        FIXED_FILTER_BANK_VIEW_ID,
341        "fixed-filter-bank-view",
342        vec![
343            response_plot(
344                FIXED_FILTER_BANK_VIEW_ID,
345                "fixed-filter-bank-plot",
346                &[(
347                    "band-gain",
348                    vec![
349                        (50.0, -3.0),
350                        (120.0, 2.0),
351                        (400.0, -5.0),
352                        (1_200.0, 4.0),
353                        (3_600.0, -1.0),
354                    ],
355                )],
356            ),
357            node(
358                "table",
359                vec![
360                    ("role", sym("fixed-filter-bank-bands")),
361                    (
362                        "rows",
363                        Expr::List(vec![
364                            band_row("50hz", 50.0, -3.0),
365                            band_row("120hz", 120.0, 2.0),
366                            band_row("400hz", 400.0, -5.0),
367                            band_row("1k2", 1_200.0, 4.0),
368                            band_row("3k6", 3_600.0, -1.0),
369                        ]),
370                    ),
371                ],
372            ),
373        ],
374    )
375}
376
377fn sequencer_step_grid_view() -> Expr {
378    view_root(
379        SEQUENCER_STEP_GRID_VIEW_ID,
380        "sequencer-step-grid",
381        vec![node(
382            "grid",
383            vec![
384                ("role", sym("sequencer-step-grid")),
385                (
386                    "rows",
387                    numeric_rows(&[
388                        vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
389                        vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
390                        vec![0.0, 0.0, 0.7, 0.0, 0.0, 0.0, 0.4, 0.0],
391                    ]),
392                ),
393                ("row-labels", string_list(&["gate", "accent", "cv"])),
394                (
395                    "col-labels",
396                    string_list(&["1", "2", "3", "4", "5", "6", "7", "8"]),
397                ),
398                ("editable", Expr::Bool(true)),
399            ],
400        )],
401    )
402}
403
404fn polyphony_activity_view() -> Expr {
405    view_root(
406        POLYPHONY_ACTIVITY_VIEW_ID,
407        "polyphony-activity-view",
408        vec![node(
409            "grid",
410            vec![
411                ("role", sym("polyphony-activity-map")),
412                (
413                    "rows",
414                    numeric_rows(&[
415                        vec![1.0, 0.8, 0.4, 0.0],
416                        vec![0.6, 1.0, 0.7, 0.2],
417                        vec![0.2, 0.5, 1.0, 0.9],
418                        vec![0.0, 0.2, 0.6, 1.0],
419                    ]),
420                ),
421                (
422                    "row-labels",
423                    string_list(&["cell-1", "cell-2", "cell-3", "cell-4"]),
424                ),
425                ("col-labels", string_list(&["vco", "vcf", "vca", "eg"])),
426            ],
427        )],
428    )
429}
430
431fn scope_spectrum_view() -> Expr {
432    view_root(
433        SCOPE_SPECTRUM_VIEW_ID,
434        "scope-spectrum-monitor",
435        vec![
436            node(
437                "waveform",
438                vec![
439                    ("role", sym("scope-monitor")),
440                    (
441                        "samples",
442                        number_list(&[0.0, 0.32, 0.74, 0.96, 0.62, 0.0, -0.58, -0.92, -0.31]),
443                    ),
444                ],
445            ),
446            node(
447                "spectrum",
448                vec![
449                    ("role", sym("spectrum-monitor")),
450                    (
451                        "bins",
452                        number_list(&[0.0, -5.0, -12.0, -18.0, -26.0, -40.0]),
453                    ),
454                ],
455            ),
456        ],
457    )
458}
459
460fn sysex_comparison_view() -> Expr {
461    view_root(
462        SYSEX_COMPARISON_VIEW_ID,
463        "sysex-comparison-view",
464        vec![
465            node(
466                "table",
467                vec![
468                    ("role", sym("sysex-format-comparison")),
469                    (
470                        "rows",
471                        Expr::List(vec![
472                            sysex_row("hex", "f0 43 00 09 20 00 7f f7"),
473                            sysex_row("binary", "11110000 01000011 00000000 00001001"),
474                            sysex_row("lisp", "(dx7-patch :algorithm 5 :feedback 7)"),
475                        ]),
476                    ),
477                ],
478            ),
479            node(
480                "badge",
481                vec![
482                    ("role", sym("round-trip-probe")),
483                    ("status", sym("ok")),
484                    (
485                        "label",
486                        Expr::String("lossless codec:lisp -> codec:binary".to_owned()),
487                    ),
488                ],
489            ),
490        ],
491    )
492}
493
494fn view_root(lens: &str, role: &str, children: Vec<Expr>) -> Expr {
495    node(
496        "stack",
497        vec![
498            ("lens", id(lens)),
499            ("role", sym(role)),
500            ("dir", sym("column")),
501            ("declaring-components", declaring_components(lens)),
502            ("children", Expr::List(children)),
503        ],
504    )
505}
506
507fn declaring_components(view_id: &str) -> Expr {
508    Expr::List(
509        SPECIALIZED_DECLARING_COMPONENTS
510            .iter()
511            .filter(|entry| entry.view_id == view_id)
512            .map(|entry| qsym("audio-synth/component", entry.component))
513            .collect(),
514    )
515}
516
517fn stage_table(stages: &[(&str, f64, f64)]) -> Expr {
518    node(
519        "table",
520        vec![
521            ("role", sym("envelope-stage-table")),
522            (
523                "rows",
524                Expr::List(
525                    stages
526                        .iter()
527                        .map(|(stage, time, level)| {
528                            data_map(vec![
529                                ("stage", sym(stage)),
530                                ("time", number(*time)),
531                                ("level", number(*level)),
532                            ])
533                        })
534                        .collect(),
535                ),
536            ),
537        ],
538    )
539}
540
541fn response_plot(lens: &str, role: &str, series: &[(&str, Vec<(f64, f64)>)]) -> Expr {
542    let (min_x, max_x, min_y, max_y) = bounds(series);
543    node(
544        "plot",
545        vec![
546            ("lens", id(lens)),
547            ("role", sym(role)),
548            (
549                "axes",
550                data_map(vec![("x", axis(min_x, max_x)), ("y", axis(min_y, max_y))]),
551            ),
552            (
553                "series",
554                Expr::List(
555                    series
556                        .iter()
557                        .map(|(name, points)| {
558                            data_map(vec![
559                                ("name", sym(name)),
560                                ("style", sym("line")),
561                                (
562                                    "points",
563                                    Expr::List(
564                                        points
565                                            .iter()
566                                            .map(|(x, y)| {
567                                                data_map(vec![("x", number(*x)), ("y", number(*y))])
568                                            })
569                                            .collect(),
570                                    ),
571                                ),
572                            ])
573                        })
574                        .collect(),
575                ),
576            ),
577        ],
578    )
579}
580
581fn bounds(series: &[(&str, Vec<(f64, f64)>)]) -> (f64, f64, f64, f64) {
582    let mut bounds: Option<(f64, f64, f64, f64)> = None;
583    for (_, points) in series {
584        for (x, y) in points {
585            bounds = Some(match bounds {
586                Some((min_x, max_x, min_y, max_y)) => {
587                    (min_x.min(*x), max_x.max(*x), min_y.min(*y), max_y.max(*y))
588                }
589                None => (*x, *x, *y, *y),
590            });
591        }
592    }
593    bounds.unwrap_or((0.0, 1.0, 0.0, 1.0))
594}
595
596fn axis(min: f64, max: f64) -> Expr {
597    data_map(vec![("min", number(min)), ("max", number(max))])
598}
599
600fn sweep_slider(param: &str, min: f64, max: f64, value: f64) -> Expr {
601    node(
602        "slider",
603        vec![
604            ("role", sym("response-sweep")),
605            ("param", sym(param)),
606            ("min", number(min)),
607            ("max", number(max)),
608            ("value", number(value)),
609        ],
610    )
611}
612
613fn band_row(name: &str, frequency: f64, gain: f64) -> Expr {
614    data_map(vec![
615        ("name", Expr::String(name.to_owned())),
616        ("frequency", number(frequency)),
617        ("gain", number(gain)),
618    ])
619}
620
621fn sysex_row(format: &str, value: &str) -> Expr {
622    data_map(vec![
623        ("format", sym(format)),
624        ("value", Expr::String(value.to_owned())),
625    ])
626}
627
628fn graph_node(name: &str) -> Expr {
629    node(
630        "node",
631        vec![("id", sym(name)), ("label", Expr::String(name.to_owned()))],
632    )
633}
634
635fn graph_edge(from: &str, to: &str) -> Expr {
636    node("edge", vec![("from", sym(from)), ("to", sym(to))])
637}
638
639fn numeric_rows(rows: &[Vec<f64>]) -> Expr {
640    Expr::List(
641        rows.iter()
642            .map(|row| Expr::List(row.iter().map(|value| number(*value)).collect()))
643            .collect(),
644    )
645}
646
647fn number_list(values: &[f64]) -> Expr {
648    Expr::List(values.iter().map(|value| number(*value)).collect())
649}
650
651fn string_list(values: &[&str]) -> Expr {
652    Expr::List(
653        values
654            .iter()
655            .map(|value| Expr::String((*value).to_owned()))
656            .collect(),
657    )
658}
659
660fn id(name: &str) -> Expr {
661    Expr::Symbol(Symbol::new(name))
662}
663
664fn qsym(namespace: &str, name: &str) -> Expr {
665    Expr::Symbol(Symbol::qualified(namespace, name))
666}
667
668use sim_value::build::float as number;