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.
The built-in effects
Section titled “The built-in effects”BUILTIN_EFFECT_IDS lists every built-in. Defaults shown:
| Builder | Key params (defaults) |
|---|---|
fx.distortion | drive 0.4, wet 1 |
fx.bitcrusher | bits 4, wet 1 |
fx.filter | frequency 2000, type "lowpass" | "highpass" | "bandpass", q 1 |
fx.delay | time 0.75 (beats), feedback 0.35, wet 0.3 |
fx.pingpong | time 0.75 (beats), feedback 0.35, wet 0.3 |
fx.reverb | decay 1.5 (seconds), wet 0.3 |
fx.chorus | frequency 1.5, depth 0.5, wet 0.4 |
fx.compressor | threshold -18, ratio 4, attack 0.005, release 0.1 |
fx.limiter | threshold -1 |
fx.eq3 | low 0, mid 0, high 0 (dB) |
fx.widener | width 0.7 |
fx.saturator | drive 0.5, character 0, wet 1 — AudioWorklet, 2× oversampled |
fx.maximizer | threshold -1, lookahead 3 (ms), release 80 (ms) — AudioWorklet |
fx.ladder | cutoff 1200, resonance 0.4, drive 1 — FAUST Moog VCF |
fx.tape | drive 0.5, warmth 0.4, wet 1 — FAUST tape colour |
fx.phaser | rate 0.5, depth 0.7, feedback 0.25, wet 1 — FAUST 4-stage |
fx.space | decay 2.5 (seconds), wet 0.3 — FAUST algorithmic reverb, deterministic offline |
fx.hall | decay 3 (seconds), wet 0.25 — FAUST algorithmic reverb, deterministic offline |
fx.plate | decay 0.5 (tank feedback), wet 0.25 — FAUST plate reverb, deterministic offline |
fx.convolver | decay 2 (seconds), wet 0.3 — Web Audio convolution, synthesized IR, deterministic offline |
fx.diode | cutoff 800, resonance 0.5, drive 1 — 303 diode-ladder filter |
fx.glue | threshold -18, ratio 2, attack 0.01, release 0.2, makeup 0 — bus/glue compressor |
fx.tilt | tilt 0 (−1 weight … +1 air) — single-knob tilt EQ |
fx.sidechain | source (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.
A track’s static chain
Section titled “A track’s static chain”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 }), ],});Send / return buses
Section titled “Send / return buses”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.
Sidechain — the pump
Section titled “Sidechain — the pump”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 }), ],});Per-play routing — .through()
Section titled “Per-play routing — .through()”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});The master chain
Section titled “The master chain”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.