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.
Quick Start
Section titled “Quick Start”// Module-level pure helperdsp fn softclip(x) -> out { return x / (1.0 + abs(x));}
// Reusable stateful componentdsp 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);dsp fn
Section titled “dsp fn”Top-level DSP helper functions. Define them at module scope — they are hoisted and available throughout the file.
Syntax
Section titled “Syntax”dsp fn name(params) -> return_name { // body}For multiple return values, use a tuple:
dsp fn name(params) -> (out_a, out_b) { // body}Behavior
Section titled “Behavior”- Module-level: Defined at the top level of a file, hoisted like regular
fn - Pure: Cannot declare
state,buffer, orparam— 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
Example
Section titled “Example”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); }}Tuple Returns
Section titled “Tuple Returns”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); }}Local Helper Functions
Section titled “Local Helper Functions”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.
Syntax
Section titled “Syntax”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.
Behavior
Section titled “Behavior”- Block-scoped: Visible only inside their parent block
- Access parent state: Can read and write the parent’s
state,buffer, andparamvariables - Shadow external helpers: A local
fnwith the same name as an externaldsp fntakes precedence - Not exported: Cannot be imported or called from outside the block
- Inlined: Zero overhead, just like
dsp fn
Example in an Effect
Section titled “Example in an Effect”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)); }}Example in an Instrument
Section titled “Example in an Instrument”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); }}dsp object
Section titled “dsp object”Stateful, reusable DSP components with their own state and buffer declarations. Define them at module scope and instantiate inside effects or instruments.
Syntax
Section titled “Syntax”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 }}statedeclares per-instance mutable variables, initialized to the given valuebufferdeclares fixed-size arrays, initialized to zero- Methods can be named anything — there is no required entry-point name
Instantiation
Section titled “Instantiation”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.
Example: One-Pole Filter
Section titled “Example: One-Pole Filter”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)); }}Multiple Instances
Section titled “Multiple Instances”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); }}Buffers in Objects
Section titled “Buffers in Objects”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); }}Objects in Instruments
Section titled “Objects in Instruments”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); }}Cross-Module Import
Section titled “Cross-Module Import”dsp fn and dsp object definitions are module-level values. Import them from other files using use:
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; }}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.
Choosing the Right Tier
Section titled “Choosing the Right Tier”dsp fn | Local fn | dsp object | |
|---|---|---|---|
| Scope | Module-level | Parent block only | Module-level |
| Own state | No | No (uses parent’s) | Yes |
| Own buffers | No | No (uses parent’s) | Yes |
| Importable | Yes | No | Yes |
| Use case | Pure math helpers | Block-internal logic | Reusable stateful components |
Complete Example
Section titled “Complete Example”A distortion effect combining all three tiers:
// Shared pure helperdsp fn tanh_approx(x) -> out { let x2 = x * x; return x * (27.0 + x2) / (27.0 + 9.0 * x2);}
// Reusable stateful componentsdsp 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) ); }}See Also
Section titled “See Also”- Effects — Custom DSP effects
- DSP Instruments — Custom synthesizers
- Signals & Automation — DSP signal blocks
- DSP Builtins Reference
- Modules — Import system