Script handlers
A script reacts to events. Each event has its own handler, written as
on <eventname> opening a block that closes with end. In Scoped
mode the editor handles the on ... end wrapper for you; this page
shows the assembled form.
Two handlers, note and release, run per voice. The rest run
globally: there is one instance of the handler, not one per voice.
Per-voice handlers see the voice's own voice var slots; global
handlers do not.
Every handler can read the always-available clock variables bpm,
time, and playing. The event-specific variables are listed under
each handler.
For the language itself, the parameter primitives, and the editor, see the scripting overview.
on init
Fires once, on the first audio block after the script is applied (or the patch is loaded). Use it to log a startup line, prime a global counter from a tuning constant, or warm up state.
Bound variables: bpm, time, playing.
on init
log("script loaded")
end
There is no matching shutdown handler. State that needs resetting is reset by re-applying the script.
on key
Fires on key-down, before a voice exists. It is the head of the note pipeline, ahead of the chord builder and the arpeggiator, so what this handler leaves behind is what those two act on.
Bound variables: pitch (0..127), velocity (0..127), channel,
length, plus the clock variables.
length is how long the note is scheduled to last, in milliseconds, the
same unit as play, ramp and after. Only a host that plays the note
from a clip and says how long it is can give one. A key played live, or
a note from a host that does not say, reads 0, so test length > 0
before treating a note as short. It is what was scheduled, not a
promise: releasing the key still ends the note.
on key
if length > 0 and length < 120
velocity = velocity * 0.55
end
end
pitch and velocity are writable here, and nowhere else. Assign to
them and the whole instrument sees the new value: the chord builder,
the arpeggiator, the voice, and MIDI Out.
on key
pitch = scale_quantize(pitch)
velocity = velocity * 0.8
end
skip() drops the key press so it makes no note. It drops the note and
not the key: the instrument still knows you are holding it, and
held_count() still counts it.
on key
if pitch < 48
skip()
end
end
Writing a pitch outside 0..127 also drops the note. It is not clamped, because a wrong note at the edge of the keyboard is worse than silence.
This is a global handler, since there is no voice yet. A voice var is
not available, and modulate from here writes the global lane, which
moves every sounding voice rather than the note arriving. To shape the
note that is arriving, use on note.
It does not fire for notes a script holds with hold(), or for the
arpeggiator's own output. A note the script generates never comes back
into the script.
on note
Fires per voice, at note-on, after the voice has been claimed but before audio for that note has rendered. Use it to make decisions that depend on which note was played and how hard.
It runs too late to change the pitch: the voice is already sounding it.
on key is the handler that can.
Bound variables: pitch (0..127), velocity (0..127), channel
(MIDI channel of the source note), length (as in on key), plus the
clock variables. Every tone of a chord carries the length of the key
that made it.
on note is the only handler where a voice var is initialised
fresh. Inside its body, the voice variables are at their declared
defaults; they keep their values across later on tick-style events
only for this voice's lifetime.
on note
if velocity > 100
ramp("filt_cutoff", 4000, 80)
end
end
A script-fired play(...) does not trigger another on note. The
handler runs for notes received from the keyboard or the host only,
not for notes the script itself starts.
on release
Fires per voice, at note-off. The voice is still alive at this point; it is in its release tail. Use it to schedule echoes, drop a parting modulation, or count releases per pitch.
Bound variables: pitch, channel, plus the clock variables. There
is no velocity here because note-off does not carry release
velocity in this version.
on release
after 200
play(pitch + 12, 50, 120)
end
end
pitch is the note that was released, which is the same note the
matching on note saw.
on tick
Fires globally, every 64 samples, and once more where a note starts between two of those. This is the tightest cadence a script can run at: about 750 times per second at 48 kHz, whatever the host's buffer size, so a script sounds the same in every project.
Bound variables: phase (0..1, the script clock's position within the
current beat), index (tick counter since script load), time
(seconds since load), plus the clock variables.
Use on tick to shape modulation that the LFOs cannot, or to advance
a step sequencer that the arpeggiator does not cover.
on tick
# Asymmetric pulse: 70% high, 30% low.
if phase < 0.7
modulate("filt_cutoff", 2000)
else
modulate("filt_cutoff", -1000)
end
end
on tick is global. To do per-voice work on every tick, work from
on note plus voice var plus after.
on beat
Fires globally on each musical beat boundary. The host's tempo and position drive the timing, so the handler is bar-locked across loops and bounces.
Bound variables: phase (0..1 within the beat at the moment the beat
fires; usually zero, but non-zero if a block crosses a beat boundary),
index (beat counter since load), plus the clock variables.
Use on beat for sequencers and step-counters. The handler is the
right home for play calls that should land on the grid.
on beat
# Two rhythms at once.
if index % 4 == 0
play(36, 110, 200)
end
if index % 6 == 0
play(43, 80, 150)
end
end
When the host is stopped, on beat does not fire. Beat counting is
tied to host transport.
on cc(N)
Fires globally when a MIDI CC with number N arrives on any channel. N
is part of the header: on cc(1) is the mod wheel, on cc(7) is
channel volume, on cc(74) is the filter-cutoff CC. A script can
have several on cc handlers, one per CC number.
Bound variables: value (0..1, the CC value normalised), channel,
plus the clock variables.
Use on cc to turn a CC into something the mod matrix cannot do:
fire a chord on a threshold, switch a discrete parameter, draw a
non-linear curve.
on cc(1)
if value > 0.5
play(60, 100, 200)
play(64, 100, 200)
play(67, 100, 200)
end
end
The CC also reaches the mod matrix in the normal way. The script handler runs in addition.
on pitchbend
Fires globally when the pitch wheel moves. Use it to draw a non-linear bend curve on top of the synth's built-in linear bend.
Bound variables: value (-1..+1, where 0 is centre), channel, plus
the clock variables.
on pitchbend
modulate("master_tune", value * value * value * 1200)
end
The synth's own pitch-bend handling still applies. The script's contribution adds on top, so a cube curve here gives a centre-soft, edge-aggressive bend without disabling the built-in one.
on aftertouch
Fires globally on channel aftertouch (the pressure value from many keyboards after a key is held). Polyphonic aftertouch is a separate modulation source in the mod matrix; this handler is the channel-pressure one.
Bound variables: value (0..1), channel, plus the clock variables.
on aftertouch
if value > 0.85
play(72, 60, 80)
end
end
Aftertouch is a modulator in the mod matrix; turning it into a note trigger or a discrete switch is script territory.
on transport
Fires globally on the edges of the host transport: when the host starts playing or stops. Use it to reset script state at the start of a section, or to log when the host moved.
Bound variables: playing (true on a start edge, false on a stop
edge), bpm.
on transport
if playing
note_count = 0
log("transport started")
end
end
on transport does not fire continuously while the host runs. It
fires at the edges only. For "every block while the host runs", use
on tick and read playing.