memlnaut-nisps/playground/faust/mod-pool.lib
monkey-w1n5t0n afff406d92 feat(playground): add Modular audio mode with shared mod pool
New "Modular" engine in a-immersive with three hot-swappable Faust
sub-engines (subtractive/additive/fm) sharing a common modulation pool:
16 ADSR slots + 32 LFO slots (single-knob sine->tri->square->saw
wavemorph) routed through a 48-source x 10-destination matrix per
engine. Per-connection scalar amounts in [-1, 1], summed at each
destination. Default MLP output count is 512 (32 mod-source params +
480 matrix cells); model reinits on sub-engine swap, count change, or
engine-param exposure toggle.

Faust layer:
- mod-pool.lib: shared ADSR/LFO/source-bus library
- gen-modular-dsp.py: byte-reproducible generator (source of truth)
- modular-subtractive: faithful Minimoog (3 osc, ladder filter, no envs)
- modular-additive: 64-partial, spectral shape + formants, no envs/LFOs
- modular-fm: 4-op matrix + self-feedback, no envs
- All three share d08=amp, d09=pan conventions
- MODULAR_DESTINATIONS.md: authoritative destination table

JS layer:
- ModularEngine: self-contained SynthEngine with getState/setState,
  setSubEngine, setModSourceCount, setExposeEngineParam
- modular-ui: drawer with sub-engine toggle, ADSR/LFO count steppers,
  per-slot enable switches, matrix grid editor (tap-cycle, long-press
  precise, right-click menu, negative amounts), preset overlay
- modular-presets: 6 named presets (Slow pad, Plucky bass, Crystal,
  DX bell, Morphing drone + default)
- a-app.js: Modular mode registered, paramMeta:change -> resizeMLP,
  modular DSP state persisted under modularDspState, window.__nisps
  debug hooks for programmatic control

Tests: tests/e2e/modular-mode.spec.js (11 Playwright tests, all passing
including DSP state survives reload, sub-engine swap keeps paramCount,
preset apply verification).

Also fixes a pre-existing build.sh bug where the -e flag caused faust
to overwrite .wasm outputs with expanded DSP source text, leaving
additive/fm-matrix/eoc-* committed as invalid WebAssembly. Rebuilt all
affected engines with the corrected script. Added an early-message
buffer to faust-worklet-processor.js so setParam calls arriving before
wasm instantiation are queued rather than dropped (needed when the user
configures modular state before clicking Start Audio).
2026-04-11 07:34:41 +02:00

435 lines
24 KiB
Text

// mod-pool.lib — Shared modulation pool (ADSRs + LFOs) for the "Modular" mode.
//
// Provides 16 ADSR slots + 32 LFO slots that can be routed through a
// per-engine modulation matrix to any parameter in the host .dsp.
// Engines import this via:
//
// mp = library("mod-pool.lib");
// sources = mp.sources(gate); // 48-wide parallel bus
// mod_x = sum(s, 48, sources : ba.selector(s, 48) * amount(s));
//
// PUBLIC API
// ----------
//
// mp.N_ADSR integer — 16
// mp.N_LFO integer — 32
// mp.N_SOURCES integer — 48 (= N_ADSR + N_LFO)
//
// mp.adsrs(gate) bus — 16-wide parallel bus, each element [0,1]
// mp.lfos bus — 32-wide parallel bus, each element [-1,+1]
// mp.sources(gate) bus — 48-wide parallel bus (adsrs(gate), lfos)
//
// PARAMETER NAMESPACE CONVENTION
// ------------------------------
// All mod-pool sliders live under groups prefixed with "MM_" so that the
// paramMeta parser can segregate them from engine sound params:
//
// MM_ADSR/NN_adsrNN_{attack,decay,sustain,release,enable} (16 slots)
// MM_LFO /NN_lfoNN_{rate,morph,enable} (32 slots)
//
// Engine-side matrix sliders (declared in the engine .dsp, not in this file)
// should follow:
//
// MM_Matrix/sNN_dNN_<destname> (48 * n_dst)
//
// where sNN = source index (0..47; 0..15 = ADSRs, 16..47 = LFOs),
// dNN = destination index, and <destname> = short destination label.
//
// MUTING
// ------
// Each slot has an "enable" hslider (0 or 1). The slot's output is multiplied
// by the enable value so a muted slot contributes 0 at the matrix input. This
// is cheap but does NOT let Faust eliminate the underlying env/LFO compute.
// Compile-time dead-code elimination of unused slots is a future concern.
//
// LFO WAVEMORPH
// -------------
// A single "morph" slider (0..1) crossfades through four shapes:
//
// [0.000, 0.333) : sine -> triangle
// [0.333, 0.666) : triangle -> square
// [0.666, 1.000] : square -> saw
//
// Implemented via nested ba.if so the chosen pair is a linear crossfade.
// The output is always in [-1,+1].
//
// ---------------------------------------------------------------------------
import("stdfaust.lib");
// Counts (engine code can reference mp.N_SOURCES etc.)
N_ADSR = 16;
N_LFO = 32;
N_SOURCES = 48;
// ---------------------------------------------------------------------------
// Per-slot hslider helpers. Unique label per slot index.
// Slot indices are 1-based in labels (adsr01..adsr16, lfo01..lfo32) to keep
// alphabetical ordering stable in the Faust JSON output.
// ---------------------------------------------------------------------------
// ADSR param factories — declared per-slot because Faust slider labels must
// be string literals known at compile time. One function per slot index.
adsr01_a = hslider("MM_ADSR/00_adsr01_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr01_d = hslider("MM_ADSR/00_adsr01_decay", 0.200, 0.001, 10.0, 0.001);
adsr01_s = hslider("MM_ADSR/00_adsr01_sustain", 0.700, 0.0, 1.0, 0.001);
adsr01_r = hslider("MM_ADSR/00_adsr01_release", 0.300, 0.01, 10.0, 0.001);
adsr01_e = hslider("MM_ADSR/00_adsr01_enable", 1.0, 0.0, 1.0, 1.0);
adsr02_a = hslider("MM_ADSR/01_adsr02_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr02_d = hslider("MM_ADSR/01_adsr02_decay", 0.200, 0.001, 10.0, 0.001);
adsr02_s = hslider("MM_ADSR/01_adsr02_sustain", 0.700, 0.0, 1.0, 0.001);
adsr02_r = hslider("MM_ADSR/01_adsr02_release", 0.300, 0.01, 10.0, 0.001);
adsr02_e = hslider("MM_ADSR/01_adsr02_enable", 0.0, 0.0, 1.0, 1.0);
adsr03_a = hslider("MM_ADSR/02_adsr03_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr03_d = hslider("MM_ADSR/02_adsr03_decay", 0.200, 0.001, 10.0, 0.001);
adsr03_s = hslider("MM_ADSR/02_adsr03_sustain", 0.700, 0.0, 1.0, 0.001);
adsr03_r = hslider("MM_ADSR/02_adsr03_release", 0.300, 0.01, 10.0, 0.001);
adsr03_e = hslider("MM_ADSR/02_adsr03_enable", 0.0, 0.0, 1.0, 1.0);
adsr04_a = hslider("MM_ADSR/03_adsr04_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr04_d = hslider("MM_ADSR/03_adsr04_decay", 0.200, 0.001, 10.0, 0.001);
adsr04_s = hslider("MM_ADSR/03_adsr04_sustain", 0.700, 0.0, 1.0, 0.001);
adsr04_r = hslider("MM_ADSR/03_adsr04_release", 0.300, 0.01, 10.0, 0.001);
adsr04_e = hslider("MM_ADSR/03_adsr04_enable", 0.0, 0.0, 1.0, 1.0);
adsr05_a = hslider("MM_ADSR/04_adsr05_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr05_d = hslider("MM_ADSR/04_adsr05_decay", 0.200, 0.001, 10.0, 0.001);
adsr05_s = hslider("MM_ADSR/04_adsr05_sustain", 0.700, 0.0, 1.0, 0.001);
adsr05_r = hslider("MM_ADSR/04_adsr05_release", 0.300, 0.01, 10.0, 0.001);
adsr05_e = hslider("MM_ADSR/04_adsr05_enable", 0.0, 0.0, 1.0, 1.0);
adsr06_a = hslider("MM_ADSR/05_adsr06_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr06_d = hslider("MM_ADSR/05_adsr06_decay", 0.200, 0.001, 10.0, 0.001);
adsr06_s = hslider("MM_ADSR/05_adsr06_sustain", 0.700, 0.0, 1.0, 0.001);
adsr06_r = hslider("MM_ADSR/05_adsr06_release", 0.300, 0.01, 10.0, 0.001);
adsr06_e = hslider("MM_ADSR/05_adsr06_enable", 0.0, 0.0, 1.0, 1.0);
adsr07_a = hslider("MM_ADSR/06_adsr07_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr07_d = hslider("MM_ADSR/06_adsr07_decay", 0.200, 0.001, 10.0, 0.001);
adsr07_s = hslider("MM_ADSR/06_adsr07_sustain", 0.700, 0.0, 1.0, 0.001);
adsr07_r = hslider("MM_ADSR/06_adsr07_release", 0.300, 0.01, 10.0, 0.001);
adsr07_e = hslider("MM_ADSR/06_adsr07_enable", 0.0, 0.0, 1.0, 1.0);
adsr08_a = hslider("MM_ADSR/07_adsr08_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr08_d = hslider("MM_ADSR/07_adsr08_decay", 0.200, 0.001, 10.0, 0.001);
adsr08_s = hslider("MM_ADSR/07_adsr08_sustain", 0.700, 0.0, 1.0, 0.001);
adsr08_r = hslider("MM_ADSR/07_adsr08_release", 0.300, 0.01, 10.0, 0.001);
adsr08_e = hslider("MM_ADSR/07_adsr08_enable", 0.0, 0.0, 1.0, 1.0);
adsr09_a = hslider("MM_ADSR/08_adsr09_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr09_d = hslider("MM_ADSR/08_adsr09_decay", 0.200, 0.001, 10.0, 0.001);
adsr09_s = hslider("MM_ADSR/08_adsr09_sustain", 0.700, 0.0, 1.0, 0.001);
adsr09_r = hslider("MM_ADSR/08_adsr09_release", 0.300, 0.01, 10.0, 0.001);
adsr09_e = hslider("MM_ADSR/08_adsr09_enable", 0.0, 0.0, 1.0, 1.0);
adsr10_a = hslider("MM_ADSR/09_adsr10_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr10_d = hslider("MM_ADSR/09_adsr10_decay", 0.200, 0.001, 10.0, 0.001);
adsr10_s = hslider("MM_ADSR/09_adsr10_sustain", 0.700, 0.0, 1.0, 0.001);
adsr10_r = hslider("MM_ADSR/09_adsr10_release", 0.300, 0.01, 10.0, 0.001);
adsr10_e = hslider("MM_ADSR/09_adsr10_enable", 0.0, 0.0, 1.0, 1.0);
adsr11_a = hslider("MM_ADSR/10_adsr11_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr11_d = hslider("MM_ADSR/10_adsr11_decay", 0.200, 0.001, 10.0, 0.001);
adsr11_s = hslider("MM_ADSR/10_adsr11_sustain", 0.700, 0.0, 1.0, 0.001);
adsr11_r = hslider("MM_ADSR/10_adsr11_release", 0.300, 0.01, 10.0, 0.001);
adsr11_e = hslider("MM_ADSR/10_adsr11_enable", 0.0, 0.0, 1.0, 1.0);
adsr12_a = hslider("MM_ADSR/11_adsr12_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr12_d = hslider("MM_ADSR/11_adsr12_decay", 0.200, 0.001, 10.0, 0.001);
adsr12_s = hslider("MM_ADSR/11_adsr12_sustain", 0.700, 0.0, 1.0, 0.001);
adsr12_r = hslider("MM_ADSR/11_adsr12_release", 0.300, 0.01, 10.0, 0.001);
adsr12_e = hslider("MM_ADSR/11_adsr12_enable", 0.0, 0.0, 1.0, 1.0);
adsr13_a = hslider("MM_ADSR/12_adsr13_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr13_d = hslider("MM_ADSR/12_adsr13_decay", 0.200, 0.001, 10.0, 0.001);
adsr13_s = hslider("MM_ADSR/12_adsr13_sustain", 0.700, 0.0, 1.0, 0.001);
adsr13_r = hslider("MM_ADSR/12_adsr13_release", 0.300, 0.01, 10.0, 0.001);
adsr13_e = hslider("MM_ADSR/12_adsr13_enable", 0.0, 0.0, 1.0, 1.0);
adsr14_a = hslider("MM_ADSR/13_adsr14_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr14_d = hslider("MM_ADSR/13_adsr14_decay", 0.200, 0.001, 10.0, 0.001);
adsr14_s = hslider("MM_ADSR/13_adsr14_sustain", 0.700, 0.0, 1.0, 0.001);
adsr14_r = hslider("MM_ADSR/13_adsr14_release", 0.300, 0.01, 10.0, 0.001);
adsr14_e = hslider("MM_ADSR/13_adsr14_enable", 0.0, 0.0, 1.0, 1.0);
adsr15_a = hslider("MM_ADSR/14_adsr15_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr15_d = hslider("MM_ADSR/14_adsr15_decay", 0.200, 0.001, 10.0, 0.001);
adsr15_s = hslider("MM_ADSR/14_adsr15_sustain", 0.700, 0.0, 1.0, 0.001);
adsr15_r = hslider("MM_ADSR/14_adsr15_release", 0.300, 0.01, 10.0, 0.001);
adsr15_e = hslider("MM_ADSR/14_adsr15_enable", 0.0, 0.0, 1.0, 1.0);
adsr16_a = hslider("MM_ADSR/15_adsr16_attack[scale:log]", 0.010, 0.001, 5.0, 0.001);
adsr16_d = hslider("MM_ADSR/15_adsr16_decay", 0.200, 0.001, 10.0, 0.001);
adsr16_s = hslider("MM_ADSR/15_adsr16_sustain", 0.700, 0.0, 1.0, 0.001);
adsr16_r = hslider("MM_ADSR/15_adsr16_release", 0.300, 0.01, 10.0, 0.001);
adsr16_e = hslider("MM_ADSR/15_adsr16_enable", 0.0, 0.0, 1.0, 1.0);
// Single-slot ADSR expression. The enable multiplier zeros muted slots.
adsr_slot(a, d, s, r, e, gate) = en.adsr(a, d, s, r, gate) : *(e);
// 16-wide parallel bus of ADSR outputs. Takes the global gate as an argument
// so engines can bind their own gate signal (noteOn/noteOff).
adsrs(gate) =
adsr_slot(adsr01_a, adsr01_d, adsr01_s, adsr01_r, adsr01_e, gate),
adsr_slot(adsr02_a, adsr02_d, adsr02_s, adsr02_r, adsr02_e, gate),
adsr_slot(adsr03_a, adsr03_d, adsr03_s, adsr03_r, adsr03_e, gate),
adsr_slot(adsr04_a, adsr04_d, adsr04_s, adsr04_r, adsr04_e, gate),
adsr_slot(adsr05_a, adsr05_d, adsr05_s, adsr05_r, adsr05_e, gate),
adsr_slot(adsr06_a, adsr06_d, adsr06_s, adsr06_r, adsr06_e, gate),
adsr_slot(adsr07_a, adsr07_d, adsr07_s, adsr07_r, adsr07_e, gate),
adsr_slot(adsr08_a, adsr08_d, adsr08_s, adsr08_r, adsr08_e, gate),
adsr_slot(adsr09_a, adsr09_d, adsr09_s, adsr09_r, adsr09_e, gate),
adsr_slot(adsr10_a, adsr10_d, adsr10_s, adsr10_r, adsr10_e, gate),
adsr_slot(adsr11_a, adsr11_d, adsr11_s, adsr11_r, adsr11_e, gate),
adsr_slot(adsr12_a, adsr12_d, adsr12_s, adsr12_r, adsr12_e, gate),
adsr_slot(adsr13_a, adsr13_d, adsr13_s, adsr13_r, adsr13_e, gate),
adsr_slot(adsr14_a, adsr14_d, adsr14_s, adsr14_r, adsr14_e, gate),
adsr_slot(adsr15_a, adsr15_d, adsr15_s, adsr15_r, adsr15_e, gate),
adsr_slot(adsr16_a, adsr16_d, adsr16_s, adsr16_r, adsr16_e, gate);
// ---------------------------------------------------------------------------
// LFO slots (32)
// ---------------------------------------------------------------------------
lfo01_r = hslider("MM_LFO/00_lfo01_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo01_m = hslider("MM_LFO/00_lfo01_morph", 0.0, 0.0, 1.0, 0.001);
lfo01_e = hslider("MM_LFO/00_lfo01_enable", 0.0, 0.0, 1.0, 1.0);
lfo02_r = hslider("MM_LFO/01_lfo02_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo02_m = hslider("MM_LFO/01_lfo02_morph", 0.0, 0.0, 1.0, 0.001);
lfo02_e = hslider("MM_LFO/01_lfo02_enable", 0.0, 0.0, 1.0, 1.0);
lfo03_r = hslider("MM_LFO/02_lfo03_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo03_m = hslider("MM_LFO/02_lfo03_morph", 0.0, 0.0, 1.0, 0.001);
lfo03_e = hslider("MM_LFO/02_lfo03_enable", 0.0, 0.0, 1.0, 1.0);
lfo04_r = hslider("MM_LFO/03_lfo04_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo04_m = hslider("MM_LFO/03_lfo04_morph", 0.0, 0.0, 1.0, 0.001);
lfo04_e = hslider("MM_LFO/03_lfo04_enable", 0.0, 0.0, 1.0, 1.0);
lfo05_r = hslider("MM_LFO/04_lfo05_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo05_m = hslider("MM_LFO/04_lfo05_morph", 0.0, 0.0, 1.0, 0.001);
lfo05_e = hslider("MM_LFO/04_lfo05_enable", 0.0, 0.0, 1.0, 1.0);
lfo06_r = hslider("MM_LFO/05_lfo06_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo06_m = hslider("MM_LFO/05_lfo06_morph", 0.0, 0.0, 1.0, 0.001);
lfo06_e = hslider("MM_LFO/05_lfo06_enable", 0.0, 0.0, 1.0, 1.0);
lfo07_r = hslider("MM_LFO/06_lfo07_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo07_m = hslider("MM_LFO/06_lfo07_morph", 0.0, 0.0, 1.0, 0.001);
lfo07_e = hslider("MM_LFO/06_lfo07_enable", 0.0, 0.0, 1.0, 1.0);
lfo08_r = hslider("MM_LFO/07_lfo08_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo08_m = hslider("MM_LFO/07_lfo08_morph", 0.0, 0.0, 1.0, 0.001);
lfo08_e = hslider("MM_LFO/07_lfo08_enable", 0.0, 0.0, 1.0, 1.0);
lfo09_r = hslider("MM_LFO/08_lfo09_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo09_m = hslider("MM_LFO/08_lfo09_morph", 0.0, 0.0, 1.0, 0.001);
lfo09_e = hslider("MM_LFO/08_lfo09_enable", 0.0, 0.0, 1.0, 1.0);
lfo10_r = hslider("MM_LFO/09_lfo10_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo10_m = hslider("MM_LFO/09_lfo10_morph", 0.0, 0.0, 1.0, 0.001);
lfo10_e = hslider("MM_LFO/09_lfo10_enable", 0.0, 0.0, 1.0, 1.0);
lfo11_r = hslider("MM_LFO/10_lfo11_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo11_m = hslider("MM_LFO/10_lfo11_morph", 0.0, 0.0, 1.0, 0.001);
lfo11_e = hslider("MM_LFO/10_lfo11_enable", 0.0, 0.0, 1.0, 1.0);
lfo12_r = hslider("MM_LFO/11_lfo12_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo12_m = hslider("MM_LFO/11_lfo12_morph", 0.0, 0.0, 1.0, 0.001);
lfo12_e = hslider("MM_LFO/11_lfo12_enable", 0.0, 0.0, 1.0, 1.0);
lfo13_r = hslider("MM_LFO/12_lfo13_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo13_m = hslider("MM_LFO/12_lfo13_morph", 0.0, 0.0, 1.0, 0.001);
lfo13_e = hslider("MM_LFO/12_lfo13_enable", 0.0, 0.0, 1.0, 1.0);
lfo14_r = hslider("MM_LFO/13_lfo14_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo14_m = hslider("MM_LFO/13_lfo14_morph", 0.0, 0.0, 1.0, 0.001);
lfo14_e = hslider("MM_LFO/13_lfo14_enable", 0.0, 0.0, 1.0, 1.0);
lfo15_r = hslider("MM_LFO/14_lfo15_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo15_m = hslider("MM_LFO/14_lfo15_morph", 0.0, 0.0, 1.0, 0.001);
lfo15_e = hslider("MM_LFO/14_lfo15_enable", 0.0, 0.0, 1.0, 1.0);
lfo16_r = hslider("MM_LFO/15_lfo16_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo16_m = hslider("MM_LFO/15_lfo16_morph", 0.0, 0.0, 1.0, 0.001);
lfo16_e = hslider("MM_LFO/15_lfo16_enable", 0.0, 0.0, 1.0, 1.0);
lfo17_r = hslider("MM_LFO/16_lfo17_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo17_m = hslider("MM_LFO/16_lfo17_morph", 0.0, 0.0, 1.0, 0.001);
lfo17_e = hslider("MM_LFO/16_lfo17_enable", 0.0, 0.0, 1.0, 1.0);
lfo18_r = hslider("MM_LFO/17_lfo18_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo18_m = hslider("MM_LFO/17_lfo18_morph", 0.0, 0.0, 1.0, 0.001);
lfo18_e = hslider("MM_LFO/17_lfo18_enable", 0.0, 0.0, 1.0, 1.0);
lfo19_r = hslider("MM_LFO/18_lfo19_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo19_m = hslider("MM_LFO/18_lfo19_morph", 0.0, 0.0, 1.0, 0.001);
lfo19_e = hslider("MM_LFO/18_lfo19_enable", 0.0, 0.0, 1.0, 1.0);
lfo20_r = hslider("MM_LFO/19_lfo20_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo20_m = hslider("MM_LFO/19_lfo20_morph", 0.0, 0.0, 1.0, 0.001);
lfo20_e = hslider("MM_LFO/19_lfo20_enable", 0.0, 0.0, 1.0, 1.0);
lfo21_r = hslider("MM_LFO/20_lfo21_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo21_m = hslider("MM_LFO/20_lfo21_morph", 0.0, 0.0, 1.0, 0.001);
lfo21_e = hslider("MM_LFO/20_lfo21_enable", 0.0, 0.0, 1.0, 1.0);
lfo22_r = hslider("MM_LFO/21_lfo22_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo22_m = hslider("MM_LFO/21_lfo22_morph", 0.0, 0.0, 1.0, 0.001);
lfo22_e = hslider("MM_LFO/21_lfo22_enable", 0.0, 0.0, 1.0, 1.0);
lfo23_r = hslider("MM_LFO/22_lfo23_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo23_m = hslider("MM_LFO/22_lfo23_morph", 0.0, 0.0, 1.0, 0.001);
lfo23_e = hslider("MM_LFO/22_lfo23_enable", 0.0, 0.0, 1.0, 1.0);
lfo24_r = hslider("MM_LFO/23_lfo24_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo24_m = hslider("MM_LFO/23_lfo24_morph", 0.0, 0.0, 1.0, 0.001);
lfo24_e = hslider("MM_LFO/23_lfo24_enable", 0.0, 0.0, 1.0, 1.0);
lfo25_r = hslider("MM_LFO/24_lfo25_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo25_m = hslider("MM_LFO/24_lfo25_morph", 0.0, 0.0, 1.0, 0.001);
lfo25_e = hslider("MM_LFO/24_lfo25_enable", 0.0, 0.0, 1.0, 1.0);
lfo26_r = hslider("MM_LFO/25_lfo26_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo26_m = hslider("MM_LFO/25_lfo26_morph", 0.0, 0.0, 1.0, 0.001);
lfo26_e = hslider("MM_LFO/25_lfo26_enable", 0.0, 0.0, 1.0, 1.0);
lfo27_r = hslider("MM_LFO/26_lfo27_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo27_m = hslider("MM_LFO/26_lfo27_morph", 0.0, 0.0, 1.0, 0.001);
lfo27_e = hslider("MM_LFO/26_lfo27_enable", 0.0, 0.0, 1.0, 1.0);
lfo28_r = hslider("MM_LFO/27_lfo28_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo28_m = hslider("MM_LFO/27_lfo28_morph", 0.0, 0.0, 1.0, 0.001);
lfo28_e = hslider("MM_LFO/27_lfo28_enable", 0.0, 0.0, 1.0, 1.0);
lfo29_r = hslider("MM_LFO/28_lfo29_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo29_m = hslider("MM_LFO/28_lfo29_morph", 0.0, 0.0, 1.0, 0.001);
lfo29_e = hslider("MM_LFO/28_lfo29_enable", 0.0, 0.0, 1.0, 1.0);
lfo30_r = hslider("MM_LFO/29_lfo30_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo30_m = hslider("MM_LFO/29_lfo30_morph", 0.0, 0.0, 1.0, 0.001);
lfo30_e = hslider("MM_LFO/29_lfo30_enable", 0.0, 0.0, 1.0, 1.0);
lfo31_r = hslider("MM_LFO/30_lfo31_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo31_m = hslider("MM_LFO/30_lfo31_morph", 0.0, 0.0, 1.0, 0.001);
lfo31_e = hslider("MM_LFO/30_lfo31_enable", 0.0, 0.0, 1.0, 1.0);
lfo32_r = hslider("MM_LFO/31_lfo32_rate[scale:log]", 1.0, 0.01, 20.0, 0.001);
lfo32_m = hslider("MM_LFO/31_lfo32_morph", 0.0, 0.0, 1.0, 0.001);
lfo32_e = hslider("MM_LFO/31_lfo32_enable", 0.0, 0.0, 1.0, 1.0);
// Wavemorph — 0..1 crossfades sine -> triangle -> square -> saw.
// Uses nested ba.if. Each shape is already in [-1,+1].
wavemorph(rate, morph) =
ba.if(morph < 0.333,
(1.0 - morph*3.0) * os.osc(rate) + (morph*3.0) * os.lf_triangle(rate),
ba.if(morph < 0.666,
(1.0 - (morph-0.333)*3.0) * os.lf_triangle(rate) + ((morph-0.333)*3.0) * os.lf_squarewave(rate),
(1.0 - (morph-0.666)*3.0) * os.lf_squarewave(rate) + ((morph-0.666)*3.0) * os.lf_saw(rate)));
lfo_slot(rate, morph, en_flag) = wavemorph(rate, morph) : *(en_flag);
lfos =
lfo_slot(lfo01_r, lfo01_m, lfo01_e),
lfo_slot(lfo02_r, lfo02_m, lfo02_e),
lfo_slot(lfo03_r, lfo03_m, lfo03_e),
lfo_slot(lfo04_r, lfo04_m, lfo04_e),
lfo_slot(lfo05_r, lfo05_m, lfo05_e),
lfo_slot(lfo06_r, lfo06_m, lfo06_e),
lfo_slot(lfo07_r, lfo07_m, lfo07_e),
lfo_slot(lfo08_r, lfo08_m, lfo08_e),
lfo_slot(lfo09_r, lfo09_m, lfo09_e),
lfo_slot(lfo10_r, lfo10_m, lfo10_e),
lfo_slot(lfo11_r, lfo11_m, lfo11_e),
lfo_slot(lfo12_r, lfo12_m, lfo12_e),
lfo_slot(lfo13_r, lfo13_m, lfo13_e),
lfo_slot(lfo14_r, lfo14_m, lfo14_e),
lfo_slot(lfo15_r, lfo15_m, lfo15_e),
lfo_slot(lfo16_r, lfo16_m, lfo16_e),
lfo_slot(lfo17_r, lfo17_m, lfo17_e),
lfo_slot(lfo18_r, lfo18_m, lfo18_e),
lfo_slot(lfo19_r, lfo19_m, lfo19_e),
lfo_slot(lfo20_r, lfo20_m, lfo20_e),
lfo_slot(lfo21_r, lfo21_m, lfo21_e),
lfo_slot(lfo22_r, lfo22_m, lfo22_e),
lfo_slot(lfo23_r, lfo23_m, lfo23_e),
lfo_slot(lfo24_r, lfo24_m, lfo24_e),
lfo_slot(lfo25_r, lfo25_m, lfo25_e),
lfo_slot(lfo26_r, lfo26_m, lfo26_e),
lfo_slot(lfo27_r, lfo27_m, lfo27_e),
lfo_slot(lfo28_r, lfo28_m, lfo28_e),
lfo_slot(lfo29_r, lfo29_m, lfo29_e),
lfo_slot(lfo30_r, lfo30_m, lfo30_e),
lfo_slot(lfo31_r, lfo31_m, lfo31_e),
lfo_slot(lfo32_r, lfo32_m, lfo32_e);
// ---------------------------------------------------------------------------
// sources(gate) — concatenated 48-wide mod-source bus.
// Order: adsr01..adsr16, lfo01..lfo32
// ---------------------------------------------------------------------------
sources(gate) = adsrs(gate), lfos;
// ---------------------------------------------------------------------------
// Individual named source functions — 48 total, in canonical order.
// Engines can reference these directly in matrix sums, since each hslider
// inside a matrix amount must have a unique compile-time label.
//
// Canonical source ordering (source index 0..47):
// 0..15 : adsr01..adsr16
// 16..47 : lfo01..lfo32
// ---------------------------------------------------------------------------
src00(gate) = adsr_slot(adsr01_a, adsr01_d, adsr01_s, adsr01_r, adsr01_e, gate);
src01(gate) = adsr_slot(adsr02_a, adsr02_d, adsr02_s, adsr02_r, adsr02_e, gate);
src02(gate) = adsr_slot(adsr03_a, adsr03_d, adsr03_s, adsr03_r, adsr03_e, gate);
src03(gate) = adsr_slot(adsr04_a, adsr04_d, adsr04_s, adsr04_r, adsr04_e, gate);
src04(gate) = adsr_slot(adsr05_a, adsr05_d, adsr05_s, adsr05_r, adsr05_e, gate);
src05(gate) = adsr_slot(adsr06_a, adsr06_d, adsr06_s, adsr06_r, adsr06_e, gate);
src06(gate) = adsr_slot(adsr07_a, adsr07_d, adsr07_s, adsr07_r, adsr07_e, gate);
src07(gate) = adsr_slot(adsr08_a, adsr08_d, adsr08_s, adsr08_r, adsr08_e, gate);
src08(gate) = adsr_slot(adsr09_a, adsr09_d, adsr09_s, adsr09_r, adsr09_e, gate);
src09(gate) = adsr_slot(adsr10_a, adsr10_d, adsr10_s, adsr10_r, adsr10_e, gate);
src10(gate) = adsr_slot(adsr11_a, adsr11_d, adsr11_s, adsr11_r, adsr11_e, gate);
src11(gate) = adsr_slot(adsr12_a, adsr12_d, adsr12_s, adsr12_r, adsr12_e, gate);
src12(gate) = adsr_slot(adsr13_a, adsr13_d, adsr13_s, adsr13_r, adsr13_e, gate);
src13(gate) = adsr_slot(adsr14_a, adsr14_d, adsr14_s, adsr14_r, adsr14_e, gate);
src14(gate) = adsr_slot(adsr15_a, adsr15_d, adsr15_s, adsr15_r, adsr15_e, gate);
src15(gate) = adsr_slot(adsr16_a, adsr16_d, adsr16_s, adsr16_r, adsr16_e, gate);
src16(gate) = lfo_slot(lfo01_r, lfo01_m, lfo01_e);
src17(gate) = lfo_slot(lfo02_r, lfo02_m, lfo02_e);
src18(gate) = lfo_slot(lfo03_r, lfo03_m, lfo03_e);
src19(gate) = lfo_slot(lfo04_r, lfo04_m, lfo04_e);
src20(gate) = lfo_slot(lfo05_r, lfo05_m, lfo05_e);
src21(gate) = lfo_slot(lfo06_r, lfo06_m, lfo06_e);
src22(gate) = lfo_slot(lfo07_r, lfo07_m, lfo07_e);
src23(gate) = lfo_slot(lfo08_r, lfo08_m, lfo08_e);
src24(gate) = lfo_slot(lfo09_r, lfo09_m, lfo09_e);
src25(gate) = lfo_slot(lfo10_r, lfo10_m, lfo10_e);
src26(gate) = lfo_slot(lfo11_r, lfo11_m, lfo11_e);
src27(gate) = lfo_slot(lfo12_r, lfo12_m, lfo12_e);
src28(gate) = lfo_slot(lfo13_r, lfo13_m, lfo13_e);
src29(gate) = lfo_slot(lfo14_r, lfo14_m, lfo14_e);
src30(gate) = lfo_slot(lfo15_r, lfo15_m, lfo15_e);
src31(gate) = lfo_slot(lfo16_r, lfo16_m, lfo16_e);
src32(gate) = lfo_slot(lfo17_r, lfo17_m, lfo17_e);
src33(gate) = lfo_slot(lfo18_r, lfo18_m, lfo18_e);
src34(gate) = lfo_slot(lfo19_r, lfo19_m, lfo19_e);
src35(gate) = lfo_slot(lfo20_r, lfo20_m, lfo20_e);
src36(gate) = lfo_slot(lfo21_r, lfo21_m, lfo21_e);
src37(gate) = lfo_slot(lfo22_r, lfo22_m, lfo22_e);
src38(gate) = lfo_slot(lfo23_r, lfo23_m, lfo23_e);
src39(gate) = lfo_slot(lfo24_r, lfo24_m, lfo24_e);
src40(gate) = lfo_slot(lfo25_r, lfo25_m, lfo25_e);
src41(gate) = lfo_slot(lfo26_r, lfo26_m, lfo26_e);
src42(gate) = lfo_slot(lfo27_r, lfo27_m, lfo27_e);
src43(gate) = lfo_slot(lfo28_r, lfo28_m, lfo28_e);
src44(gate) = lfo_slot(lfo29_r, lfo29_m, lfo29_e);
src45(gate) = lfo_slot(lfo30_r, lfo30_m, lfo30_e);
src46(gate) = lfo_slot(lfo31_r, lfo31_m, lfo31_e);
src47(gate) = lfo_slot(lfo32_r, lfo32_m, lfo32_e);