Script examples
A cookbook. Every entry on this page is one of the sample scripts that ship
with the synth, in the script-samples folder and in the editor's sample
picker. The sample files are the single source of truth: this page is
generated from them, so what you read here is exactly what you can drop into
the editor.
For the language and primitives, see the overview. For per-handler details, see the handlers page.
Each entry shows the snippet wrapped in the handler it belongs to, then
explains what makes it worth writing as a script instead of a routing. A
Globals entry is a declaration you place in the Globals scope; pair it with
the handler that uses it.
Globals
Declarations that live in the Globals scope. A handler sample that reads
hits, note_count, or a seed expects the matching declaration here.
Per voice counter
voice var hits = 0
A per-voice counter that survives across note events for one voice. The mod matrix has no user-defined state - it can only sum modulation sources. Use this kind of variable to track "how often has THIS note been hit" or "what stage is this voice in".
Global counter
var note_count = 0
A global counter shared across every voice and every event. The mod matrix has no way to count notes - this is script-only.
Deterministic randomness
seed 42
Pin the random seed so a generative script renders identically on
every load and every offline bounce. Without a seed line the script
is reproducible by default; use seed live to randomise per session.
on init
Runs once when the script loads. Prime global state or log a startup line.
One shot setup
on init
log("script loaded")
end
on init runs ONCE, immediately after the script loads. It is the
only event of its kind - useful for setup logging or for stamping a
global var with a starting value derived from the patch.
on note
Fires per voice at note-on. Decisions that depend on which note was played and how hard.
Velocity threshold
on note
if velocity > 100
# ramp(paramPath, targetOffset, durationMs) glides the script's
# offset to `targetOffset` over `durationMs`. The offset is added
# to the parameter's base value - here, +4000 Hz on filter cutoff.
ramp("filt_cutoff", 4000, 80)
end
end
Discrete branch on velocity: the mod matrix can only SCALE velocity (continuous, linear). A script can act only above a threshold - here, only hard hits open the filter.
Pitch bucket
on note
if pitch < 48 # below C3 - bass register
modulate("filt_reso", 0.3)
elif pitch < 72 # C3..B4 - mid register
modulate("filt_reso", 0.1)
end
end
Different behaviour per pitch range. The mod matrix has no notion of "if pitch is below 48" - it only multiplies a pitch-derived source. This sample adds resonance only to bass-register notes.
Pitch reference: 36 = C2 (bass), 48 = C3, 60 = C4 (middle C), 72 = C5, 84 = C6.
Bloom then decay
on note
ramp("filt_cutoff", 4000, 80) # open quickly: +4000 Hz over 80 ms
after 200 # 200 ms later...
ramp("filt_cutoff", 0, 600) # ...glide back to 0 over 600 ms
end
end
Time-staged behaviour: open the filter NOW, then close it 200 ms
later. after schedules a deferred block - the mod matrix has no
concept of "do X, then later do Y" for a single voice.
Count hits
on note
hits = hits + 1
if hits % 3 == 0 # every third hit
modulate("master_vol", 0.2)
end
end
Increment a per-voice counter on each note. Requires a matching
voice var hits = 0 in Globals (one slot per voice; persists for
that voice's lifetime).
Use the counter to drive behaviour the engine cannot - e.g. "every third hit on this voice is louder".
Relative pitch layer
on note
if velocity > 100 # only on hard hits
# play(pitch, velocity, durationMs) - softer than the main voice
# so the layer sits underneath rather than fighting it.
play(pitch + 7, 70, 250)
end
end
Add a one-shot harmony layer a perfect fifth above whatever key was just played. The mod matrix can transpose oscillators by a fixed interval, but it cannot spawn an extra voice at a relative pitch - that requires a script.
pitch is the bound variable from the note-on, so pitch + 7
(semitones) tracks the performance instead of sitting on a fixed
note. Useful for parallel-fifth leads, octave doublers (+ 12),
bass sub-octave layers (- 12), or chord stabs on accented hits.
Random pan scatter
on note
modulate("osc1_pan", (random() - 0.5) * 1.6)
end
Scatter every note to a different point in the stereo field. A per-note random offset is something the mod matrix cannot do - an S&H LFO is global and time-driven, not re-rolled per voice.
osc1_pan is -1 (hard left) .. +1 (hard right); (random() - 0.5) * 1.6 spreads notes across roughly 80% of the width, kept off the extremes.
on release
Fires per voice at note-off, while the voice is still in its release tail.
Post release echo
on release
after 200
# play(pitch, velocity, durationMs) - pitch+12 is one octave up
# from the released note, velocity 50 = soft, 120 ms duration.
play(pitch + 12, 50, 120)
end
end
Trigger another note 200 ms AFTER the user lifts the key. The synth's own release behaviour cannot fire a fresh note - only the script can.
Darken the tail
on release
ramp("filt_cutoff", -3000, 500)
end
Round off the release: as soon as the key is lifted, glide this voice's filter cutoff down so the tail fades darker than it played. The release envelope shapes level, not timbre - only a script can tie a cutoff move to note-off for the same voice.
Cutoff offsets are in Hz; -3000 over 500 ms pulls the brightness out gradually as the note rings down.
on tick
Fires once per audio block, the tightest cadence a script runs at. Shape modulation the LFOs cannot.
Stepped wavetable
on tick
modulate("osc1_wt_pos", floor(phase * 4) / 4)
end
Quantize the wavetable position to 4 discrete frames per beat. The mod matrix and built-in LFOs always interpolate smoothly between values - only a script can write a stepped, non-smooth target.
floor(phase * 4) is 0, 1, 2 or 3 across the beat. Dividing by 4 gives osc1_wt_pos values 0.0, 0.25, 0.5, 0.75 - a 4-step staircase.
Asymmetric pulse
on tick
if phase < 0.7 # first 70% of the beat
modulate("filt_cutoff", 2000)
else
modulate("filt_cutoff", -1000)
end
end
Asymmetric square: 70% of each beat the cutoff is up, 30% down. The built-in LFO square shape is fixed at 50/50 duty - only a script can shape its own pulse width like this.
Filter cutoff offsets are in Hz (raw parameter units).
Trance gate
on tick
if phase > 0.5
modulate("master_vol", -1)
else
modulate("master_vol", 0)
end
end
An eighth-note volume gate: full level for the first half of every beat, silenced for the second. The classic "trance gate" chop. A gate LFO could approximate it, but here the duty is a number you read and edit directly.
phase runs 0..1 across the beat. modulate latches and holds across blocks, so the open half must restore 0 explicitly - otherwise the -1 offset (which pulls master_vol to its 0 floor) would stick shut.
on beat
Fires on each musical beat boundary. The home for sequencers and play
calls that land on the grid.
Polyrhythm
on beat
if index % 4 == 0
play(36, 110, 200)
end
if index % 6 == 0
play(43, 80, 150)
end
end
Two independent rhythms running simultaneously - a kick every 4 beats and a snare every 6. The arpeggiator can only play ONE rhythm at a time; a script can interleave any number.
Evolving pattern
on beat
play(60 + (index % 12), 90, 100)
end
A pattern that changes WITH the bar - pitch climbs one semitone per beat. Neither the arpeggiator nor the motif player can derive note choice from a running global counter like this.
Random octave bass
on beat
play(36 + floor(random() * 13), 100, 160)
end
Fire a random bass note inside one octave on every beat. With the default fixed seed the sequence is identical on every playback, so a bounce and a live take agree - generative but reproducible. The arpeggiator and motif player follow a written pattern; this invents one from the RNG.
36 is C2; floor(random() * 13) adds 0..12 semitones (a full octave).
on cc
Fires when a Control Change arrives. Turn a controller into a trigger or a discrete switch the mod matrix cannot reach.
Wheel triggers chord
on cc(1)
if value > 0.5
play(60, 100, 200)
play(64, 100, 200)
play(67, 100, 200)
end
end
Crossing the wheel above half fires a chord. A CC in the mod matrix can only modulate - it cannot fire notes. Only a script can turn an incoming MIDI CC into a note trigger.
Discrete switch
on cc(1)
set("filt_type", floor(value * 6))
end
CC selects a discrete filter type (LP12 / LP24 / HP / ...). The mod
matrix never targets stepped/discrete parameters - only continuous
ones. set is script-only and the right tool for switch parameters.
on pitchbend
Fires on the pitch wheel. Reshape the wheel response beyond the built-in linear bend.
Cubic bend
on pitchbend
modulate("master_tune", value * value * value * 1200)
end
A non-linear pitchbend curve: gentle near the centre, aggressive at the extremes. The synth's built-in bend is strictly linear; the mod-matrix bend source is also linear.
on aftertouch
Fires on channel pressure. Turn pressure into a trigger instead of a modulator.
Pressure triggers note
on aftertouch
if value > 0.85
# play(pitch, velocity, durationMs) - pitch 72 = C5, vel 60 = soft,
# 80 ms before the synth's normal release takes over.
play(72, 60, 80)
end
end
Hard pressure (above 85% of max) fires a one-shot note. Aftertouch in the mod matrix can only act as a modulator; only a script can use it to TRIGGER a note when it crosses a threshold.
on transport
Fires on a host play/stop edge. React to the transport and reset script- owned state.
Counter reset
on transport
if playing
note_count = 0
log("transport started")
end
end
Reset the shared counter when the host hits play. Pair this with the
"Global counter" Globals sample, which declares var note_count = 0.
The arpeggiator and motif player react to transport but cannot reset
script-owned state - only the script can.
Determinism
By default two listens to the same patch playing the same notes produce the
same audio. The script clock counts samples since load, not wall-clock time,
and random() is seeded from a fixed patch seed, so an offline bounce and a
live take agree. The generative samples above (random() bass, scattered
pan, index-driven patterns) are reproducible for this reason; add seed live in Globals only when you want a patch to vary between takes.