Skip to content

Your First Instrument

On this page you write a synthesizer in the resonon language. You start with a sine wave and end with a filtered synth that you change while it plays.

An effect changes audio that already exists. An instrument makes audio from notes: a pattern sends it notes, and it turns each one into sound.

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 Your First Effect. Open src/main.non, evaluate RESET; on its own, then remove that line. Check that the earlier drum track is silent. The synth below makes its own sound; it needs no samples or plugin.

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.set_instrument(SineOsc());
synth << [c4 e4 g4 c5];
PLAY;

You hear four notes, played by a synth that you wrote.

  • dsp instrument SineOsc { … } defines a new instrument called SineOsc.
  • fn render(note, velocity, gate) runs one time for each sample, and returns a left and a right sample. note is the MIDI note number, velocity is how hard it was played (0 to 1), and gate is 1 while the note is held and 0 after it is released.
  • __native("mtof", note) changes the note number into a frequency in Hz. phase goes from 0 to 1 once for each period of that frequency, and sin turns it into a wave.
  • SineOsc() makes one of these instruments, and set_instrument puts it on the track.

Send the track a chord:

synth << [c4:maj e4:min];

You hear three notes at the same time. The instrument gives each note its own voice: its own copy of render, with its own phase. That is what voice state means. Each voice starts again from 0.0 when its note starts, so the notes don’t get in each other’s way.

A sine that starts and stops at full level clicks. Give each voice an envelope, which fades the note in while it is held and out after it is released:

dsp instrument Synth {
param attack: 0.002 range(0.0001, 0.1);
param release: 0.001 range(0.0001, 0.1);
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 sample = osc * env;
return (sample, sample);
}
}
synth.set_instrument(Synth());
synth << [c4 _ e4 _ g4 _ c5 _];

The notes are softer at the start and ring out at the end.

env moves a small step toward target in each sample. While the note is held, target is the velocity and the step is attack; after the release, target is 0 and the step is release. A smaller step is a slower fade.

Write a second instrument, FilteredSynth. It is the same as Synth, but its oscillator is a sawtooth, which has a lot of high harmonics, and a lowpass filter follows it:

dsp instrument FilteredSynth {
param attack: 0.002 range(0.0001, 0.1);
param release: 0.001 range(0.0001, 0.1);
param cutoff: 1200 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 = phase * 2.0 - 1.0;
let target = gate * velocity;
if gate > 0.0 {
env = env + (target - env) * attack;
} else {
env = env + (target - env) * release;
}
let sample = __native("svf_lp", osc * env, cutoff, 1.5);
return (sample, sample);
}
}
let lead = FilteredSynth();
synth.set_instrument(lead);

__native("svf_lp", input, cutoff, resonance) is a built-in lowpass filter. The DSP builtins list every function you can call like this. This time you kept the instrument in lead, so you can reach its knobs.

Set a knob by name, or send it a signal:

use "std/signals" { Sine };
print(lead.parameters());
lead.set_parameter("release", 0.0002);
lead.parameter("cutoff") << Sine(2).range_exp(300, 6000);

parameters() returns every knob with its range and value, and print shows them as a table. The notes now ring longer, and the filter opens and closes twice every cycle.

The instrument must be on the track before you can read its knobs: FilteredSynth() only describes it, and set_instrument builds it.

Change the waveform line to a square wave, then select only the dsp instrument FilteredSynth { … } block and run it:

dsp instrument FilteredSynth {
param attack: 0.002 range(0.0001, 0.1);
param release: 0.001 range(0.0001, 0.1);
param cutoff: 1200 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 = if phase < 0.5 { 1.0 } else { -1.0 };
let target = gate * velocity;
if gate > 0.0 {
env = env + (target - env) * attack;
} else {
env = env + (target - env) * release;
}
let sample = __native("svf_lp", osc * env, cutoff, 1.5);
return (sample, sample);
}
}

The sound changes with no click, and notes that are playing fade into the new version. The filter sweep and the longer release are still there: an edit changes the code, not the knobs.

You wrote a polyphonic synth with an envelope and a filter, and changed it while it played.