Skip to content

Effects

Effects are pure data. An fx.* builder returns an EffectInstance ({ effectId, params }); the runtime maps that to the actual audio nodes. Times are in beats so chains stay tempo-relative, and wet is 0..1 everywhere (0 = dry only).

import { fx } from "@barline/core";

Effects appear in three places: a track’s static effects chain, a bus’s chain, and a per-play .through() route.

BUILTIN_EFFECT_IDS lists every built-in. Defaults shown:

BuilderKey params (defaults)
fx.distortiondrive 0.4, wet 1
fx.bitcrusherbits 4, wet 1
fx.filterfrequency 2000, type "lowpass" | "highpass" | "bandpass", q 1
fx.delaytime 0.75 (beats), feedback 0.35, wet 0.3
fx.pingpongtime 0.75 (beats), feedback 0.35, wet 0.3
fx.reverbdecay 1.5 (seconds), wet 0.3
fx.chorusfrequency 1.5, depth 0.5, wet 0.4
fx.compressorthreshold -18, ratio 4, attack 0.005, release 0.1
fx.limiterthreshold -1
fx.eq3low 0, mid 0, high 0 (dB)
fx.widenerwidth 0.7
fx.saturatordrive 0.5, character 0, wet 1 — AudioWorklet, 2× oversampled
fx.maximizerthreshold -1, lookahead 3 (ms), release 80 (ms) — AudioWorklet
fx.laddercutoff 1200, resonance 0.4, drive 1 — FAUST Moog VCF
fx.tapedrive 0.5, warmth 0.4, wet 1 — FAUST tape colour
fx.phaserrate 0.5, depth 0.7, feedback 0.25, wet 1 — FAUST 4-stage
fx.spacedecay 2.5 (seconds), wet 0.3 — FAUST algorithmic reverb, deterministic offline
fx.halldecay 3 (seconds), wet 0.25 — FAUST algorithmic reverb, deterministic offline
fx.platedecay 0.5 (tank feedback), wet 0.25 — FAUST plate reverb, deterministic offline
fx.convolverdecay 2 (seconds), wet 0.3 — Web Audio convolution, synthesized IR, deterministic offline
fx.diodecutoff 800, resonance 0.5, drive 1 — 303 diode-ladder filter
fx.gluethreshold -18, ratio 2, attack 0.01, release 0.2, makeup 0 — bus/glue compressor
fx.tilttilt 0 (−1 weight … +1 air) — single-knob tilt EQ
fx.sidechainsource (required), amount 0.7, release 0.15

The saturator/maximizer (hand-written AudioWorklets) and ladder/tape/phaser (FAUST-compiled) gracefully fall back to a plain Tone.js node, with a one-time warning, when their worklet/wasm artifacts aren’t available — playback never breaks.

Declared once in song.track({ effects: [...] }), applied in series, instrument → effects → mixer:

const kick = song.track({
name: "kick",
output: { kind: "synth", synth: { id: "kick" } },
effects: [
fx.distortion({ drive: 0.35, wet: 0.6 }), // hard-techno kicks are not clean
fx.eq3({ low: 2, mid: -1, high: 1 }),
fx.limiter({ threshold: -2 }),
],
});

A bus is a shared space — instead of a reverb per track, tracks tap their post-fader signal into a bus, and the bus runs one chain into the master. Declare buses with song.bus(name, { effects, volume }), and route into them with each track’s sends: { busName: level } (a linear 0..1 send level).

song.bus("space", {
effects: [fx.reverb({ decay: 6, wet: 1 }), fx.eq3({ low: -10 })],
volume: -6,
});
song.bus("echo", {
effects: [
fx.pingpong({ time: 0.75, feedback: 0.45, wet: 1 }), // dotted-8th, in beats
fx.filter({ frequency: 6500, type: "lowpass", q: 0.7 }),
],
volume: -8,
});
const clap = song.track({
name: "clap",
output: { kind: "synth", synth: { id: "clap" } },
sends: { space: 0.4, echo: 0.1 }, // big room, slight repeat
});

A return should be all-wet (wet: 1) — the dry path already went straight to the master. The bus strips show up in the mixer.

fx.sidechain({ source, amount, release }) ducks this chain’s gain in time with another track’s signal. The runtime envelope-follows the source track’s pre-fader signal and ducks by 1 - amount × envelope. Source binding is lazy, so declaration order doesn’t matter; a missing source warns once and passes through dry.

const rumble = song.track({
name: "rumble",
output: { kind: "synth", synth: { id: "bass" } },
effects: [
fx.filter({ frequency: 700, type: "lowpass", q: 1.2 }),
fx.sidechain({ source: "kick", amount: 0.8, release: 0.12 }), // the pump
fx.compressor({ threshold: -20, ratio: 5 }),
],
});

play().through(...effects) gives that one assignment a dedicated instrument instance routed through its own effect chain, so the effects color only that pattern’s events while track automation, the static chain, the fader, and the sends still apply. Other play() calls on the same track keep the shared instrument. Identical effect lists reuse a cached chain across bars (tails survive), and it renders correctly in offline WAV export too.

song.section("drop1", bars(16), () => {
stab
.play(stabsA)
.gain(0.5)
.through(fx.delay({ time: 1.5, feedback: 0.3, wet: 0.25 })); // delay colors only the stabs
});
song.section("breakdown", bars(16), () => {
stab
.play(hymn)
.gain(0.7)
.through(fx.reverb({ decay: 8, wet: 0.5 })); // a wash that only lives here
});

song.setMasterEffects([...]) sets the master-bus chain (read-only in the Devices panel — the master chain is code-owned by design). If a song never calls it, the runtime installs a tuned default (glue compressor → tilt EQ → maximizer). song.setMasterEffects([]) opts out of the default explicitly.