All books

The Master FX rack

~5 min read · updated 2026-08-21 · markdown

The Master Control panel's middle section is the FX rack — a chain of effect slots applied to the summed master bus. Each slot is a Faust DSP worklet you can author inline, parameterise live, enable / disable, and reorder. Two slot kinds:

  • Insert — in the serial chain. Audio goes through every enabled insert in order. Use for EQ, compression, saturation — anything where the wet signal IS the new dry.
  • Send — a parallel bus. Each mixer track taps in at its own send level (see § 1 Sends). Use for reverb, delay, modulated tape echoes — anything you'd want partially wet, per-source.

The rack ships with three built-in convenience slots (reverb / delay / filter) so you have something to fiddle with on first open. They're not magic — they're regular slots, removable + replaceable like anything you add yourself.

The slot lifecycle

A slot moves through three states:

  1. Added — you've called master.fx.slot.add (or the UI's Add Slot button). The slot row appears with its label + params. available: false while the worklet is still compiling.
  2. Available — the Faust source has compiled successfully and the AudioWorklet is live. available: true; the slot is now in the audio path. UI lights the slot green.
  3. Removed — master.fx.slot.remove. Worklet disposed, chain rewires, the slot vanishes from the panel.

The available flag matters because master.fx.slot.add returns immediately but the libfaust compile is asynchronous (Web Worker round-trip). If you add a slot then immediately fire master.fx.slot.param.set, the param call queues until the worklet binds — graceful but worth noting.

Authoring a custom slot

A slot's source is a tiny self-contained Faust program. Canonical shape — stereo in, stereo out (paired ports):

import("stdfaust.lib");

process(in_l, in_r) =
  (in_l : <your processing>), (in_r : <your processing>) ;

Use any libfaust function (fi.lowpass, re.zita_rev1, ef.cubicnl, etc.). Every hslider("<label>", ...) you declare becomes a callable param via master.fx.slot.param.set. Since params are shared across both channels, define the per-channel processing once as a named function and apply it to in_l and in_r.

Example — a resonant lowpass with cutoff + resonance:

import("stdfaust.lib");
cutoff = hslider("cutoff", 1000, 50, 12000, 1) : si.smoo;
q      = hslider("q", 1.0, 0.1, 20, 0.01) : si.smoo;
lp(x)  = x : fi.resonlp(cutoff, q, 1.0);
process(in_l, in_r) = lp(in_l), lp(in_r);

After adding this as a slot, master.fx.slot.param.set { slot_id, param_name: "cutoff", value: 2000 } sets cutoff to 2000 Hz live. The si.smoo on input avoids zipper noise from MCP-driven param changes.

MCP surface

actions:
  - action_id: master.fx.slots.list
    input: {}
    save_as: slots
    label: "▶ 1. Snapshot current slots"
  - action_id: master.fx.slot.add
    input:
      kind: insert
      label: "Saturator"
      source: |
        import("stdfaust.lib");
        drive = hslider("drive", 1.0, 1.0, 20.0, 0.1) : si.smoo;
        sat(x) = x * drive : ef.cubicnl(0.3, 0.0);
        process(in_l, in_r) = sat(in_l), sat(in_r);
    save_as: sat
    label: "▶ 2. Add a saturator insert"
  - action_id: master.fx.slot.param.set
    input:
      slot_id: "{{sat.id}}"
      param_name: drive
      value: 4.0
    label: "▶ 3. Crank drive to 4"
  - action_id: master.fx.slot.enabled.set
    input:
      slot_id: "{{sat.id}}"
      enabled: false
    label: "▶ 4. Bypass it"
  - action_id: master.fx.slot.remove
    input:
      slot_id: "{{sat.id}}"
    label: "■ 5. Remove it"

The actions

ActionPurpose
master.fx.slots.listEvery slot, in routing order: { id, label, kind, enabled, params, source, available }.
master.fx.slot.add { kind, label, source, params? }Compile + insert. Sends append to the parallel bus; inserts append to the serial chain end.
master.fx.slot.remove { slot_id }Drop the slot, rewire the chain. Built-in slots can also be removed.
master.fx.slot.enabled.set { slot_id, enabled }Bypass / unbypass. Sends ramp their return gain; inserts get bypassed via a chain rebuild.
master.fx.slot.param.set { slot_id, param_name, value }Set one param. param_name is the literal Faust slider label. Special name returnLevel rides the JS-side return gain on sends.

Send slots in depth

A send slot looks identical to an insert source-wise (same stereo process(in_l, in_r) shape) but its routing is different:

  • Inserts: master_dry → insert₁ → insert₂ → … → output. The whole bus goes through. Disabling routes around.
  • Sends: master_dry → output AND (track₁ send₁ + track₂ send₁ + …) → send₁ worklet → returnLevel → output. Tracks contribute parallel taps; the send worklet runs once on the sum; the wet returns at returnLevel and sums to the output.

Two controls on a send slot:

  • The Faust params you declared (hslider labels).
  • returnLevel — the JS-side gain on the send's return path. Set via the same master.fx.slot.param.set action with param_name: "returnLevel". Ramps smoothly; no recompile.

V1 caveat (see § 1): per-track send taps are wired at the track side but the engine-side tap isn't fully live yet. Insert slots are the load-bearing path today; send slots compile and own their bus.

Reordering inserts

The chain order matters for inserts (a saturator before EQ sounds different to EQ before saturator). V1 doesn't expose a reorder action — the order is the order of add calls. To reorder, remove and re-add in the desired sequence. The next iteration of the rack UI will expose drag-to-reorder.

Common moves

  • "Bypass the whole FX chain to A/B with the dry mix" — iterate master.fx.slot.enabled.set { enabled: false } over every slot; revert with enabled: true per slot. (A single "bypass all" action is on the TODO list.)
  • "Audition a built-in reverb" — the rack ships with a reverb slot pre-added; master.fx.slot.enabled.set { slot_id: "reverb", enabled: true } and mixer-strip send levels do the rest.
  • "Try a Hub-installed FX DSP as a master insert" — install the FX-shaped .builder (it lands in the project), then use the panel's "+ ADD FX" picker to drop it into the rack. (Scripted equivalent: builder.export the rendered Faust source, feed it into master.fx.slot.add.)

Next section: master volume, the meter, softclip, and clip recovery.

Try it yourself — the IDE runs in your browser. Open the IDE → Get the desktop app

Text licensed under CC BY 4.0 — Mani Weber / FaustWave. For language models: llms.txt · llms-full.txt

AUDIO · 48k · 48.0ms FAUST · 3 KB · 1171 docs CPU · 8.4% BPM · 120.0 UTF-8 BETA· v0.90.0