Skip to content

DSP Functions & Objects

Resonon provides three tiers of DSP code reuse: top-level dsp fn for pure helpers, local fn inside DSP blocks for block-scoped logic, and dsp object for stateful reusable components. All three work inside dsp effect, dsp instrument, and dsp signal blocks and are inlined at compile time with zero overhead.

// Module-level pure helper
dsp fn softclip(x) -> out {
return x / (1.0 + abs(x));
}
// Reusable stateful component
dsp object OnePole {
state prev: 0.0;
fn filter(input, cutoff) -> out {
prev = prev + cutoff * (input - prev);
return prev;
}
}
dsp effect SoftDrive {
param drive: 2.0 range(1, 10);
param tone: 0.3 range(0, 1);
// Local helper — scoped to this effect
fn apply_drive(x) -> out {
return softclip(x * drive);
}
fn process(left, right) -> (out_l, out_r) {
let lp = OnePole();
let driven_l = apply_drive(left);
let driven_r = apply_drive(right);
return (lp.filter(driven_l, tone), lp.filter(driven_r, tone));
}
}
let fx = SoftDrive();
drums.load_effect(fx);

Top-level DSP helper functions. Define them at module scope — they are hoisted and available throughout the file.

dsp fn name(params) -> return_name {
// body
}

For multiple return values, use a tuple:

dsp fn name(params) -> (out_a, out_b) {
// body
}
  • Module-level: Defined at the top level of a file, hoisted like regular fn
  • Pure: Cannot declare state, buffer, or param — inputs in, outputs out
  • Inlined: The compiler inlines calls at each call site, so there is no function-call overhead
  • Importable: Visible to other files via use — see Cross-Module Import
dsp fn clamp(x, lo, hi) -> out {
if x < lo {
return lo;
}
if x > hi {
return hi;
}
return x;
}
dsp effect SafeGain {
param level: 1.0 range(0, 4);
fn process(input) -> output {
return clamp(input * level, -1.0, 1.0);
}
}

When a helper computes two values, return a named tuple:

dsp fn stereo_pan(mono, pan) -> (left, right) {
return (mono * (1.0 - pan), mono * pan);
}
dsp effect Panner {
param pan: 0.5 range(0, 1);
fn process(input) -> (out_l, out_r) {
return stereo_pan(input, pan);
}
}

Any fn inside a dsp effect, dsp instrument, dsp signal, or dsp object that is not the entry-point function (process or render) is a local helper.

dsp effect Name {
fn helper_name(params) -> return_name {
// body — can read and write parent state
}
fn process(input) -> output {
return helper_name(input);
}
}

No special keyword is needed — just define additional fn declarations alongside process or render.

  • Block-scoped: Visible only inside their parent block
  • Access parent state: Can read and write the parent’s state, buffer, and param variables
  • Shadow external helpers: A local fn with the same name as an external dsp fn takes precedence
  • Not exported: Cannot be imported or called from outside the block
  • Inlined: Zero overhead, just like dsp fn
dsp effect Compressor {
param threshold: 0.5 range(0, 1);
param ratio: 4.0 range(1, 20);
state env: 0.0;
fn detect(x) -> out {
let level = abs(x);
env = env + (level - env) * 0.001;
return env;
}
fn compress(x, level) -> out {
if level > threshold {
let excess = level - threshold;
let reduced = threshold + excess / ratio;
return x * (reduced / level);
}
return x;
}
fn process(left, right) -> (out_l, out_r) {
let level = detect(left + right);
return (compress(left, level), compress(right, level));
}
}
dsp instrument PluckSynth {
param decay: 0.995 range(0.9, 0.9999);
voice state phase: 0.0;
voice state env: 1.0;
fn oscillator(note) -> out {
let freq = mtof(note);
phase = fract(phase + freq * INV_SR);
return sin(phase * TWOPI);
}
fn render(note, velocity, gate) -> (out_l, out_r) {
let osc = oscillator(note);
env = env * decay;
if gate < 1.0 { env = env * 0.99; }
let sample = osc * env * velocity;
return (sample, sample);
}
}

Stateful, reusable DSP components with their own state and buffer declarations. Define them at module scope and instantiate inside effects or instruments.

dsp object Name {
state var_name: initial_value;
buffer buf_name(size);
fn method_name(params) -> return_name {
// body — can read and write this object's state and buffers
}
}
  • state declares per-instance mutable variables, initialized to the given value
  • buffer declares fixed-size arrays, initialized to zero
  • Methods can be named anything — there is no required entry-point name

Create instances with call syntax inside an effect or instrument body:

dsp effect MyEffect {
fn process(input) -> output {
let filt = OnePole(); // creates an instance
return filt.filter(input, 0.01);
}
}

Each let binding creates an independent instance with its own copy of all state and buffer variables.

dsp object OnePole {
state prev: 0.0;
fn lp(input, cutoff) -> out {
prev = prev + cutoff * (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));
}
}

Each instance maintains independent state:

dsp object Accumulator {
state sum: 0.0;
fn add(x) -> out {
sum = sum + x;
return sum;
}
}
dsp effect DualAccumulator {
fn process(input) -> output {
let a = Accumulator();
let b = Accumulator();
// a and b accumulate independently
return a.add(input) + b.add(input);
}
}

Objects can use buffer for delay lines, circular buffers, and similar:

dsp object DelayLine {
state write_pos: 0;
buffer buf(48000);
fn write(x) -> out {
buf[write_pos] = x;
write_pos = (write_pos + 1) % 48000;
return x;
}
fn read(delay_samples) -> out {
let rp = write_pos - delay_samples;
if rp < 0 { rp = rp + 48000; }
return buf[rp];
}
}
dsp effect PingPong {
param time: 0.25 range(0.01, 1);
param feedback: 0.4 range(0, 0.95);
fn process(left, right) -> (out_l, out_r) {
let dl_l = DelayLine();
let dl_r = DelayLine();
let samples = time * sr;
let tap_l = dl_l.read(samples);
let tap_r = dl_r.read(samples);
dl_l.write(left + tap_r * feedback);
dl_r.write(right + tap_l * feedback);
return (left + tap_l, right + tap_r);
}
}

dsp object works the same way inside dsp instrument blocks:

dsp object ADSR {
state level: 0.0;
state stage: 0;
fn tick(gate, attack, decay, sustain, release) -> out {
if gate > 0.0 {
if stage == 0 {
level = level + attack;
if level >= 1.0 { level = 1.0; stage = 1; }
}
if stage == 1 {
level = level - decay * (level - sustain);
}
} else {
level = level * (1.0 - release);
stage = 0;
}
return level;
}
}
dsp instrument EnvSynth {
param attack: 0.01 range(0.001, 0.5);
param decay: 0.005 range(0.001, 0.1);
param sustain: 0.7 range(0, 1);
param release: 0.005 range(0.001, 0.1);
voice state phase: 0.0;
fn render(note, velocity, gate) -> (out_l, out_r) {
let env = ADSR();
let amp = env.tick(gate, attack, decay, sustain, release);
let freq = mtof(note);
phase = fract(phase + freq * INV_SR);
let sample = sin(phase * TWOPI) * amp * velocity;
return (sample, sample);
}
}

dsp fn and dsp object definitions are module-level values. Import them from other files using use:

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

Mark definitions as private to prevent export:

private dsp fn internal_helper(x) -> out {
return x * x;
}

See Modules for full import details.

dsp fnLocal fndsp object
ScopeModule-levelParent block onlyModule-level
Own stateNoNo (uses parent’s)Yes
Own buffersNoNo (uses parent’s)Yes
ImportableYesNoYes
Use casePure math helpersBlock-internal logicReusable stateful components

A distortion effect combining all three tiers:

// Shared pure helper
dsp fn tanh_approx(x) -> out {
let x2 = x * x;
return x * (27.0 + x2) / (27.0 + 9.0 * x2);
}
// Reusable stateful components
dsp object DCBlocker {
state x_prev: 0.0;
state y_prev: 0.0;
fn run(input) -> out {
let y = input - x_prev + 0.995 * y_prev;
x_prev = input;
y_prev = y;
return y;
}
}
dsp object ToneFilter {
state prev: 0.0;
fn lp(input, cutoff) -> out {
prev = prev + cutoff * (input - prev);
return prev;
}
}
dsp effect TubeOverdrive {
param drive: 3.0 range(1, 20);
param mix: 0.5 range(0, 1);
param tone: 0.4 range(0, 1);
// Local helpers for this effect only
fn saturate(x) -> out {
return tanh_approx(x * drive);
}
fn blend(dry, wet) -> out {
return dry * (1.0 - mix) + wet * mix;
}
fn process(left, right) -> (out_l, out_r) {
let dc_l = DCBlocker();
let dc_r = DCBlocker();
let lp_l = ToneFilter();
let lp_r = ToneFilter();
let wet_l = dc_l.run(saturate(left));
let wet_r = dc_r.run(saturate(right));
return (
lp_l.lp(blend(left, wet_l), tone),
lp_r.lp(blend(right, wet_r), tone)
);
}
}