auracle_grammar/presets.rs
1//! Hand-designed preset patches: a starting vocabulary for the bank, and a
2//! fast way to seed the taste model ("keep the ones you like").
3//!
4//! Every preset must compile and pass the vetting gate — pinned by a test in
5//! the features crate's dependents (see auracle-wasm tests).
6//!
7//! # Every parameter here is normalized; none of them are in units
8//!
9//! A `PatchTree` field is `0.0..=1.0` and the musical meaning lives in the
10//! compiler and in quiver. Writing these by feel rather than by the maps is
11//! how the first nine went wrong in a way nobody could see: **every modulated
12//! preset in the library ran between 0.033 Hz and 0.165 Hz** — six to thirty
13//! seconds per cycle — against an audition phrase whose longest held note is
14//! 1.8 s. The library demonstrated no audible modulation at all, and since the
15//! warm start draws its whole cold-start evidence from these patches, two of
16//! the fifteen features the taste model reasons over (`centroid_std`,
17//! `flux_mean`) were pinned to the floor for every observation it started
18//! from. `Dub Echo`'s `time: 0.45` was likewise 30 ms — a flanger.
19//!
20//! So: **use the maps.** They are the arithmetic, not a style guide.
21//!
22//! | field | map | worked examples |
23//! |---|---|---|
24//! | `Filter.cutoff` | `20·1000^x` Hz | .3→159 · .5→632 · .7→2.5k · .85→7.1k |
25//! | `AmpEnv.*`, `Env.*` | `1 ms·10000^x` **time constant** | .0→1 ms · .25→10 ms · .5→100 ms · .75→1 s |
26//! | `Lfo.rate`, `Rand.rate` | `0.01·3000^x` Hz | .42→0.29 · .49→0.51 · .58→1.0 · .75→4.05 |
27//! | `Delay.time` | `1 ms·2000^x` | .54→60 ms · .63→120 ms · .79→420 ms |
28//! | `Chorus.rate` | `0.1·50^x` Hz | .15→0.18 · .25→0.27 · .35→0.39 · .5→0.71 |
29//! | `Reverb.size` | Freeverb room `0.28+0.7x` | .9→0.91 |
30//! | `Filter.resonance` | `×0.85` | 1.0 is 0.85, never self-oscillation |
31//! | `Delay.feedback` | `×0.7` | .62→0.43 — the grammar cannot run away |
32//! | `Phaser.rate` | `0.05·100^x` Hz | .25→0.16 · .35→0.25 · .5→0.5 · .7→1.25 |
33//! | `Phaser.feedback` | `(2x−1)·0.7` | .5 is *no* feedback; below it the sign flips |
34//! | `Bitcrush.bits` | `1 + 15x` bits | .2→4 · .35→6.3 · .6→10 · 1.0→16 (clean) |
35//! | `Bitcrush.downsample` | `1 + 63x` × slower | .1→7.3× · .3→20× · .5→33× |
36//! | `Follow.release` | `1 + 999x` ms | .2→200 ms · .4→400 ms · .7→700 ms |
37//!
38//! `AmpEnv.sustain` and every `mix`/`balance`/`depth` are levels, not times.
39//!
40//! **The envelope map is a time constant, not a duration.** `compile.rs` runs
41//! the ADSR in exponential mode, so the mapped value is a one-pole τ: reaching
42//! 90% takes ≈2.3 τ and settling takes ≈6.9 τ. `Cathedral`'s attack of `0.75`
43//! maps to 1.0 s and *measures* 1.397 s to its knee. Read every envelope
44//! number here as "about 1.4× longer than the table says", and remember the
45//! phrase's longest note is 1.8 s — an attack much past `0.75` will still be
46//! climbing when the note ends.
47//!
48//! **`Supersaw` is ~14 dB quieter than `Vco`.** Quiver scales `Vco` and
49//! `Noise` outputs by 5.0 and leaves `Supersaw` at unity. That is invisible
50//! for anything downstream that only cares about relative level, and fatal for
51//! `Fold`, which folds `x / 5.0` and therefore never reaches its threshold on
52//! a supersaw. Build folded patches on `Vco`.
53//!
54//! # What the grammar cannot do
55//!
56//! Worth knowing before designing: there is no FM, no oscillator sync and no
57//! PWM. `Vco.detune` on a lone oscillator only transposes the whole patch a
58//! few cents sharp; it is only musical inside a `Mix` (or a `RingMod`) of two
59//! sources.
60//!
61//! Pitch modulation **does** exist as of wave 2A — `Vco` and `Supersaw` carry
62//! a slot that lands on their pitch offset (±0.5 octave at full depth), which
63//! is where vibrato and pitch envelopes come from.
64//!
65//! What a `ModNode` reaches is one parameter per module, and the choice is
66//! the compiler's, not the patch's: `Vco`/`Supersaw` pitch, `Filter.cutoff`,
67//! `Fold.threshold`, `Delay.time`, `Chorus.depth`, `Reverb.size`,
68//! `Distortion.drive`, `Bitcrush.bits`, `Phaser.depth`, `Flanger.depth`,
69//! `Tremolo.depth`, `Vibrato.depth`, `Eq.mid`, `Granular.position`,
70//! `Wavetable.morph`, `Pluck.damping`, `Formant.vowel`, `Shift.semis`,
71//! `Comp`/`Gate` threshold, `Duck.amount` and `Vocoder.bands`. Only `Mix` and
72//! `RingMod` have no slot — both of their inputs are audio and their one knob
73//! is the blend.
74//!
75//! `ModNode::Follow` is the odd one: it listens to the owning module's *own*
76//! input, so it only says something on a module whose input has dynamics to
77//! follow. On a source's slot it is silence, deliberately.
78//!
79//! # Levels are not comparable, and the dynamics modules care
80//!
81//! `Comp`, `Duck` and `Gate` are keyed by a second branch whose **absolute**
82//! level decides everything. Measured as mean `|x|` on a held note: a sine
83//! vco is 3.18 V, a supersaw ≈0.6 V, a plucked string **0.14 V**. The
84//! threshold knobs are geometric over 0.05–5 V (`compile::map::detector_volts`)
85//! so all three are reachable, but a key branch swapped from a pluck to a vco
86//! moves the useful threshold by 25 dB — about a third of the knob.
87//!
88//! # The gate is not the constraint
89//!
90//! Vetting rejects only non-finite output, RMS < 1e-4, peak > 3.5, and
91//! `|mean|/rms > 0.6`. It catches pathology, not taste. Design musically and
92//! let the gate do its narrow job.
93//!
94//! # This is also the model's vocabulary lesson
95//!
96//! The bank's auto-namer (`auracle-session::naming`) fits its adjectives to
97//! the pool's *own* spread and suppresses any axis whose range falls below a
98//! just-noticeable difference. A library clustered in one corner therefore
99//! produces a bank that honestly reports itself as `Soft Lead`, `Soft Lead 2`,
100//! `Soft Lead 3`. Coverage here is what gives the whole app its adjectives.
101
102use crate::term::{
103 AmpEnv, AudioNode, DriveMode, FilterKind, ModNode, ModOp, NoiseColor, PairOp, PatchTree,
104 TableShape, Uid, Waveform,
105};
106
107fn amp(attack: f64, decay: f64, sustain: f64, release: f64) -> AmpEnv {
108 AmpEnv {
109 attack,
110 decay,
111 sustain,
112 release,
113 }
114}
115
116/// A vco with no pitch modulation — the shape every preset written before
117/// wave 2A had. Presets that *want* the pitch slot spell out the full literal,
118/// which is also what makes them findable.
119fn vco(wave: Waveform, octave: i8, detune: f64) -> AudioNode {
120 AudioNode::Vco {
121 uid: Uid::NEW,
122 wave,
123 octave,
124 detune,
125 mod_depth: 0.0,
126 modulation: ModNode::None,
127 }
128}
129
130/// A supersaw with no pitch modulation.
131fn supersaw(octave: i8, detune: f64, mix: f64) -> AudioNode {
132 AudioNode::Supersaw {
133 uid: Uid::NEW,
134 octave,
135 detune,
136 mix,
137 mod_depth: 0.0,
138 modulation: ModNode::None,
139 }
140}
141
142/// What a preset is *for*, so the browser can group them and the warm start
143/// can sample across the space instead of down one corner of it.
144///
145/// A `&'static str` rather than an enum: it exists to be grouped by and shown,
146/// it crosses into JS as JSON, and nothing dispatches on it.
147pub type Category = &'static str;
148
149/// The categories, in display order.
150pub const CATEGORIES: [Category; 7] = ["bass", "lead", "keys", "pad", "texture", "perc", "weird"];
151
152/// One built-in patch, with the copy that makes it findable.
153pub struct Preset {
154 /// Display name.
155 pub name: &'static str,
156 /// Which family it belongs to; one of [`CATEGORIES`].
157 pub category: Category,
158 /// One line on what it sounds like. Shown in the browser row and its
159 /// tooltip — the handbook delivered where the user is already looking.
160 pub blurb: &'static str,
161 /// The patch.
162 pub tree: PatchTree,
163}
164
165/// The built-in presets, as `(name, tree)` pairs.
166///
167/// Retained as the compatibility shim over [`preset_bank`]; new callers that
168/// want categories or copy should use that instead.
169pub fn presets() -> Vec<(&'static str, PatchTree)> {
170 preset_bank()
171 .into_iter()
172 .map(|p| (p.name, p.tree))
173 .collect()
174}
175
176/// The built-in preset library, in category order.
177pub fn preset_bank() -> Vec<Preset> {
178 use AudioNode::*;
179 vec![
180 // ---------------------------------------------------------------
181 // BASS
182 // ---------------------------------------------------------------
183 Preset {
184 name: "First Bass",
185 category: "bass",
186 blurb: "the plain correct one — measure the others against it",
187 tree: PatchTree {
188 amp: amp(0.03, 0.35, 0.55, 0.2),
189 root: Filter {
190 uid: Uid::NEW,
191 kind: FilterKind::Ladder,
192 cutoff: 0.45,
193 resonance: 0.55,
194 mod_depth: 0.6,
195 modulation: ModNode::Env {
196 uid: Uid::NEW,
197 attack: 0.02,
198 decay: 0.45,
199 },
200 input: Box::new(vco(Waveform::Saw, -1, 0.5)),
201 },
202 },
203 },
204 Preset {
205 name: "Sub & Sparkle",
206 category: "bass",
207 blurb: "weight underneath, air above, nothing in the middle",
208 tree: PatchTree {
209 amp: amp(0.05, 0.35, 0.65, 0.35),
210 root: Mix {
211 uid: Uid::NEW,
212 balance: 0.35,
213 a: Box::new(vco(Waveform::Sine, -1, 0.5)),
214 b: Box::new(Filter {
215 uid: Uid::NEW,
216 kind: FilterKind::SvfHp,
217 cutoff: 0.7,
218 resonance: 0.3,
219 mod_depth: 0.0,
220 modulation: ModNode::None,
221 input: Box::new(vco(Waveform::Saw, 1, 0.6)),
222 }),
223 },
224 },
225 },
226 Preset {
227 name: "Acid Line",
228 category: "bass",
229 // Ladder at high resonance with a fast env sweep is the 303; the
230 // decay (0.30 ≈ 16 ms) is what makes it spit rather than wobble.
231 blurb: "a 303 that got out of the cage",
232 tree: PatchTree {
233 amp: amp(0.0, 0.42, 0.3, 0.3),
234 root: Filter {
235 uid: Uid::NEW,
236 kind: FilterKind::Ladder,
237 cutoff: 0.3,
238 resonance: 0.8,
239 mod_depth: 0.65,
240 modulation: ModNode::Env {
241 uid: Uid::NEW,
242 attack: 0.0,
243 decay: 0.3,
244 },
245 input: Box::new(vco(Waveform::Saw, -1, 0.52)),
246 },
247 },
248 },
249 Preset {
250 name: "Reese",
251 category: "bass",
252 // Two saws a hair apart is the only place `detune` is musical —
253 // on a lone oscillator it just transposes the patch sharp.
254 blurb: "two saws beating against each other, low and wide",
255 tree: PatchTree {
256 amp: amp(0.02, 0.5, 0.75, 0.35),
257 root: Filter {
258 uid: Uid::NEW,
259 kind: FilterKind::SvfLp,
260 cutoff: 0.45,
261 resonance: 0.35,
262 mod_depth: 0.25,
263 modulation: ModNode::Lfo {
264 uid: Uid::NEW,
265 wave: Waveform::Triangle,
266 rate: 0.49, // 0.5 Hz — one full sweep per held note
267 },
268 input: Box::new(Mix {
269 uid: Uid::NEW,
270 balance: 0.5,
271 a: Box::new(vco(Waveform::Saw, -1, 0.3)),
272 b: Box::new(vco(Waveform::Saw, -1, 0.85)),
273 }),
274 },
275 },
276 },
277 Preset {
278 name: "Anvil",
279 category: "bass",
280 blurb: "a square driven into the folder, then hammered flat",
281 tree: PatchTree {
282 amp: amp(0.0, 0.45, 0.45, 0.3),
283 root: Filter {
284 uid: Uid::NEW,
285 kind: FilterKind::Ladder,
286 cutoff: 0.42,
287 resonance: 0.35,
288 mod_depth: 0.0,
289 modulation: ModNode::None,
290 input: Box::new(Fold {
291 uid: Uid::NEW,
292 threshold: 0.35,
293 mod_depth: 0.4,
294 modulation: ModNode::Env {
295 uid: Uid::NEW,
296 attack: 0.0,
297 decay: 0.325,
298 },
299 input: Box::new(vco(Waveform::Square, -1, 0.5)),
300 }),
301 },
302 },
303 },
304 Preset {
305 name: "Iron Bass",
306 category: "bass",
307 // Tube drive is the asymmetric shaper, so this patch is also the
308 // library's only user of the DC blocker on a non-ladder path.
309 // The follower makes the drive track the note's own envelope:
310 // hard at the attack, cleaner as it decays, which is what a real
311 // overdriven amp does and what a fixed drive knob cannot.
312 blurb: "an amp being pushed harder than it wants to go",
313 tree: PatchTree {
314 amp: amp(0.0, 0.45, 0.6, 0.3),
315 root: Filter {
316 uid: Uid::NEW,
317 kind: FilterKind::Ladder,
318 cutoff: 0.48,
319 resonance: 0.3,
320 mod_depth: 0.0,
321 modulation: ModNode::None,
322 input: Box::new(Distortion {
323 uid: Uid::NEW,
324 drive: 0.55,
325 tone: 0.45,
326 mode: DriveMode::Tube,
327 mod_depth: 0.6,
328 modulation: ModNode::Follow {
329 uid: Uid::NEW,
330 sens: 0.55,
331 release: 0.35,
332 },
333 input: Box::new(vco(Waveform::Saw, -1, 0.5)),
334 }),
335 },
336 },
337 },
338 Preset {
339 name: "Held Under",
340 category: "bass",
341 // Sidechain compression, which needs two branches and therefore
342 // could not be written before wave 2B. The plucked string is
343 // never heard; it only tells the detector when to pull the sub
344 // down. Threshold 0.3 is ≈0.2 V on the geometric detector map —
345 // under the string's envelope peak, so every strike bites.
346 blurb: "a sub that gets pushed down every time the string speaks",
347 tree: PatchTree {
348 amp: amp(0.05, 0.4, 0.8, 0.35),
349 root: Comp {
350 uid: Uid::NEW,
351 threshold: 0.3,
352 ratio: 0.7, // ≈14:1 — limiting, so the pump is not subtle
353 makeup: 0.25, // 1.75×, giving the level back between hits
354 mod_depth: 0.0,
355 modulation: ModNode::None,
356 input: Box::new(Filter {
357 uid: Uid::NEW,
358 kind: FilterKind::SvfLp,
359 cutoff: 0.4, // ≈316 Hz
360 resonance: 0.3,
361 mod_depth: 0.0,
362 modulation: ModNode::None,
363 input: Box::new(vco(Waveform::Saw, -1, 0.5)),
364 }),
365 sidechain: Box::new(Pluck {
366 uid: Uid::NEW,
367 octave: 0,
368 damping: 0.3,
369 brightness: 0.8,
370 mod_depth: 0.0,
371 modulation: ModNode::None,
372 }),
373 },
374 },
375 },
376 // ---------------------------------------------------------------
377 // LEAD
378 // ---------------------------------------------------------------
379 Preset {
380 name: "Folded Lead",
381 category: "lead",
382 blurb: "buzzing, metallic, never quite still",
383 tree: PatchTree {
384 amp: amp(0.02, 0.3, 0.7, 0.3),
385 root: Fold {
386 uid: Uid::NEW,
387 threshold: 0.4,
388 mod_depth: 0.5,
389 modulation: ModNode::Lfo {
390 uid: Uid::NEW,
391 wave: Waveform::Triangle,
392 rate: 0.6, // 1.2 Hz — was 0.35 (0.165 Hz), inaudible
393 },
394 input: Box::new(vco(Waveform::Saw, 0, 0.55)),
395 },
396 },
397 },
398 Preset {
399 name: "Hornet",
400 category: "lead",
401 blurb: "nasal and angry; cuts through anything",
402 tree: PatchTree {
403 amp: amp(0.15, 0.4, 0.65, 0.3),
404 root: Filter {
405 uid: Uid::NEW,
406 kind: FilterKind::SvfBp,
407 cutoff: 0.62,
408 resonance: 0.7,
409 mod_depth: 0.25,
410 modulation: ModNode::Rand {
411 uid: Uid::NEW,
412 rate: 0.62,
413 glide: 0.0,
414 }, // ~1.4 Hz stepping
415 input: Box::new(vco(Waveform::Square, 0, 0.5)),
416 },
417 },
418 },
419 Preset {
420 name: "Solo Flight",
421 category: "lead",
422 blurb: "wide enough to sing, not so wide it stops being one voice",
423 tree: PatchTree {
424 amp: amp(0.3, 0.45, 0.78, 0.45),
425 root: Chorus {
426 uid: Uid::NEW,
427 rate: 0.3,
428 depth: 0.35,
429 mix: 0.3,
430 mod_depth: 0.0,
431 modulation: ModNode::None,
432 input: Box::new(Filter {
433 uid: Uid::NEW,
434 kind: FilterKind::SvfLp,
435 cutoff: 0.72,
436 resonance: 0.3,
437 mod_depth: 0.18,
438 modulation: ModNode::Lfo {
439 uid: Uid::NEW,
440 wave: Waveform::Sine,
441 rate: 0.55, // 0.8 Hz — a slow breath, still audible
442 },
443 input: Box::new(supersaw(0, 0.25, 0.45)),
444 }),
445 },
446 },
447 },
448 Preset {
449 name: "Telegraph",
450 category: "lead",
451 blurb: "every note answers itself once and stops",
452 tree: PatchTree {
453 amp: amp(0.15, 0.45, 0.55, 0.35),
454 root: Delay {
455 uid: Uid::NEW,
456 time: 0.63, // 120 ms — a slapback, not a comb
457 feedback: 0.4,
458 mix: 0.32,
459 mod_depth: 0.0,
460 modulation: ModNode::None,
461 input: Box::new(Filter {
462 uid: Uid::NEW,
463 kind: FilterKind::SvfLp,
464 cutoff: 0.68,
465 resonance: 0.4,
466 mod_depth: 0.35,
467 modulation: ModNode::Env {
468 uid: Uid::NEW,
469 attack: 0.0,
470 decay: 0.42,
471 },
472 input: Box::new(vco(Waveform::Triangle, 1, 0.5)),
473 }),
474 },
475 },
476 },
477 Preset {
478 name: "Wobble Board",
479 category: "lead",
480 // The headline of wave 2A: `mod_depth` 0.09 is ±54 cents, which is
481 // a singer's vibrato rather than a siren. The taper reaches ±0.5
482 // octave at 1.0, so the musical corner is the bottom tenth — small
483 // to dial, and the alternative was not having pitch modulation.
484 blurb: "a lead that sings — the first patch here that can vibrato",
485 tree: PatchTree {
486 amp: amp(0.04, 0.4, 0.7, 0.3),
487 root: Filter {
488 uid: Uid::NEW,
489 kind: FilterKind::Ladder,
490 cutoff: 0.55,
491 resonance: 0.4,
492 mod_depth: 0.0,
493 modulation: ModNode::None,
494 input: Box::new(Vco {
495 uid: Uid::NEW,
496 wave: Waveform::Saw,
497 octave: 0,
498 detune: 0.5,
499 mod_depth: 0.09,
500 modulation: ModNode::Lfo {
501 uid: Uid::NEW,
502 wave: Waveform::Sine,
503 rate: 0.78, // 5.2 Hz
504 },
505 }),
506 },
507 },
508 },
509 Preset {
510 name: "Falling Sign",
511 category: "lead",
512 // The other pitch-slot idiom, and the one that argues for the
513 // wide taper: a unipolar mod envelope on pitch starts the note
514 // 3.3 semitones sharp and lets it fall in over 63 ms. A ceiling
515 // tight enough to make vibrato comfortable would not reach here.
516 blurb: "every note drops into tune from a third above",
517 tree: PatchTree {
518 amp: amp(0.0, 0.45, 0.6, 0.25),
519 root: Filter {
520 uid: Uid::NEW,
521 kind: FilterKind::SvfLp,
522 cutoff: 0.6,
523 resonance: 0.35,
524 mod_depth: 0.0,
525 modulation: ModNode::None,
526 input: Box::new(Vco {
527 uid: Uid::NEW,
528 wave: Waveform::Square,
529 octave: 0,
530 detune: 0.5,
531 mod_depth: 0.55,
532 modulation: ModNode::Env {
533 uid: Uid::NEW,
534 attack: 0.0,
535 decay: 0.45, // 63 ms
536 },
537 }),
538 },
539 },
540 },
541 Preset {
542 name: "Loudhailer",
543 category: "lead",
544 // The eq's three bands are bipolar: 0.5 is 0 dB, so 0.15 is
545 // −8.4 dB and 0.75 is +6 dB. The mod slot reaches the mid gain at
546 // its own volt scale, so depth 0.45 is a ±5.4 dB pump — a wah
547 // built out of a tone control.
548 blurb: "a megaphone with a hand moving over the mouth",
549 tree: PatchTree {
550 amp: amp(0.02, 0.4, 0.7, 0.25),
551 root: Eq {
552 uid: Uid::NEW,
553 low: 0.15,
554 mid: 0.75,
555 high: 0.2,
556 mod_depth: 0.45,
557 modulation: ModNode::Lfo {
558 uid: Uid::NEW,
559 wave: Waveform::Triangle,
560 rate: 0.52, // 0.64 Hz
561 },
562 input: Box::new(Filter {
563 uid: Uid::NEW,
564 kind: FilterKind::Ladder,
565 cutoff: 0.55,
566 resonance: 0.5,
567 mod_depth: 0.0,
568 modulation: ModNode::None,
569 input: Box::new(vco(Waveform::Saw, 0, 0.5)),
570 }),
571 },
572 },
573 },
574 Preset {
575 name: "Fifth Wheel",
576 category: "lead",
577 // The shifter is 0.79 on a `(2x−1)·2.5 V` knob into quiver's
578 // `cv/5·24` semitones: +1.45 V, i.e. +7.0 semitones — a fifth, not
579 // an "up a bit". At mix 0.5 both voices are present, which is what
580 // makes it a harmony rather than a transposition.
581 blurb: "plays a fifth above itself, in parallel, slightly grainy",
582 tree: PatchTree {
583 amp: amp(0.15, 0.4, 0.7, 0.3),
584 root: Shift {
585 uid: Uid::NEW,
586 semis: 0.79,
587 // 41 ms grains: long enough to track the fundamental of a
588 // lead register, short enough that the harmony arrives
589 // with the note rather than behind it.
590 window: 0.35,
591 mix: 0.5,
592 mod_depth: 0.0,
593 modulation: ModNode::None,
594 input: Box::new(Filter {
595 uid: Uid::NEW,
596 kind: FilterKind::Ladder,
597 cutoff: 0.55, // ≈1.2 kHz
598 resonance: 0.45,
599 mod_depth: 0.5,
600 modulation: ModNode::Env {
601 uid: Uid::NEW,
602 attack: 0.05,
603 decay: 0.45, // ≈45 ms
604 },
605 input: Box::new(vco(Waveform::Saw, 0, 0.5)),
606 }),
607 },
608 },
609 },
610 // ---------------------------------------------------------------
611 // KEYS
612 // ---------------------------------------------------------------
613 Preset {
614 name: "Pluck",
615 category: "keys",
616 blurb: "woody and dry; gone before you lift the key",
617 tree: PatchTree {
618 amp: amp(0.0, 0.25, 0.05, 0.25),
619 root: Filter {
620 uid: Uid::NEW,
621 kind: FilterKind::SvfBp,
622 cutoff: 0.6,
623 resonance: 0.45,
624 mod_depth: 0.5,
625 modulation: ModNode::Env {
626 uid: Uid::NEW,
627 attack: 0.0,
628 decay: 0.25,
629 },
630 input: Box::new(vco(Waveform::Triangle, 0, 0.5)),
631 },
632 },
633 },
634 Preset {
635 name: "Bell Jar",
636 category: "keys",
637 // Folding a sine is the cheapest way to inharmonic partials in a
638 // grammar with no FM and no ring mod.
639 blurb: "struck glass, heard from across the room",
640 tree: PatchTree {
641 amp: amp(0.0, 0.55, 0.0, 0.7),
642 root: Reverb {
643 uid: Uid::NEW,
644 size: 0.7,
645 damp: 0.3,
646 mix: 0.4,
647 mod_depth: 0.0,
648 modulation: ModNode::None,
649 input: Box::new(Fold {
650 uid: Uid::NEW,
651 threshold: 0.55,
652 mod_depth: 0.35,
653 modulation: ModNode::Env {
654 uid: Uid::NEW,
655 attack: 0.0,
656 decay: 0.35,
657 },
658 input: Box::new(vco(Waveform::Sine, 1, 0.5)),
659 }),
660 },
661 },
662 },
663 Preset {
664 name: "Tine",
665 category: "keys",
666 blurb: "electric-piano-shaped, without pretending to be one",
667 tree: PatchTree {
668 amp: amp(0.0, 0.6, 0.25, 0.5),
669 root: Chorus {
670 uid: Uid::NEW,
671 rate: 0.2,
672 depth: 0.3,
673 mix: 0.3,
674 mod_depth: 0.0,
675 modulation: ModNode::None,
676 input: Box::new(Mix {
677 uid: Uid::NEW,
678 balance: 0.3,
679 a: Box::new(vco(Waveform::Sine, 0, 0.5)),
680 b: Box::new(Filter {
681 uid: Uid::NEW,
682 kind: FilterKind::SvfHp,
683 cutoff: 0.78,
684 resonance: 0.2,
685 mod_depth: 0.0,
686 modulation: ModNode::None,
687 input: Box::new(vco(Waveform::Triangle, 2, 0.5)),
688 }),
689 }),
690 },
691 },
692 },
693 Preset {
694 name: "Coin Toss",
695 category: "keys",
696 blurb: "a resonant burble that never settles",
697 tree: PatchTree {
698 amp: amp(0.0, 0.42, 0.0, 0.35),
699 root: Filter {
700 uid: Uid::NEW,
701 kind: FilterKind::Ladder,
702 cutoff: 0.55,
703 resonance: 0.6,
704 mod_depth: 0.3,
705 modulation: ModNode::Rand {
706 uid: Uid::NEW,
707 rate: 0.75,
708 glide: 0.0,
709 }, // ~4 Hz
710 input: Box::new(vco(Waveform::Saw, 0, 0.5)),
711 },
712 },
713 },
714 Preset {
715 name: "Gut String",
716 category: "keys",
717 // Karplus-Strong excites once per gate edge and then rings from
718 // its own loop, so the amp envelope is a window on the decay
719 // rather than the decay itself — hence the long sustain on a
720 // patch that plainly dies away.
721 blurb: "a string that was plucked, not switched on",
722 tree: PatchTree {
723 amp: amp(0.0, 0.5, 0.8, 0.4),
724 root: Pluck {
725 uid: Uid::NEW,
726 octave: 0,
727 damping: 0.62,
728 brightness: 0.55,
729 mod_depth: 0.0,
730 modulation: ModNode::None,
731 },
732 },
733 },
734 Preset {
735 name: "Ghost Bell",
736 category: "keys",
737 // The classic ring-mod bell: sum and difference tones of two
738 // sines a fifth-and-an-octave apart are inharmonic, which is what
739 // makes it read as struck metal rather than as a note. `mix`
740 // keeps some dry carrier so the patch still has a pitch.
741 blurb: "two sines multiplied into something that never had a pitch",
742 tree: PatchTree {
743 amp: amp(0.0, 0.5, 0.0, 0.65),
744 root: Reverb {
745 uid: Uid::NEW,
746 size: 0.72,
747 damp: 0.35,
748 mix: 0.4,
749 mod_depth: 0.0,
750 modulation: ModNode::None,
751 input: Box::new(RingMod {
752 uid: Uid::NEW,
753 mix: 0.68,
754 a: Box::new(vco(Waveform::Sine, 1, 0.5)),
755 b: Box::new(vco(Waveform::Sine, 2, 0.62)),
756 }),
757 },
758 },
759 },
760 Preset {
761 name: "Choirboy",
762 category: "keys",
763 // quiver's `vowel` port is a continuous position across A/E/I/O/U,
764 // not a five-way switch — so an LFO on it *slides* between vowels,
765 // which is a spectral movement the feature extractor's centroid
766 // and rolloff see directly.
767 blurb: "vowels, sung by something with no mouth",
768 tree: PatchTree {
769 amp: amp(0.12, 0.5, 0.75, 0.4),
770 root: Reverb {
771 uid: Uid::NEW,
772 size: 0.7,
773 damp: 0.4,
774 mix: 0.4,
775 mod_depth: 0.0,
776 modulation: ModNode::None,
777 input: Box::new(Formant {
778 uid: Uid::NEW,
779 vowel: 0.25,
780 shift: 0.55,
781 octave: 0,
782 mod_depth: 0.5,
783 modulation: ModNode::Lfo {
784 uid: Uid::NEW,
785 wave: Waveform::Triangle,
786 rate: 0.45, // 0.37 Hz
787 },
788 }),
789 },
790 },
791 },
792 Preset {
793 name: "Twelve String",
794 category: "keys",
795 // The shifter at 0.79 is +7.0 semitones (its knob is ±12 over the
796 // port's ±24, so the mod cable can reach the rest); at mix 0.5 the
797 // original is still there, so one key gives a string and its
798 // fifth. On a plucked source that is a twelve-string, not a
799 // transposition.
800 blurb: "one string, and its fifth, struck together",
801 tree: PatchTree {
802 amp: amp(0.0, 0.45, 0.25, 0.35),
803 root: Reverb {
804 uid: Uid::NEW,
805 size: 0.5,
806 damp: 0.55,
807 mix: 0.25,
808 mod_depth: 0.0,
809 modulation: ModNode::None,
810 input: Box::new(AudioNode::Shift {
811 uid: Uid::NEW,
812 semis: 0.79,
813 window: 0.3, // ≈35 ms grains — tracks a key register
814 mix: 0.5,
815 mod_depth: 0.0,
816 modulation: ModNode::None,
817 input: Box::new(AudioNode::Pluck {
818 uid: Uid::NEW,
819 octave: 0,
820 damping: 0.55,
821 brightness: 0.55,
822 mod_depth: 0.0,
823 modulation: ModNode::None,
824 }),
825 }),
826 },
827 },
828 },
829 // ---------------------------------------------------------------
830 // PAD
831 // ---------------------------------------------------------------
832 Preset {
833 name: "Cathedral",
834 category: "pad",
835 blurb: "slow, sacred and wide",
836 tree: PatchTree {
837 // Was 0.4 — 40 ms, an organ stab into a hall. 0.75 is ~1 s,
838 // which is what "slow" claimed all along.
839 amp: amp(0.75, 0.4, 0.75, 0.7),
840 root: Reverb {
841 uid: Uid::NEW,
842 size: 0.85,
843 damp: 0.35,
844 mix: 0.5,
845 mod_depth: 0.0,
846 modulation: ModNode::None,
847 input: Box::new(Filter {
848 uid: Uid::NEW,
849 kind: FilterKind::SvfLp,
850 cutoff: 0.55,
851 resonance: 0.25,
852 mod_depth: 0.3,
853 modulation: ModNode::Rand {
854 uid: Uid::NEW,
855 rate: 0.45,
856 glide: 0.0,
857 }, // 0.37 Hz
858 input: Box::new(vco(Waveform::Triangle, 0, 0.55)),
859 }),
860 },
861 },
862 },
863 Preset {
864 name: "Glass Pad",
865 category: "pad",
866 blurb: "bright, transparent, faintly fragile",
867 tree: PatchTree {
868 amp: amp(0.55, 0.4, 0.8, 0.65),
869 root: Chorus {
870 uid: Uid::NEW,
871 rate: 0.25,
872 depth: 0.5,
873 mix: 0.45,
874 mod_depth: 0.0,
875 modulation: ModNode::None,
876 input: Box::new(Filter {
877 uid: Uid::NEW,
878 kind: FilterKind::SvfLp,
879 cutoff: 0.65,
880 resonance: 0.2,
881 mod_depth: 0.25,
882 modulation: ModNode::Lfo {
883 uid: Uid::NEW,
884 wave: Waveform::Sine,
885 rate: 0.49, // 0.5 Hz — was 0.2 (0.05 Hz, 20 s/cycle)
886 },
887 input: Box::new(supersaw(0, 0.4, 0.6)),
888 }),
889 },
890 },
891 },
892 Preset {
893 name: "Detune Dream",
894 category: "pad",
895 blurb: "a wide smear that drifts off the end of the note",
896 tree: PatchTree {
897 amp: amp(0.35, 0.4, 0.75, 0.55),
898 root: Delay {
899 uid: Uid::NEW,
900 time: 0.72, // 250 ms — was 0.3 (8 ms), a metallic comb
901 feedback: 0.45,
902 mix: 0.35,
903 mod_depth: 0.0,
904 modulation: ModNode::None,
905 input: Box::new(Chorus {
906 uid: Uid::NEW,
907 rate: 0.3,
908 depth: 0.55,
909 mix: 0.4,
910 mod_depth: 0.0,
911 modulation: ModNode::None,
912 input: Box::new(supersaw(0, 0.55, 0.5)),
913 }),
914 },
915 },
916 },
917 Preset {
918 name: "Ember",
919 category: "pad",
920 blurb: "a pad that glows rather than shines",
921 tree: PatchTree {
922 amp: amp(0.75, 0.55, 0.8, 0.85),
923 root: Reverb {
924 uid: Uid::NEW,
925 size: 0.75,
926 damp: 0.55,
927 mix: 0.45,
928 mod_depth: 0.0,
929 modulation: ModNode::None,
930 input: Box::new(Filter {
931 uid: Uid::NEW,
932 kind: FilterKind::SvfLp,
933 cutoff: 0.42,
934 resonance: 0.25,
935 mod_depth: 0.25,
936 modulation: ModNode::Lfo {
937 uid: Uid::NEW,
938 wave: Waveform::Sine,
939 rate: 0.45, // 0.37 Hz
940 },
941 input: Box::new(Mix {
942 uid: Uid::NEW,
943 balance: 0.4,
944 a: Box::new(vco(Waveform::Sine, -1, 0.5)),
945 b: Box::new(Noise {
946 uid: Uid::NEW,
947 color: NoiseColor::Pink,
948 }),
949 }),
950 }),
951 },
952 },
953 },
954 Preset {
955 name: "Slow Weather",
956 category: "pad",
957 blurb: "never sits in the same place twice",
958 tree: PatchTree {
959 amp: amp(0.79, 0.5, 0.85, 0.8),
960 root: Reverb {
961 uid: Uid::NEW,
962 size: 0.65,
963 damp: 0.4,
964 mix: 0.35,
965 mod_depth: 0.0,
966 modulation: ModNode::None,
967 input: Box::new(Filter {
968 uid: Uid::NEW,
969 kind: FilterKind::SvfBp,
970 cutoff: 0.5,
971 resonance: 0.45,
972 mod_depth: 0.5,
973 modulation: ModNode::Lfo {
974 uid: Uid::NEW,
975 wave: Waveform::Triangle,
976 rate: 0.4, // 0.25 Hz — slow, but it does arrive
977 },
978 input: Box::new(supersaw(0, 0.6, 0.55)),
979 }),
980 },
981 },
982 },
983 Preset {
984 name: "Morph Pad",
985 category: "pad",
986 // The whole point of the wavetable: the LFO walks `morph` across
987 // the crossfade between adjacent tables, so the spectrum breathes
988 // without the filter moving at all.
989 blurb: "one oscillator slowly turning into a different one",
990 tree: PatchTree {
991 amp: amp(0.6, 0.45, 0.8, 0.7),
992 root: Reverb {
993 uid: Uid::NEW,
994 size: 0.68,
995 damp: 0.4,
996 mix: 0.35,
997 mod_depth: 0.0,
998 modulation: ModNode::None,
999 input: Box::new(Filter {
1000 uid: Uid::NEW,
1001 kind: FilterKind::SvfLp,
1002 cutoff: 0.62,
1003 resonance: 0.25,
1004 mod_depth: 0.0,
1005 modulation: ModNode::None,
1006 input: Box::new(Wavetable {
1007 uid: Uid::NEW,
1008 table: TableShape::Saw,
1009 octave: 0,
1010 morph: 0.3,
1011 mod_depth: 0.6,
1012 modulation: ModNode::Lfo {
1013 uid: Uid::NEW,
1014 wave: Waveform::Sine,
1015 rate: 0.44, // 0.34 Hz — one sweep per held note
1016 },
1017 }),
1018 }),
1019 },
1020 },
1021 },
1022 Preset {
1023 name: "Sweep Machine",
1024 category: "pad",
1025 // Feedback above 0.5 is positive (the map is bipolar and centred),
1026 // which turns the notches into resonant peaks — the difference
1027 // between a phaser you notice and one you don't.
1028 blurb: "notches walking up and down a wall of saws",
1029 tree: PatchTree {
1030 amp: amp(0.45, 0.45, 0.85, 0.6),
1031 root: Phaser {
1032 uid: Uid::NEW,
1033 rate: 0.35, // 0.25 Hz
1034 depth: 0.75,
1035 feedback: 0.78,
1036 mod_depth: 0.0,
1037 modulation: ModNode::None,
1038 input: Box::new(supersaw(0, 0.45, 0.55)),
1039 },
1040 },
1041 },
1042 Preset {
1043 name: "Sea Change",
1044 category: "pad",
1045 // Fully wet, which is the whole distinction from a chorus: at
1046 // `mix` 0.5 the dry copy beats against the shifted one and this
1047 // becomes `Detune Dream` with extra steps.
1048 blurb: "a pad that will not quite sit still in tune",
1049 tree: PatchTree {
1050 amp: amp(0.35, 0.5, 0.8, 0.5),
1051 root: Vibrato {
1052 uid: Uid::NEW,
1053 rate: 0.55, // 1.6 Hz on quiver's own 0.1·150^x map
1054 depth: 0.3,
1055 mix: 1.0,
1056 mod_depth: 0.4,
1057 modulation: ModNode::Lfo {
1058 uid: Uid::NEW,
1059 wave: Waveform::Sine,
1060 rate: 0.4, // 0.25 Hz
1061 },
1062 input: Box::new(supersaw(0, 0.35, 0.5)),
1063 },
1064 },
1065 },
1066 Preset {
1067 name: "Pump Room",
1068 category: "pad",
1069 // The first patch in the library whose level is driven by a
1070 // *second* signal: the plucked string is never heard, it only
1071 // tells the ducker when to get out of the way. Threshold 0.4 is
1072 // ≈0.32 V on the geometric detector map, just under the string's
1073 // own envelope peak, so every note ducks and then recovers.
1074 blurb: "a wide pad that steps aside every time the string is struck",
1075 tree: PatchTree {
1076 amp: amp(0.4, 0.5, 0.85, 0.5),
1077 root: Duck {
1078 uid: Uid::NEW,
1079 amount: 0.85,
1080 threshold: 0.4,
1081 release: 0.6, // ≈600 ms — the recovery is the groove
1082 mod_depth: 0.0,
1083 modulation: ModNode::None,
1084 input: Box::new(Chorus {
1085 uid: Uid::NEW,
1086 rate: 0.25, // 0.27 Hz
1087 depth: 0.6,
1088 mix: 0.5,
1089 mod_depth: 0.0,
1090 modulation: ModNode::None,
1091 input: Box::new(Filter {
1092 uid: Uid::NEW,
1093 kind: FilterKind::SvfLp,
1094 cutoff: 0.62, // ≈2.0 kHz
1095 resonance: 0.25,
1096 mod_depth: 0.0,
1097 modulation: ModNode::None,
1098 input: Box::new(supersaw(-1, 0.4, 0.55)),
1099 }),
1100 }),
1101 key: Box::new(Pluck {
1102 uid: Uid::NEW,
1103 octave: -1,
1104 damping: 0.35,
1105 brightness: 0.75,
1106 mod_depth: 0.0,
1107 modulation: ModNode::None,
1108 }),
1109 },
1110 },
1111 },
1112 Preset {
1113 name: "Tidal",
1114 category: "pad",
1115 // `Or` fires wherever *either* pattern does, which on 8-against-12
1116 // is a dense but uneven figure — the opposite bargain to Ticker's
1117 // `And`. On chorus depth rather than on a cutoff it reads as the
1118 // width breathing rather than as a rhythm, which is what keeps it
1119 // a pad.
1120 blurb: "width that swells on two rhythms at once",
1121 tree: PatchTree {
1122 amp: amp(0.5, 0.5, 0.9, 0.55),
1123 root: Chorus {
1124 uid: Uid::NEW,
1125 rate: 0.25, // 0.27 Hz
1126 depth: 0.35,
1127 mix: 0.6,
1128 mod_depth: 0.8,
1129 modulation: ModNode::Pair {
1130 uid: Uid::NEW,
1131 kind: PairOp::Or,
1132 a: Box::new(ModNode::Euclid {
1133 uid: Uid::NEW,
1134 rate: 0.55, // ≈83 bpm
1135 steps: 0.303,
1136 pulses: 0.230,
1137 }),
1138 b: Box::new(ModNode::Euclid {
1139 uid: Uid::NEW,
1140 rate: 0.55,
1141 steps: 0.613,
1142 pulses: 0.190,
1143 }),
1144 },
1145 input: Box::new(Filter {
1146 uid: Uid::NEW,
1147 kind: FilterKind::SvfLp,
1148 cutoff: 0.62, // ≈1.6 kHz
1149 resonance: 0.15,
1150 mod_depth: 0.0,
1151 modulation: ModNode::None,
1152 input: Box::new(supersaw(-1, 0.45, 0.5)),
1153 }),
1154 },
1155 },
1156 },
1157 // ---------------------------------------------------------------
1158 // TEXTURE
1159 // ---------------------------------------------------------------
1160 Preset {
1161 name: "Noise Wash",
1162 category: "texture",
1163 blurb: "coastal fog with a pitch somewhere inside it",
1164 tree: PatchTree {
1165 amp: amp(0.7, 0.5, 0.75, 0.8),
1166 root: Filter {
1167 uid: Uid::NEW,
1168 kind: FilterKind::SvfLp,
1169 cutoff: 0.45,
1170 resonance: 0.5,
1171 mod_depth: 0.5,
1172 modulation: ModNode::Lfo {
1173 uid: Uid::NEW,
1174 wave: Waveform::Sine,
1175 rate: 0.44, // 0.34 Hz — was 0.15 (0.033 Hz, 30 s/cycle)
1176 },
1177 input: Box::new(Noise {
1178 uid: Uid::NEW,
1179 color: NoiseColor::Pink,
1180 }),
1181 },
1182 },
1183 },
1184 Preset {
1185 name: "Dub Echo",
1186 category: "texture",
1187 blurb: "the mixing desk is the instrument",
1188 tree: PatchTree {
1189 amp: amp(0.02, 0.3, 0.5, 0.4),
1190 root: Delay {
1191 uid: Uid::NEW,
1192 // Was 0.45 — 31 ms, which is a flanger. 0.79 is ~420 ms,
1193 // which is the sound the name has always promised.
1194 time: 0.79,
1195 feedback: 0.75,
1196 mix: 0.5,
1197 mod_depth: 0.0,
1198 modulation: ModNode::None,
1199 input: Box::new(Filter {
1200 uid: Uid::NEW,
1201 kind: FilterKind::SvfLp,
1202 cutoff: 0.5,
1203 resonance: 0.35,
1204 mod_depth: 0.0,
1205 modulation: ModNode::None,
1206 input: Box::new(vco(Waveform::Square, 0, 0.5)),
1207 }),
1208 },
1209 },
1210 },
1211 Preset {
1212 name: "Static Ocean",
1213 category: "texture",
1214 blurb: "surf heard from a headland",
1215 tree: PatchTree {
1216 amp: amp(0.85, 0.5, 0.85, 0.9),
1217 root: Reverb {
1218 uid: Uid::NEW,
1219 size: 0.9,
1220 damp: 0.6,
1221 mix: 0.55,
1222 mod_depth: 0.0,
1223 modulation: ModNode::None,
1224 input: Box::new(Filter {
1225 uid: Uid::NEW,
1226 kind: FilterKind::SvfBp,
1227 cutoff: 0.55,
1228 resonance: 0.65,
1229 mod_depth: 0.55,
1230 modulation: ModNode::Lfo {
1231 uid: Uid::NEW,
1232 wave: Waveform::Sine,
1233 rate: 0.42, // 0.29 Hz
1234 },
1235 input: Box::new(Noise {
1236 uid: Uid::NEW,
1237 color: NoiseColor::White,
1238 }),
1239 }),
1240 },
1241 },
1242 },
1243 Preset {
1244 name: "Rotor",
1245 category: "texture",
1246 blurb: "a machine idling in the next room",
1247 tree: PatchTree {
1248 amp: amp(0.65, 0.5, 0.9, 0.6),
1249 root: Chorus {
1250 uid: Uid::NEW,
1251 rate: 0.45,
1252 depth: 0.6,
1253 mix: 0.5,
1254 mod_depth: 0.0,
1255 modulation: ModNode::None,
1256 input: Box::new(Filter {
1257 uid: Uid::NEW,
1258 kind: FilterKind::SvfLp,
1259 cutoff: 0.42,
1260 resonance: 0.4,
1261 mod_depth: 0.4,
1262 modulation: ModNode::Rand {
1263 uid: Uid::NEW,
1264 rate: 0.58,
1265 glide: 0.0,
1266 }, // ~1.0 Hz
1267 input: Box::new(vco(Waveform::Square, -1, 0.5)),
1268 }),
1269 },
1270 },
1271 },
1272 Preset {
1273 name: "Long Room",
1274 category: "texture",
1275 blurb: "a drone that builds itself while you hold the key",
1276 tree: PatchTree {
1277 amp: amp(0.7, 0.5, 0.85, 0.9),
1278 root: Reverb {
1279 uid: Uid::NEW,
1280 size: 0.8,
1281 damp: 0.45,
1282 mix: 0.4,
1283 mod_depth: 0.0,
1284 modulation: ModNode::None,
1285 input: Box::new(Delay {
1286 uid: Uid::NEW,
1287 time: 0.82, // ~500 ms
1288 feedback: 0.62,
1289 mix: 0.45,
1290 mod_depth: 0.0,
1291 modulation: ModNode::None,
1292 input: Box::new(Filter {
1293 uid: Uid::NEW,
1294 kind: FilterKind::SvfLp,
1295 cutoff: 0.45,
1296 resonance: 0.3,
1297 mod_depth: 0.22,
1298 modulation: ModNode::Lfo {
1299 uid: Uid::NEW,
1300 wave: Waveform::Triangle,
1301 rate: 0.42, // 0.29 Hz — 0.38 was 0.21, under the floor
1302 },
1303 input: Box::new(vco(Waveform::Sine, -1, 0.5)),
1304 }),
1305 }),
1306 },
1307 },
1308 },
1309 Preset {
1310 name: "Handheld",
1311 category: "texture",
1312 // S&H on the bit depth, with the glide open a little so the
1313 // quantizer slides between resolutions instead of stepping —
1314 // the difference between a broken machine and a dying one.
1315 blurb: "a sound remembered by something with not enough memory",
1316 tree: PatchTree {
1317 amp: amp(0.02, 0.4, 0.6, 0.3),
1318 root: Bitcrush {
1319 uid: Uid::NEW,
1320 bits: 0.32,
1321 downsample: 0.42,
1322 mod_depth: 0.45,
1323 modulation: ModNode::Rand {
1324 uid: Uid::NEW,
1325 rate: 0.55, // 0.82 Hz
1326 glide: 0.35,
1327 },
1328 input: Box::new(vco(Waveform::Square, 0, 0.5)),
1329 },
1330 },
1331 },
1332 Preset {
1333 name: "Jet Wash",
1334 category: "texture",
1335 // `feedback` 0.78 is a *positive* 0.39 on the bipolar map — the
1336 // resonant side, where the comb peaks ring rather than the
1337 // notches deepening. Below 0.5 the same knob flips the sign.
1338 blurb: "white noise pushed through a jet engine",
1339 tree: PatchTree {
1340 amp: amp(0.25, 0.5, 0.8, 0.5),
1341 root: Flanger {
1342 uid: Uid::NEW,
1343 rate: 0.45, // 0.4 Hz on quiver's own 0.05·100^x map
1344 depth: 0.75,
1345 feedback: 0.78,
1346 mod_depth: 0.35,
1347 modulation: ModNode::Lfo {
1348 uid: Uid::NEW,
1349 wave: Waveform::Sine,
1350 rate: 0.42, // 0.29 Hz
1351 },
1352 input: Box::new(AudioNode::Noise {
1353 uid: Uid::NEW,
1354 color: NoiseColor::White,
1355 }),
1356 },
1357 },
1358 },
1359 Preset {
1360 name: "Cloud Chamber",
1361 category: "texture",
1362 // `position` is kept near the write head on purpose: quiver's
1363 // grain buffer is 96 000 samples and starts empty, so a granulator
1364 // parked at the far end of it reads silence for the first two
1365 // seconds of a five-second phrase.
1366 blurb: "a triangle taken apart and reassembled in the wrong order",
1367 tree: PatchTree {
1368 amp: amp(0.3, 0.5, 0.85, 0.6),
1369 root: Reverb {
1370 uid: Uid::NEW,
1371 size: 0.75,
1372 damp: 0.5,
1373 mix: 0.45,
1374 mod_depth: 0.0,
1375 modulation: ModNode::None,
1376 input: Box::new(Granular {
1377 uid: Uid::NEW,
1378 position: 0.08,
1379 size: 0.35, // 181 ms grains
1380 density: 0.75, // 15 grains/second, so ~2.8 overlapping
1381 mod_depth: 0.5,
1382 modulation: ModNode::Lfo {
1383 uid: Uid::NEW,
1384 wave: Waveform::Sine,
1385 rate: 0.43, // 0.31 Hz
1386 },
1387 input: Box::new(vco(Waveform::Triangle, 0, 0.5)),
1388 }),
1389 },
1390 },
1391 },
1392 Preset {
1393 name: "Vox Machina",
1394 category: "texture",
1395 // A vocoder needs both branches to be right or it says nothing: a
1396 // sine carrier has no energy in fifteen of the sixteen bands, and
1397 // a static modulator makes a fixed filter. So the carrier is a
1398 // supersaw and the modulator's vowel is swept a full cycle per
1399 // held note — the sweep *is* the patch.
1400 blurb: "a saw stack made to speak, one vowel at a time",
1401 tree: PatchTree {
1402 amp: amp(0.25, 0.45, 0.85, 0.4),
1403 root: Vocoder {
1404 uid: Uid::NEW,
1405 bands: 0.75, // ≈13 bands
1406 attack: 0.15, // ≈38 ms
1407 release: 0.35,
1408 mod_depth: 0.0,
1409 modulation: ModNode::None,
1410 carrier: Box::new(supersaw(0, 0.5, 0.65)),
1411 modulator: Box::new(Formant {
1412 uid: Uid::NEW,
1413 vowel: 0.25,
1414 shift: 0.5, // no formant shift; the sweep is the vowel
1415 octave: 0,
1416 mod_depth: 0.7,
1417 modulation: ModNode::Lfo {
1418 uid: Uid::NEW,
1419 wave: Waveform::Triangle,
1420 rate: 0.49, // 0.5 Hz — one sweep per held note
1421 },
1422 }),
1423 },
1424 },
1425 },
1426 Preset {
1427 name: "Glass Rain",
1428 category: "texture",
1429 // `Hold` samples another modulator on its own clock, which is what
1430 // separates it from `s&h rand`: the values are not noise, they are
1431 // a sine caught at moments. On the granulator's read position that
1432 // makes the grains jump between a handful of places in the buffer
1433 // instead of sweeping through it — the difference between rain and
1434 // a smear.
1435 blurb: "grains caught from a few places at once, never sweeping",
1436 tree: PatchTree {
1437 amp: amp(0.35, 0.5, 0.85, 0.55),
1438 root: AudioNode::Granular {
1439 uid: Uid::NEW,
1440 position: 0.5,
1441 size: 0.25, // ≈130 ms grains
1442 density: 0.75, // ≈15 per second
1443 mod_depth: 0.8,
1444 modulation: ModNode::Op {
1445 uid: Uid::NEW,
1446 kind: ModOp::Hold,
1447 p0: 0.55, // ≈83 bpm — about one jump per bar
1448 p1: 0.0,
1449 input: Box::new(ModNode::Lfo {
1450 uid: Uid::NEW,
1451 wave: Waveform::Sine,
1452 rate: 0.52, // 0.64 Hz, so successive samples differ
1453 }),
1454 },
1455 input: Box::new(vco(Waveform::Triangle, 0, 0.5)),
1456 },
1457 },
1458 },
1459 Preset {
1460 name: "One Way",
1461 category: "texture",
1462 // `Rectify` in `positive` mode throws away the bottom half of the
1463 // LFO, so the fold threshold is only ever pushed *down* from where
1464 // the knob set it and the patch has a resting state it returns to.
1465 // A bipolar LFO on the same cable would fold and unfold
1466 // symmetrically, which reads as a wobble rather than as a bloom.
1467 blurb: "folds harder in bursts and always settles back",
1468 tree: PatchTree {
1469 amp: amp(0.3, 0.45, 0.8, 0.4),
1470 root: Reverb {
1471 uid: Uid::NEW,
1472 size: 0.6,
1473 damp: 0.5,
1474 mix: 0.35,
1475 mod_depth: 0.0,
1476 modulation: ModNode::None,
1477 input: Box::new(Fold {
1478 uid: Uid::NEW,
1479 threshold: 0.7,
1480 mod_depth: 0.7,
1481 modulation: ModNode::Op {
1482 uid: Uid::NEW,
1483 kind: ModOp::Rectify,
1484 p0: 0.5, // cell centre of `positive`
1485 p1: 0.0,
1486 input: Box::new(ModNode::Lfo {
1487 uid: Uid::NEW,
1488 wave: Waveform::Triangle,
1489 rate: 0.5, // 0.55 Hz
1490 }),
1491 },
1492 input: Box::new(vco(Waveform::Triangle, 0, 0.5)),
1493 }),
1494 },
1495 },
1496 },
1497 // ---------------------------------------------------------------
1498 // PERC
1499 // ---------------------------------------------------------------
1500 Preset {
1501 name: "Flint",
1502 category: "perc",
1503 blurb: "a struck spark",
1504 tree: PatchTree {
1505 amp: amp(0.0, 0.35, 0.0, 0.25),
1506 root: Filter {
1507 uid: Uid::NEW,
1508 kind: FilterKind::SvfHp,
1509 cutoff: 0.72,
1510 resonance: 0.55,
1511 mod_depth: 0.3,
1512 modulation: ModNode::Env {
1513 uid: Uid::NEW,
1514 attack: 0.0,
1515 decay: 0.2,
1516 },
1517 input: Box::new(Noise {
1518 uid: Uid::NEW,
1519 color: NoiseColor::White,
1520 }),
1521 },
1522 },
1523 },
1524 Preset {
1525 name: "Deadfall",
1526 category: "perc",
1527 blurb: "a tuned drum with the room taken out",
1528 tree: PatchTree {
1529 amp: amp(0.0, 0.45, 0.0, 0.3),
1530 root: Filter {
1531 uid: Uid::NEW,
1532 kind: FilterKind::Ladder,
1533 cutoff: 0.5,
1534 resonance: 0.3,
1535 mod_depth: 0.55,
1536 modulation: ModNode::Env {
1537 uid: Uid::NEW,
1538 attack: 0.0,
1539 decay: 0.2,
1540 },
1541 input: Box::new(vco(Waveform::Sine, -2, 0.5)),
1542 },
1543 },
1544 },
1545 Preset {
1546 name: "Ricochet",
1547 category: "perc",
1548 blurb: "a metallic ping bouncing down a corridor",
1549 tree: PatchTree {
1550 amp: amp(0.0, 0.4, 0.0, 0.25),
1551 root: Delay {
1552 uid: Uid::NEW,
1553 time: 0.54, // 60 ms — tight enough to read as a bounce
1554 feedback: 0.68,
1555 mix: 0.55,
1556 mod_depth: 0.0,
1557 modulation: ModNode::None,
1558 input: Box::new(Filter {
1559 uid: Uid::NEW,
1560 kind: FilterKind::SvfBp,
1561 cutoff: 0.7,
1562 resonance: 0.65,
1563 mod_depth: 0.25,
1564 modulation: ModNode::Env {
1565 uid: Uid::NEW,
1566 attack: 0.0,
1567 decay: 0.15,
1568 },
1569 input: Box::new(vco(Waveform::Square, 1, 0.5)),
1570 }),
1571 },
1572 },
1573 },
1574 Preset {
1575 name: "Heartbeat",
1576 category: "perc",
1577 // `shape` 0.8 is most of the way to triangle, which is what makes
1578 // the pulse arrive rather than swell — a sine tremolo at this
1579 // depth reads as a wobble, not a beat.
1580 blurb: "a square wave with a pulse, and it quickens",
1581 tree: PatchTree {
1582 amp: amp(0.02, 0.45, 0.7, 0.25),
1583 root: Tremolo {
1584 uid: Uid::NEW,
1585 rate: 0.62, // 2.7 Hz on quiver's own 0.1·200^x map
1586 depth: 0.85,
1587 shape: 0.8,
1588 mod_depth: 0.4,
1589 modulation: ModNode::Env {
1590 uid: Uid::NEW,
1591 attack: 0.3,
1592 decay: 0.6,
1593 },
1594 input: Box::new(Filter {
1595 uid: Uid::NEW,
1596 kind: FilterKind::SvfLp,
1597 cutoff: 0.45,
1598 resonance: 0.4,
1599 mod_depth: 0.0,
1600 modulation: ModNode::None,
1601 input: Box::new(vco(Waveform::Square, -1, 0.5)),
1602 }),
1603 },
1604 },
1605 },
1606 Preset {
1607 name: "Morse",
1608 category: "perc",
1609 // The drone underneath is continuous; what you hear is the
1610 // string's own envelope cut into it. Range 0.95 means a shut gate
1611 // passes almost nothing, so the patch is percussive without any
1612 // percussive source in it.
1613 blurb: "a drone you only hear while the string is still ringing",
1614 tree: PatchTree {
1615 amp: amp(0.1, 0.4, 0.9, 0.4),
1616 root: Gate {
1617 uid: Uid::NEW,
1618 threshold: 0.45,
1619 range: 0.95,
1620 release: 0.25, // ≈133 ms — a tight close
1621 mod_depth: 0.0,
1622 modulation: ModNode::None,
1623 input: Box::new(Filter {
1624 uid: Uid::NEW,
1625 kind: FilterKind::SvfBp,
1626 cutoff: 0.55, // ≈1.2 kHz
1627 resonance: 0.6,
1628 mod_depth: 0.0,
1629 modulation: ModNode::None,
1630 input: Box::new(supersaw(0, 0.35, 0.5)),
1631 }),
1632 sidechain: Box::new(Pluck {
1633 uid: Uid::NEW,
1634 octave: -1,
1635 damping: 0.35,
1636 brightness: 0.75,
1637 mod_depth: 0.0,
1638 modulation: ModNode::None,
1639 }),
1640 },
1641 },
1642 },
1643 Preset {
1644 name: "Ticker",
1645 category: "perc",
1646 // Two euclidean patterns ANDed: the filter only opens where both
1647 // agree, which on 8-against-9 is a sparse figure that takes 72
1648 // steps to repeat. `esteps` 0.303 is 8 and 0.380 is 9 (quiver
1649 // takes `2 + cv·14.99` and the compiler feeds it `0.14 + 0.86x`);
1650 // `epulses` 0.230 is 3 of 8 and 0.122 is 3 of 9.
1651 blurb: "a filter opening only where two rhythms agree",
1652 tree: PatchTree {
1653 amp: amp(0.0, 0.25, 0.9, 0.15),
1654 root: Filter {
1655 uid: Uid::NEW,
1656 kind: FilterKind::SvfBp,
1657 cutoff: 0.66, // ≈1.9 kHz
1658 resonance: 0.55,
1659 mod_depth: 0.8,
1660 modulation: ModNode::Pair {
1661 uid: Uid::NEW,
1662 kind: PairOp::And,
1663 a: Box::new(ModNode::Euclid {
1664 uid: Uid::NEW,
1665 rate: 0.62, // ≈107 bpm
1666 steps: 0.303,
1667 pulses: 0.230,
1668 }),
1669 b: Box::new(ModNode::Euclid {
1670 uid: Uid::NEW,
1671 rate: 0.62,
1672 steps: 0.380,
1673 pulses: 0.122,
1674 }),
1675 },
1676 input: Box::new(AudioNode::Noise {
1677 uid: Uid::NEW,
1678 color: NoiseColor::White,
1679 }),
1680 },
1681 },
1682 },
1683 Preset {
1684 name: "Woodblock",
1685 category: "perc",
1686 // The pluck's own decay is the sound; the bandpass only decides
1687 // which block. Brightness high and decay low is a small hard
1688 // object — the string's loop filter opens as `damping` rises, so
1689 // 0.2 is a short, dry ring rather than a long one.
1690 blurb: "small, hard and hollow; struck once",
1691 tree: PatchTree {
1692 amp: amp(0.0, 0.3, 0.0, 0.2),
1693 root: Filter {
1694 uid: Uid::NEW,
1695 kind: FilterKind::SvfBp,
1696 cutoff: 0.6, // ≈1.0 kHz
1697 resonance: 0.6,
1698 mod_depth: 0.0,
1699 modulation: ModNode::None,
1700 input: Box::new(AudioNode::Pluck {
1701 uid: Uid::NEW,
1702 octave: 1,
1703 damping: 0.2,
1704 brightness: 0.85,
1705 mod_depth: 0.0,
1706 modulation: ModNode::None,
1707 }),
1708 },
1709 },
1710 },
1711 Preset {
1712 name: "Gated Snare",
1713 category: "perc",
1714 // The gate is keyed by the pluck, not by the noise it passes —
1715 // which is the whole point of a second audio branch. The key is a
1716 // plucked string at ≈0.14 V mean, so the threshold sits low on the
1717 // geometric detector map; 0.45 is where a struck string opens it
1718 // and lets it shut again inside one note.
1719 blurb: "noise, allowed through only while a string is ringing",
1720 tree: PatchTree {
1721 amp: amp(0.0, 0.4, 0.4, 0.2),
1722 root: AudioNode::Gate {
1723 uid: Uid::NEW,
1724 threshold: 0.45,
1725 range: 0.85,
1726 release: 0.2,
1727 mod_depth: 0.0,
1728 modulation: ModNode::None,
1729 input: Box::new(Filter {
1730 uid: Uid::NEW,
1731 kind: FilterKind::SvfHp,
1732 cutoff: 0.5, // ≈630 Hz
1733 resonance: 0.2,
1734 mod_depth: 0.0,
1735 modulation: ModNode::None,
1736 input: Box::new(AudioNode::Noise {
1737 uid: Uid::NEW,
1738 color: NoiseColor::White,
1739 }),
1740 }),
1741 sidechain: Box::new(AudioNode::Pluck {
1742 uid: Uid::NEW,
1743 octave: 0,
1744 damping: 0.3,
1745 brightness: 0.6,
1746 mod_depth: 0.0,
1747 modulation: ModNode::None,
1748 }),
1749 },
1750 },
1751 },
1752 // ---------------------------------------------------------------
1753 // WEIRD
1754 // ---------------------------------------------------------------
1755 Preset {
1756 name: "Two Minds",
1757 category: "weird",
1758 // `Switch` picks between its two branches on the level of `b`, so
1759 // the *modulator itself* changes shape mid-note: while the fast
1760 // LFO is high the cutoff follows the slow one, and while it is low
1761 // the fast one takes over. Neither rate divides the other, so the
1762 // handover never lands in the same place twice.
1763 blurb: "a filter modulated by two minds that keep interrupting each other",
1764 tree: PatchTree {
1765 amp: amp(0.3, 0.4, 0.8, 0.4),
1766 root: Filter {
1767 uid: Uid::NEW,
1768 kind: FilterKind::SvfLp,
1769 cutoff: 0.5, // ≈630 Hz
1770 resonance: 0.5,
1771 mod_depth: 0.75,
1772 modulation: ModNode::Pair {
1773 uid: Uid::NEW,
1774 kind: PairOp::Switch,
1775 a: Box::new(ModNode::Lfo {
1776 uid: Uid::NEW,
1777 wave: Waveform::Triangle,
1778 rate: 0.42, // 0.29 Hz
1779 }),
1780 b: Box::new(ModNode::Lfo {
1781 uid: Uid::NEW,
1782 wave: Waveform::Sine,
1783 rate: 0.58, // 1.0 Hz
1784 }),
1785 },
1786 input: Box::new(supersaw(0, 0.4, 0.5)),
1787 },
1788 },
1789 },
1790 Preset {
1791 name: "Ceiling",
1792 category: "weird",
1793 // `Max` is a floor, not a ceiling — the name is what it does to
1794 // the *sound*. The envelope closes the filter after each attack,
1795 // but the LFO underneath it never lets the cutoff fall past its
1796 // own peak, so the patch decays to a breathing plateau instead of
1797 // to silence.
1798 blurb: "decays toward a moving floor and never reaches it",
1799 tree: PatchTree {
1800 amp: amp(0.05, 0.4, 0.7, 0.35),
1801 root: Filter {
1802 uid: Uid::NEW,
1803 kind: FilterKind::Ladder,
1804 cutoff: 0.35, // ≈220 Hz
1805 resonance: 0.55,
1806 mod_depth: 0.6,
1807 modulation: ModNode::Pair {
1808 uid: Uid::NEW,
1809 kind: PairOp::Max,
1810 a: Box::new(ModNode::Env {
1811 uid: Uid::NEW,
1812 attack: 0.05, // ≈2.5 ms
1813 decay: 0.5, // ≈100 ms
1814 }),
1815 b: Box::new(ModNode::Lfo {
1816 uid: Uid::NEW,
1817 wave: Waveform::Sine,
1818 rate: 0.49, // 0.51 Hz
1819 }),
1820 },
1821 input: Box::new(vco(Waveform::Saw, -1, 0.5)),
1822 },
1823 },
1824 },
1825 Preset {
1826 name: "Undertow",
1827 category: "weird",
1828 // `Min` is the opposite bargain: the room only opens where *both*
1829 // modulators agree to open it, so two slow sines a fifth apart in
1830 // rate produce a swell that mostly does not arrive. The patch is
1831 // built to be listened to across several notes rather than one.
1832 blurb: "a room that only opens when two slow tides agree",
1833 tree: PatchTree {
1834 amp: amp(0.55, 0.5, 0.85, 0.6),
1835 root: Reverb {
1836 uid: Uid::NEW,
1837 size: 0.55,
1838 damp: 0.4,
1839 mix: 0.7,
1840 mod_depth: 0.9,
1841 modulation: ModNode::Pair {
1842 uid: Uid::NEW,
1843 kind: PairOp::Min,
1844 a: Box::new(ModNode::Lfo {
1845 uid: Uid::NEW,
1846 wave: Waveform::Sine,
1847 rate: 0.4, // 0.25 Hz
1848 }),
1849 b: Box::new(ModNode::Lfo {
1850 uid: Uid::NEW,
1851 wave: Waveform::Sine,
1852 rate: 0.46, // 0.38 Hz
1853 }),
1854 },
1855 input: Box::new(Filter {
1856 uid: Uid::NEW,
1857 kind: FilterKind::SvfLp,
1858 cutoff: 0.55, // ≈890 Hz
1859 resonance: 0.2,
1860 mod_depth: 0.0,
1861 modulation: ModNode::None,
1862 input: Box::new(supersaw(-1, 0.5, 0.6)),
1863 }),
1864 },
1865 },
1866 },
1867 Preset {
1868 name: "Sour Mash",
1869 category: "weird",
1870 // A square LFO on a fold threshold is the only hard-switching
1871 // modulation the grammar can make. Nothing else uses it.
1872 //
1873 // The input is deliberately a pair of `Vco`s and **not** a
1874 // `Supersaw`. Quiver's wavefolder folds `x / 5.0`, and `Vco` and
1875 // `Noise` scale their outputs `* 5.0` while `Supersaw` does not
1876 // (`oscillators.rs`, `outputs.set(10, output)`) — measured peaks
1877 // 4.94 against 0.74. Fed a supersaw, the folder never reaches its
1878 // own threshold: sweeping it 0.25 → 1.0 gave bit-identical output,
1879 // and this patch, whose entire premise is a hard-switching folder,
1880 // measured as the 4th most static of the 29. Anything built on
1881 // `Fold` needs a source at rack level.
1882 blurb: "pure character, no manners, and no filter at all",
1883 tree: PatchTree {
1884 amp: amp(0.2, 0.45, 0.7, 0.35),
1885 root: Fold {
1886 uid: Uid::NEW,
1887 threshold: 0.3,
1888 mod_depth: 0.6,
1889 modulation: ModNode::Lfo {
1890 uid: Uid::NEW,
1891 wave: Waveform::Square,
1892 rate: 0.6, // 1.2 Hz — audibly switching
1893 },
1894 input: Box::new(Mix {
1895 uid: Uid::NEW,
1896 balance: 0.45,
1897 a: Box::new(vco(Waveform::Saw, 0, 0.5)),
1898 b: Box::new(vco(Waveform::Square, -1, 0.5)),
1899 }),
1900 },
1901 },
1902 },
1903 Preset {
1904 name: "Wrong Number",
1905 category: "weird",
1906 blurb: "a narrow resonant peak, hunting for a pitch",
1907 tree: PatchTree {
1908 amp: amp(0.15, 0.5, 0.6, 0.35),
1909 root: Filter {
1910 uid: Uid::NEW,
1911 kind: FilterKind::SvfBp,
1912 cutoff: 0.45,
1913 resonance: 0.82,
1914 mod_depth: 0.7,
1915 modulation: ModNode::Rand {
1916 uid: Uid::NEW,
1917 rate: 0.8,
1918 glide: 0.0,
1919 }, // ~6 Hz
1920 input: Box::new(vco(Waveform::Triangle, 0, 0.5)),
1921 },
1922 },
1923 },
1924 Preset {
1925 name: "Inside Out",
1926 category: "weird",
1927 // The one place an Env *attack* is long: the highpass corner
1928 // climbs for most of a second after the note starts, so the body
1929 // thins out as the note sustains and the transient is the last
1930 // thing left.
1931 blurb: "the note arrives some time after the transient does",
1932 tree: PatchTree {
1933 amp: amp(0.6, 0.5, 0.7, 0.45),
1934 root: Chorus {
1935 uid: Uid::NEW,
1936 rate: 0.35, // 0.39 Hz — 0.15 was 0.18, slower than any LFO here
1937 depth: 0.7,
1938 mix: 0.6,
1939 mod_depth: 0.0,
1940 modulation: ModNode::None,
1941 input: Box::new(Filter {
1942 uid: Uid::NEW,
1943 kind: FilterKind::SvfHp,
1944 cutoff: 0.8,
1945 resonance: 0.45,
1946 mod_depth: 0.65,
1947 modulation: ModNode::Env {
1948 uid: Uid::NEW,
1949 attack: 0.6,
1950 decay: 0.65,
1951 },
1952 input: Box::new(Mix {
1953 uid: Uid::NEW,
1954 balance: 0.55,
1955 a: Box::new(vco(Waveform::Saw, -1, 0.5)),
1956 b: Box::new(vco(Waveform::Square, 1, 0.5)),
1957 }),
1958 }),
1959 },
1960 },
1961 },
1962 // ---------------------------------------------------------------
1963 // The three below could not have existed before wave 2C, when
1964 // modulation stopped being a flat list of leaves. Each is built on a
1965 // different one of the new productions.
1966 // ---------------------------------------------------------------
1967 Preset {
1968 name: "Ask The Dice",
1969 category: "lead",
1970 // The whole point of the quantizer, and the reason `mod_depth` is
1971 // exactly 1.0 here rather than a taste value: the cable's gain at
1972 // full depth is 0.1, which is precisely what turns the quantizer's
1973 // grid into whole semitones on the pitch offset. Anywhere else on
1974 // the knob the intervals are a stretched tuning — still a melody,
1975 // but not this one.
1976 blurb: "a random melody, quantized to A minor and glided into",
1977 tree: PatchTree {
1978 amp: amp(0.02, 0.3, 0.5, 0.25),
1979 root: Filter {
1980 uid: Uid::NEW,
1981 kind: FilterKind::SvfLp,
1982 cutoff: 0.62,
1983 resonance: 0.4,
1984 mod_depth: 0.35,
1985 modulation: ModNode::Env {
1986 uid: Uid::NEW,
1987 attack: 0.02,
1988 decay: 0.3,
1989 },
1990 input: Box::new(Vco {
1991 uid: Uid::NEW,
1992 wave: Waveform::Saw,
1993 octave: 0,
1994 detune: 0.5,
1995 mod_depth: 1.0,
1996 // The wave's headline term, two processors deep:
1997 // noise sampled and held, snapped to a scale, then
1998 // glided between. The slew is last so it glides
1999 // *between notes* — put it under the quantizer and it
2000 // would only smooth the noise before it was snapped,
2001 // which is inaudible.
2002 modulation: ModNode::Op {
2003 uid: Uid::NEW,
2004 kind: ModOp::Slew,
2005 p0: 0.18, // ≈35 ms up…
2006 p1: 0.3, // …and ≈90 ms down: a fall is a slur
2007 input: Box::new(ModNode::Op {
2008 uid: Uid::NEW,
2009 kind: ModOp::Quantize,
2010 p0: 9.5 / 12.0, // root A
2011 p1: 2.5 / 7.0, // minor
2012 input: Box::new(ModNode::Rand {
2013 uid: Uid::NEW,
2014 rate: 0.62, // ≈1.6 Hz — a note every beat
2015 glide: 0.0, // the slew above owns the glide
2016 }),
2017 }),
2018 },
2019 }),
2020 },
2021 },
2022 },
2023 Preset {
2024 name: "Nine Against Four",
2025 category: "pad",
2026 // Two euclidean patterns of different lengths XORed: the combined
2027 // gate does not repeat until their step counts do, so a pad that
2028 // is nominally static never opens the same way twice inside a
2029 // phrase. This is the `Pair` production's reason for existing.
2030 blurb: "two rhythms disagreeing, opening the filter where they do",
2031 tree: PatchTree {
2032 amp: amp(0.25, 0.5, 0.85, 0.6),
2033 root: Reverb {
2034 uid: Uid::NEW,
2035 size: 0.7,
2036 damp: 0.45,
2037 mix: 0.45,
2038 mod_depth: 0.0,
2039 modulation: ModNode::None,
2040 input: Box::new(Filter {
2041 uid: Uid::NEW,
2042 kind: FilterKind::SvfLp,
2043 cutoff: 0.32,
2044 resonance: 0.5,
2045 mod_depth: 0.75,
2046 modulation: ModNode::Pair {
2047 uid: Uid::NEW,
2048 kind: PairOp::Xor,
2049 a: Box::new(ModNode::Euclid {
2050 uid: Uid::NEW,
2051 rate: 0.62, // ≈120 BPM
2052 steps: 0.36, // 8 steps
2053 pulses: 0.34, // 3 of 8
2054 }),
2055 b: Box::new(ModNode::Euclid {
2056 uid: Uid::NEW,
2057 rate: 0.62, // the same clock…
2058 steps: 0.44, // …over 9 steps, so they drift
2059 pulses: 0.28, // 3 of 9
2060 }),
2061 },
2062 input: Box::new(supersaw(-1, 0.4, 0.55)),
2063 }),
2064 },
2065 },
2066 },
2067 Preset {
2068 name: "Long Way Down",
2069 category: "texture",
2070 // The slew limiter on its own, at the top of its travel: a
2071 // stepped random source becomes a drift with no steps left in it
2072 // at all. `Rand`'s own `glide` cannot reach here — it shares one
2073 // knob between rise and fall and runs on quiver's raw map, where
2074 // everything under a second and a half lives in the bottom
2075 // quarter.
2076 blurb: "a random source with every step sanded off it",
2077 tree: PatchTree {
2078 amp: amp(0.4, 0.6, 0.9, 0.7),
2079 root: Filter {
2080 uid: Uid::NEW,
2081 kind: FilterKind::SvfBp,
2082 cutoff: 0.5,
2083 resonance: 0.6,
2084 mod_depth: 0.85,
2085 modulation: ModNode::Op {
2086 uid: Uid::NEW,
2087 kind: ModOp::Slew,
2088 p0: 0.85, // ≈1.2 s up…
2089 p1: 0.55, // …and ≈0.5 s down, so it falls faster
2090 input: Box::new(ModNode::Rand {
2091 uid: Uid::NEW,
2092 rate: 0.5, // ≈0.55 Hz
2093 glide: 0.0, // hard steps in; the slew makes the ramp
2094 }),
2095 },
2096 input: Box::new(Mix {
2097 uid: Uid::NEW,
2098 balance: 0.4,
2099 a: Box::new(supersaw(0, 0.6, 0.4)),
2100 b: Box::new(AudioNode::Noise {
2101 uid: Uid::NEW,
2102 color: NoiseColor::Pink,
2103 }),
2104 }),
2105 },
2106 },
2107 },
2108 ]
2109}
2110
2111#[cfg(test)]
2112mod tests {
2113 use super::*;
2114 use std::collections::HashSet;
2115
2116 /// `0.01·3000^x` Hz, the LFO/S&H rate map (quiver `oscillators.rs`).
2117 fn mod_hz(cv: f64) -> f64 {
2118 0.01 * 3000f64.powf(cv)
2119 }
2120
2121 /// `0.1·50^x` Hz — the chorus runs its own, much narrower map, which is
2122 /// why it needs its own conversion rather than sharing the LFO's. Missing
2123 /// this is how `Inside Out` ended up with a 0.18 Hz chorus: slower than
2124 /// any LFO in the library, and slower than the floor the library enforces
2125 /// on LFOs, while sitting outside the test that enforces it.
2126 fn chorus_hz(cv: f64) -> f64 {
2127 0.1 * 50f64.powf(cv)
2128 }
2129
2130 fn hz_of(kind: &str, cv: f64) -> f64 {
2131 if kind == "chorus" {
2132 chorus_hz(cv)
2133 } else {
2134 mod_hz(cv)
2135 }
2136 }
2137
2138 /// Walk every modulation source in the library.
2139 fn mod_sources() -> Vec<(&'static str, &'static str, f64)> {
2140 fn walk(
2141 n: &AudioNode,
2142 name: &'static str,
2143 out: &mut Vec<(&'static str, &'static str, f64)>,
2144 ) {
2145 // A free function, not a closure over `out`: the chorus arm has
2146 // to push its own rate *and* note its slot, and a closure holding
2147 // `out` mutably cannot coexist with that.
2148 fn note(
2149 m: &ModNode,
2150 name: &'static str,
2151 out: &mut Vec<(&'static str, &'static str, f64)>,
2152 ) {
2153 match m {
2154 ModNode::Lfo { rate, .. } => out.push((name, "lfo", *rate)),
2155 ModNode::Rand { rate, .. } => out.push((name, "s&h", *rate)),
2156 ModNode::Env { .. } | ModNode::Follow { .. } | ModNode::None => {}
2157 // A shaper's rate is its subterm's, so recurse rather
2158 // than reporting the wrapper.
2159 ModNode::Op { input, .. } => note(input, name, out),
2160 ModNode::Pair { a, b, .. } => {
2161 note(a, name, out);
2162 note(b, name, out);
2163 }
2164 ModNode::Euclid { rate, .. } => out.push((name, "euclid", *rate)),
2165 }
2166 }
2167 match n {
2168 AudioNode::Chorus {
2169 rate,
2170 modulation,
2171 input,
2172 ..
2173 } => {
2174 out.push((name, "chorus", *rate));
2175 note(modulation, name, out);
2176 walk(input, name, out);
2177 }
2178 AudioNode::Filter {
2179 modulation, input, ..
2180 }
2181 | AudioNode::Fold {
2182 modulation, input, ..
2183 }
2184 | AudioNode::Delay {
2185 modulation, input, ..
2186 }
2187 | AudioNode::Reverb {
2188 modulation, input, ..
2189 }
2190 | AudioNode::Distortion {
2191 modulation, input, ..
2192 }
2193 | AudioNode::Bitcrush {
2194 modulation, input, ..
2195 }
2196 | AudioNode::Phaser {
2197 modulation, input, ..
2198 }
2199 | AudioNode::Flanger {
2200 modulation, input, ..
2201 }
2202 | AudioNode::Tremolo {
2203 modulation, input, ..
2204 }
2205 | AudioNode::Vibrato {
2206 modulation, input, ..
2207 }
2208 | AudioNode::Eq {
2209 modulation, input, ..
2210 }
2211 | AudioNode::Granular {
2212 modulation, input, ..
2213 }
2214 | AudioNode::Shift {
2215 modulation, input, ..
2216 } => {
2217 note(modulation, name, out);
2218 walk(input, name, out);
2219 }
2220 // The 2B binaries: a slot *and* two branches, and the control
2221 // branch's own LFOs are as audible as any other — it drives a
2222 // detector, and a detector hears everything the ear does.
2223 AudioNode::Comp {
2224 modulation,
2225 input,
2226 sidechain: other,
2227 ..
2228 }
2229 | AudioNode::Gate {
2230 modulation,
2231 input,
2232 sidechain: other,
2233 ..
2234 }
2235 | AudioNode::Duck {
2236 modulation,
2237 input,
2238 key: other,
2239 ..
2240 }
2241 | AudioNode::Vocoder {
2242 modulation,
2243 carrier: input,
2244 modulator: other,
2245 ..
2246 } => {
2247 note(modulation, name, out);
2248 walk(input, name, out);
2249 walk(other, name, out);
2250 }
2251 // The two oscillators' slots reach *pitch* rather than a
2252 // timbre parameter, but a rate is a rate: an LFO too slow to
2253 // complete a cycle inside a note is as inaudible on pitch as
2254 // it is on a cutoff.
2255 AudioNode::Wavetable { modulation, .. }
2256 | AudioNode::Pluck { modulation, .. }
2257 | AudioNode::Formant { modulation, .. }
2258 | AudioNode::Vco { modulation, .. }
2259 | AudioNode::Supersaw { modulation, .. } => note(modulation, name, out),
2260 AudioNode::Mix { a, b, .. } | AudioNode::RingMod { a, b, .. } => {
2261 walk(a, name, out);
2262 walk(b, name, out);
2263 }
2264 // Neither has a modulation slot or a child to walk.
2265 AudioNode::Noise { .. } | AudioNode::Silence { .. } => {}
2266 }
2267 }
2268 let mut out = Vec::new();
2269 for p in preset_bank() {
2270 walk(&p.tree.root, p.name, &mut out);
2271 }
2272 out
2273 }
2274
2275 /// The regression this library shipped with for its whole life.
2276 ///
2277 /// Every LFO and S&H in the original nine sat between 0.033 Hz and
2278 /// 0.165 Hz — 6 to 30 seconds per cycle — while the audition phrase's
2279 /// longest held note is 1.8 s. Nothing modulated audibly, anywhere, and
2280 /// the warm start taught the model its first eighteen preferences from
2281 /// exactly those patches. The bug was invisible because `rate: 0.15`
2282 /// looks like a slow-ish number rather than a half-minute.
2283 ///
2284 /// **The floor is set by the bug it has to catch**, which is the only
2285 /// honest way to pick one. The four original offenders were 0.0332,
2286 /// 0.0496, 0.1648 and 0.1648 Hz; a floor of 0.15 Hz — the first number
2287 /// that "felt slow" — would have waved two of them straight through,
2288 /// while the module doc two hundred lines up calls 0.165 Hz inaudible. A
2289 /// gate that disagrees with its own file is not a gate. 0.2 Hz clears all
2290 /// four with margin and still leaves room for a genuinely slow pad drift.
2291 #[test]
2292 fn every_modulation_source_is_audible_within_one_note() {
2293 const FLOOR_HZ: f64 = 0.2;
2294 let slow: Vec<String> = mod_sources()
2295 .into_iter()
2296 .filter(|(_, kind, cv)| hz_of(kind, *cv) < FLOOR_HZ)
2297 .map(|(name, kind, cv)| {
2298 let f = hz_of(kind, cv);
2299 format!(
2300 "{name} {kind} rate {cv} = {f:.3} Hz ({:.1} s per cycle)",
2301 1.0 / f
2302 )
2303 })
2304 .collect();
2305 assert!(
2306 slow.is_empty(),
2307 "modulation too slow to hear in a 1.8 s note (floor {FLOOR_HZ} Hz):\n {}",
2308 slow.join("\n ")
2309 );
2310 }
2311
2312 /// Every one of the original four offenders must fail the gate. Without
2313 /// this, the floor is a number someone liked rather than one that catches
2314 /// the bug — which is exactly how the first version of it let half of them
2315 /// through.
2316 #[test]
2317 fn the_floor_catches_every_original_offender() {
2318 for cv in [0.15, 0.20, 0.35, 0.35] {
2319 let f = mod_hz(cv);
2320 assert!(
2321 f < 0.2,
2322 "rate {cv} = {f:.4} Hz would now pass the floor it exists to catch"
2323 );
2324 }
2325 }
2326
2327 /// What the library exercises, tallied in one place so the walker takes
2328 /// one argument instead of eight.
2329 #[derive(Default)]
2330 struct Coverage {
2331 waves: HashSet<String>,
2332 lfo_waves: HashSet<String>,
2333 kinds: HashSet<String>,
2334 colors: HashSet<String>,
2335 tables: HashSet<String>,
2336 drive_modes: HashSet<String>,
2337 octaves: HashSet<i8>,
2338 mods: HashSet<&'static str>,
2339 nodes: HashSet<&'static str>,
2340 }
2341
2342 impl Coverage {
2343 fn note_mod(&mut self, m: &ModNode) {
2344 match m {
2345 ModNode::None => {
2346 self.mods.insert("none");
2347 }
2348 ModNode::Lfo { wave, .. } => {
2349 self.mods.insert("lfo");
2350 self.lfo_waves.insert(format!("{wave:?}"));
2351 }
2352 ModNode::Env { .. } => {
2353 self.mods.insert("env");
2354 }
2355 ModNode::Rand { .. } => {
2356 self.mods.insert("rand");
2357 }
2358 ModNode::Follow { .. } => {
2359 self.mods.insert("follow");
2360 }
2361 ModNode::Euclid { .. } => {
2362 self.mods.insert("euclid");
2363 }
2364 ModNode::Op { kind, input, .. } => {
2365 self.mods.insert(kind.label());
2366 self.note_mod(input);
2367 }
2368 ModNode::Pair { kind, a, b, .. } => {
2369 self.mods.insert(kind.label());
2370 self.note_mod(a);
2371 self.note_mod(b);
2372 }
2373 };
2374 }
2375
2376 fn walk(&mut self, n: &AudioNode) {
2377 match n {
2378 AudioNode::Vco {
2379 wave,
2380 octave,
2381 modulation,
2382 ..
2383 } => {
2384 self.nodes.insert("vco");
2385 self.waves.insert(format!("{wave:?}"));
2386 self.octaves.insert(*octave);
2387 self.note_mod(modulation);
2388 }
2389 AudioNode::Supersaw {
2390 octave, modulation, ..
2391 } => {
2392 self.nodes.insert("supersaw");
2393 self.octaves.insert(*octave);
2394 self.note_mod(modulation);
2395 }
2396 AudioNode::Formant {
2397 octave, modulation, ..
2398 } => {
2399 self.nodes.insert("formant");
2400 self.octaves.insert(*octave);
2401 self.note_mod(modulation);
2402 }
2403 AudioNode::Noise { color, .. } => {
2404 self.nodes.insert("noise");
2405 self.colors.insert(format!("{color:?}"));
2406 }
2407 AudioNode::Silence { .. } => {
2408 self.nodes.insert("silence");
2409 }
2410 AudioNode::Wavetable {
2411 table,
2412 octave,
2413 modulation,
2414 ..
2415 } => {
2416 self.nodes.insert("wavetable");
2417 self.tables.insert(format!("{table:?}"));
2418 self.octaves.insert(*octave);
2419 self.note_mod(modulation);
2420 }
2421 AudioNode::Pluck {
2422 octave, modulation, ..
2423 } => {
2424 self.nodes.insert("pluck");
2425 self.octaves.insert(*octave);
2426 self.note_mod(modulation);
2427 }
2428 AudioNode::Mix { a, b, .. } => {
2429 self.nodes.insert("mix");
2430 self.walk(a);
2431 self.walk(b);
2432 }
2433 AudioNode::RingMod { a, b, .. } => {
2434 self.nodes.insert("ringmod");
2435 self.walk(a);
2436 self.walk(b);
2437 }
2438 AudioNode::Filter {
2439 kind,
2440 modulation,
2441 input,
2442 ..
2443 } => {
2444 self.nodes.insert("filter");
2445 self.kinds.insert(format!("{kind:?}"));
2446 self.note_mod(modulation);
2447 self.walk(input);
2448 }
2449 AudioNode::Fold {
2450 modulation, input, ..
2451 } => {
2452 self.nodes.insert("fold");
2453 self.note_mod(modulation);
2454 self.walk(input);
2455 }
2456 AudioNode::Delay {
2457 modulation, input, ..
2458 } => {
2459 self.nodes.insert("delay");
2460 self.note_mod(modulation);
2461 self.walk(input);
2462 }
2463 AudioNode::Chorus {
2464 modulation, input, ..
2465 } => {
2466 self.nodes.insert("chorus");
2467 self.note_mod(modulation);
2468 self.walk(input);
2469 }
2470 AudioNode::Reverb {
2471 modulation, input, ..
2472 } => {
2473 self.nodes.insert("reverb");
2474 self.note_mod(modulation);
2475 self.walk(input);
2476 }
2477 AudioNode::Distortion {
2478 mode,
2479 modulation,
2480 input,
2481 ..
2482 } => {
2483 self.nodes.insert("distortion");
2484 self.drive_modes.insert(format!("{mode:?}"));
2485 self.note_mod(modulation);
2486 self.walk(input);
2487 }
2488 AudioNode::Bitcrush {
2489 modulation, input, ..
2490 } => {
2491 self.nodes.insert("bitcrush");
2492 self.note_mod(modulation);
2493 self.walk(input);
2494 }
2495 AudioNode::Phaser {
2496 modulation, input, ..
2497 } => {
2498 self.nodes.insert("phaser");
2499 self.note_mod(modulation);
2500 self.walk(input);
2501 }
2502 AudioNode::Flanger {
2503 modulation, input, ..
2504 } => {
2505 self.nodes.insert("flanger");
2506 self.note_mod(modulation);
2507 self.walk(input);
2508 }
2509 AudioNode::Tremolo {
2510 modulation, input, ..
2511 } => {
2512 self.nodes.insert("tremolo");
2513 self.note_mod(modulation);
2514 self.walk(input);
2515 }
2516 AudioNode::Vibrato {
2517 modulation, input, ..
2518 } => {
2519 self.nodes.insert("vibrato");
2520 self.note_mod(modulation);
2521 self.walk(input);
2522 }
2523 AudioNode::Eq {
2524 modulation, input, ..
2525 } => {
2526 self.nodes.insert("eq");
2527 self.note_mod(modulation);
2528 self.walk(input);
2529 }
2530 AudioNode::Granular {
2531 modulation, input, ..
2532 } => {
2533 self.nodes.insert("granular");
2534 self.note_mod(modulation);
2535 self.walk(input);
2536 }
2537 AudioNode::Shift {
2538 modulation, input, ..
2539 } => {
2540 self.nodes.insert("shift");
2541 self.note_mod(modulation);
2542 self.walk(input);
2543 }
2544 AudioNode::Comp {
2545 modulation,
2546 input,
2547 sidechain,
2548 ..
2549 } => {
2550 self.nodes.insert("comp");
2551 self.note_mod(modulation);
2552 self.walk(input);
2553 self.walk(sidechain);
2554 }
2555 AudioNode::Duck {
2556 modulation,
2557 input,
2558 key,
2559 ..
2560 } => {
2561 self.nodes.insert("duck");
2562 self.note_mod(modulation);
2563 self.walk(input);
2564 self.walk(key);
2565 }
2566 AudioNode::Gate {
2567 modulation,
2568 input,
2569 sidechain,
2570 ..
2571 } => {
2572 self.nodes.insert("gate");
2573 self.note_mod(modulation);
2574 self.walk(input);
2575 self.walk(sidechain);
2576 }
2577 AudioNode::Vocoder {
2578 modulation,
2579 carrier,
2580 modulator,
2581 ..
2582 } => {
2583 self.nodes.insert("vocoder");
2584 self.note_mod(modulation);
2585 self.walk(carrier);
2586 self.walk(modulator);
2587 }
2588 }
2589 }
2590 }
2591
2592 /// The preset library doubles as the instrument's documentation and as the
2593 /// taste model's first evidence, so it has to actually exercise the
2594 /// grammar. Before this it used white noise never, octave ±2 never,
2595 /// `Ladder`/`Rand`/`Fold`/`Mix` once each, and never nested a `Mix`.
2596 #[test]
2597 fn the_library_covers_the_grammar() {
2598 let mut cov = Coverage::default();
2599 for p in preset_bank() {
2600 cov.walk(&p.tree.root);
2601 }
2602 let Coverage {
2603 waves,
2604 lfo_waves,
2605 kinds,
2606 colors,
2607 tables,
2608 drive_modes,
2609 octaves,
2610 mods,
2611 nodes,
2612 } = cov;
2613
2614 assert_eq!(waves.len(), 4, "not every oscillator waveform is used");
2615 assert!(
2616 lfo_waves.len() >= 3,
2617 "the LFO is only ever run as {lfo_waves:?} — its waveform is a real timbral choice"
2618 );
2619 assert_eq!(kinds.len(), 4, "not every filter kind is used: {kinds:?}");
2620 assert_eq!(colors.len(), 2, "one of the noise colours is never heard");
2621 assert!(
2622 !tables.is_empty(),
2623 "the wavetable oscillator is never heard"
2624 );
2625 assert!(!drive_modes.is_empty(), "the distortion is never heard");
2626 // Every modulation **source** — the five leaf kinds plus the empty
2627 // slot — has to appear, on the same argument as the node list: a
2628 // source no preset demonstrates is a source nobody discovers.
2629 //
2630 // The eleven **shapers** are held to a weaker bar, and deliberately.
2631 // They are a combinatorial space rather than a list (any of them over
2632 // any source, two deep), so "one preset each" would be eleven near
2633 // duplicates that teach the same lesson; what the library owes is that
2634 // each of the three new *productions* — a leaf generator, a unary
2635 // processor, a binary combiner — is shown at least once, so the shape
2636 // of the sort is discoverable from the bank.
2637 for src in ["none", "lfo", "env", "rand", "follow", "euclid"] {
2638 assert!(mods.contains(src), "no preset uses the {src} modulator");
2639 }
2640 // The shapers *were* held to a weaker bar than the sources, on the
2641 // argument above that they are a combinatorial space rather than a
2642 // list. That bar turned out to be too weak to be worth having: it
2643 // passed with `rectify`, `hold` and five of the six combiners in zero
2644 // presets, which is exactly the "a source nobody demonstrates is a
2645 // source nobody discovers" failure the source rule exists to prevent —
2646 // and the bank's auto-namer has no adjectives for a region of the
2647 // grammar the library never visits.
2648 //
2649 // Each of the ten now needs one preset. They are not near-duplicates
2650 // in practice: `min` and `max` are opposite bargains on the same two
2651 // modulators, `and` and `or` are opposite densities on the same two
2652 // patterns, and `rectify` is the one that gives a modulator a resting
2653 // state. If a future op genuinely has nothing of its own to show, the
2654 // honest move is to cut it from the palette rather than to lower this.
2655 for op in [
2656 "quantize", "slew", "rectify", "hold", "min", "max", "and", "or", "xor", "switch",
2657 ] {
2658 assert!(
2659 mods.contains(op),
2660 "no preset uses the {op} modulation shaper: {mods:?}"
2661 );
2662 }
2663 assert_eq!(nodes.len(), 26, "not every audio node is used: {nodes:?}");
2664 for oct in [-2, -1, 0, 1, 2] {
2665 assert!(octaves.contains(&oct), "octave {oct} is never used");
2666 }
2667 }
2668
2669 /// A patch that contains a wavefolder must actually fold.
2670 ///
2671 /// `Sour Mash` shipped in the first draft of this library with a folder
2672 /// that did nothing at all: sweeping its threshold across the whole range
2673 /// produced *bit-identical* audio. Quiver's `Wavefolder` folds `x / 5.0`,
2674 /// and `Vco`/`Noise` scale their outputs `* 5.0` while `Supersaw` leaves
2675 /// them at unity — so on a supersaw the signal never reaches the fold
2676 /// threshold and the module is a wire. Nothing caught it: the patch
2677 /// compiled, vetted, rendered, and sounded like a plain supersaw with a
2678 /// confident blurb about hard-switching character.
2679 ///
2680 /// So: for every preset with a `Fold` in it, moving the threshold from
2681 /// hard to soft has to move the audio.
2682 #[test]
2683 fn every_wavefolder_actually_folds() {
2684 fn set_fold(n: &AudioNode, t: f64) -> AudioNode {
2685 match n {
2686 // The modulation is deliberately stripped. Sweeping the base
2687 // threshold while an LFO still rides on it makes the two
2688 // renders differ for reasons that have nothing to do with
2689 // folding — which is exactly how the first version of this
2690 // test passed on the very bug it was written to catch.
2691 AudioNode::Fold { input, .. } => AudioNode::Fold {
2692 uid: Uid::NEW,
2693 threshold: t,
2694 mod_depth: 0.0,
2695 modulation: ModNode::None,
2696 input: Box::new(set_fold(input, t)),
2697 },
2698 AudioNode::Filter {
2699 kind,
2700 cutoff,
2701 resonance,
2702 mod_depth,
2703 modulation,
2704 input,
2705 ..
2706 } => AudioNode::Filter {
2707 uid: Uid::NEW,
2708 kind: *kind,
2709 cutoff: *cutoff,
2710 resonance: *resonance,
2711 mod_depth: *mod_depth,
2712 modulation: modulation.clone(),
2713 input: Box::new(set_fold(input, t)),
2714 },
2715 AudioNode::Delay {
2716 time,
2717 feedback,
2718 mix,
2719 mod_depth,
2720 modulation,
2721 input,
2722 ..
2723 } => AudioNode::Delay {
2724 uid: Uid::NEW,
2725 time: *time,
2726 feedback: *feedback,
2727 mix: *mix,
2728 mod_depth: *mod_depth,
2729 modulation: modulation.clone(),
2730 input: Box::new(set_fold(input, t)),
2731 },
2732 AudioNode::Chorus {
2733 rate,
2734 depth,
2735 mix,
2736 mod_depth,
2737 modulation,
2738 input,
2739 ..
2740 } => AudioNode::Chorus {
2741 uid: Uid::NEW,
2742 rate: *rate,
2743 depth: *depth,
2744 mix: *mix,
2745 mod_depth: *mod_depth,
2746 modulation: modulation.clone(),
2747 input: Box::new(set_fold(input, t)),
2748 },
2749 AudioNode::Reverb {
2750 size,
2751 damp,
2752 mix,
2753 mod_depth,
2754 modulation,
2755 input,
2756 ..
2757 } => AudioNode::Reverb {
2758 uid: Uid::NEW,
2759 size: *size,
2760 damp: *damp,
2761 mix: *mix,
2762 mod_depth: *mod_depth,
2763 modulation: modulation.clone(),
2764 input: Box::new(set_fold(input, t)),
2765 },
2766 AudioNode::Distortion {
2767 drive,
2768 tone,
2769 mode,
2770 mod_depth,
2771 modulation,
2772 input,
2773 ..
2774 } => AudioNode::Distortion {
2775 uid: Uid::NEW,
2776 drive: *drive,
2777 tone: *tone,
2778 mode: *mode,
2779 mod_depth: *mod_depth,
2780 modulation: modulation.clone(),
2781 input: Box::new(set_fold(input, t)),
2782 },
2783 AudioNode::Mix { balance, a, b, .. } => AudioNode::Mix {
2784 uid: Uid::NEW,
2785 balance: *balance,
2786 a: Box::new(set_fold(a, t)),
2787 b: Box::new(set_fold(b, t)),
2788 },
2789 other => other.clone(),
2790 }
2791 }
2792 fn has_fold(n: &AudioNode) -> bool {
2793 match n {
2794 AudioNode::Fold { .. } => true,
2795 AudioNode::Filter { input, .. }
2796 | AudioNode::Delay { input, .. }
2797 | AudioNode::Chorus { input, .. }
2798 | AudioNode::Reverb { input, .. }
2799 | AudioNode::Distortion { input, .. }
2800 | AudioNode::Bitcrush { input, .. }
2801 | AudioNode::Phaser { input, .. } => has_fold(input),
2802 AudioNode::Mix { a, b, .. } | AudioNode::RingMod { a, b, .. } => {
2803 has_fold(a) || has_fold(b)
2804 }
2805 _ => false,
2806 }
2807 }
2808
2809 const SR: f64 = 44_100.0;
2810 let render = |tree: &PatchTree| -> f64 {
2811 quiver::rng::seed(0x0F01_D5EE);
2812 let mut v = crate::compile(tree, SR).expect("preset compiles");
2813 v.pitch.set(0.0);
2814 v.gate.set(5.0);
2815 let buf: Vec<(f64, f64)> = (0..44_100).map(|_| v.patch.tick()).collect();
2816 (buf.iter().map(|(l, _)| l * l).sum::<f64>() / buf.len() as f64).sqrt()
2817 };
2818
2819 let mut checked = 0;
2820 for p in preset_bank() {
2821 if !has_fold(&p.tree.root) {
2822 continue;
2823 }
2824 checked += 1;
2825 let hard = PatchTree {
2826 amp: p.tree.amp.clone(),
2827 root: set_fold(&p.tree.root, 0.2),
2828 };
2829 let soft = PatchTree {
2830 amp: p.tree.amp.clone(),
2831 root: set_fold(&p.tree.root, 0.95),
2832 };
2833 let (a, b) = (render(&hard), render(&soft));
2834 let rel = (a - b).abs() / a.max(b).max(1e-12);
2835 assert!(
2836 rel > 0.02,
2837 "{}: folding is inert — threshold 0.2 gives rms {a:.6}, 0.95 gives {b:.6} \
2838 (relative difference {rel:.5}). A `Fold` fed a source that never reaches \
2839 its threshold is a wire.",
2840 p.name
2841 );
2842 }
2843 assert!(checked >= 3, "only {checked} folded presets found");
2844 }
2845
2846 /// Categories are what the browser groups by and what the warm start
2847 /// samples across, so a typo'd one would silently create an eighth family
2848 /// of one — and quietly bias the cold start toward it.
2849 #[test]
2850 fn every_preset_declares_a_known_category_and_copy() {
2851 for p in preset_bank() {
2852 assert!(
2853 CATEGORIES.contains(&p.category),
2854 "{}: unknown category {:?}",
2855 p.name,
2856 p.category
2857 );
2858 assert!(!p.blurb.is_empty(), "{}: no blurb", p.name);
2859 }
2860 for c in CATEGORIES {
2861 let n = preset_bank().iter().filter(|p| p.category == c).count();
2862 assert!(
2863 n >= 3,
2864 "category {c} has only {n} presets — too thin to sample"
2865 );
2866 }
2867 }
2868
2869 /// Names are the handle the user carries back to the bank; two presets
2870 /// with one name is two patches nobody can tell apart.
2871 #[test]
2872 fn preset_names_are_unique() {
2873 let bank = preset_bank();
2874 let unique: HashSet<&str> = bank.iter().map(|p| p.name).collect();
2875 assert_eq!(unique.len(), bank.len(), "duplicate preset name");
2876 }
2877}