Skip to main content

auracle_grammar/
prior.rs

1//! The patch prior: a typed PCFG over [`PatchTree`] terms as a fugue program.
2//!
3//! Every node at tree path `p` (root key `"node"`, children `"node/0"`,
4//! `"node/0/1"`, …; a processor's modulation slot at `"<p>/m"`) emits real
5//! probabilistic choices at path-keyed addresses:
6//!
7//! | Site | Address | Distribution |
8//! |---|---|---|
9//! | source-vs-processor | `<p>#leaf` | `Bernoulli(source_prob)` (forced at max depth) |
10//! | source kind | `<p>#src` | `Categorical(source_weights)` |
11//! | processor kind | `<p>#op` | `Categorical(op_weights)` |
12//! | modulation kind | `<p>/m#mod` | `Categorical(mod_weights)` (leaves only at max mod depth; never empty below a processor) |
13//! | CV-processor kind | `<p>/m#modop` | uniform over [`ModOp::ALL`] |
14//! | CV-combiner kind | `<p>/m#pairop` | uniform over [`PairOp::ALL`] |
15//!
16//! Modulation is a **recursive sort** as of wave 2C: a term's subterms live at
17//! `<p>/m/0` and `<p>/m/1`, the same child convention the audio tree uses and
18//! unambiguous because every modulation key sits below a `/m`. Its parsimony
19//! pressure is [`PatchGrammarPrior::max_mod_depth`] — see
20//! [`PatchGrammarPrior::mod_weights_at`], which is where both of the sort's
21//! renormalizations happen.
22//!
23//! A modulation slot hangs off every module that has somewhere to send it.
24//! As of wave 2B the exceptions are `Noise` (its only site is a colour switch)
25//! and `Mix`/`RingMod` (both inputs are audio, and the one knob is the blend).
26//! Having two audio children is *not* an exception: the four 2B dynamics
27//! productions take two subterms and carry a slot as well.
28//! | discrete params | `<p>#wave` / `#oct` / `#color` / `#fkind` / `#table` / `#dmode` | uniform categoricals |
29//! | continuous params | `<p>#det`, `#cut`, `#res`, … | `Uniform(0, 1)` |
30//!
31//! The amplitude envelope lives at `amp#attack` … `amp#release`.
32//!
33//! Because the structure of an execution is encoded in its own choices, the
34//! generic trace machinery (subtree regeneration MH, subtree-swap crossover)
35//! applies unchanged — this mirrors fugue-evo's `ArithmeticGrammarPrior`
36//! design, with quiver signal sorts in place of arithmetic types.
37//!
38//! Deeper patches pay more prior mass by construction: parsimony pressure is
39//! the grammar itself, not an ad-hoc penalty.
40
41use fugue::{addr, sample, Bernoulli, Categorical, Model, ModelExt, Uniform};
42use fugue_evo::inference::prior::GenomePrior;
43use rand::Rng;
44
45use crate::term::{
46    AmpEnv, AudioNode, DriveMode, FilterKind, ModNode, ModOp, NoiseColor, PairOp, PatchTree,
47    TableShape, Uid, Waveform,
48};
49
50/// Source-kind categorical order: Vco, Supersaw, Noise, Wavetable, Pluck,
51/// Formant.
52///
53/// These three counts are the **persisted wire format** — [`crate::genome`]
54/// writes the chosen index into the trace — so the orders are append-only.
55pub const N_SOURCES: usize = 7;
56/// Processor-kind categorical order: Mix, Filter, Fold, Delay, Chorus,
57/// Reverb, Distortion, Bitcrush, Phaser, RingMod, Flanger, Tremolo, Vibrato,
58/// Eq, Granular, Shift, Comp, Duck, Gate, Vocoder.
59pub const N_OPS: usize = 20;
60/// Modulation-kind categorical order: None, Lfo, Env, Rand, Follow, Euclid,
61/// Op, Pair.
62///
63/// The last three arrived in wave 2C, when modulation became a recursive sort:
64/// `Euclid` is a fifth leaf, `Op` wraps one modulation term and `Pair` two.
65pub const N_MODS: usize = 8;
66/// Unary CV-processor categorical order — [`ModOp::ALL`].
67pub const N_MOD_OPS: usize = 4;
68/// Binary CV-combiner categorical order — [`PairOp::ALL`].
69pub const N_PAIR_OPS: usize = 6;
70/// The first `#mod` index that is **not** a leaf. Kinds at or above it recurse
71/// and are what [`PatchGrammarPrior::max_mod_depth`] switches off.
72const MOD_FIRST_BRANCH: usize = 6;
73
74/// The typed PCFG over patch terms.
75#[derive(Clone, Debug)]
76pub struct PatchGrammarPrior {
77    /// Probability that a node (below max depth) is a source leaf.
78    pub source_prob: f64,
79    /// Maximum tree depth; nodes at this depth are forced to be sources.
80    pub max_depth: usize,
81    /// Maximum nesting depth of a modulation term: a term at this depth is
82    /// forced to be a leaf, exactly as [`Self::max_depth`] forces `#leaf`.
83    ///
84    /// This is the mod sort's **only** parsimony pressure. The audio tree pays
85    /// for its own size in prior mass because every extra node is another
86    /// `#leaf`/`#op` draw; a modulation chain pays the same way, but nothing
87    /// about the *audio* term's mass objects to a forty-node CV chain that
88    /// moves one knob, so the ceiling has to be explicit. 2 means a term may
89    /// wrap at most two processors before it bottoms out in a leaf.
90    pub max_mod_depth: usize,
91    /// Weights over source kinds
92    /// `[Vco, Supersaw, Noise, Wavetable, Pluck, Formant, Silence]`.
93    pub source_weights: [f64; N_SOURCES],
94    /// Weights over processor kinds
95    /// `[Mix, Filter, Fold, Delay, Chorus, Reverb, Distortion, Bitcrush,
96    /// Phaser, RingMod, Flanger, Tremolo, Vibrato, Eq, Granular, Shift, Comp,
97    /// Duck, Gate, Vocoder]`.
98    pub op_weights: [f64; N_OPS],
99    /// Weights over modulation kinds
100    /// `[None, Lfo, Env, Rand, Follow, Euclid, Op, Pair]`.
101    pub mod_weights: [f64; N_MODS],
102}
103
104impl Default for PatchGrammarPrior {
105    fn default() -> Self {
106        Self {
107            source_prob: 0.4,
108            max_depth: 5,
109            // Two processors above a leaf is already `s&h → quantize → slew`,
110            // which is the deepest idiom anyone reaches for; a third adds a
111            // stage nobody can hear separately. It is also a *stack* budget:
112            // the compiler recurses by value, the wasm build only just fits
113            // its 8 MB stack with the audio recursion alone, and every level
114            // here is a second recursion sitting on top of that one.
115            max_mod_depth: 2,
116            // Vco stays the staple and supersaw second; wavetable is a real
117            // alternative but a new one; noise, pluck and formant are spices —
118            // the last two especially, because a plucked string and a vowel
119            // are each a whole character rather than a layer inside someone
120            // else's patch.
121            //
122            // `Silence` is last and is not a spice: it is the socket a player
123            // left unplugged, and the prior's job with it is only to make it
124            // *representable*. At weight zero the grammar gives `p = 0` to any
125            // tree containing one, `log p` is −∞, and MH rejects every proposal
126            // that touches a hand-made hole — a patch would become un-evolvable
127            // by the act of unplugging something. 0.5% keeps `log p` finite
128            // while making a prior draw that contains one rare; a tree that is
129            // *all* silence renders silent, the vet gate quarantines it, and
130            // evolution learns to avoid it. Its real prevalence is set by the
131            // player's edits, not by this number, which is unusual among kinds
132            // and is the reason a rate this small is not a reason to leave it
133            // out of φ.
134            source_weights: [0.34, 0.24, 0.13, 0.13, 0.08, 0.08, 0.005],
135            // Filter carries subtractive identity and stays dominant — half
136            // again the next-largest weight, and three to sixteen times any
137            // of the colour and movement modules. Mix keeps branching alive;
138            // distortion sits beside the wavefolder.
139            //
140            // Wave 2A's five newcomers are motion and tone rather than
141            // structure, so their mass comes out of the existing time-fx and
142            // out of mix, never out of the filter. Granular is the rarest
143            // thing in the grammar: it is a texture you reach for
144            // deliberately, and a pool full of it is unplayable.
145            //
146            // Bitcrush, phaser, ring mod and granular are spice and must
147            // **stay** spice. A uniform pad across fifteen operators would
148            // make the first generation after this update granular ring-mod
149            // mush — and, worse, would put the user's entire accumulated
150            // taste history off-distribution, since every observation in it
151            // was collected under a prior that could not draw those modules
152            // at all.
153            //
154            // Wave 2B's four *binary* newcomers are the structurally
155            // significant part of this table, and they are held down for a
156            // reason the unary waves did not have: every one of them recurses
157            // twice, so their weight buys tree size — which the grammar's
158            // parsimony pressure pays for in prior mass and the render budget
159            // pays for in seconds. Mix and ring mod were 17.0% of op mass;
160            // adding the four at a naive weight would have pushed branching
161            // past a quarter of all ops. At these weights it reaches 20.6%,
162            // and the measured mean term size moves by a few percent rather
163            // than by a factor.
164            //
165            // Their order among themselves is how often you would reach for
166            // one: a pitch shifter is a harmony device (and unary, so it costs
167            // nothing structural), a compressor is common, a ducker and a gate
168            // are gestures, and a vocoder is a whole patch's identity — the
169            // same argument that keeps pluck and formant low among sources.
170            //
171            // [mix, filter, fold, delay, chorus, reverb, distortion,
172            //  bitcrush, phaser, ringmod, flanger, tremolo, vibrato, eq,
173            //  granular, shift, comp, duck, gate, vocoder]
174            op_weights: [
175                0.14, 0.24, 0.075, 0.085, 0.065, 0.055, 0.075, 0.02, 0.02, 0.018, 0.02, 0.028,
176                0.028, 0.047, 0.014, 0.022, 0.016, 0.014, 0.010, 0.008,
177            ],
178            // Most slots stay empty; envelopes still slightly beat LFOs for
179            // the filter-sweep idiom; the follower is rarer than either but
180            // must be reachable; S&H stays rare.
181            //
182            // Wave 2C's three are **spice, and have to stay spice**, for a
183            // reason the op table does not have: they are the only
184            // productions in the grammar that recurse *inside a slot*, so
185            // their weight buys mod-chain length rather than variety. A pool
186            // in which most modulators arrive wrapped in two processors is a
187            // pool of patches that all sound like a sample-and-hold, and the
188            // user's whole taste history was collected under a prior that
189            // could not draw them at all.
190            //
191            // The old five keep their relative proportions and are scaled by
192            // 0.915 to make room, so nothing already learned about LFOs
193            // against envelopes moves. Of the 8.5% that buys: euclid takes
194            // the largest share because it is a leaf — it costs one node and
195            // is the only rhythmic modulator in the palette; `op` is next
196            // because it is the family the whole wave is for; `pair` is the
197            // smallest by a wide margin because it is the only production
198            // that draws **two** subterms, and at 1.5% a two-branch chain is
199            // ~0.6% of filled slots rather than the 4% a naive weight gives.
200            //
201            // [none, lfo, env, rand, follow, euclid, op, pair]
202            mod_weights: [0.40, 0.18, 0.20, 0.055, 0.08, 0.03, 0.04, 0.015],
203        }
204    }
205}
206
207fn child_key(key: &str, i: usize) -> String {
208    format!("{key}/{i}")
209}
210
211fn mod_key(key: &str) -> String {
212    format!("{key}/m")
213}
214
215fn u01() -> Uniform {
216    Uniform::new(0.0, 1.0).expect("valid unit uniform")
217}
218
219fn uniform_cat(n: usize) -> Categorical {
220    Categorical::new(vec![1.0 / n as f64; n]).expect("valid uniform categorical")
221}
222
223fn weighted_cat(weights: &[f64]) -> Categorical {
224    let total: f64 = weights.iter().sum();
225    Categorical::new(weights.iter().map(|w| w / total).collect()).expect("valid categorical")
226}
227
228/// Sample a run of `Uniform(0,1)` parameter sites at `key`, in order.
229///
230/// Exactly the hand-nested `bind` chain the older two- and three-parameter
231/// arms below spell out — same addresses, same order, same prior mass. It
232/// exists because the palette's new processors carry four continuous knobs
233/// *plus* a modulation subterm, and six levels of nested closure is a place
234/// where a mis-typed address hides rather than shows.
235fn u01_seq(key: String, sites: &'static [&'static str]) -> Model<Vec<f64>> {
236    match sites.split_first() {
237        None => fugue::pure(Vec::new()),
238        Some((site, rest)) => sample(addr!(key.clone(), *site), u01()).bind(move |v| {
239            u01_seq(key.clone(), rest).map(move |mut tail| {
240                tail.insert(0, v);
241                tail
242            })
243        }),
244    }
245}
246
247impl PatchGrammarPrior {
248    fn source_model(&self, key: String) -> Model<AudioNode> {
249        let weights = self.source_weights;
250        // Five of the six sources own a modulation slot, so the source model
251        // needs the grammar config the processor model already carried.
252        let cfg = self.clone();
253        sample(addr!(key.clone(), "src"), weighted_cat(&weights)).bind(move |src| match src {
254            0 => {
255                let k = key.clone();
256                let cfg = cfg.clone();
257                sample(addr!(k.clone(), "wave"), uniform_cat(Waveform::ALL.len())).bind(move |w| {
258                    let k2 = k.clone();
259                    let cfg2 = cfg.clone();
260                    sample(addr!(k2.clone(), "oct"), uniform_cat(5)).bind(move |o| {
261                        let k3 = k2.clone();
262                        let cfg3 = cfg2.clone();
263                        u01_seq(k3.clone(), &["det", "mdepth"]).bind(move |p| {
264                            cfg3.mod_model(mod_key(&k3), 0, true)
265                                .map(move |m| AudioNode::Vco {
266                                    uid: Uid::NEW,
267                                    wave: Waveform::from_index(w),
268                                    octave: o as i8 - 2,
269                                    detune: p[0],
270                                    mod_depth: p[1],
271                                    modulation: m,
272                                })
273                        })
274                    })
275                })
276            }
277            1 => {
278                let k = key.clone();
279                let cfg = cfg.clone();
280                sample(addr!(k.clone(), "oct"), uniform_cat(5)).bind(move |o| {
281                    let k2 = k.clone();
282                    let cfg2 = cfg.clone();
283                    u01_seq(k2.clone(), &["det", "smix", "mdepth"]).bind(move |p| {
284                        cfg2.mod_model(mod_key(&k2), 0, true)
285                            .map(move |m| AudioNode::Supersaw {
286                                uid: Uid::NEW,
287                                octave: o as i8 - 2,
288                                detune: p[0],
289                                mix: p[1],
290                                mod_depth: p[2],
291                                modulation: m,
292                            })
293                    })
294                })
295            }
296            2 => sample(
297                addr!(key.clone(), "color"),
298                uniform_cat(NoiseColor::ALL.len()),
299            )
300            .map(|c| AudioNode::Noise {
301                uid: Uid::NEW,
302                color: NoiseColor::from_index(c),
303            }),
304            3 => {
305                let k = key.clone();
306                let cfg = cfg.clone();
307                sample(
308                    addr!(k.clone(), "table"),
309                    uniform_cat(TableShape::ALL.len()),
310                )
311                .bind(move |tb| {
312                    let k2 = k.clone();
313                    let cfg2 = cfg.clone();
314                    sample(addr!(k2.clone(), "oct"), uniform_cat(5)).bind(move |o| {
315                        let k3 = k2.clone();
316                        let cfg3 = cfg2.clone();
317                        u01_seq(k3.clone(), &["morph", "mdepth"]).bind(move |p| {
318                            cfg3.mod_model(mod_key(&k3), 0, true).map(move |m| {
319                                AudioNode::Wavetable {
320                                    uid: Uid::NEW,
321                                    table: TableShape::from_index(tb),
322                                    octave: o as i8 - 2,
323                                    morph: p[0],
324                                    mod_depth: p[1],
325                                    modulation: m,
326                                }
327                            })
328                        })
329                    })
330                })
331            }
332            4 => {
333                let k = key.clone();
334                let cfg = cfg.clone();
335                sample(addr!(k.clone(), "oct"), uniform_cat(5)).bind(move |o| {
336                    let k2 = k.clone();
337                    let cfg2 = cfg.clone();
338                    u01_seq(k2.clone(), &["damp", "bright", "mdepth"]).bind(move |p| {
339                        cfg2.mod_model(mod_key(&k2), 0, true)
340                            .map(move |m| AudioNode::Pluck {
341                                uid: Uid::NEW,
342                                octave: o as i8 - 2,
343                                damping: p[0],
344                                brightness: p[1],
345                                mod_depth: p[2],
346                                modulation: m,
347                            })
348                    })
349                })
350            }
351            5 => {
352                let k = key.clone();
353                sample(addr!(k.clone(), "oct"), uniform_cat(5)).bind(move |o| {
354                    let k2 = k.clone();
355                    let cfg2 = cfg.clone();
356                    u01_seq(k2.clone(), &["vowel", "fshift", "mdepth"]).bind(move |p| {
357                        cfg2.mod_model(mod_key(&k2), 0, true)
358                            .map(move |m| AudioNode::Formant {
359                                uid: Uid::NEW,
360                                vowel: p[0],
361                                shift: p[1],
362                                octave: o as i8 - 2,
363                                mod_depth: p[2],
364                                modulation: m,
365                            })
366                    })
367                })
368            }
369            // Index 6. A catch-all rather than `6 =>` because the match is on
370            // a `usize` and needs one; `weighted_cat` cannot return anything
371            // above `N_SOURCES - 1`, so this arm is reached for 6 and nothing
372            // else. It samples no sites at all, which is what makes a hole the
373            // cheapest leaf in the grammar.
374            _ => fugue::pure(AudioNode::Silence { uid: Uid::NEW }),
375        })
376    }
377
378    /// The `#mod` weights in force at one point in a modulation term.
379    ///
380    /// Two renormalizations, both by zeroing a weight and letting
381    /// [`weighted_cat`] divide by what is left — which keeps the categorical's
382    /// **arity at eight everywhere**, so the value stored in the trace is
383    /// always the absolute kind index and [`crate::genome`]'s encoding stays
384    /// site-for-site identical to a generative run.
385    ///
386    /// - **At the depth bound**, `Op` and `Pair` go to zero, so the term is
387    ///   forced to bottom out in a leaf. This is exactly how [`Self::max_depth`]
388    ///   already forces `#leaf` true in the audio tree.
389    /// - **Below a processor**, `None` goes to zero. A quantizer with nothing
390    ///   under it emits a constant and a logic gate fed two zeroes is stuck
391    ///   low; both are a module on the rack that cannot make a sound. The
392    ///   alternative — sample the degenerate term and fold it away — is not
393    ///   available here, because the generative model and the trace encoding
394    ///   are asserted to emit the same choices and a folded term does not.
395    ///   `ModNode::None` therefore stays reachable at the top of every slot,
396    ///   which is where it means "no modulation", and nowhere else.
397    fn mod_weights_at(&self, depth: usize, root: bool) -> [f64; N_MODS] {
398        let mut w = self.mod_weights;
399        if !root {
400            w[0] = 0.0;
401        }
402        if depth >= self.max_mod_depth {
403            for slot in w.iter_mut().skip(MOD_FIRST_BRANCH) {
404                *slot = 0.0;
405            }
406        }
407        w
408    }
409
410    /// A modulation term at nesting `depth`; `root` marks the top of a slot,
411    /// the one place an *empty* term is a legal draw.
412    fn mod_model(&self, key: String, depth: usize, root: bool) -> Model<ModNode> {
413        let weights = self.mod_weights_at(depth, root);
414        let cfg = self.clone();
415        sample(addr!(key.clone(), "mod"), weighted_cat(&weights)).bind(move |kind| match kind {
416            0 => fugue::pure(ModNode::None),
417            1 => {
418                let k = key.clone();
419                sample(addr!(k.clone(), "wave"), uniform_cat(Waveform::ALL.len())).bind(move |w| {
420                    sample(addr!(k.clone(), "rate"), u01()).map(move |r| ModNode::Lfo {
421                        uid: Uid::NEW,
422                        wave: Waveform::from_index(w),
423                        rate: r,
424                    })
425                })
426            }
427            2 => {
428                let k = key.clone();
429                sample(addr!(k.clone(), "att"), u01()).bind(move |a| {
430                    sample(addr!(k.clone(), "dec"), u01()).map(move |d| ModNode::Env {
431                        uid: Uid::NEW,
432                        attack: a,
433                        decay: d,
434                    })
435                })
436            }
437            3 => u01_seq(key.clone(), &["rate", "glide"]).map(|p| ModNode::Rand {
438                uid: Uid::NEW,
439                rate: p[0],
440                glide: p[1],
441            }),
442            4 => u01_seq(key.clone(), &["sens", "rel"]).map(|p| ModNode::Follow {
443                uid: Uid::NEW,
444                sens: p[0],
445                release: p[1],
446            }),
447            5 => u01_seq(key.clone(), &["erate", "esteps", "epulses"]).map(|p| ModNode::Euclid {
448                uid: Uid::NEW,
449                rate: p[0],
450                steps: p[1],
451                pulses: p[2],
452            }),
453            // The two recursive arms. Draw order — kind, then the op's own
454            // knobs, then the subterms left to right — is the order
455            // `crate::genome` encodes them in, and the two must not disagree.
456            6 => {
457                let k = key.clone();
458                let cfg = cfg.clone();
459                sample(addr!(k.clone(), "modop"), uniform_cat(N_MOD_OPS)).bind(move |o| {
460                    let kind = ModOp::from_index(o);
461                    let k2 = k.clone();
462                    let cfg2 = cfg.clone();
463                    u01_seq(k2.clone(), kind.param_sites()).bind(move |p| {
464                        let p1 = p.get(1).copied().unwrap_or(0.0);
465                        let p0 = p[0];
466                        cfg2.mod_model(child_key(&k2, 0), depth + 1, false)
467                            .map(move |input| ModNode::Op {
468                                uid: Uid::NEW,
469                                kind,
470                                p0,
471                                p1,
472                                input: Box::new(input),
473                            })
474                    })
475                })
476            }
477            _ => {
478                let k = key.clone();
479                let cfg = cfg.clone();
480                sample(addr!(k.clone(), "pairop"), uniform_cat(N_PAIR_OPS)).bind(move |o| {
481                    let kind = PairOp::from_index(o);
482                    let (ka, kb) = (child_key(&k, 0), child_key(&k, 1));
483                    let cfg2 = cfg.clone();
484                    cfg.mod_model(ka, depth + 1, false).bind(move |a| {
485                        cfg2.mod_model(kb.clone(), depth + 1, false)
486                            .map(move |b| ModNode::Pair {
487                                uid: Uid::NEW,
488                                kind,
489                                a: Box::new(a.clone()),
490                                b: Box::new(b),
491                            })
492                    })
493                })
494            }
495        })
496    }
497
498    fn audio_model(&self, key: String, depth: usize) -> Model<AudioNode> {
499        let cfg = self.clone();
500        let p_leaf = if depth >= cfg.max_depth {
501            1.0
502        } else {
503            cfg.source_prob
504        };
505        sample(
506            addr!(key.clone(), "leaf"),
507            Bernoulli::new(p_leaf).expect("valid leaf probability"),
508        )
509        .bind(move |is_leaf| {
510            if is_leaf {
511                cfg.source_model(key.clone())
512            } else {
513                let cfg2 = cfg.clone();
514                let key2 = key.clone();
515                sample(addr!(key.clone(), "op"), weighted_cat(&cfg.op_weights))
516                    .bind(move |op| cfg2.op_model(key2.clone(), op, depth))
517            }
518        })
519    }
520
521    /// A production with **two** audio subterms *and* a modulation slot — the
522    /// wave-2B dynamics family.
523    ///
524    /// Four continuous sites, then the slot, then `/0` and `/1` in that order,
525    /// which is the order [`crate::genome`] encodes them in. Written once
526    /// rather than four times for the reason [`u01_seq`] exists: the arms
527    /// differ only in which variant they assemble, and five levels of nested
528    /// `bind` is where a mis-typed address hides instead of showing.
529    fn binary_mod_op<F>(
530        &self,
531        key: String,
532        sites: &'static [&'static str],
533        depth: usize,
534        build: F,
535    ) -> Model<AudioNode>
536    where
537        F: FnOnce(Vec<f64>, ModNode, AudioNode, AudioNode) -> AudioNode + Send + 'static,
538    {
539        let (ka, kb) = (child_key(&key, 0), child_key(&key, 1));
540        let (cfg_m, cfg_a, cfg_b) = (self.clone(), self.clone(), self.clone());
541        u01_seq(key.clone(), sites).bind(move |p| {
542            cfg_m.mod_model(mod_key(&key), 0, true).bind(move |m| {
543                cfg_a.audio_model(ka, depth + 1).bind(move |a| {
544                    cfg_b
545                        .audio_model(kb, depth + 1)
546                        .map(move |b| build(p, m, a, b))
547                })
548            })
549        })
550    }
551
552    fn op_model(&self, key: String, op: usize, depth: usize) -> Model<AudioNode> {
553        let cfg = self.clone();
554        match op {
555            // Mix
556            0 => {
557                let (ka, kb) = (child_key(&key, 0), child_key(&key, 1));
558                let (cfg_a, cfg_b) = (cfg.clone(), cfg.clone());
559                sample(addr!(key, "bal"), u01()).bind(move |bal| {
560                    let cfg_b = cfg_b.clone();
561                    let kb = kb.clone();
562                    cfg_a.audio_model(ka.clone(), depth + 1).bind(move |a| {
563                        cfg_b
564                            .audio_model(kb.clone(), depth + 1)
565                            .map(move |b| AudioNode::Mix {
566                                uid: Uid::NEW,
567                                balance: bal,
568                                a: Box::new(a.clone()),
569                                b: Box::new(b),
570                            })
571                    })
572                })
573            }
574            // Filter
575            1 => {
576                let k = key.clone();
577                sample(
578                    addr!(k.clone(), "fkind"),
579                    uniform_cat(FilterKind::ALL.len()),
580                )
581                .bind(move |fk| {
582                    let k2 = k.clone();
583                    let cfg2 = cfg.clone();
584                    sample(addr!(k2.clone(), "cut"), u01()).bind(move |cut| {
585                        let k3 = k2.clone();
586                        let cfg3 = cfg2.clone();
587                        sample(addr!(k3.clone(), "res"), u01()).bind(move |res| {
588                            let k4 = k3.clone();
589                            let cfg4 = cfg3.clone();
590                            sample(addr!(k4.clone(), "mdepth"), u01()).bind(move |md| {
591                                let k5 = k4.clone();
592                                let cfg5 = cfg4.clone();
593                                cfg4.mod_model(mod_key(&k5), 0, true).bind(move |m| {
594                                    let m = m.clone();
595                                    cfg5.audio_model(child_key(&k5, 0), depth + 1).map(
596                                        move |input| AudioNode::Filter {
597                                            uid: Uid::NEW,
598                                            kind: FilterKind::from_index(fk),
599                                            cutoff: cut,
600                                            resonance: res,
601                                            mod_depth: md,
602                                            input: Box::new(input),
603                                            modulation: m.clone(),
604                                        },
605                                    )
606                                })
607                            })
608                        })
609                    })
610                })
611            }
612            // Fold
613            2 => {
614                let k = key.clone();
615                sample(addr!(k.clone(), "thresh"), u01()).bind(move |t| {
616                    let k2 = k.clone();
617                    let cfg2 = cfg.clone();
618                    sample(addr!(k2.clone(), "mdepth"), u01()).bind(move |md| {
619                        let k3 = k2.clone();
620                        let cfg3 = cfg2.clone();
621                        cfg2.mod_model(mod_key(&k3), 0, true).bind(move |m| {
622                            let m = m.clone();
623                            cfg3.audio_model(child_key(&k3, 0), depth + 1)
624                                .map(move |input| AudioNode::Fold {
625                                    uid: Uid::NEW,
626                                    threshold: t,
627                                    mod_depth: md,
628                                    input: Box::new(input),
629                                    modulation: m.clone(),
630                                })
631                        })
632                    })
633                })
634            }
635            // Delay
636            3 => {
637                let k = key.clone();
638                u01_seq(k.clone(), &["time", "fb", "dmix", "mdepth"]).bind(move |p| {
639                    let (time, fb, mix, md) = (p[0], p[1], p[2], p[3]);
640                    let (k2, cfg2) = (k.clone(), cfg.clone());
641                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
642                        let m = m.clone();
643                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
644                            .map(move |input| AudioNode::Delay {
645                                uid: Uid::NEW,
646                                time,
647                                feedback: fb,
648                                mix,
649                                mod_depth: md,
650                                input: Box::new(input),
651                                modulation: m.clone(),
652                            })
653                    })
654                })
655            }
656            // Chorus
657            4 => {
658                let k = key.clone();
659                u01_seq(k.clone(), &["crate", "cdepth", "cmix", "mdepth"]).bind(move |p| {
660                    let (rate, dep, mix, md) = (p[0], p[1], p[2], p[3]);
661                    let (k2, cfg2) = (k.clone(), cfg.clone());
662                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
663                        let m = m.clone();
664                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
665                            .map(move |input| AudioNode::Chorus {
666                                uid: Uid::NEW,
667                                rate,
668                                depth: dep,
669                                mix,
670                                mod_depth: md,
671                                input: Box::new(input),
672                                modulation: m.clone(),
673                            })
674                    })
675                })
676            }
677            // Reverb
678            5 => {
679                let k = key.clone();
680                u01_seq(k.clone(), &["rsize", "rdamp", "rmix", "mdepth"]).bind(move |p| {
681                    let (size, damp, mix, md) = (p[0], p[1], p[2], p[3]);
682                    let (k2, cfg2) = (k.clone(), cfg.clone());
683                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
684                        let m = m.clone();
685                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
686                            .map(move |input| AudioNode::Reverb {
687                                uid: Uid::NEW,
688                                size,
689                                damp,
690                                mix,
691                                mod_depth: md,
692                                input: Box::new(input),
693                                modulation: m.clone(),
694                            })
695                    })
696                })
697            }
698            // Distortion
699            6 => {
700                let k = key.clone();
701                sample(addr!(k.clone(), "dmode"), uniform_cat(DriveMode::ALL.len())).bind(
702                    move |dm| {
703                        let (k2, cfg2) = (k.clone(), cfg.clone());
704                        u01_seq(k2.clone(), &["drive", "tone", "mdepth"]).bind(move |p| {
705                            let (drive, tone, md) = (p[0], p[1], p[2]);
706                            let (k3, cfg3) = (k2.clone(), cfg2.clone());
707                            cfg2.mod_model(mod_key(&k3), 0, true).bind(move |m| {
708                                let m = m.clone();
709                                cfg3.audio_model(child_key(&k3, 0), depth + 1)
710                                    .map(move |input| AudioNode::Distortion {
711                                        uid: Uid::NEW,
712                                        drive,
713                                        tone,
714                                        mode: DriveMode::from_index(dm),
715                                        mod_depth: md,
716                                        input: Box::new(input),
717                                        modulation: m.clone(),
718                                    })
719                            })
720                        })
721                    },
722                )
723            }
724            // Bitcrush
725            7 => {
726                let k = key.clone();
727                u01_seq(k.clone(), &["bits", "dsamp", "mdepth"]).bind(move |p| {
728                    let (bits, dsamp, md) = (p[0], p[1], p[2]);
729                    let (k2, cfg2) = (k.clone(), cfg.clone());
730                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
731                        let m = m.clone();
732                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
733                            .map(move |input| AudioNode::Bitcrush {
734                                uid: Uid::NEW,
735                                bits,
736                                downsample: dsamp,
737                                mod_depth: md,
738                                input: Box::new(input),
739                                modulation: m.clone(),
740                            })
741                    })
742                })
743            }
744            // Phaser
745            8 => {
746                let k = key.clone();
747                u01_seq(k.clone(), &["prate", "pdepth", "pfb", "mdepth"]).bind(move |p| {
748                    let (rate, dep, fb, md) = (p[0], p[1], p[2], p[3]);
749                    let (k2, cfg2) = (k.clone(), cfg.clone());
750                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
751                        let m = m.clone();
752                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
753                            .map(move |input| AudioNode::Phaser {
754                                uid: Uid::NEW,
755                                rate,
756                                depth: dep,
757                                feedback: fb,
758                                mod_depth: md,
759                                input: Box::new(input),
760                                modulation: m.clone(),
761                            })
762                    })
763                })
764            }
765            // Ring mod — the second binary production, so it recurses twice
766            // exactly as Mix does.
767            9 => {
768                let (ka, kb) = (child_key(&key, 0), child_key(&key, 1));
769                let (cfg_a, cfg_b) = (cfg.clone(), cfg.clone());
770                sample(addr!(key, "rgmix"), u01()).bind(move |mix| {
771                    let cfg_b = cfg_b.clone();
772                    let kb = kb.clone();
773                    cfg_a.audio_model(ka.clone(), depth + 1).bind(move |a| {
774                        cfg_b
775                            .audio_model(kb.clone(), depth + 1)
776                            .map(move |b| AudioNode::RingMod {
777                                uid: Uid::NEW,
778                                mix,
779                                a: Box::new(a.clone()),
780                                b: Box::new(b),
781                            })
782                    })
783                })
784            }
785            10 => {
786                let k = key.clone();
787                u01_seq(k.clone(), &["frate", "fdepth", "ffb", "mdepth"]).bind(move |p| {
788                    let (rate, dep, feedback, md) = (p[0], p[1], p[2], p[3]);
789                    let (k2, cfg2) = (k.clone(), cfg.clone());
790                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
791                        let m = m.clone();
792                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
793                            .map(move |input| AudioNode::Flanger {
794                                uid: Uid::NEW,
795                                rate,
796                                depth: dep,
797                                feedback,
798                                mod_depth: md,
799                                input: Box::new(input),
800                                modulation: m.clone(),
801                            })
802                    })
803                })
804            }
805            11 => {
806                let k = key.clone();
807                u01_seq(k.clone(), &["trate", "tdepth", "tshape", "mdepth"]).bind(move |p| {
808                    let (rate, dep, shape, md) = (p[0], p[1], p[2], p[3]);
809                    let (k2, cfg2) = (k.clone(), cfg.clone());
810                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
811                        let m = m.clone();
812                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
813                            .map(move |input| AudioNode::Tremolo {
814                                uid: Uid::NEW,
815                                rate,
816                                depth: dep,
817                                shape,
818                                mod_depth: md,
819                                input: Box::new(input),
820                                modulation: m.clone(),
821                            })
822                    })
823                })
824            }
825            12 => {
826                let k = key.clone();
827                u01_seq(k.clone(), &["vrate", "vdepth", "vmix", "mdepth"]).bind(move |p| {
828                    let (rate, dep, mix, md) = (p[0], p[1], p[2], p[3]);
829                    let (k2, cfg2) = (k.clone(), cfg.clone());
830                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
831                        let m = m.clone();
832                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
833                            .map(move |input| AudioNode::Vibrato {
834                                uid: Uid::NEW,
835                                rate,
836                                depth: dep,
837                                mix,
838                                mod_depth: md,
839                                input: Box::new(input),
840                                modulation: m.clone(),
841                            })
842                    })
843                })
844            }
845            13 => {
846                let k = key.clone();
847                u01_seq(k.clone(), &["low", "mid", "high", "mdepth"]).bind(move |p| {
848                    let (low, mid, high, md) = (p[0], p[1], p[2], p[3]);
849                    let (k2, cfg2) = (k.clone(), cfg.clone());
850                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
851                        let m = m.clone();
852                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
853                            .map(move |input| AudioNode::Eq {
854                                uid: Uid::NEW,
855                                low,
856                                mid,
857                                high,
858                                mod_depth: md,
859                                input: Box::new(input),
860                                modulation: m.clone(),
861                            })
862                    })
863                })
864            }
865            14 => {
866                let k = key.clone();
867                u01_seq(k.clone(), &["gpos", "gsize", "gdens", "mdepth"]).bind(move |p| {
868                    let (position, size, density, md) = (p[0], p[1], p[2], p[3]);
869                    let (k2, cfg2) = (k.clone(), cfg.clone());
870                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
871                        let m = m.clone();
872                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
873                            .map(move |input| AudioNode::Granular {
874                                uid: Uid::NEW,
875                                position,
876                                size,
877                                density,
878                                mod_depth: md,
879                                input: Box::new(input),
880                                modulation: m.clone(),
881                            })
882                    })
883                })
884            }
885            // Pitch shift — unary, despite arriving with the binary family.
886            15 => {
887                let k = key.clone();
888                u01_seq(k.clone(), &["semis", "window", "smix", "mdepth"]).bind(move |p| {
889                    let (semis, window, mix, md) = (p[0], p[1], p[2], p[3]);
890                    let (k2, cfg2) = (k.clone(), cfg.clone());
891                    cfg.mod_model(mod_key(&k2), 0, true).bind(move |m| {
892                        let m = m.clone();
893                        cfg2.audio_model(child_key(&k2, 0), depth + 1)
894                            .map(move |input| AudioNode::Shift {
895                                uid: Uid::NEW,
896                                semis,
897                                window,
898                                mix,
899                                mod_depth: md,
900                                input: Box::new(input),
901                                modulation: m.clone(),
902                            })
903                    })
904                })
905            }
906            // The four binary productions: each recurses twice, exactly as
907            // Mix and RingMod do, and carries a modulation slot besides.
908            16 => self.binary_mod_op(
909                key,
910                &["thresh", "ratio", "makeup", "mdepth"],
911                depth,
912                |p, m, input, sidechain| AudioNode::Comp {
913                    uid: Uid::NEW,
914                    threshold: p[0],
915                    ratio: p[1],
916                    makeup: p[2],
917                    mod_depth: p[3],
918                    input: Box::new(input),
919                    sidechain: Box::new(sidechain),
920                    modulation: m,
921                },
922            ),
923            17 => self.binary_mod_op(
924                key,
925                &["amount", "dthresh", "drel", "mdepth"],
926                depth,
927                |p, m, input, key_input| AudioNode::Duck {
928                    uid: Uid::NEW,
929                    amount: p[0],
930                    threshold: p[1],
931                    release: p[2],
932                    mod_depth: p[3],
933                    input: Box::new(input),
934                    key: Box::new(key_input),
935                    modulation: m,
936                },
937            ),
938            18 => self.binary_mod_op(
939                key,
940                &["gthresh", "range", "grel", "mdepth"],
941                depth,
942                |p, m, input, sidechain| AudioNode::Gate {
943                    uid: Uid::NEW,
944                    threshold: p[0],
945                    range: p[1],
946                    release: p[2],
947                    mod_depth: p[3],
948                    input: Box::new(input),
949                    sidechain: Box::new(sidechain),
950                    modulation: m,
951                },
952            ),
953            _ => self.binary_mod_op(
954                key,
955                &["bands", "vatt", "vrel", "mdepth"],
956                depth,
957                |p, m, carrier, modulator| AudioNode::Vocoder {
958                    uid: Uid::NEW,
959                    bands: p[0],
960                    attack: p[1],
961                    release: p[2],
962                    mod_depth: p[3],
963                    carrier: Box::new(carrier),
964                    modulator: Box::new(modulator),
965                    modulation: m,
966                },
967            ),
968        }
969    }
970
971    /// Draw a tree with a plain RNG (no trace) — the classic-layer sampler
972    /// mirroring [`Self::model`]. Used by `EvolutionaryGenome::generate`.
973    pub fn sample_with_rng<R: Rng>(&self, rng: &mut R) -> PatchTree {
974        let amp = AmpEnv {
975            attack: rng.gen::<f64>(),
976            decay: rng.gen::<f64>(),
977            sustain: rng.gen::<f64>(),
978            release: rng.gen::<f64>(),
979        };
980        let root = self.sample_audio(rng, 0);
981        PatchTree { amp, root }
982    }
983
984    fn sample_audio<R: Rng>(&self, rng: &mut R, depth: usize) -> AudioNode {
985        let is_leaf = depth >= self.max_depth || rng.gen_bool(self.source_prob);
986        if is_leaf {
987            match weighted_choice(rng, &self.source_weights) {
988                0 => AudioNode::Vco {
989                    uid: Uid::NEW,
990                    wave: Waveform::from_index(rng.gen_range(0..Waveform::ALL.len())),
991                    octave: rng.gen_range(0..5) as i8 - 2,
992                    detune: rng.gen(),
993                    mod_depth: rng.gen(),
994                    modulation: self.sample_mod(rng, 0, true),
995                },
996                1 => AudioNode::Supersaw {
997                    uid: Uid::NEW,
998                    octave: rng.gen_range(0..5) as i8 - 2,
999                    detune: rng.gen(),
1000                    mix: rng.gen(),
1001                    mod_depth: rng.gen(),
1002                    modulation: self.sample_mod(rng, 0, true),
1003                },
1004                2 => AudioNode::Noise {
1005                    uid: Uid::NEW,
1006                    color: NoiseColor::from_index(rng.gen_range(0..NoiseColor::ALL.len())),
1007                },
1008                3 => AudioNode::Wavetable {
1009                    uid: Uid::NEW,
1010                    table: TableShape::from_index(rng.gen_range(0..TableShape::ALL.len())),
1011                    octave: rng.gen_range(0..5) as i8 - 2,
1012                    morph: rng.gen(),
1013                    mod_depth: rng.gen(),
1014                    modulation: self.sample_mod(rng, 0, true),
1015                },
1016                4 => AudioNode::Pluck {
1017                    uid: Uid::NEW,
1018                    octave: rng.gen_range(0..5) as i8 - 2,
1019                    damping: rng.gen(),
1020                    brightness: rng.gen(),
1021                    mod_depth: rng.gen(),
1022                    modulation: self.sample_mod(rng, 0, true),
1023                },
1024                _ => AudioNode::Formant {
1025                    uid: Uid::NEW,
1026                    vowel: rng.gen(),
1027                    shift: rng.gen(),
1028                    octave: rng.gen_range(0..5) as i8 - 2,
1029                    mod_depth: rng.gen(),
1030                    modulation: self.sample_mod(rng, 0, true),
1031                },
1032            }
1033        } else {
1034            match weighted_choice(rng, &self.op_weights) {
1035                0 => AudioNode::Mix {
1036                    uid: Uid::NEW,
1037                    balance: rng.gen(),
1038                    a: Box::new(self.sample_audio(rng, depth + 1)),
1039                    b: Box::new(self.sample_audio(rng, depth + 1)),
1040                },
1041                1 => AudioNode::Filter {
1042                    uid: Uid::NEW,
1043                    kind: FilterKind::from_index(rng.gen_range(0..FilterKind::ALL.len())),
1044                    cutoff: rng.gen(),
1045                    resonance: rng.gen(),
1046                    mod_depth: rng.gen(),
1047                    modulation: self.sample_mod(rng, 0, true),
1048                    input: Box::new(self.sample_audio(rng, depth + 1)),
1049                },
1050                2 => AudioNode::Fold {
1051                    uid: Uid::NEW,
1052                    threshold: rng.gen(),
1053                    mod_depth: rng.gen(),
1054                    modulation: self.sample_mod(rng, 0, true),
1055                    input: Box::new(self.sample_audio(rng, depth + 1)),
1056                },
1057                3 => AudioNode::Delay {
1058                    uid: Uid::NEW,
1059                    time: rng.gen(),
1060                    feedback: rng.gen(),
1061                    mix: rng.gen(),
1062                    mod_depth: rng.gen(),
1063                    modulation: self.sample_mod(rng, 0, true),
1064                    input: Box::new(self.sample_audio(rng, depth + 1)),
1065                },
1066                4 => AudioNode::Chorus {
1067                    uid: Uid::NEW,
1068                    rate: rng.gen(),
1069                    depth: rng.gen(),
1070                    mix: rng.gen(),
1071                    mod_depth: rng.gen(),
1072                    modulation: self.sample_mod(rng, 0, true),
1073                    input: Box::new(self.sample_audio(rng, depth + 1)),
1074                },
1075                5 => AudioNode::Reverb {
1076                    uid: Uid::NEW,
1077                    size: rng.gen(),
1078                    damp: rng.gen(),
1079                    mix: rng.gen(),
1080                    mod_depth: rng.gen(),
1081                    modulation: self.sample_mod(rng, 0, true),
1082                    input: Box::new(self.sample_audio(rng, depth + 1)),
1083                },
1084                6 => AudioNode::Distortion {
1085                    uid: Uid::NEW,
1086                    drive: rng.gen(),
1087                    tone: rng.gen(),
1088                    mode: DriveMode::from_index(rng.gen_range(0..DriveMode::ALL.len())),
1089                    mod_depth: rng.gen(),
1090                    modulation: self.sample_mod(rng, 0, true),
1091                    input: Box::new(self.sample_audio(rng, depth + 1)),
1092                },
1093                7 => AudioNode::Bitcrush {
1094                    uid: Uid::NEW,
1095                    bits: rng.gen(),
1096                    downsample: rng.gen(),
1097                    mod_depth: rng.gen(),
1098                    modulation: self.sample_mod(rng, 0, true),
1099                    input: Box::new(self.sample_audio(rng, depth + 1)),
1100                },
1101                8 => AudioNode::Phaser {
1102                    uid: Uid::NEW,
1103                    rate: rng.gen(),
1104                    depth: rng.gen(),
1105                    feedback: rng.gen(),
1106                    mod_depth: rng.gen(),
1107                    modulation: self.sample_mod(rng, 0, true),
1108                    input: Box::new(self.sample_audio(rng, depth + 1)),
1109                },
1110                9 => AudioNode::RingMod {
1111                    uid: Uid::NEW,
1112                    mix: rng.gen(),
1113                    a: Box::new(self.sample_audio(rng, depth + 1)),
1114                    b: Box::new(self.sample_audio(rng, depth + 1)),
1115                },
1116                10 => AudioNode::Flanger {
1117                    uid: Uid::NEW,
1118                    rate: rng.gen(),
1119                    depth: rng.gen(),
1120                    feedback: rng.gen(),
1121                    mod_depth: rng.gen(),
1122                    modulation: self.sample_mod(rng, 0, true),
1123                    input: Box::new(self.sample_audio(rng, depth + 1)),
1124                },
1125                11 => AudioNode::Tremolo {
1126                    uid: Uid::NEW,
1127                    rate: rng.gen(),
1128                    depth: rng.gen(),
1129                    shape: rng.gen(),
1130                    mod_depth: rng.gen(),
1131                    modulation: self.sample_mod(rng, 0, true),
1132                    input: Box::new(self.sample_audio(rng, depth + 1)),
1133                },
1134                12 => AudioNode::Vibrato {
1135                    uid: Uid::NEW,
1136                    rate: rng.gen(),
1137                    depth: rng.gen(),
1138                    mix: rng.gen(),
1139                    mod_depth: rng.gen(),
1140                    modulation: self.sample_mod(rng, 0, true),
1141                    input: Box::new(self.sample_audio(rng, depth + 1)),
1142                },
1143                13 => AudioNode::Eq {
1144                    uid: Uid::NEW,
1145                    low: rng.gen(),
1146                    mid: rng.gen(),
1147                    high: rng.gen(),
1148                    mod_depth: rng.gen(),
1149                    modulation: self.sample_mod(rng, 0, true),
1150                    input: Box::new(self.sample_audio(rng, depth + 1)),
1151                },
1152                14 => AudioNode::Granular {
1153                    uid: Uid::NEW,
1154                    position: rng.gen(),
1155                    size: rng.gen(),
1156                    density: rng.gen(),
1157                    mod_depth: rng.gen(),
1158                    modulation: self.sample_mod(rng, 0, true),
1159                    input: Box::new(self.sample_audio(rng, depth + 1)),
1160                },
1161                15 => AudioNode::Shift {
1162                    uid: Uid::NEW,
1163                    semis: rng.gen(),
1164                    window: rng.gen(),
1165                    mix: rng.gen(),
1166                    mod_depth: rng.gen(),
1167                    modulation: self.sample_mod(rng, 0, true),
1168                    input: Box::new(self.sample_audio(rng, depth + 1)),
1169                },
1170                // The draw order below mirrors `op_model`'s: params, slot,
1171                // `/0`, `/1`. It has to, or the two samplers disagree about
1172                // which subtree came from which RNG state.
1173                16 => AudioNode::Comp {
1174                    uid: Uid::NEW,
1175                    threshold: rng.gen(),
1176                    ratio: rng.gen(),
1177                    makeup: rng.gen(),
1178                    mod_depth: rng.gen(),
1179                    modulation: self.sample_mod(rng, 0, true),
1180                    input: Box::new(self.sample_audio(rng, depth + 1)),
1181                    sidechain: Box::new(self.sample_audio(rng, depth + 1)),
1182                },
1183                17 => AudioNode::Duck {
1184                    uid: Uid::NEW,
1185                    amount: rng.gen(),
1186                    threshold: rng.gen(),
1187                    release: rng.gen(),
1188                    mod_depth: rng.gen(),
1189                    modulation: self.sample_mod(rng, 0, true),
1190                    input: Box::new(self.sample_audio(rng, depth + 1)),
1191                    key: Box::new(self.sample_audio(rng, depth + 1)),
1192                },
1193                18 => AudioNode::Gate {
1194                    uid: Uid::NEW,
1195                    threshold: rng.gen(),
1196                    range: rng.gen(),
1197                    release: rng.gen(),
1198                    mod_depth: rng.gen(),
1199                    modulation: self.sample_mod(rng, 0, true),
1200                    input: Box::new(self.sample_audio(rng, depth + 1)),
1201                    sidechain: Box::new(self.sample_audio(rng, depth + 1)),
1202                },
1203                _ => AudioNode::Vocoder {
1204                    uid: Uid::NEW,
1205                    bands: rng.gen(),
1206                    attack: rng.gen(),
1207                    release: rng.gen(),
1208                    mod_depth: rng.gen(),
1209                    modulation: self.sample_mod(rng, 0, true),
1210                    carrier: Box::new(self.sample_audio(rng, depth + 1)),
1211                    modulator: Box::new(self.sample_audio(rng, depth + 1)),
1212                },
1213            }
1214        }
1215    }
1216
1217    /// [`Self::mod_model`] with a plain RNG. Same weights, same depth rule,
1218    /// same draw order — the two samplers must agree on which trees exist.
1219    fn sample_mod<R: Rng>(&self, rng: &mut R, depth: usize, root: bool) -> ModNode {
1220        match weighted_choice(rng, &self.mod_weights_at(depth, root)) {
1221            0 => ModNode::None,
1222            1 => ModNode::Lfo {
1223                uid: Uid::NEW,
1224                wave: Waveform::from_index(rng.gen_range(0..Waveform::ALL.len())),
1225                rate: rng.gen(),
1226            },
1227            2 => ModNode::Env {
1228                uid: Uid::NEW,
1229                attack: rng.gen(),
1230                decay: rng.gen(),
1231            },
1232            3 => ModNode::Rand {
1233                uid: Uid::NEW,
1234                rate: rng.gen(),
1235                glide: rng.gen(),
1236            },
1237            4 => ModNode::Follow {
1238                uid: Uid::NEW,
1239                sens: rng.gen(),
1240                release: rng.gen(),
1241            },
1242            5 => ModNode::Euclid {
1243                uid: Uid::NEW,
1244                rate: rng.gen(),
1245                steps: rng.gen(),
1246                pulses: rng.gen(),
1247            },
1248            6 => {
1249                let kind = ModOp::from_index(rng.gen_range(0..N_MOD_OPS));
1250                let two = kind.param_sites().len() > 1;
1251                let p0 = rng.gen();
1252                // The one-parameter ops must not consume a second draw: their
1253                // `p1` is not a trace site, so drawing one here would put the
1254                // two samplers on different RNG states.
1255                let p1 = if two { rng.gen() } else { 0.0 };
1256                ModNode::Op {
1257                    uid: Uid::NEW,
1258                    kind,
1259                    p0,
1260                    p1,
1261                    input: Box::new(self.sample_mod(rng, depth + 1, false)),
1262                }
1263            }
1264            _ => ModNode::Pair {
1265                uid: Uid::NEW,
1266                kind: PairOp::from_index(rng.gen_range(0..N_PAIR_OPS)),
1267                a: Box::new(self.sample_mod(rng, depth + 1, false)),
1268                b: Box::new(self.sample_mod(rng, depth + 1, false)),
1269            },
1270        }
1271    }
1272}
1273
1274fn weighted_choice<R: Rng>(rng: &mut R, weights: &[f64]) -> usize {
1275    let total: f64 = weights.iter().sum();
1276    let mut x = rng.gen::<f64>() * total;
1277    for (i, w) in weights.iter().enumerate() {
1278        x -= w;
1279        if x <= 0.0 {
1280            return i;
1281        }
1282    }
1283    weights.len() - 1
1284}
1285
1286impl GenomePrior for PatchGrammarPrior {
1287    type Genome = PatchTree;
1288
1289    fn model(&self) -> Model<PatchTree> {
1290        let cfg = self.clone();
1291        sample(addr!("amp", "attack"), u01()).bind(move |a| {
1292            let cfg = cfg.clone();
1293            sample(addr!("amp", "decay"), u01()).bind(move |d| {
1294                let cfg = cfg.clone();
1295                sample(addr!("amp", "sustain"), u01()).bind(move |s| {
1296                    let cfg = cfg.clone();
1297                    sample(addr!("amp", "release"), u01()).bind(move |r| {
1298                        cfg.audio_model("node".to_string(), 0)
1299                            .map(move |root| PatchTree {
1300                                amp: AmpEnv {
1301                                    attack: a,
1302                                    decay: d,
1303                                    sustain: s,
1304                                    release: r,
1305                                },
1306                                root,
1307                            })
1308                    })
1309                })
1310            })
1311        })
1312    }
1313    // `trace_of` uses the default: it delegates to `TraceGenome::to_trace`,
1314    // whose canonical encoding (crate::genome) IS this grammar's address
1315    // scheme — the two cannot drift apart without breaking the round-trip
1316    // property test.
1317}