This owner contains the six-slot stereo engine previously authored by MultiFX, now shared by MultiFX, Granular, Loudmatch, Crea Synth and Sampler. It composes existing DSP leaves, retains permanent slot states through order edits, and stages clearing behind bounded dry/wet topology transitions. Allocation happens during construction, preparation and explicit family admission, outside the realtime callback; processing is bounded and allocation-free.
Parameters
setParameter(path, value) validates against the shared schema domain and refuses a malformed value without changing the pending configuration; setParameterIndex is the same write by numeric index (0 to 104).
| Path | Accepts |
|---|---|
chain.order | Integer 0 to 719, the lexicographic rank of a six-slot order |
slots.s0..s5.type | Integer 0 (empty) to 81, the append-only algorithm ID range |
slots.s0..s5.enabled | Exactly 0 or 1 |
slots.s0..s5.macro1..macro4, .mix | 0 to 1 |
master.mix | 0 to 1 |
master.output | 0 to 1.5 |
slots.sN.makeupDb | 0 to 24 dB |
slots.sN.detectorHpHz | 20 to 2,000 Hz |
slots.sN.reverbMotionRateHz, reverbMotionDepthPercent | 0.01 to 2 Hz (default 0.27); 0 to 100 % (default 75) |
slots.sN.phaserPhaseDegrees, phaserFeedbackPercent | 0 to 180° (default 0); −75 to 75 % (default 0) |
slots.sN.reverbDiffusionPercent, reverbWidthPercent | 0 to 100 % (default 100); 0 to 150 % (default 100) |
slots.sN.reverbTailLowCutHz, reverbTailHighCutHz | 20 to 2,000 Hz (default 20); 500 to 18,000 Hz (default 18,000) |
Other defaults come from the host schema, which applies them after prepare.
Lifecycle
createState (live), createLiveState, createLazyState or createFixedState → prepare (a rate inside the processing envelope, a maximum block of exactly 128 frames, stereo in and stereo out) → for lazy and fixed chains sizeForType(slot, type) outside realtime → setParameter → process → reset. isIdle reports when a span would be the identity.
Author-selected topology mode
Choose the mode in the authored Processor's createState, rather than adding a face parameter or changing modes during playback:
| Constructor | Routing and storage | Settings while playing |
|---|---|---|
createLiveState() | Live enable/type/order changes, full family storage sized in prepare, dry/clear/fade scheduler | Existing per-effect control smoothing; structural controls commit at dry boundaries |
createLazyState() | The same live lifecycle, with family storage admitted explicitly through sizeForType | Same live controls after admission |
createFixedState() | Fixed enable/type/order, only admitted family storage, no transition configuration snapshots or per-sample scheduler | Existing per-effect smoothing; writes requiring history invalidation return false |
createState() is the default Processor lifecycle entry and selects live mode. The explicit constructors expose topologyMode as MULTI_EFFECT_CHAIN_TOPOLOGY_LIVE or MULTI_EFFECT_CHAIN_TOPOLOGY_FIXED. This is a construction choice, not a saved/automated numeric parameter.
For a fixed embedder, call prepare, then sizeForType(state, slot, type) for each selected family, then push the authored parameter snapshot. The first nonempty process span commits that topology immediately and locks it. Unlike live mode it needs no startup fade; the host owns starting playback. Identical routing replays are legal. Changed enable/type/order and settings classified by slotNeedsClear are refused without changing the desired snapshot or revision. Those controls require live mode, or reset/prepare and reconfiguration while stopped. sizeForType is refused before preparation (avoiding a temporary maximum-rate allocation) and once fixed processing has started.
Slot Mix and master Mix remain continuous controls in fixed mode, including zero: the enabled effects keep ticking so a mix ramp never disconnects them or discards their history. An active zero-mix chain therefore still costs its wet DSP. Only a truly empty, settled chain qualifies for isIdle. Cutoff, compatible delay retuning, drive, makeup, mix and other settings keep the same smoothing owned by their effect controls; fixed mode does not replace those controls with unsmoothed writes. Discrete modes and history-resetting retunes are not converted into continuous parameters merely because the routing is fixed.
MultiFX, Loudmatch, Crea Synth and the sampler's main inserts explicitly select live mode: their controls or preset recall can change the running topology. The sampler's lazily sized buses also retain live mode. An embedded fixed chain or pedal with constant routing can select the fixed constructor; a pedal whose footswitches/presets change enabled effects or routing needs live mode.
Both modes use the same slot implementations, control mappings and f32 mix law. Fixed mode dispatch is selected once per process call and has no per-frame wet fade calculation, scheduler advancement, commit check or clear servicing. Its execution order contains only enabled slots, avoiding inactive-slot scans. Its pending/in-flight configuration buffers are empty, and its unused scheduler buffers alias the committed order/tags. This reduces source-state storage and runtime work; it does not claim that every compiler removes all unreachable live-mode code from the generated binary. Native/code-size specialization must be measured separately. Regression evidence is in ``.
The compiled fixed-chain fixture shows the authoring pattern: a fixed Filter → Tone Shape → Delay chain with only Cutoff, Mix and Output exposed. It is qualification code, not another engine or a production module.
Verification — 2026-10-14
Run the focused source and compiled mode checks from the repository root:
bash pnpm --filter @maruaudio/dsp exec node --import tsx --test test/multi-effect-chain-topology.test.ts
bash pnpm --filter @maruaudio/module-toolchain exec node --import tsx --test
bash pnpm --filter @maruaudio/module-multifx exec node --import tsx --test --test-name-pattern 'Multifx authored source and fresh Maru compiler WASM reactor agree on the qualification tape' test/offline-wasm-parity.test.mjsThe nine mode tests passed: immediate fixed startup, compact order, storage admission, atomic refusal of structural writes, reset, continuous smoothing, zero-mix history retention, idle eligibility, host-rate/block-partition parity, and bounded live switch/rapid-retarget steps. The fixed three-effect fixture rendered 1,536 stereo frames with exact source/WASM/native equality, no nonfinite samples, peak 0.1835811883 and RMS 0.0576748664. Its source closure was `; the fresh WASM was , and the native archive was `.
The live MultiFX qualification tape rendered 12,288 frames with exact source/fresh-WASM equality. Source closure: `. Observed PCM remained `. This change preserves the existing live fade law and timing.
Prepared source typed-array backing stores, deduplicated: fixed one-filter chain 196,520 bytes; identically admitted lazy-live chain 197,876 bytes; fully provisioned live rack 12,213,596 bytes. Most memory savings come from selective family admission, already available to lazy-live users; omitting transition snapshots saves another 1,356 bytes. A source-JS benchmark (median of five, 256,000 frames per trial) measured 38.17 ms fixed and 38.67 ms lazy-live for the same settled filter. Treat this small timing difference as an observation on this machine, not a portable speedup claim. These are not native heap or pedal RAM measurements. The fixed fixture still emitted a 2,923,024-byte WASM and 1,455,424-byte native archive; fixed mode is not effect-code pruning.
MultiFX's 163, Loudmatch's 26 and Crea Synth's 58 source tests passed, including the connected-rack and embedded-chain checks. DSP and all four consumer typechecks passed. Sampler passed 82 of 84 tests, including compiled audio parity and effect-bus/idle transitions; two existing fixtures fail because authoring/make-render-fixture.mjs reads the removed kernel.paramLanes config and tests/processor-lowering.test.ts pins a closure predating newer DSP leaves. The module-toolchain typecheck remains red only in existing octave-bands-lowering.test.ts:157 and shoebox-ism-lowering.test.ts:156 Uint8Array<ArrayBufferLike>/BufferSource assignments. These baseline issues were not changed or bypassed. No new catalog publication, native plug-in shell, browser playback or physical pedal qualification was performed by this change.
The focused public @maruaudio/dsp/processors/multi-effect-chain surface exports its Processor lifecycle and state. constants owns numeric type IDs, parameter indices, rate/block bounds and per-slot advanced limits. controls owns numeric four-macro profile ranges. The host supplies schema defaults and calls static setParameter paths matching chain.order, slots.s0..s5, master.mix and master.output. The fixed four-macro mapping is part of this shared profile, not a dynamically extensible effect registry. Any future different mapping needs an explicit contract rather than an unnoticed changed numeric meaning.
Filters, dynamics, distortion, delays, modulation, reverbs and stereo/spectral algorithms remain in their focused leaves. This directory owns only their shared profile composition, parameter mapping, per-slot retained state and scheduler integration. It has no SDK metadata, UI, application imports, devices, filesystem or runtime provider choice. Shared schema, family metadata and presentation belong to @maruaudio/audio-sdk/effects/multi-effect-chain/*; product identities, composed schemas and presets remain module-owned. The composition review distinguishes embedded DSP reuse, independent rack modules and custom faces. Tests include the shared DSP and consuming module suites; generated target qualification still uses each product.
Realtime cost outside the active path (). Only slots in a snapshot's active mask are configured; an inactive slot becomes active only through a topology commit that invalidates it, which configures it unsmoothed, so a host default push of typed but disabled slots runs no family coefficient setup. prepare runs the realtime path once over a private silent span and restores the prepared control state, so lazily compiled code (JS bytecode, lazily compiled WASM) is compiled there rather than in the host's first callback. isIdle(state) reports when a span would be the identity input + 0 and change no state (no slot active, nothing in transition, pending or clearing, master output exactly 1, writes synchronized); an embedder such as the sampler skips the call then and writes input + 0 itself. Multi-FX, Loudmatch and Crea Synth may use the same query.
Lazily sized chains (). createLazyState() builds the same chain with every sizable member — delay lines, reverb networks, chorus, flanger, vibrato and the filter banks — pointing at one shared placeholder per chain, about 150 kB against ~12 MB (JS) for a prepared createState() chain at 48 kHz. sizeForType(state, slot, type) allocates the family type needs in that slot; it allocates, so an embedder calls it only from a non-realtime entry (the sampler's bus-table loadResource) before it writes the type. Until then setParameter refuses a type write to an unsized family, so a placeholder is never configured, ticked or cleared. A sized lazy chain renders bit-identically to a full one (`). parameterValueIsValid is the schema-domain check alone, for an embedder that validates a stored configuration whole before writing any of it. MultiFX, Loudmatch and Crea Synth keep createState()`.
Sample rate (). The chain is host-native: prepare accepts any rate in the processing envelope (isProcessingRateSupported), and every leaf derives its coefficients and lengths from Hz, ms or seconds at that rate. Storage is sized in prepare, not in createState, under the documented exception in the sample-rate architecture: a slot's delay lines and reverb networks are about 2 MB at 48 kHz, so sizing every chain for MAX_PROCESSING_RATE would cost about 48 MB per instance. createState() therefore builds the chain on shared placeholders with every family marked sized; the first prepare (and any later prepare at a different rate) rebuilds each slot for the host rate with rebuildSlotForSampleRate, which allocates on the control thread. A prepare at the same rate only resets. A lazy chain rebuilds just the families it has sized, and sizeForType sizes at the prepared rate (or for MAX_PROCESSING_RATE before the first prepare, rebuilt by it). 48 kHz output is bit-identical to the former fixed 48 kHz chain. The serial stack's topology fade stays 256 frames at every rate.
When to use it
Embedding the MultiFX six-slot effect rack in an instrument or effect, with live reordering and switching (live or lazy mode) or a constant routing chosen by the author (fixed mode).
Limits
- Six slots, stereo in and out, 128-frame blocks. A topology change crosses a 256-frame dry transition through
@maru/processors-serial-effect-stack. - A fixed chain refuses structural writes once processing has started; a lazy chain refuses a type whose family is not yet sized.
sizeForTypeand aprepareat a new rate allocate, so call them outside the realtime callback.