Skip to main content

auracle_grammar/
describe.rs

1//! Rack description: a frontend-facing view of a [`PatchTree`] as modules,
2//! knobs, and wires.
3//!
4//! Every knob carries the **trace address** of the choice site it displays
5//! (`node/0#cut`, `amp#attack`, …) — the same addresses the grammar samples,
6//! [`crate::genome`] encodes, and MH proposes over. That makes the panel a
7//! *direct* view of the genome: turning a knob is an edit at that address
8//! ([`crate::edit::set_param`]) and locking a knob is a constraint on that
9//! address during refinement.
10
11use serde::{Deserialize, Serialize};
12
13use crate::term::{
14    quant_root_index, quant_scale_index, rect_mode_index, AudioNode, DriveMode, FilterKind,
15    ModNode, ModOp, NoiseColor, PatchTree, TableShape, Waveform, QUANT_ROOTS, QUANT_SCALES,
16    RECT_MODES,
17};
18
19/// What kind of control a knob is.
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "t", rename_all = "snake_case")]
22pub enum KnobKind {
23    /// Continuous parameter, normalized `[0, 1]` (an `F64` trace site).
24    Continuous,
25    /// A small enum selector (a `Usize` trace site); `value` is the index.
26    Enum {
27        /// Display names, in categorical index order.
28        options: Vec<String>,
29    },
30    /// Octave selector: `Usize` site `0..=4`, displayed as `−2..=+2`.
31    Octave,
32}
33
34/// One knob on a module faceplate.
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
36pub struct Knob {
37    /// Full trace address (`key#site`).
38    pub addr: String,
39    /// Silkscreen label.
40    pub label: String,
41    /// Current value: normalized `[0,1]` for continuous, index for enums.
42    pub value: f64,
43    /// Control kind.
44    pub kind: KnobKind,
45}
46
47/// One module faceplate in the rack.
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct RackModule {
50    /// Trace key of the node (`node`, `node/0`, `node/0/m`, `node/0/m/0`,
51    /// `amp`). A modulation chain's stages nest under the slot with the same
52    /// `/0`, `/1` child convention the audio tree uses.
53    pub key: String,
54    /// Stable node identity — [`Uid`](crate::Uid), flattened to its number, or
55    /// `0` for the `amp` pseudo-module, which is the envelope wrapping the term
56    /// rather than a node in it.
57    ///
58    /// `key` is the wire protocol: it is what a `StructOp` names, what a trace
59    /// address is built from, and what the engine understands. `uid` is the
60    /// *identity* — the same module before and after an insert above it, a
61    /// delete beside it, or a generation of refinement — and it is what the
62    /// panel keys locks, hand positions and selection by. Anything that has to
63    /// survive an edit is keyed by this; anything the engine has to read is
64    /// keyed by `key`.
65    pub uid: u64,
66    /// Machine kind tag (`vco`, `filter`, `lfo`, `amp`, …).
67    pub kind: String,
68    /// Silkscreen title.
69    pub title: String,
70    /// Distance from the root (root = 0); layout hint for column placement.
71    pub column: usize,
72    /// True for modulation-sort modules — the ones that live at or below a
73    /// `<key>/m` slot rather than in the audio path.
74    ///
75    /// As of wave 2C that is a whole sort rather than four leaves: the
76    /// generators (lfo, mod env, s&h rand, follower, euclid), the CV
77    /// processors that wrap them (quantize, slew, rectify, hold) and the
78    /// combiners that join two (min, max, and, or, xor, switch). A chain is
79    /// drawn as a run of modules with `mod` wires between them, each one
80    /// column further from the destination.
81    pub is_mod: bool,
82    /// The knobs, in faceplate order.
83    pub knobs: Vec<Knob>,
84    /// Structural choice addresses owned by this module (`#leaf`, `#src`,
85    /// `#op`, `#mod`, and any *empty* mod slot it guards). Locking the module
86    /// means locking these plus all knob addresses — evolution can then not
87    /// replace or restructure it.
88    pub structural_addrs: Vec<String>,
89}
90
91/// A patch cable between two modules.
92#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
93pub struct Wire {
94    /// Source module key.
95    pub from: String,
96    /// Destination module key.
97    pub to: String,
98    /// `"audio"` or `"mod"`.
99    pub kind: String,
100}
101
102/// The full rack view of one patch.
103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
104pub struct RackDescription {
105    /// Modules, root-first depth-first.
106    pub modules: Vec<RackModule>,
107    /// Patch cables.
108    pub wires: Vec<Wire>,
109}
110
111/// Display names for the waveform categorical, in index order.
112pub fn waveform_options() -> Vec<String> {
113    Waveform::ALL.iter().map(|w| w.port_name().into()).collect()
114}
115
116/// Display names for the noise-color categorical, in index order.
117pub fn noise_options() -> Vec<String> {
118    NoiseColor::ALL
119        .iter()
120        .map(|c| c.port_name().into())
121        .collect()
122}
123
124/// Display names for the filter-kind categorical, in index order.
125pub fn filter_options() -> Vec<String> {
126    ["svf lp", "svf bp", "svf hp", "ladder"]
127        .iter()
128        .map(|s| s.to_string())
129        .collect()
130}
131
132/// Display names for the wavetable categorical, in index order.
133pub fn table_options() -> Vec<String> {
134    TableShape::ALL.iter().map(|t| t.label().into()).collect()
135}
136
137/// Display names for the distortion-mode categorical, in index order.
138pub fn drive_mode_options() -> Vec<String> {
139    DriveMode::ALL.iter().map(|m| m.label().into()).collect()
140}
141
142fn knob_c(key: &str, site: &str, label: &str, value: f64) -> Knob {
143    Knob {
144        addr: format!("{key}#{site}"),
145        label: label.into(),
146        value,
147        kind: KnobKind::Continuous,
148    }
149}
150
151fn knob_e(key: &str, site: &str, label: &str, index: usize, options: Vec<String>) -> Knob {
152    Knob {
153        addr: format!("{key}#{site}"),
154        label: label.into(),
155        value: index as f64,
156        kind: KnobKind::Enum { options },
157    }
158}
159
160fn knob_oct(key: &str, octave: i8) -> Knob {
161    Knob {
162        addr: format!("{key}#oct"),
163        label: "octave".into(),
164        value: (octave + 2) as f64,
165        kind: KnobKind::Octave,
166    }
167}
168
169/// Describe a modulation term as a **chain** of modules feeding `parent_key`.
170///
171/// Modulation is a recursive sort as of wave 2C, so this walks the term the
172/// way `describe_node` walks the audio tree: the processor is pushed first,
173/// then a `mod` wire from each of its subterms, then the subterms themselves
174/// at the next column. `structural_addrs` names both `#mod` and the op's own
175/// `#modop`/`#pairop`, so locking a shaper pins what it is as well as that it
176/// is there.
177fn describe_mod(
178    m: &ModNode,
179    key: &str,
180    parent_key: &str,
181    column: usize,
182    out: &mut RackDescription,
183    parent_structural: &mut Vec<String>,
184) {
185    let mut structural = vec![format!("{key}#mod")];
186    // The recursive arms push their own module and then recurse, so they
187    // return early rather than falling through to the leaf tail.
188    match m {
189        ModNode::None => {
190            // The empty slot's choice site belongs to the parent: locking the
191            // parent pins "no modulation" in place.
192            parent_structural.push(format!("{key}#mod"));
193            return;
194        }
195        ModNode::Op {
196            kind,
197            p0,
198            p1,
199            input,
200            ..
201        } => {
202            structural.push(format!("{key}#modop"));
203            let sites = kind.param_sites();
204            let knobs = match kind {
205                ModOp::Quantize => vec![
206                    // Both plates carry their current selection, because the
207                    // sites behind them are continuous: quiver reads `root`
208                    // and `scale` as `(cv·11.99)` and `(cv·6.99)` quantized
209                    // *inside the module*, so the genome site is an f64 and a
210                    // bare number on the faceplate would tell the player
211                    // nothing about which scale they are in.
212                    knob_c(
213                        key,
214                        "qroot",
215                        &format!("root · {}", QUANT_ROOTS[quant_root_index(*p0)]),
216                        *p0,
217                    ),
218                    knob_c(
219                        key,
220                        "qscale",
221                        &format!("scale · {}", QUANT_SCALES[quant_scale_index(*p1)]),
222                        *p1,
223                    ),
224                ],
225                ModOp::Slew => vec![
226                    knob_c(key, "rise", "rise", *p0),
227                    knob_c(key, "fall", "fall", *p1),
228                ],
229                ModOp::Rectify => vec![knob_c(
230                    key,
231                    "rmode",
232                    &format!("mode · {}", RECT_MODES[rect_mode_index(*p0)]),
233                    *p0,
234                )],
235                ModOp::Hold => vec![knob_c(key, sites[0], "rate", *p0)],
236            };
237            out.modules.push(RackModule {
238                key: key.into(),
239                uid: m.uid().map_or(0, |u| u.0),
240                kind: kind.label().into(),
241                title: kind.label().into(),
242                column,
243                is_mod: true,
244                knobs,
245                structural_addrs: structural,
246            });
247            out.wires.push(Wire {
248                from: key.into(),
249                to: parent_key.into(),
250                kind: "mod".into(),
251            });
252            let child = format!("{key}/0");
253            out.wires.push(Wire {
254                from: child.clone(),
255                to: key.into(),
256                kind: "mod".into(),
257            });
258            let mut ignored = Vec::new();
259            describe_mod(input, &child, key, column + 1, out, &mut ignored);
260            return;
261        }
262        ModNode::Pair { kind, a, b, .. } => {
263            structural.push(format!("{key}#pairop"));
264            out.modules.push(RackModule {
265                key: key.into(),
266                uid: m.uid().map_or(0, |u| u.0),
267                kind: kind.label().into(),
268                title: kind.label().into(),
269                column,
270                is_mod: true,
271                knobs: Vec::new(),
272                structural_addrs: structural,
273            });
274            out.wires.push(Wire {
275                from: key.into(),
276                to: parent_key.into(),
277                kind: "mod".into(),
278            });
279            let (ka, kb) = (format!("{key}/0"), format!("{key}/1"));
280            for k in [&ka, &kb] {
281                out.wires.push(Wire {
282                    from: k.clone(),
283                    to: key.into(),
284                    kind: "mod".into(),
285                });
286            }
287            let mut ignored = Vec::new();
288            describe_mod(a, &ka, key, column + 1, out, &mut ignored);
289            describe_mod(b, &kb, key, column + 1, out, &mut ignored);
290            return;
291        }
292        _ => {}
293    }
294    let (kind, title, knobs) = match m {
295        // Handled above; the compiler cannot see that.
296        ModNode::None | ModNode::Op { .. } | ModNode::Pair { .. } => return,
297        ModNode::Lfo { wave, rate, .. } => (
298            "lfo",
299            "lfo",
300            vec![
301                knob_e(key, "wave", "wave", wave.index(), waveform_options()),
302                knob_c(key, "rate", "rate", *rate),
303            ],
304        ),
305        ModNode::Env { attack, decay, .. } => (
306            "modenv",
307            "mod env",
308            vec![
309                knob_c(key, "att", "attack", *attack),
310                knob_c(key, "dec", "decay", *decay),
311            ],
312        ),
313        ModNode::Rand { rate, glide, .. } => (
314            "rand",
315            "s&h rand",
316            vec![
317                knob_c(key, "rate", "rate", *rate),
318                knob_c(key, "glide", "glide", *glide),
319            ],
320        ),
321        ModNode::Follow { sens, release, .. } => (
322            "follow",
323            "follower",
324            vec![
325                knob_c(key, "sens", "sens", *sens),
326                knob_c(key, "rel", "release", *release),
327            ],
328        ),
329        ModNode::Euclid {
330            rate,
331            steps,
332            pulses,
333            ..
334        } => (
335            "euclid",
336            "euclid",
337            vec![
338                knob_c(key, "erate", "rate", *rate),
339                knob_c(key, "esteps", "steps", *steps),
340                knob_c(key, "epulses", "pulses", *pulses),
341            ],
342        ),
343    };
344    out.modules.push(RackModule {
345        key: key.into(),
346        uid: m.uid().map_or(0, |u| u.0),
347        kind: kind.into(),
348        title: title.into(),
349        column,
350        is_mod: true,
351        knobs,
352        structural_addrs: structural,
353    });
354    out.wires.push(Wire {
355        from: key.into(),
356        to: parent_key.into(),
357        kind: "mod".into(),
358    });
359}
360
361/// Push a module that owns a modulation slot, then its slot, then its audio
362/// input (`None` for a source, which has a slot but no input).
363///
364/// The slot has to be described *after* the module is pushed — an empty one
365/// contributes its `#mod` address to the owner's `structural_addrs`, so the
366/// owner's entry is patched once the slot is known — and *before* the input
367/// subtree, so the rack stays root-first depth-first. Getting that order
368/// right in eleven places by hand is how it goes wrong in one of them.
369fn push_modulated(
370    out: &mut RackDescription,
371    mut module: RackModule,
372    input: Option<&AudioNode>,
373    modulation: &ModNode,
374) {
375    let key = module.key.clone();
376    let column = module.column;
377    let mut structural = std::mem::take(&mut module.structural_addrs);
378    let idx = out.modules.len();
379    out.modules.push(module);
380
381    let child = format!("{key}/0");
382    if input.is_some() {
383        out.wires.push(Wire {
384            from: child.clone(),
385            to: key.clone(),
386            kind: "audio".into(),
387        });
388    }
389    describe_mod(
390        modulation,
391        &format!("{key}/m"),
392        &key,
393        column + 1,
394        out,
395        &mut structural,
396    );
397    out.modules[idx].structural_addrs = structural;
398    if let Some(input) = input {
399        describe_node(input, &child, column + 1, out);
400    }
401}
402
403/// Push a binary node (`mix`, `ringmod`) and recurse into both branches.
404fn push_binary(out: &mut RackDescription, module: RackModule, a: &AudioNode, b: &AudioNode) {
405    push_binary_with(out, module, a, b, None);
406}
407
408/// [`push_binary`] for a binary node that *also* owns a modulation slot — the
409/// wave-2B dynamics family, whose `/1` branch is a control signal and whose
410/// slot reaches a real parameter besides.
411///
412/// Kept separate from [`push_modulated`] rather than generalizing it, because
413/// the two differ in more than a branch count: `push_modulated` draws the
414/// input wire only when there *is* one (a source has a slot and no input),
415/// while every node here has exactly two.
416fn push_binary_modulated(
417    out: &mut RackDescription,
418    module: RackModule,
419    a: &AudioNode,
420    b: &AudioNode,
421    modulation: &ModNode,
422) {
423    push_binary_with(out, module, a, b, Some(modulation));
424}
425
426fn push_binary_with(
427    out: &mut RackDescription,
428    mut module: RackModule,
429    a: &AudioNode,
430    b: &AudioNode,
431    modulation: Option<&ModNode>,
432) {
433    let (key, column) = (module.key.clone(), module.column);
434    let mut structural = std::mem::take(&mut module.structural_addrs);
435    let idx = out.modules.len();
436    out.modules.push(module);
437    let (ka, kb) = (format!("{key}/0"), format!("{key}/1"));
438    for k in [&ka, &kb] {
439        out.wires.push(Wire {
440            from: k.clone(),
441            to: key.clone(),
442            kind: "audio".into(),
443        });
444    }
445    // Same ordering rule as `push_modulated`: the slot is described after the
446    // module is pushed (an empty one hands its `#mod` address back to the
447    // owner) and before the branches, so the rack stays root-first
448    // depth-first.
449    if let Some(m) = modulation {
450        describe_mod(
451            m,
452            &format!("{key}/m"),
453            &key,
454            column + 1,
455            out,
456            &mut structural,
457        );
458    }
459    out.modules[idx].structural_addrs = structural;
460    describe_node(a, &ka, column + 1, out);
461    describe_node(b, &kb, column + 1, out);
462}
463
464fn describe_node(n: &AudioNode, key: &str, column: usize, out: &mut RackDescription) {
465    let leaf_src = vec![format!("{key}#leaf"), format!("{key}#src")];
466    let leaf_op = vec![format!("{key}#leaf"), format!("{key}#op")];
467    // Every module below shares everything but its kind, title and knobs.
468    let module = |kind: &str, title: &str, knobs: Vec<Knob>, structural: Vec<String>| RackModule {
469        key: key.into(),
470        uid: n.uid().0,
471        kind: kind.into(),
472        title: title.into(),
473        column,
474        is_mod: false,
475        knobs,
476        structural_addrs: structural,
477    };
478    match n {
479        AudioNode::Vco {
480            wave,
481            octave,
482            detune,
483            mod_depth,
484            modulation,
485            ..
486        } => push_modulated(
487            out,
488            module(
489                "vco",
490                "vco",
491                vec![
492                    knob_e(key, "wave", "wave", wave.index(), waveform_options()),
493                    knob_oct(key, *octave),
494                    knob_c(key, "det", "detune", *detune),
495                    knob_c(key, "mdepth", "mod depth", *mod_depth),
496                ],
497                leaf_src,
498            ),
499            // A source has a modulation slot but no audio input to show — and
500            // on the two oscillators that slot reaches *pitch*.
501            None,
502            modulation,
503        ),
504        AudioNode::Supersaw {
505            octave,
506            detune,
507            mix,
508            mod_depth,
509            modulation,
510            ..
511        } => push_modulated(
512            out,
513            module(
514                "supersaw",
515                "supersaw",
516                vec![
517                    knob_oct(key, *octave),
518                    knob_c(key, "det", "detune", *detune),
519                    knob_c(key, "smix", "mix", *mix),
520                    knob_c(key, "mdepth", "mod depth", *mod_depth),
521                ],
522                leaf_src,
523            ),
524            None,
525            modulation,
526        ),
527        AudioNode::Formant {
528            vowel,
529            shift,
530            octave,
531            mod_depth,
532            modulation,
533            ..
534        } => push_modulated(
535            out,
536            module(
537                "formant",
538                "formant",
539                vec![
540                    knob_c(key, "vowel", "vowel", *vowel),
541                    knob_c(key, "fshift", "shift", *shift),
542                    knob_oct(key, *octave),
543                    knob_c(key, "mdepth", "mod depth", *mod_depth),
544                ],
545                leaf_src,
546            ),
547            None,
548            modulation,
549        ),
550        AudioNode::Noise { color, .. } => out.modules.push(module(
551            "noise",
552            "noise",
553            vec![knob_e(
554                key,
555                "color",
556                "color",
557                color.index(),
558                noise_options(),
559            )],
560            leaf_src,
561        )),
562        // No knobs, because there is nothing to set. The rack has drawn a
563        // dashed EMPTY plate at holes for some time; what is new is that the
564        // plate and the patch underneath it now agree, so it describes the
565        // term rather than annotating one.
566        AudioNode::Silence { .. } => {
567            out.modules
568                .push(module("silence", "empty", Vec::new(), leaf_src))
569        }
570        AudioNode::Wavetable {
571            table,
572            octave,
573            morph,
574            mod_depth,
575            modulation,
576            ..
577        } => push_modulated(
578            out,
579            module(
580                "wavetable",
581                "wavetable",
582                vec![
583                    knob_e(key, "table", "table", table.index(), table_options()),
584                    knob_oct(key, *octave),
585                    knob_c(key, "morph", "morph", *morph),
586                    knob_c(key, "mdepth", "mod depth", *mod_depth),
587                ],
588                leaf_src,
589            ),
590            // A source has a modulation slot but no audio input to show.
591            None,
592            modulation,
593        ),
594        AudioNode::Pluck {
595            octave,
596            damping,
597            brightness,
598            mod_depth,
599            modulation,
600            ..
601        } => push_modulated(
602            out,
603            module(
604                "pluck",
605                "pluck",
606                vec![
607                    knob_oct(key, *octave),
608                    // Labelled "decay", not "damping". quiver's port opens the
609                    // loop filter as it rises ("higher damping = brighter",
610                    // oscillators.rs), so it lengthens and brightens the
611                    // string — which is the opposite of what every synthesist
612                    // means by damping. Naming it for what it does beats
613                    // inverting it and then having to invert the mod cable to
614                    // match.
615                    knob_c(key, "damp", "decay", *damping),
616                    knob_c(key, "bright", "brightness", *brightness),
617                    knob_c(key, "mdepth", "mod depth", *mod_depth),
618                ],
619                leaf_src,
620            ),
621            None,
622            modulation,
623        ),
624        AudioNode::Mix { balance, a, b, .. } => push_binary(
625            out,
626            module(
627                "mix",
628                "mix",
629                vec![knob_c(key, "bal", "balance", *balance)],
630                leaf_op,
631            ),
632            a,
633            b,
634        ),
635        AudioNode::RingMod { mix, a, b, .. } => push_binary(
636            out,
637            module(
638                "ringmod",
639                "ring mod",
640                vec![knob_c(key, "rgmix", "mix", *mix)],
641                leaf_op,
642            ),
643            a,
644            b,
645        ),
646        AudioNode::Filter {
647            kind,
648            cutoff,
649            resonance,
650            mod_depth,
651            input,
652            modulation,
653            ..
654        } => push_modulated(
655            out,
656            module(
657                "filter",
658                // The ladder is a different circuit with a different
659                // reputation; the panel says so even though the kind is one
660                // enum site.
661                match kind {
662                    FilterKind::Ladder => "ladder",
663                    _ => "filter",
664                },
665                vec![
666                    knob_e(key, "fkind", "mode", kind.index(), filter_options()),
667                    knob_c(key, "cut", "cutoff", *cutoff),
668                    knob_c(key, "res", "resonance", *resonance),
669                    knob_c(key, "mdepth", "mod depth", *mod_depth),
670                ],
671                leaf_op,
672            ),
673            Some(input),
674            modulation,
675        ),
676        AudioNode::Fold {
677            threshold,
678            mod_depth,
679            input,
680            modulation,
681            ..
682        } => push_modulated(
683            out,
684            module(
685                "fold",
686                "wavefolder",
687                vec![
688                    knob_c(key, "thresh", "fold", *threshold),
689                    knob_c(key, "mdepth", "mod depth", *mod_depth),
690                ],
691                leaf_op,
692            ),
693            Some(input),
694            modulation,
695        ),
696        AudioNode::Delay {
697            time,
698            feedback,
699            mix,
700            mod_depth,
701            input,
702            modulation,
703            ..
704        } => push_modulated(
705            out,
706            module(
707                "delay",
708                "delay",
709                vec![
710                    knob_c(key, "time", "time", *time),
711                    knob_c(key, "fb", "feedback", *feedback),
712                    knob_c(key, "dmix", "mix", *mix),
713                    knob_c(key, "mdepth", "mod depth", *mod_depth),
714                ],
715                leaf_op,
716            ),
717            Some(input),
718            modulation,
719        ),
720        AudioNode::Chorus {
721            rate,
722            depth,
723            mix,
724            mod_depth,
725            input,
726            modulation,
727            ..
728        } => push_modulated(
729            out,
730            module(
731                "chorus",
732                "chorus",
733                vec![
734                    knob_c(key, "crate", "rate", *rate),
735                    knob_c(key, "cdepth", "depth", *depth),
736                    knob_c(key, "cmix", "mix", *mix),
737                    knob_c(key, "mdepth", "mod depth", *mod_depth),
738                ],
739                leaf_op,
740            ),
741            Some(input),
742            modulation,
743        ),
744        AudioNode::Reverb {
745            size,
746            damp,
747            mix,
748            mod_depth,
749            input,
750            modulation,
751            ..
752        } => push_modulated(
753            out,
754            module(
755                "reverb",
756                "reverb",
757                vec![
758                    knob_c(key, "rsize", "size", *size),
759                    knob_c(key, "rdamp", "damp", *damp),
760                    knob_c(key, "rmix", "mix", *mix),
761                    knob_c(key, "mdepth", "mod depth", *mod_depth),
762                ],
763                leaf_op,
764            ),
765            Some(input),
766            modulation,
767        ),
768        AudioNode::Distortion {
769            drive,
770            tone,
771            mode,
772            mod_depth,
773            input,
774            modulation,
775            ..
776        } => push_modulated(
777            out,
778            module(
779                "distortion",
780                "distortion",
781                vec![
782                    knob_c(key, "drive", "drive", *drive),
783                    knob_c(key, "tone", "tone", *tone),
784                    knob_e(key, "dmode", "mode", mode.index(), drive_mode_options()),
785                    knob_c(key, "mdepth", "mod depth", *mod_depth),
786                ],
787                leaf_op,
788            ),
789            Some(input),
790            modulation,
791        ),
792        AudioNode::Bitcrush {
793            bits,
794            downsample,
795            mod_depth,
796            input,
797            modulation,
798            ..
799        } => push_modulated(
800            out,
801            module(
802                "bitcrush",
803                "bitcrush",
804                vec![
805                    knob_c(key, "bits", "bits", *bits),
806                    knob_c(key, "dsamp", "rate", *downsample),
807                    knob_c(key, "mdepth", "mod depth", *mod_depth),
808                ],
809                leaf_op,
810            ),
811            Some(input),
812            modulation,
813        ),
814        AudioNode::Phaser {
815            rate,
816            depth,
817            feedback,
818            mod_depth,
819            input,
820            modulation,
821            ..
822        } => push_modulated(
823            out,
824            module(
825                "phaser",
826                "phaser",
827                vec![
828                    knob_c(key, "prate", "rate", *rate),
829                    knob_c(key, "pdepth", "depth", *depth),
830                    knob_c(key, "pfb", "feedback", *feedback),
831                    knob_c(key, "mdepth", "mod depth", *mod_depth),
832                ],
833                leaf_op,
834            ),
835            Some(input),
836            modulation,
837        ),
838        AudioNode::Flanger {
839            rate,
840            depth,
841            feedback,
842            mod_depth,
843            input,
844            modulation,
845            ..
846        } => push_modulated(
847            out,
848            module(
849                "flanger",
850                "flanger",
851                vec![
852                    knob_c(key, "frate", "rate", *rate),
853                    knob_c(key, "fdepth", "depth", *depth),
854                    knob_c(key, "ffb", "feedback", *feedback),
855                    knob_c(key, "mdepth", "mod depth", *mod_depth),
856                ],
857                leaf_op,
858            ),
859            Some(input),
860            modulation,
861        ),
862        AudioNode::Tremolo {
863            rate,
864            depth,
865            shape,
866            mod_depth,
867            input,
868            modulation,
869            ..
870        } => push_modulated(
871            out,
872            module(
873                "tremolo",
874                "tremolo",
875                vec![
876                    knob_c(key, "trate", "rate", *rate),
877                    knob_c(key, "tdepth", "depth", *depth),
878                    knob_c(key, "tshape", "shape", *shape),
879                    knob_c(key, "mdepth", "mod depth", *mod_depth),
880                ],
881                leaf_op,
882            ),
883            Some(input),
884            modulation,
885        ),
886        AudioNode::Vibrato {
887            rate,
888            depth,
889            mix,
890            mod_depth,
891            input,
892            modulation,
893            ..
894        } => push_modulated(
895            out,
896            module(
897                "vibrato",
898                "vibrato",
899                vec![
900                    knob_c(key, "vrate", "rate", *rate),
901                    knob_c(key, "vdepth", "depth", *depth),
902                    knob_c(key, "vmix", "mix", *mix),
903                    knob_c(key, "mdepth", "mod depth", *mod_depth),
904                ],
905                leaf_op,
906            ),
907            Some(input),
908            modulation,
909        ),
910        AudioNode::Eq {
911            low,
912            mid,
913            high,
914            mod_depth,
915            input,
916            modulation,
917            ..
918        } => push_modulated(
919            out,
920            module(
921                "eq",
922                "eq",
923                vec![
924                    knob_c(key, "low", "low", *low),
925                    knob_c(key, "mid", "mid", *mid),
926                    knob_c(key, "high", "high", *high),
927                    knob_c(key, "mdepth", "mod depth", *mod_depth),
928                ],
929                leaf_op,
930            ),
931            Some(input),
932            modulation,
933        ),
934        AudioNode::Granular {
935            position,
936            size,
937            density,
938            mod_depth,
939            input,
940            modulation,
941            ..
942        } => push_modulated(
943            out,
944            module(
945                "granular",
946                "granular",
947                vec![
948                    knob_c(key, "gpos", "position", *position),
949                    knob_c(key, "gsize", "size", *size),
950                    knob_c(key, "gdens", "density", *density),
951                    knob_c(key, "mdepth", "mod depth", *mod_depth),
952                ],
953                leaf_op,
954            ),
955            Some(input),
956            modulation,
957        ),
958        AudioNode::Shift {
959            semis,
960            window,
961            mix,
962            mod_depth,
963            input,
964            modulation,
965            ..
966        } => push_modulated(
967            out,
968            module(
969                "shift",
970                "pitch shift",
971                vec![
972                    knob_c(key, "semis", "shift", *semis),
973                    knob_c(key, "window", "window", *window),
974                    knob_c(key, "smix", "mix", *mix),
975                    knob_c(key, "mdepth", "mod depth", *mod_depth),
976                ],
977                leaf_op,
978            ),
979            Some(input),
980            modulation,
981        ),
982        // The four binary dynamics modules. Their `/0` and `/1` child keys are
983        // what the frontend hangs its per-module jack labels off (`in`/`key`,
984        // `carrier`/`modulator`, …), so neither the order nor the spelling can
985        // move without renaming those.
986        AudioNode::Comp {
987            threshold,
988            ratio,
989            makeup,
990            mod_depth,
991            input,
992            sidechain,
993            modulation,
994            ..
995        } => push_binary_modulated(
996            out,
997            module(
998                "comp",
999                "compressor",
1000                vec![
1001                    knob_c(key, "thresh", "threshold", *threshold),
1002                    knob_c(key, "ratio", "ratio", *ratio),
1003                    knob_c(key, "makeup", "makeup", *makeup),
1004                    knob_c(key, "mdepth", "mod depth", *mod_depth),
1005                ],
1006                leaf_op,
1007            ),
1008            input,
1009            sidechain,
1010            modulation,
1011        ),
1012        AudioNode::Duck {
1013            amount,
1014            threshold,
1015            release,
1016            mod_depth,
1017            input,
1018            key: key_input,
1019            modulation,
1020            ..
1021        } => push_binary_modulated(
1022            out,
1023            module(
1024                "duck",
1025                "ducker",
1026                vec![
1027                    knob_c(key, "amount", "amount", *amount),
1028                    knob_c(key, "dthresh", "threshold", *threshold),
1029                    knob_c(key, "drel", "release", *release),
1030                    knob_c(key, "mdepth", "mod depth", *mod_depth),
1031                ],
1032                leaf_op,
1033            ),
1034            input,
1035            key_input,
1036            modulation,
1037        ),
1038        AudioNode::Gate {
1039            threshold,
1040            range,
1041            release,
1042            mod_depth,
1043            input,
1044            sidechain,
1045            modulation,
1046            ..
1047        } => push_binary_modulated(
1048            out,
1049            module(
1050                "gate",
1051                "gate",
1052                vec![
1053                    knob_c(key, "gthresh", "threshold", *threshold),
1054                    knob_c(key, "range", "range", *range),
1055                    knob_c(key, "grel", "release", *release),
1056                    knob_c(key, "mdepth", "mod depth", *mod_depth),
1057                ],
1058                leaf_op,
1059            ),
1060            input,
1061            sidechain,
1062            modulation,
1063        ),
1064        AudioNode::Vocoder {
1065            bands,
1066            attack,
1067            release,
1068            mod_depth,
1069            carrier,
1070            modulator,
1071            modulation,
1072            ..
1073        } => push_binary_modulated(
1074            out,
1075            module(
1076                "vocoder",
1077                "vocoder",
1078                vec![
1079                    knob_c(key, "bands", "bands", *bands),
1080                    knob_c(key, "vatt", "attack", *attack),
1081                    knob_c(key, "vrel", "release", *release),
1082                    knob_c(key, "mdepth", "mod depth", *mod_depth),
1083                ],
1084                leaf_op,
1085            ),
1086            carrier,
1087            modulator,
1088            modulation,
1089        ),
1090    }
1091}
1092
1093/// Describe a patch as a rack of modules, knobs, and wires.
1094///
1095/// The amp/VCA stage (mandatory on every voice) appears as an `amp` module at
1096/// column 0 with the audio root wired into it.
1097pub fn describe(tree: &PatchTree) -> RackDescription {
1098    let mut out = RackDescription {
1099        modules: Vec::new(),
1100        wires: Vec::new(),
1101    };
1102    out.modules.push(RackModule {
1103        key: "amp".into(),
1104        uid: 0,
1105        kind: "amp".into(),
1106        title: "env / out".into(),
1107        column: 0,
1108        is_mod: false,
1109        knobs: vec![
1110            knob_c("amp", "attack", "attack", tree.amp.attack),
1111            knob_c("amp", "decay", "decay", tree.amp.decay),
1112            knob_c("amp", "sustain", "sustain", tree.amp.sustain),
1113            knob_c("amp", "release", "release", tree.amp.release),
1114        ],
1115        structural_addrs: Vec::new(),
1116    });
1117    out.wires.push(Wire {
1118        from: "node".into(),
1119        to: "amp".into(),
1120        kind: "audio".into(),
1121    });
1122    describe_node(&tree.root, "node", 1, &mut out);
1123    out
1124}