Skip to content

RT Scripting: Sustain Pedal (Pass 1)

A sustain pedal held down needs to let notes ring on past the moment the key is released, then cut them all off together the instant the pedal comes back up. Neither half of that is something the zone map or the processor chain can do on its own — it requires intercepting note-off events and the pedal’s MIDI controller before they reach the source, which makes it a job for an RT script: a Lua processor sitting in the event chain that sees every note and controller event as it happens.

Right-click patches -> main -> scripts in the file tree and choose Create new Script.

Creating a new script in the scripts folder

Name it pedal, then double-click it to open.

As with the zone mapping earlier, this walkthrough turns to the built-in AI before writing anything by hand. Open the AI chat panel the same way as before — the icon button in the top right of the editor window. This time the panel opens empty: pedal scripting is handled by a different AI agent than the one used for sample mapping, with its own, separate conversation.

With pedal.script open, type something like the following into the chat input and submit it:

create a sustain pedal script for my piano

As before, results can vary. Often the agent understands the request immediately and produces a complete, working script on the first attempt. In other cases it may ask a follow-up question first, or produce a script with a minor flaw that needs a correction round — usually resolved within a reply or two.

For completeness — and as a reference to compare an AI-generated result against — here is a script that implements the intended behaviour correctly. Open pedal.script in the editor and paste the following code. IMPORTANT: make sure to delete everything that’s already in the editor first! Select everything with ctrl+A / command+A, then paste the code over it:

-- BEGIN_INFO_BLOCK
scriptInfo = {
scope = 'event-processor',
parameters = {}
}
-- END_INFO_BLOCK
local pedalDown = false
local heldNotes = {}
function onProcessEvents(blockLen, events)
for i, event in ipairs(events) do
if event.type == sys_event_types.controller and event.controller == 64 then
local isDown = event.value >= 64
if isDown and not pedalDown then
pedalDown = true
elseif not isDown and pedalDown then
pedalDown = false
for id, _ in pairs(heldNotes) do
sys_set_note_length(event.offset, id)
end
heldNotes = {}
end
elseif event.type == sys_event_types.noteOff then
if pedalDown then
sys_set_note_length_to_infinite(event.id)
heldNotes[event.id] = true
end
end
end
end
if event.type == sys_event_types.controller and event.controller == 64 then
local isDown = event.value >= 64
if isDown and not pedalDown then
pedalDown = true
elseif not isDown and pedalDown then
...

The sustain pedal is standard MIDI CC 64. By convention, values from 0–63 mean “up” and 64–127 mean “down” — this script uses that same threshold (event.value >= 64) to collapse the continuous controller value down to a simple boolean, pedalDown. Half-pedaling (partial sustain, as a real piano’s mechanism allows) isn’t modelled here; the pedal is treated as fully up or fully down.

The and not pedalDown / and pedalDown guards make sure the transition logic only runs once per actual state change, not on every CC64 message — some controllers send a continuous stream of values while the pedal is held, not just a single message at the moment it crosses the threshold.

elseif event.type == sys_event_types.noteOff then
if pedalDown then
sys_set_note_length_to_infinite(event.id)
heldNotes[event.id] = true
end
end

Normally, a note-off event would end the note immediately. Here, if the pedal happens to be down at that exact moment, the note-off is effectively intercepted: sys_set_note_length_to_infinite extends the note indefinitely instead of letting it stop, and its event id is recorded in heldNotes so it can be found again once the pedal comes back up.

If the pedal is not down when a note-off arrives, this branch does nothing — the event passes through untouched and the note stops normally, exactly as it would without this script in place.

elseif not isDown and pedalDown then
pedalDown = false
for id, _ in pairs(heldNotes) do
sys_set_note_length(event.offset, id)
end
heldNotes = {}
end

The moment the pedal transitions back to “up,” every note being held in heldNotes gets its length fixed with sys_set_note_length(event.offset, id) — using the pedal-up event’s own offset as the note’s actual end point, so every held note stops in sync, exactly when the pedal lifts. heldNotes is then cleared, ready for the next pedal press.

Switch to the main.flr layer editor. Drag a Script processor from the processor list and drop it before the source processor — the same position used for the sequencer script in the 303 walkthrough, so it sees every note and controller event before the source acts on them.

Script processor inserted before the source processor

Drag pedal.script from the file tree and drop it into the script processor where it reads <drop a script here>.

Unfold the script console. It should report successfully compiled.

Script console showing "successfully compiled"

With that in place, the piano can be played with correct sustain-pedal behaviour: holding the pedal down lets notes ring on past key-up, and releasing it cuts every held note at once.

  • End of Pass 1: instrument is playable, sound-designed, and pedal-aware