Skip to content

Core Concepts

Scripts receive four event types: noteOn, noteOff, controller, and timer. The first two map directly to traditional MIDI — a script sees a noteOn when a key is pressed and a noteOff when it is released. This means a script can react to either edge independently, which is necessary for things like sustain pedal logic, legato detection, or choking voices on release.

Controller events (CC, pitch bend, aftertouch) represent instantaneous state changes with no duration.

Timer events are generated by the script itself on a schedule you define.

Internally, the engine represents notes as single events with a duration rather than as on/off pairs. This is worth knowing because it shapes how emitted notes work: when a script plays a note it specifies a length in seconds rather than scheduling a separate note-off. A note played from a live keyboard has infinite length until the release arrives. The details are covered in the event type reference; for now, just know the distinction exists between what comes in (noteOn/noteOff) and what goes out (a length-bearing note event).

Every script processor owns a queue of pending events. At the start of each render block, incoming events are placed into this queue. When onProcessEvents returns, everything still in the queue is forwarded to the next processor.

This has a simple but important consequence: forwarding is the default. A script that does nothing to an event will pass it through automatically. To actively do something, a script either creates new events or removes existing ones.

To prevent an event from reaching the next processor, call sys_dispose_event with its ID:

sys_dispose_event(event.id) -- remove one event
sys_dispose_all_events() -- remove everything currently in the queue

A script that calls sys_dispose_all_events() at the top of onProcessEvents acts as a complete MIDI silencer — nothing passes through unless the script explicitly creates new events of its own.

sys_create_note_event, sys_create_control_event, sys_create_timer_event, and sys_create_script_event add new events to the queue. They are forwarded alongside any events that were not disposed.

To change an event’s properties (pitch, velocity, etc.), dispose the original and create a replacement with the modified values. The new event enters the queue at the same logical time offset as the original.

Every script must begin with a script info block — a Lua table called scriptInfo enclosed between two mandatory comment markers:

-- BEGIN_INFO_BLOCK
scriptInfo = {
scope = 'event-processor',
parameters = {
-- optional parameter definitions
}
}
-- END_INFO_BLOCK

The markers are required and must contain nothing but the scriptInfo definition. scope must always be 'event-processor'.

The parameters table lets a script declare engine parameters it owns. These behave like any other engine parameter: they can be bound to UI controls, saved with the patch, and automated by the host. They also give the script a way to receive per-instance configuration — a harmonizer can expose a Transpose interval parameter, a delay can expose a Time parameter in milliseconds, and so on.

parameters = {
Transpose = {
type = 'interval',
minValue = -24,
maxValue = 24,
defaultValue = 0,
},
}

A parameter’s current value is readable directly from the definition — but only inside a callback (not at top-level script load time):

function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
if event.type == sys_event_types.noteOn then
local shifted = event.note + scriptInfo.parameters.Transpose.value
end
end
end

The .value shorthand is the most convenient way to read an own parameter inside a callback. When you need the UID — for instance to pass to sys_subscribe_to_parameter or sys_set_parameter_value — use sys_get_own_parameter_uid("Name").

To read any parameter’s value outside a callback — at top-level script load time, for instance — use sys_get_parameter_value. It has the opposite restriction from .value: valid only at top level, never inside a callback. Its use is discouraged in general; sys_subscribe_to_parameter is almost always the better choice, since its return value already gives you the current value and its callback keeps you in sync with future changes.

See the API Reference for the full list of supported parameter types.

Each script processor has a completely isolated Lua environment. Scripts do not share globals, upvalues, or any state. The only ways to communicate across scripts are:

  1. Script events — create a new event carrying an arbitrary payload; downstream scripts receive it through the normal event flow (see Script Events)
  2. Cross-script messages — send Lua values to a named channel; UI scripts can subscribe to these (see Cross-Script Messaging)

Note that cross-script messages cannot be used for RT-to-RT communication — RT processors execute in a fixed order and message delivery does not fit that model. Use script events for passing information from one RT script to the next.

Script processors execute in the order they appear in the layer’s processor chain. The first processor sees the raw incoming events; each subsequent processor sees only what the previous one forwarded. Order matters — place processors that filter or reshape events before the ones that depend on the result.