Effects
An effect processes a track’s audio after the instrument — an echo, a filter, a reverb. In DAW terms these are insert effects: they sit in the track’s signal chain, each one feeding the next, before the result reaches the master.
The built-in effects live in the std/effects module. Import the ones you need:
use "std/effects" { Delay, Lowpass, Reverb };Here’s a beat with an echo on it:
use "std/instruments" { Sampler, Kit };use "std/effects" { Delay };
let drums = AudioTrack("drums");drums.instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
drums.fx(#[Delay("delay", 0.25, 0.5)]);
PLAY;Delay("delay", 0.25, 0.5) describes a delay effect; fx is what builds it and
sets it as the track’s effect chain. The drums now repeat with a quarter-second echo.
A Constructor Is a Description
Section titled “A Constructor Is a Description”Delay("delay", 0.25, 0.5) does not build a delay. It writes down three things —
the slot name "delay", what kind of effect to build, and the settings to start it
with — and hands you a handle. drums.fx(#[...]) is what builds it. Until that
line runs, nothing is compiled, nothing is loaded, and nothing makes a sound.
That splits a handle’s methods in two:
- Writes are recorded.
param_set,param_set_norm,set_programandload_stateon a handle that isn’t placed yet remember what you asked for and apply it to the instance the track builds. Setting before placing is fine — it seeds the slot. - Reads need an instance.
param,param_get,params,programs,save_state,connect_inputand the GUI methods have nothing to read until the effect is on a track, and the error says so.
let echo = Delay("delay", 0.25, 0.5);echo.param_set("Time", 0.4); // fine — recorded, applied when it is builtecho.params(); // error: "delay" is not on a track yet
drums.fx(#[echo]); // built hereecho.params(); // now reads the live effect.name() is the exception — it’s the one thing a description already knows.
Once placed, the handle stops being a description and becomes a reference to “the
slot called delay on that track”, re-resolved on every call. It keeps working
across re-executes, even when the slot underneath is rebuilt — see
Live Editing.
Plugins have two moments rather than one: the plugin name is resolved on the constructor line, so a typo or a skipped bundle is flagged where you typed it, while the load and activation wait for placement. See Plugins.
Building a Chain
Section titled “Building a Chain”fx takes the chain as an ordered list, and order matters — each effect
processes what the one before it produced. Put a delay before a lowpass, and the
filter smooths the echoes; swap the order and you’d echo an already-filtered
signal:
use "std/instruments" { Sampler, Kit };use "std/effects" { Delay, Lowpass };
let drums = AudioTrack("drums");drums.instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
drums.fx(#[Delay("delay", 0.25, 0.5), Lowpass("lowpass", 800)]); // signal -> delay -> lowpass -> out
PLAY;fx sets the entire chain in one call, so you list every effect in the
array — the array order is the signal order:
use "std/instruments" { Sampler, Kit };use "std/effects" { Highpass, Delay };
let hats = AudioTrack("hats");hats.instrument(Sampler(Kit("CR-78")));hats << [hh*8];
hats.fx(#[Highpass("highpass", 2000), Delay("delay", 0.125, 0.3)]);
PLAY;show(track) prints the full chain in order if you lose track of what’s loaded.
Re-running a Chain
Section titled “Re-running a Chain”Because fx sets the whole chain, the obvious worry is that re-running it throws
away what’s running. It doesn’t. The name you gave each effect is its slot
identity, and fx matches the list you passed against the chain that’s already
there, name by name:
use "std/instruments" { Sampler, Kit };use "std/effects" { Delay, Lowpass };
let drums = AudioTrack("drums");drums.instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
drums.fx(#[Delay("delay", 0.25, 0.5)]);
// re-run with the filter added — the delay is not rebuiltdrums.fx(#[Delay("delay", 0.25, 0.5), Lowpass("lowpass", 800)]);
PLAY;A name that’s already in the chain keeps its live effect — its current parameter values and any modulation running on them survive untouched, and the constructor arguments on that line are ignored. They only ever seed a slot that is being created for the first time. A name that isn’t in the chain yet is built and inserted; a name you removed from the array is taken out; and moving a name reorders the chain around it. So you edit the array and re-evaluate, and the chain becomes what you wrote — while the music keeps playing.
The one thing that does start a slot over is changing what kind of effect it
is. Put a Lowpass("delay", 800) where a Delay("delay", …) used to be and the
name no longer describes the same effect, so the slot is rebuilt from the
arguments you just gave — there are no old parameters left to carry across.
Two more rules fall out of all this:
- Names must be unique within a chain. Two slots called
"delay"on one track is an error, not a guess. Different tracks are free to reuse a name. fx(#[])clears the chain, because an empty list is still a description of what you want.
This is also why a handle keeps working after a re-run: once placed, it refers to “the slot with that name”, not to whatever instance happened to exist when you made it — see A Constructor Is a Description.
The Built-in Effects
Section titled “The Built-in Effects”Delay("delay", time, feedback) is an echo. time is the delay in seconds; feedback
(0–1) is how much of the output loops back in, so higher values mean more, longer
repeats. Delay("delay", 0.25, 0.5) echoes every quarter second at half feedback.
Filters
Section titled “Filters”The filters all take a cutoff frequency in Hz, plus an optional resonance (Q)
as a second argument. Resonance defaults to 0.707 — a flat, neutral response —
and rising values narrow and emphasise the cutoff into a sharper peak.
Lowpass("lowpass", cutoff)keeps lows, cuts highs — warmer, darker.Highpass("highpass", cutoff)keeps highs, cuts lows — thinner, airier.Bandpass("bandpass", cutoff)keeps a band around the cutoff, cutting either side.Notch("notch", cutoff)is the inverse — removes a band, e.g.Notch("notch", 60)to kill mains hum.Allpass("allpass", cutoff, q)passes everything but shifts phase — a phaser building block.Peak("peak", cutoff)is a bell: a boost or emphasis around the cutoff.
use "std/instruments" { Sampler, Kit };use "std/effects" { Lowpass, Highpass };
let drums = AudioTrack("drums");drums.instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
drums.fx(#[ Lowpass("lowpass", 800, 3.0), // resonant lowpass at 800 Hz, Q 3.0 Highpass("highpass", 120), // clear out the sub-rumble]);
PLAY;Reverb
Section titled “Reverb”Two reverbs add space. Reverb("reverb", size, damping) is a room/hall: size (0–1) sets
how large the space feels, damping (0–1) how quickly the highs decay. The
PlateReverb models a lusher, more diffuse plate with its own size, damping,
diffusion, and pre-delay controls.
use "std/instruments" { Sampler, Kit };use "std/effects" { Reverb };
let drums = AudioTrack("drums");drums.instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
drums.fx(#[Reverb("reverb", 0.8, 0.3)]); // big room, gentle damping
PLAY;For wet/dry control — keeping the dry signal and sending only a copy through the reverb — use a send to a dedicated effect track, covered in Routing, Buses & Sends.
Overdrive("overdrive", amount) and Distortion("distortion", amount) add harmonics and grit, from gentle
warmth to hard clipping as amount rises:
use "std/instruments" { Sampler, Kit };use "std/effects" { Overdrive };
let drums = AudioTrack("drums");drums.instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
drums.fx(#[Overdrive("overdrive", 0.5)]);
PLAY;Changing Parameters
Section titled “Changing Parameters”You’ll often want to adjust an effect after creating it. Keep a reference to it,
place it on a track with .fx(), then set parameters by name through that handle
(why). There are two ways, and they do the same
thing for a static value:
use "std/effects" { Delay };
let drums = AudioTrack("drums");let echo = Delay("delay", 0.25, 0.5);drums.fx(#[echo]); // place it — now the handle is live
echo.param_set("Time", 0.4); // method formecho.param("Time") << 0.15; // operator formparam_set returns the effect, so calls chain. The << form is the one to
remember, though, because it accepts more than a fixed number: feed it a moving
signal and the parameter sweeps over time.
echo.param("Feedback") << Sine(0.2).range(0.2, 0.7); // feedback driftsThat’s modulation — the bridge from static effects to living, moving ones — and it’s the subject of the next-but-one chapter.
Next Steps
Section titled “Next Steps”You can load effects, chain them in order, and dial their parameters. Next, wire tracks together, then make those parameters move.
- Routing, Buses & Sends — shared effect
buses, sends, and submixes with
>> - Signals & Automation — drive any parameter with LFOs and envelopes
- Plugins — load external VST3 and CLAP effects alongside the built-ins