Skip to content

Your First Script

This chapter walks through writing a minimal RT script from scratch. By the end you will have a working transposer — a processor that shifts every incoming note up or down by a configurable interval before passing it on.

A transposer is the simplest useful RT script:

  • Receives every noteOn and noteOff event
  • Adds a semitone offset to the pitch
  • Forwards the modified note
  • Passes all other events (controllers, timers) through unchanged

Every script must start with a script info block. For the transposer, we want one parameter — the semitone offset — so the player can adjust it from the UI without touching the script:

-- BEGIN_INFO_BLOCK
scriptInfo = {
scope = 'event-processor',
parameters = {
Transpose = {
type = 'interval',
minValue = -24,
maxValue = 24,
defaultValue = 0,
},
}
}
-- END_INFO_BLOCK

The interval type represents a semitone offset — exactly what we need. Once this script is loaded, a Transpose parameter will appear in the layer editor and can be bound to a UI control, automated, or adjusted per-preset.

Understanding the NoteOn / NoteOff Relationship

Section titled “Understanding the NoteOn / NoteOff Relationship”

Before writing any note-manipulation code, there is one key thing to understand: a noteOn and its matching noteOff carry the same event.id, because they refer to the same underlying engine note event.

This means you cannot simply replace both independently. The correct approach is:

  1. On noteOn: create a new note event (with infinite length, since the release is in the future), store a mapping from the incoming ID to the new event’s ID, and dispose the original.
  2. On noteOff: look up the mapping to find the note we created, call sys_set_note_length to finalize its length at the release time, and dispose the original noteOff.

The mapping table must be script-level (outside the callback) so it persists between blocks.

local active = {} -- maps incoming event id -> created event id
function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
local t = scriptInfo.parameters.Transpose.value
if event.type == sys_event_types.noteOn then
-- Create a transposed note with infinite length
local newId = sys_create_note_event(
event.offset, event.channel, event.note + t, event.velocity, nil)
-- Remember the mapping so we can finalize on noteOff
active[event.id] = newId
-- Remove the original from the queue
sys_dispose_event(event.id)
end
end
end
elseif event.type == sys_event_types.noteOff then
local newId = active[event.id]
if newId then
-- Finalize the length of the created note at the release position
sys_set_note_length(event.offset, newId)
active[event.id] = nil
end
sys_dispose_event(event.id)
end

The if newId guard handles edge cases such as a noteOff arriving without a prior noteOn (e.g. on script load with keys already held).

Forwarding is the default: any event not explicitly disposed passes through automatically. Controllers, timers, and anything else the script does not touch require no code at all.

-- BEGIN_INFO_BLOCK
scriptInfo = {
scope = 'event-processor',
parameters = {
Transpose = {
type = 'interval',
minValue = -24,
maxValue = 24,
defaultValue = 0,
},
}
}
-- END_INFO_BLOCK
local active = {} -- maps incoming event id -> created event id
function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
local t = scriptInfo.parameters.Transpose.value
if event.type == sys_event_types.noteOn then
local newId = sys_create_note_event(
event.offset, event.channel, event.note + t, event.velocity, nil)
active[event.id] = newId
sys_dispose_event(event.id)
elseif event.type == sys_event_types.noteOff then
local newId = active[event.id]
if newId then
sys_set_note_length(event.offset, newId)
active[event.id] = nil
end
sys_dispose_event(event.id)
end
end
end

Once the transposer works, experiment with:

  • Transposing down instead of up (negative offset)
  • Randomising the offset within a range on each noteOn
  • Only transposing notes above a certain pitch threshold
  • Keeping the original note and adding a transposed copy alongside it — the seed of a harmonizer (covered fully in Examples)