Skip to content

Your First Effect

On this page you write your own audio effects in the resonon language: a gain stage, a tremolo, an echo and a drive. At the end, you change an effect while you hear it.

An effect changes the audio that goes through a track. In a DAW, it is an insert on a channel strip. Here, you write what it does to each sample.

Work in a project, as in Your First Sound. Write each step into src/main.non, select it, and press Cmd+Enter. Every step adds to what the previous one built.

Finish Language for a Reusable Piece and Signals and Modulation. Open src/main.non, evaluate RESET; on its own, then remove that line. The session should be silent before you create the drums and gain stage. No plugin is needed.

The simplest effect changes the volume:

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.set_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
let gain = Gain("gain");
drums.set_effects(#[gain]);
PLAY;

You hear the beat, and it goes through the effect that you wrote.

  • dsp effect Gain { … } defines a new effect called Gain.
  • param level: 1.0 range(0, 2); is a knob. Its default is 1.0, and it goes from 0 to 2. In the effect, you read it like a variable.
  • fn process(left, right) -> (out_l, out_r) runs one time for each sample — 48,000 times each second at 48 kHz. It gets the left and right input sample and returns the left and right output sample.
  • Gain("gain") makes one of these effects, with the name gain. set_effects puts it on the track, the same as a built-in effect.

The knob is on the effect you kept in gain. Set it with <<:

gain.parameter("level") << 0.3;

The beat is quieter. A knob also takes a signal, so it can move by itself:

use "std/signals" { Sine };
gain.parameter("level") << Sine(1).range(0.2, 1.0);

The volume now swells up and down once in each cycle. You did not write that into the effect: any knob that you declare with param can be set and modulated from outside.

process forgets everything between two samples. A state variable keeps its value. A tremolo needs one, to know where its wobble is:

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);
}
}
drums.set_effects(#[gain, Tremolo("tremolo")]);

The beat pulses four times a second, after the gain stage.

  • phase goes from 0 to 1, and moves on by speed * INV_SR in each sample. INV_SR is one divided by the sample rate, so phase goes around speed times each second.
  • __native("fract", …) keeps it between 0 and 1, and __native("sin", …) turns it into a wave. __native calls resonon’s built-in DSP functions. Hover a name in your editor to see what it does.

An echo must keep a lot of samples. A buffer is a fixed-size array for that, and its index wraps around by itself, so it works as a circular delay line:

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 rp = wp + 48000.0 - time * SR;
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);
}
}
drums.set_effects(#[gain, Echo("echo")]);

Each hit now repeats a quarter of a second later, and fades out.

wp is where the effect writes, and rp is where it reads, time * SR samples back. SR is the sample rate. The chain is now the gain stage and the echo: the tremolo is gone, because set_effects sets the whole chain.

A long process is hard to read. Put a part of it into its own function, in the same block:

dsp effect Drive {
param drive: 4.0 range(1, 10);
param tone: 3000 range(200, 12000);
fn soft_clip(x) -> out {
return x / (1.0 + __native("abs", x));
}
fn process(left, right) -> (out_l, out_r) {
let l = soft_clip(left * drive);
let r = soft_clip(right * drive);
return (__native("svf_lp", l, tone, 0.7), __native("svf_lp", r, tone, 0.7));
}
}
let drive = Drive("drive");
drums.set_effects(#[drive, Echo("echo")]);

The drums are louder and rougher, and the echo is still there.

soft_clip pushes a loud signal smoothly toward -1 and 1. __native("svf_lp", input, cutoff, resonance) is a built-in lowpass filter that takes away the harsh top. The DSP builtins list every function you can call like this.

Turn the tone down first:

drive.parameter("tone") << 800;

Now edit the effect itself. Change the soft_clip line to a harder clip, then select only the dsp effect Drive { … } block and run it:

dsp effect Drive {
param drive: 4.0 range(1, 10);
param tone: 3000 range(200, 12000);
fn soft_clip(x) -> out {
return __native("clamp", x, -0.5, 0.5);
}
fn process(left, right) -> (out_l, out_r) {
let l = soft_clip(left * drive);
let r = soft_clip(right * drive);
return (__native("svf_lp", l, tone, 0.7), __native("svf_lp", r, tone, 0.7));
}
}

The sound changes with no click, and the beat does not stop. The tone is still at 800: an edit changes the code, not the knobs you have turned. If you add or remove a param, the knobs no longer match — run the set_effects line again to build the effect again from its defaults.

You wrote an effect, gave it knobs, state and a buffer, and changed it while it played.