Skip to content

Plugins

Resonon’s built-in effects and samplers cover a lot of ground, but sometimes you want a specific third-party reverb, or a synth you already know. Resonon loads CLAP and VST3 plugins — as effects in a track’s chain, or as the instrument on a track — and lets you read, set, and modulate their parameters from code.

Two reverb plugins ship bundled with resonon and load instantly, with no setup. Name one with Effect, and put it on a track:

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
let reverb = Effect("reverb", "resonon-reverb"); // bundled — no scan needed
drums.fx(#[reverb]); // placing it is what loads it
reverb.param_set("Room", 0.8); // now the handle is live
PLAY;

Effect("reverb", "resonon-reverb") names the plugin; fx is what loads it and sets it as the track’s chain, like any built-in effect; param_set then dials the live instance (A constructor is a description). The other bundled plugin is "resonon-reverb-plate", a lusher plate reverb.

Writing a parameter before placement is allowed, and does the obvious thing — it seeds the slot, and the value lands on whatever instance the .fx() line builds. Reading one before placement is not; there is nothing there to read yet.

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
let reverb = Effect("reverb", "resonon-reverb");
reverb.param_set("Room", 0.8); // recorded now...
reverb.param_set("Mix", 0.4);
drums.fx(#[reverb]); // ...and applied to the instance built here
show(reverb.param_get("Room")); // 0.8 — reading needs the placement above
PLAY;

resonon keeps a plugin database of what you have installed, and maintains it for you. Every run that starts the audio engine — running a file, the REPL, a server — sweeps the CLAP and VST3 folders in the background, and only opens bundles that are new or have changed since last time — so the sweep after installing one plugin costs one plugin, not your whole folder. Normally you never think about this.

The folders swept are the standard ones for your OS, CLAP_PATH and VST3_PATH, and every [folders] plugins entry in ~/.resonon/preferences.toml. Those entries are read before the sweep dispatches, so a folder of your own is included in it from the first run — which is the concrete reason the folder list is data and not a function call. resonon config plugins add writes one without opening the file, and reload_preferences() scans a folder you just added without restarting.

You can still ask for a sweep explicitly, which is what you want right after installing something mid-session:

let plugins = plugin_scan();
show(plugins.length()); // how many were found
print(plugins); // formatted table
let reverbs = plugins.filter("reverb"); // narrow by name substring
print(reverbs);

plugin_scan() returns a PluginList with .length(), .filter(name), .get(index), and .data (the raw entries, each carrying name, id, vendor, and format). In VSCode the same thing is on the palette as RESONON: Rescan Plugins, and from a terminal it is resonon plugin scan.

To turn automatic sweeping off — scanning does run third-party code, and you may prefer to say when — uncheck resonon.plugins.autoScan in VSCode, pass --plugin-auto-scan=false, or set RESONON_PLUGIN_AUTO_SCAN=0 (Configuration). Plugins loaded by path or filename still resolve without any sweep; only lookup by display name depends on one — and that now holds for your own folders too, which it could not while they were named by a call nothing outside a running session executed.

That on-demand resolve is not free the first time. A bundle the database has never seen — or one whose file has changed since it was recorded — is scanned right then, in a separate process, and your code waits for it: normally a moment, but up to 30 seconds for a plugin that is slow to load. The result is remembered, so it is a once-per-bundle cost.

That resolve happens on the constructor line, not at placement: Effect(...) looks the name up straight away, so a typo — or a bundle the scanner skipped — is underlined where you wrote it. What waits for .fx() / .instrument() is the expensive half: opening the binary, activating it, enumerating its parameters. So a plugin that resolves but fails to load reports on the placement line instead.

Effect(slot, name) describes an effect; Instrument(name) describes an instrument. Neither loads anything — but both resolve the name immediately, the same way, in order: a full path ending in .clap/.vst3; a filename found in your plugin folders; or a display name from the plugin database. If nothing matches, resonon lists the locations it searched and the plugins it knows about, on the constructor line.

let shimmer = Effect("shimmer", "Valhalla Shimmer"); // by display name
let fx = Effect("fx", "/path/to/plugin.clap"); // by full path
let synth = Instrument("Surge XT"); // an instrument plugin

A track hosts a plugin instrument exactly like a built-in one:

let synth = Instrument("Surge XT");
let lead = AudioTrack("lead");
lead.instrument(synth);
lead << [C4 E4 G4 C5];

When a single bundle contains several plugins, pass the plugin ID as a second argument to pick one — Effect("multi", "Multi-FX", "com.vendor.multi-fx.chorus"). With both a CLAP and a VST3 build of the same plugin installed, CLAP is tried first; the plugin ID is how a piece says which build it means, and it says so in the piece rather than in a machine-wide setting (Configuration).

Effect(...) is a description, not a loaded plugin (A constructor is a description). Two things happen at two different times: the plugin’s name is resolved on the constructor line, and the plugin is loaded and activated when a track places it. So place it first with .fx(), then inspect and dial it through the same handle. (Reading a parameter off an unplaced effect is an error that says as much.)

.params() prints every parameter with its range, default, and current value; pass a substring to filter a plugin with a long list:

let reverb = Effect("reverb", "resonon-reverb");
master.fx(#[reverb]); // place it — now it's live
reverb.params(); // all of them
reverb.params("room"); // just the matching ones

Read a value with .param_get(name), and set one with .param_set(name, value), which returns the plugin so calls chain:

let reverb = Effect("reverb", "resonon-reverb");
master.fx(#[reverb]);
reverb.param_set("Room", 0.8)
.param_set("Damp", 0.4)
.param_set("Mix", 0.5);
show(reverb.param_get("Room")); // 0.8

When you’d rather work in normalised terms, .param_set_norm(name, 0..1) maps a 0..1 value across the parameter’s real range — including any non-linear VST3 curve.

The << shorthand sets a parameter too, and it’s the one that also accepts a moving signal — so the same syntax that sets a value can modulate it:

use "std/signals" { Sine };
let reverb = Effect("reverb", "resonon-reverb");
master.fx(#[reverb]);
reverb.param("Mix") << 0.4; // set a value
reverb.param("Room") << Sine(0.2).range(0.3, 0.9); // ...or modulate it

See Signals & Automation for the full set of modulation sources.

Many third-party plugins have a graphical editor. Check with .supports_gui(), then open and close the window with .show_gui() and .hide_gui():

let lead = AudioTrack("lead");
let synth = Instrument("Surge XT");
lead.instrument(synth); // built here — the GUI methods need the instance
if synth.supports_gui() {
synth.show_gui();
}

The bundled resonon reverbs have no GUI — .supports_gui() returns false — so you drive them with .params() and param_set / <<. Most third-party plugins do.

.save_state(path) writes a plugin’s full state — every parameter plus internal data — to a file, and .load_state(path) reads it back into an instance. Both are chainable:

let lead = AudioTrack("lead");
let synth = Instrument("Surge XT");
lead.instrument(synth);
synth.save_state("presets/my_lead.preset"); // a read: needs the live plugin
let backing = AudioTrack("backing");
let synth2 = Instrument("Surge XT");
synth2.load_state("presets/my_lead.preset"); // a write: recorded, applied at placement
backing.instrument(synth2);

This is the write/read asymmetry in miniature: save_state reads a running plugin, so it needs the instrument on a track first, while load_state writes, so it can run before placement and is replayed onto the instance the track builds.

You can scan, load, and control external instruments and effects. Next, bring external sound into resonon.