Skip to content

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.

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_program and load_state on 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_input and 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 built
echo.params(); // error: "delay" is not on a track yet
drums.fx(#[echo]); // built here
echo.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.

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.

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 rebuilt
drums.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.

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.

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;

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;

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 form
echo.param("Time") << 0.15; // operator form

param_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 drifts

That’s modulation — the bridge from static effects to living, moving ones — and it’s the subject of the next-but-one chapter.

You can load effects, chain them in order, and dial their parameters. Next, wire tracks together, then make those parameters move.