Skip to content

Timers and Envelopes

A timer is a script-generated event that fires at a future time. Unlike note or controller events, timers originate inside a script — not from incoming MIDI. They are the mechanism for anything that needs to happen on a schedule: note delays, arpeggiator steps, sequencer pulses, LFO ticks.

A timer can be configured to fire once, or to repeat at a fixed interval until explicitly cancelled. Recurring timers are the natural fit for periodic tasks (arp, sequencer); one-shot timers suit delayed actions (echo, staccato shortening).

-- One-shot: fires once after 0.5 seconds
local id = sys_create_timer_event(0, 0.5, 1)
-- Recurring: fires indefinitely every quarter note
local id = sys_create_timer_event(0, sys_get_quarter_note_length(), nil)
-- Cancel an indefinite timer
sys_dispose_event(id)

Timers arrive as events inside onProcessEvents. Handle them by checking event.type:

function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
if event.type == sys_event_types.timer then
-- event.id is the same id returned by sys_create_timer_event
end
end
end

A recurring timer fires on every beat and emits a note. The timer is started once at top level (when the script loads):

-- BEGIN_INFO_BLOCK
scriptInfo = { scope = 'event-processor', parameters = {} }
-- END_INFO_BLOCK
local pulseTimerId = sys_create_timer_event(0, sys_get_quarter_note_length(), nil)
function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
if event.type == sys_event_types.timer and event.id == pulseTimerId then
sys_create_note_event(event.offset, 1, 36, 100, 0.05) -- short kick pulse
end
end
end

For playing back a MIDI file continuously, use onProcessBlock rather than timers — see Examples for a complete implementation.


A custom control is a piecewise-linear envelope that is attached to a note event and modulates a target parameter over the note’s lifetime. You build the envelope by inserting individual breakpoints — each one a time/value coordinate. The engine draws a straight ramp between consecutive breakpoints.

Custom control events are created by the script and forwarded to the engine alongside the linked note. They are not received as incoming events.

Each breakpoint specifies a target UID — the destination being modulated. There are two built-in targets available from the sys_default_target_uids global:

KeyTarget
sourceVolumeVolume of the PCM source
sourceTuneTuning of the PCM source

For other modulation destinations, pass the component UID of a script modulator configured in the layer. This lets you drive filter cutoff, send levels, or any other addressable parameter.

Each call to sys_create_custom_control_event inserts one breakpoint:

sys_create_custom_control_event(offset, linkedEventId, targetUid, value)
  • offset — block-relative time of the breakpoint (same coordinate as event.offset)
  • linkedEventId — the note event this envelope belongs to
  • targetUid — what to modulate
  • value — the envelope value at this point in time

The envelope implicitly starts at its default value (1.0 for sourceVolume, 0.0 for sourceTune) at the note’s start. Breakpoints then define how the value evolves.

function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
if event.type == sys_event_types.noteOn then
-- Create a 0.5-second note
local id = sys_create_note_event(
event.offset, event.channel, event.note,
event.velocity, 0.5)
-- Add a breakpoint at the note's end: ramp volume from 1 → 0
sys_create_custom_control_event(
event.offset + 0.5,
id,
sys_default_target_uids.sourceVolume,
0)
sys_dispose_event(event.id)
elseif event.type == sys_event_types.noteOff then
sys_dispose_event(event.id)
end
end
end

Call sys_create_custom_control_event multiple times on the same note to build a more complex shape:

local id = sys_create_note_event(
event.offset, event.channel, event.note,
event.velocity, 2.0)
-- Attack: ramp up to 1.2 in 0.05s (overshoot)
sys_create_custom_control_event(event.offset + 0.05, id,
sys_default_target_uids.sourceVolume, 1.2)
-- Decay: settle to 0.8 by 0.2s
sys_create_custom_control_event(event.offset + 0.2, id,
sys_default_target_uids.sourceVolume, 0.8)
-- Release: fade to 0 at note end
sys_create_custom_control_event(event.offset + 2.0, id,
sys_default_target_uids.sourceVolume, 0)

You can attach envelopes to multiple targets on the same note by calling sys_create_custom_control_event with different targetUid values. Each target is evaluated independently:

-- Volume fade
sys_create_custom_control_event(event.offset + 1.0, id,
sys_default_target_uids.sourceVolume, 0)
-- Pitch slide (e.g. +2 semitones by note end)
sys_create_custom_control_event(event.offset + 1.0, id,
sys_default_target_uids.sourceTune, 2)