Skip to content

Share DSP Code

DSP code is shared in three ways. All three are inlined when the block is compiled, so a call costs nothing at audio rate. Pick the smallest one that does the job:

You need…Write
a helper for one blocka fn inside that block
the same pure function in several blocksa dsp fn at the top of the file
a component with its own state, in several placesa dsp object at the top of the file

Any fn in a block besides process or render is a helper. It can read and write the block’s param, state and buffer names, but only sees its own arguments — pass it what it needs:

use "std/instruments" { Sampler, Kit };
dsp effect SoftClipper {
param drive: 2.0 range(1, 10);
param mix: 0.5 range(0, 1);
fn soft_clip(x) -> out {
return x / (1.0 + __native("abs", x));
}
fn blend(dry, wet) -> out {
return dry * (1.0 - mix) + wet * mix;
}
fn process(left, right) -> (out_l, out_r) {
return (blend(left, soft_clip(left * drive)), blend(right, soft_clip(right * drive)));
}
}
let drums = AudioTrack("drums");
drums.set_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.set_effects(#[SoftClipper("softclipper")]);
PLAY;

Move the helper out of the block and prefix it with dsp. Every dsp block in the file can call it. A dsp fn has no param, state or buffer, and returns one value — call it once per channel:

dsp fn softclip(x) -> out {
return x / (1.0 + __native("abs", x));
}
dsp effect Saturate {
param drive: 2.0 range(1, 10);
fn process(left, right) -> (out_l, out_r) {
return (softclip(left * drive), softclip(right * drive));
}
}
dsp instrument ClippedSine {
voice state phase: 0.0;
fn render(note, velocity, gate) -> (out_l, out_r) {
phase = __native("fract", phase + __native("mtof", note) * INV_SR);
let sample = softclip(__native("sin", phase * TWOPI) * 4.0) * velocity * gate;
return (sample, sample);
}
}

When the shared code must remember something — a filter’s last sample, a delay line — write a dsp object. Give it state and buffer declarations and methods with any names. Each let x = OnePole() in a block is a separate copy with its own state, so make one per channel:

use "std/instruments" { Sampler, Kit };
dsp object OnePole {
state prev: 0.0;
fn lp(input, coeff) -> out {
prev = prev + coeff * (input - prev);
return prev;
}
}
dsp effect Smooth {
param amount: 0.1 range(0.001, 1);
fn process(left, right) -> (out_l, out_r) {
let lp_l = OnePole();
let lp_r = OnePole();
return (lp_l.lp(left, amount), lp_r.lp(right, amount));
}
}
let drums = AudioTrack("drums");
drums.set_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.set_effects(#[Smooth("smooth")]);
PLAY;

An object has no param: its block’s params reach it as arguments, like amount above.

Put the shared code in a module, and import it into another module with a selective use:

filters.non
dsp fn softclip(x) -> out {
return x / (1.0 + __native("abs", x));
}
dsp object OnePole {
state prev: 0.0;
fn lp(input, coeff) -> out {
prev = prev + coeff * (input - prev);
return prev;
}
}
warm.non
use "./filters" { softclip, OnePole };
dsp effect Warm {
param tone: 0.2 range(0, 1);
fn process(input) -> output {
let lp = OnePole();
return lp.lp(softclip(input), tone);
}
}

Only a module’s dsp blocks can call imported DSP code — not the blocks in the file you run or in a selection you execute. To use softclip from your piece, import Warm, the block that calls it:

use "./warm" { Warm };
let bass = AudioTrack("bass");
bass.set_effects(#[Warm("warm")]);

Mark a definition private to keep it out of the module’s exports.

  • DSP Blocks — the rules for helpers, dsp fn and dsp object
  • Modules — use, selective imports and private