DSP Signals, Functions & Objects
Once you’ve written a few effects and instruments, two needs show up. First, you start repeating yourself — the same soft-clip or one-pole filter copied into block after block. Second, you want modulation the built-in LFOs can’t give you: a filtered-noise source, a beat-synced ramp, something driven by live MIDI.
This chapter covers both. The first half is about reusing DSP code — three ways
to factor it out, from a quick local helper to a shared, stateful component. The
second half introduces the dsp signal — a modulation source you write
yourself, compiled by the same engine as your effects and instruments.
Reusing DSP Code
Section titled “Reusing DSP Code”Resonon gives you three tiers of reuse, all inlined at compile time with zero
overhead, all usable inside dsp effect, dsp instrument, and dsp signal:
- a local
fn— a helper scoped to one block, - a
dsp fn— a pure helper shared across the whole file (and importable), - a
dsp object— a reusable component that carries its own state.
Here’s all three working together before we take them one at a time:
use "std/instruments" { Sampler, Kit };
// Module-level pure helperdsp fn softclip(x) -> out { return x / (1.0 + __native("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_l = OnePole(); let lp_r = OnePole(); return (lp_l.filter(apply_drive(left), tone), lp_r.filter(apply_drive(right), tone)); }}
let drums = AudioTrack("drums");drums.load_instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];drums.load_effect(SoftDrive());
PLAY;Local Helper Functions
Section titled “Local Helper Functions”You’ve already met these in the effects and instruments chapters: any fn inside a
DSP block that isn’t the entry point (process or render) is a local helper. No
keyword needed — just define it. It’s visible only inside its block, can read and
write the block’s state, buffer, and param, and shadows any top-level dsp fn
of the same name.
dsp effect Name { fn helper(x) -> out { // can read/write this block's state and params return x * 2.0; }
fn process(input) -> output { return helper(input); }}Reach for a local helper first. Promote it only when you need the same code in another block.
dsp fn — Shared Pure Helpers
Section titled “dsp fn — Shared Pure Helpers”A dsp fn is a pure function defined at the top level of a file: inputs in,
outputs out, no state, buffer, or param. It’s hoisted like a regular fn, so
it’s available everywhere in the file, and it can be imported from other files.
dsp fn clamp01(x) -> out { return __native("clamp", x, 0.0, 1.0);}
dsp effect SafeGain { param level: 1.0 range(0, 4);
fn process(input) -> output { return clamp01(input * level); }}A dsp fn returns a single value. To process both channels of a stereo effect,
call the helper once per channel — that’s the pattern every effect in these
chapters uses:
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 object — Reusable Stateful Components
Section titled “dsp object — Reusable Stateful Components”A dsp fn can’t remember anything between calls. When a helper needs its own state
— a filter’s previous sample, a delay line’s buffer — make it a dsp object.
Define it at module scope with state and buffer declarations, then instantiate
it inside an effect or instrument. Each instance gets an independent copy of all its
state.
use "std/instruments" { Sampler, Kit };
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(); // each let is an independent instance let lp_r = OnePole(); return (lp_l.lp(left, amount), lp_r.lp(right, amount)); }}
let drums = AudioTrack("drums");drums.load_instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];drums.load_effect(Smooth());
PLAY;A few rules worth knowing:
- Methods can be named anything — there’s no required entry point.
- An object can declare
buffers for delay lines and circular buffers. - An object cannot declare
param— parameters belong to the containing effect or instrument, which can pass their values into the object’s methods.
Choosing the Right Tier
Section titled “Choosing the Right Tier”local fn | dsp fn | dsp object | |
|---|---|---|---|
| Scope | one block | whole file | whole file |
| Own state / buffers | no (uses parent’s) | no | yes |
| Importable | no | yes | yes |
| Best for | block-internal logic | pure math helpers | reusable stateful parts |
Sharing Across Files
Section titled “Sharing Across Files”dsp fn and dsp object are module-level values, so you import them with use
like anything else:
dsp fn softclip(x) -> out { return x / (1.0 + __native("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); }}Prefix a definition with private to keep it from being exported. See
Modules for the full import system.
DSP Signals
Section titled “DSP Signals”The built-in LFOs and envelopes from Signals & Automation
cover most modulation. When they don’t — when you need per-sample logic, internal
state, filtered noise, or live MIDI — write a dsp signal. It compiles through
the same pipeline as effects and instruments, and you bind it to any parameter with
<<, exactly like a built-in LFO.
A dsp signal has one entry point, fn process() -> out, that returns one value
per sample:
use "std/instruments" { Sampler, Kit };use "std/effects" { Lowpass };
dsp signal SmoothRamp { state phase: 0.0;
fn process() -> out { phase = __native("fract", phase + CPS * INV_SR); return phase; }}
let drums = AudioTrack("drums");drums.load_instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
let filter = Lowpass(2000);drums.load_effect(filter);filter.param("Cutoff") << SmoothRamp().range_exp(300, 4000);
PLAY;CPS is cycles-per-second and INV_SR is 1 / sample_rate, so each sample
advances phase by exactly one cycle’s fraction — a sample-accurate 0→1 ramp.
You instantiate the signal with () and bind it with <<, attaching .range_exp
to map its 0..1 output onto the filter’s cutoff range.
Declaring an Output Range
Section titled “Declaring an Output Range”A signal’s natural output is bipolar -1..1. Declare range: when it’s
something else, so the engine and the scope know its bounds:
dsp signal UnipolarRamp { range: 0..1 state phase: 0.0;
fn process() -> out { phase = __native("fract", phase + CPS * INV_SR); return phase; }}State, Parameters, and Helpers
Section titled “State, Parameters, and Helpers”A dsp signal uses the same state, param, buffer, and local-helper building
blocks as everything else. Here’s a parameterized LFO with a one-pole-smoothed
noise source split into a helper:
dsp signal Wobble { param speed: 4.0 range(0.1, 40); param depth: 0.5 range(0, 1); state phase: 0.0;
fn process() -> out { phase = __native("fract", phase + speed * CPS * INV_SR); return 0.5 + depth * 0.5 * __native("sin", phase * TWOPI); }}
dsp signal SmoothNoise { param smoothing: 0.001 range(0.0001, 0.1); state prev: 0.0;
fn smooth(input) -> out { prev = prev + smoothing * (input - prev); return prev; }
fn process() -> out { return smooth(__native("noise")); }}dsp signal can also use module-level dsp fn and dsp object, just like effects
and instruments do — the same softclip or OnePole you wrote above works here.
Phase and Edge Builtins
Section titled “Phase and Edge Builtins”Accumulating a phase by hand works, but for clock-synced modulation Resonon gives you ready-made signal constants. They cost no state and stay locked to the transport:
| Constant | Meaning |
|---|---|
CYCLE_PHASE | 0→1 sawtooth that resets every cycle |
BEAT_PHASE | 0→1 sawtooth that resets every beat |
CYCLE_EDGE | 1.0 on the exact sample a cycle boundary is crossed, else 0.0 |
BEAT_EDGE | 1.0 on the exact sample a beat boundary is crossed, else 0.0 |
CYCLE | the current cycle number |
CPS / BPC | cycles-per-second and beats-per-cycle |
TIME | elapsed seconds since the signal started |
CYCLE_PHASE replaces a manual phase accumulator for cycle-length ramps:
dsp signal CycleRamp { range: 0..1 fn process() -> out { return CYCLE_PHASE; }}The edge constants fire for a single sample at a boundary — perfect for sample-and-hold or resetting an accumulator. This holds a fresh random value at the start of every beat:
dsp signal BeatPulse { range: 0..1 state held: 0.0;
fn process() -> out { if BEAT_EDGE > 0.0 { held = __native("noise") * 0.5 + 0.5; } else { held = held; } return held; }}Upstream Signal Inputs
Section titled “Upstream Signal Inputs”A dsp signal can take other signals as per-sample inputs. Declare them as
parameters of process, then pass signals in at instantiation:
dsp signal Wobble { param speed: 4.0 range(0.1, 40); state phase: 0.0; fn process() -> out { phase = __native("fract", phase + speed * CPS * INV_SR); return 0.5 + 0.5 * __native("sin", phase * TWOPI); }}
dsp signal Remap { param lo: 0; param hi: 1; fn process(source) -> out { return lo + source * (hi - lo); }}
let ranged = Remap(Wobble()); // positionallet ranged_named = Remap(source: Wobble()); // namedlet half = Remap(0.5); // a number auto-wraps as a constant signallet chain = Remap(Wobble()).range_exp(200, 8000); // signals chainNumbers passed where a signal is expected auto-wrap as constant signals, so you can mix the two freely.
Signal-Valued Parameters
Section titled “Signal-Valued Parameters”Parameters aren’t limited to static values either — pass a signal as a param
argument and it modulates that parameter per sample. Positional arguments fill the
upstream inputs first, then the parameters in declaration order:
// Named: modulate the lo bound with another signallet dynamic = Remap(Wobble(), lo: Wobble(), hi: 0.9);
// With no upstream inputs, positional args fill params directlylet fast = Wobble(8.0); // speed = 8let modulated = Wobble(Wobble()); // speed modulated by another WobbleReading Live MIDI
Section titled “Reading Live MIDI”Inside a dsp signal you can read live MIDI input directly. These builtins refresh
once per audio block from shared atomics — no state needed:
cc(n)— MIDI CCn, normalized0.0–1.0, any channel;cc(channel, n)for a specific channel (0–15)aftertouch()— channel pressure0.0–1.0;aftertouch(channel)for one channelpitchbend()— pitch bend-1.0–1.0;pitchbend(channel)for one channel
dsp signal CcFilter { fn process(source) -> out { let cutoff = __native("cc", 74) * 10000.0 + 200.0; return __native("svf_lp", source, cutoff, 0.707); }}Here CC 74 sweeps a filter cutoff in real time. For the full MIDI-input story — channels, controllers, MPE — see MIDI In & CC.
Putting It Together
Section titled “Putting It Together”A signal makes a great modulation source for anything that takes << — effect
parameters, instrument parameters, even a track’s volume:
use "std/instruments" { Sampler, Kit };use "std/effects" { Lowpass, Delay };
dsp signal SmoothNoise { param smoothing: 0.001 range(0.0001, 0.1); state prev: 0.0; fn process() -> out { prev = prev + smoothing * (__native("noise") - prev); return prev; }}
dsp signal Wobble { param speed: 4.0 range(0.1, 40); state phase: 0.0; fn process() -> out { phase = __native("fract", phase + speed * CPS * INV_SR); return 0.5 + 0.5 * __native("sin", phase * TWOPI); }}
let drums = AudioTrack("drums");drums.load_instrument(Sampler(Kit("CR-78")));drums << [bd sd [bd bd] sd];
let filter = Lowpass(2000);let delay = Delay(0.25, 0.4);drums.load_effect(filter);drums.load_effect(delay);
filter.param("Cutoff") << SmoothNoise().range_exp(300, 4000);delay.param("Feedback") << Wobble().range(0.1, 0.6);drums.volume << Wobble().range(-6, 0);
PLAY;Next Steps
Section titled “Next Steps”You can now factor DSP code for reuse and build your own modulation sources. When even hand-written Resonon DSP isn’t enough — you need a tight Rust loop, a crate, or system access — drop down to native code:
- Going Native — extend Resonon with Rust extensions
- Signals & Automation — the built-in LFOs, ramps, and envelopes these signals sit alongside
- MIDI In & CC — driving signals from external MIDI