Skip to content

Custom DSP

So far you’ve played samples and chained built-in effects. Now let’s build your own. In Resonon you can write audio effects and instruments directly in the language — think of it as coding your own plugin, except it lives right inside your live session. Your code runs at the audio sample rate with nothing between you and the signal: no plugin host, no external tools, just the math you write.

An effect transforms audio as it passes through a track — in DAW terms, an insert in the channel’s signal chain. The simplest one just turns the volume up or down. Write this into a .non file, select it, and press Cmd+Enter:

use "std/instruments" { Sampler, Kit };
dsp effect Gain {
param level: 1.0 range(0, 2);
fn process(left, right) -> (out_l, out_r) {
return (left * level, right * level);
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.load_effect(Gain());
PLAY;

You’ve heard this beat before — but now it’s running through an effect you wrote. Here’s the part that matters:

fn process(left, right) -> (out_l, out_r) {
return (left * level, right * level);
}

process is the heart of every effect. It runs once per audio sample — tens of thousands of times a second — receiving the current left and right input and returning the processed output. Here it just scales both channels by level.

Load your effect onto a track exactly like a built-in one, with load_effect. A track runs its effects in the order you load them.

param declares a knob: a value with a default and an optional range. The range is what a UI or a modulation source maps onto.

use "std/instruments" { Sampler, Kit };
dsp effect Tremolo {
param speed: 4.0 range(0.1, 20);
param depth: 0.5 range(0, 1);
state phase: 0.0;
fn process(left, right) -> (out_l, out_r) {
phase = __native("fract", phase + speed * INV_SR);
let wobble = 1.0 - depth * (0.5 + 0.5 * __native("sin", phase * TWOPI));
return (left * wobble, right * wobble);
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
let trem = Tremolo();
drums.load_effect(trem);
PLAY;

Inside process, a parameter is just a plain identifier — read speed and depth like any variable. From the outside, you steer them with the << operator, the same one you use to send patterns to a track:

use "std/signals" { Sine };
trem.param("speed") << Sine(0.25).range(2, 8); // sweep speed with an LFO
trem.param("depth") << 0.7; // or pin it to a fixed value

A bare parameter can’t remember anything between samples. state can. State variables persist from one process call to the next, which is what you need for anything that accumulates over time — envelopes, phase, running averages.

This envelope follower ducks the signal in proportion to how loud it is:

use "std/instruments" { Sampler, Kit };
dsp effect EnvFollower {
param attack: 0.01 range(0.001, 0.5);
param release: 0.1 range(0.01, 2.0);
state env: 0.0;
fn process(left, right) -> (out_l, out_r) {
let input_level = __native("abs", left) + __native("abs", right);
if input_level > env {
env = env + (input_level - env) * attack;
} else {
env = env + (input_level - env) * release;
}
let gain = 1.0 / (1.0 + env * 4.0);
return (left * gain, right * gain);
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.load_effect(EnvFollower());
PLAY;

env tracks the input level, rising quickly (attack) and falling slowly (release) — the same one-pole smoothing trick shows up all over DSP.

For delays, reverbs, or wavetables you need to store many samples at once. A buffer is a fixed-size array; indices wrap around automatically, so you can treat it as a circular delay line. This echo writes into a buffer and reads back out a quarter-second later:

use "std/instruments" { Sampler, Kit };
dsp effect Echo {
param time: 0.25 range(0.001, 1.0);
param feedback: 0.5 range(0, 0.99);
param mix: 0.4 range(0, 1);
buffer buf_l(48000);
buffer buf_r(48000);
state wp: 0;
fn process(left, right) -> (out_l, out_r) {
let delay_samples = time * SR;
let rp = wp + 48000.0 - delay_samples;
let delayed_l = buf_l[rp];
let delayed_r = buf_r[rp];
buf_l[wp] = left + delayed_l * feedback;
buf_r[wp] = right + delayed_r * feedback;
wp = (wp + 1.0) % 48000.0;
return (left + delayed_l * mix, right + delayed_r * mix);
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.load_effect(Echo());
PLAY;

SR is the sample rate (e.g. 48000) and INV_SR is 1.0 / SR — both are always available inside a DSP block, so you can write times in seconds and convert to samples.

When process gets busy, factor pieces out into helper functions defined inside the same block. They can read and write the effect’s state and buffers:

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 process(left, right) -> (out_l, out_r) {
let clipped_l = soft_clip(left * drive);
let clipped_r = soft_clip(right * drive);
return (left * (1.0 - mix) + clipped_l * mix,
right * (1.0 - mix) + clipped_r * mix);
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.load_effect(SoftClipper());
PLAY;

You’ve been calling __native(...) all along — that’s how you reach Resonon’s optimized DSP primitives from inside a block. Beyond the math (sin, abs, sqrt, clamp, …) there’s a family of state-variable filters. This auto-wah sweeps a lowpass with an internal LFO:

use "std/instruments" { Sampler, Kit };
dsp effect AutoFilter {
param cutoff: 2000 range(20, 20000);
param resonance: 1.5 range(0.5, 10);
state phase: 0.0;
fn process(left, right) -> (out_l, out_r) {
phase = __native("fract", phase + 0.5 * INV_SR);
let lfo = 0.5 + 0.5 * __native("sin", phase * TWOPI);
let freq = 200.0 + lfo * cutoff;
return (__native("svf_lp", left, freq, resonance),
__native("svf_lp", right, freq, resonance));
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.load_effect(AutoFilter());
PLAY;

A few of the most reaching-for ones:

NameWhat it does
svf_lp, svf_hp, svf_bpLowpass / highpass / bandpass filters — (input, cutoff, resonance)
onepoleOne-pole smoothing — (input, coeff)
delay, delay_interpSample-delay lines — (input, time_samples)
mtof, ftomConvert between MIDI note and frequency

The full catalogue — every filter, every constant — lives in the DSP Builtins reference.

An effect transforms incoming audio; an instrument generates it from notes. A dsp instrument is polyphonic, and its render function runs once per sample per active voice — Resonon hands you one voice per held note and sums them for you. Here’s a sine oscillator:

dsp instrument SineOsc {
voice state phase: 0.0;
fn render(note, velocity, gate) -> (out_l, out_r) {
let freq = __native("mtof", note);
phase = __native("fract", phase + freq * INV_SR);
let sample = __native("sin", phase * TWOPI) * velocity * gate;
return (sample, sample);
}
}
let synth = AudioTrack("synth");
synth.load_instrument(SineOsc());
synth << [C4 E4 G4 C5];
PLAY;

render receives three values for the note it’s voicing:

ParameterDescription
noteMIDI note number (fractional values give you microtonal pitches)
velocityHow hard the note was struck, normalized 0.01.0
gate1.0 while the note is held, 0.0 after note-off

__native("mtof", note) turns the note number into a frequency in Hz. Notice voice state rather than plain state: each voice gets its own copy of phase, reset on every note-on, so overlapping notes don’t smear into each other.

A raw oscillator clicks on and off. Give each voice an amplitude envelope and a filter and it starts to sound like a synth:

dsp instrument FilteredSynth {
param attack: 0.01 range(0.001, 1.0);
param release: 0.2 range(0.01, 2.0);
param cutoff: 4000 range(20, 20000);
voice state phase: 0.0;
voice state env: 0.0;
fn render(note, velocity, gate) -> (out_l, out_r) {
let freq = __native("mtof", note);
phase = __native("fract", phase + freq * INV_SR);
let osc = __native("sin", phase * TWOPI);
let target = gate * velocity;
if gate > 0.0 {
env = env + (target - env) * attack;
} else {
env = env + (target - env) * release;
}
let filtered = __native("svf_lp", osc * env, cutoff, 1.5);
return (filtered, filtered);
}
}
let lead = AudioTrack("lead");
lead.load_instrument(FilteredSynth());
lead << [C4 _ E4 _ G4 _ C5 _];
PLAY;

Instruments take the same parameter controls as effects. Set a value by name, or modulate it with << just like the Tremolo earlier:

let synth = FilteredSynth();
synth.params(); // print every parameter and its value
synth.param_set("cutoff", 2000); // set one by name
synth.param_set("attack", 0.1)
.param_set("release", 0.5); // calls chain
synth.param("cutoff") << Sine(2).range(500, 6000); // or modulate it live

You can now build effects and synths from scratch — the same primitives power everything from a gentle tremolo to a screaming acid lead.