Stl.Pedals¶
Stl.Pedals handles sustain (CC 64) and sostenuto (CC 67) pedal logic for the IVLS voice system. It works entirely through the IVLS runtime API — pedal-held notes are reparented into a dedicated runtime so they survive the normal NoteOff voice release, and are properly cleaned up when the pedal is lifted.
Why This Exists¶
Without pedal logic at the voice level, releasing a key always kills the voice. The sustain pedal needs to hold voices alive after key release, then release them all when the pedal is lifted. Sostenuto has more complex semantics — it latches only the notes physically held at the moment the pedal goes down. Stl.Pedals implements both through the IVLS runtime system, which is designed exactly for this kind of deferred-release ownership.
Key Concepts¶
Key State Tracking¶
Three parallel arrays track per-note, per-channel state:
| Array | Purpose |
|---|---|
keys.pressed[ch, note] |
Physical key is currently held down |
keys.active[ch, note] |
Note is audibly sounding (key OR pedal) |
keys.pedal_type[ch, note] |
Which pedal holds this note (SUS, SOS, or -1) |
keys.KeyBuffer is a 2D ADT list (channel × slot) holding voice references for all active notes per channel. It is the source of truth for pedal reparenting.
Runtime-Based Voice Ownership¶
When a pedal goes down, a new runtime container is created:
pedal.runtime[midi_ch, pedal.type.SUS] := ivls.create_runtime()Notes are moved into this runtime via ivls.reparent_voice. The runtime keeps voices alive independent of their original note-event parent. When the pedal is lifted, each held note is handled based on its current state:
- Physical key still down → restored to original parent via
ivls.restore_parent(vo) - Key up, SOS was held but SUS still active → transfers to SUS runtime
- Key up, no remaining pedal → removed from
KeyBufferand released
Finally, ivls.release_runtime(runtime) destroys the empty container.
NoteOn Behavior¶
cb NoteOn:
keys.pressed[mp.midi_ch, mp.note] := ON
keys.active[mp.midi_ch, mp.note] := ON
ivls.pass() // downstream play node fires
if pedal.multi_states[mp.midi_ch, pedal.type.SUS] = TRUE
ivls.reparent_voice(NodeEnv[nenv].sentVoice, pedal.runtime[mp.midi_ch, pedal.type.SUS])
end if
keys.KeyBuffer.append(mp.midi_ch, NodeEnv[nenv].sentVoice)ivls.pass() fires first so the downstream play node creates the Kontakt event. The voice is then moved into the sustain runtime (if the pedal is already held) using NodeEnv[nenv].sentVoice.
NoteOff Behavior¶
If no pedal holds the note (pedal_type = -1), the note is marked inactive and the voice release propagates normally. If a pedal holds the note, keys.pressed is cleared but keys.active remains ON and the voice stays alive in the pedal runtime.
Sustain vs. Sostenuto Semantics¶
Sustain grabs all currently active notes on pedal down, except notes already owned by SOS.
Sostenuto latches only notes physically held at the instant the pedal depresses. Notes pressed after SOS goes down are not captured. When SOS is released while SUS is held, SOS-tagged notes that no longer have a physical key down migrate to the SUS runtime rather than being freed immediately.
Positioning in the Flow¶
Stl.Pedals must appear before the play-event node in the flow, because it calls ivls.pass() and then captures NodeEnv[nenv].sentVoice to reparent the voice that was triggered downstream:
... → Stl.RoundRobins → Stl.Pedals → Stl.PlayEventPlacing it after the play node would mean sentVoice is stale.
Public API¶
function keys.get_pedal_state(midi_ch, type) -> return
// Returns pedal.state.UP or pedal.state.DOWN
function keys.get_pedal_value(midi_ch, type) -> return
// Returns raw CC value (0–127)Use keys.get_pedal_state to query pedal status from other nodes — for example, a legato node that wants to suppress legato behavior when the sustain pedal is held.
Connections to Other Parts of IVLS¶
Stl.Pedals is positioned just before stl-play-event in the flow so it can capture the played voice handle. The voice runtime system it uses (ivls.create_runtime, ivls.reparent_voice, ivls.release_runtime) is the same API used by other ownership-holding systems. stl-legato implementations may query keys.get_pedal_state to gate legato detection when the sustain pedal is held.
Patterns and Caveats¶
- Flow ordering is critical. Pedals before PlayEvent is non-negotiable.
NodeEnv[nenv].sentVoiceholds the voice that was ultimately triggered downstream afterivls.pass(). This is the voice to reparent, notself.- The
keys.KeyBuffermust be cleaned up correctly on NoteOff. Ifkeys.activeremainsONafter a voice is released without pedal, subsequent pedal-down events may incorrectly capture a dead voice.
Related¶
- stl-play-event — PlayEvent fires downstream from Pedals; voice reparenting depends on this ordering
- stl-legato — legato implementations may query
keys.get_pedal_state - voices — runtime ownership is part of the voice tree manipulation API