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}