Skip to content

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:

  1. Physical key still down → restored to original parent via ivls.restore_parent(vo)
  2. Key up, SOS was held but SUS still active → transfers to SUS runtime
  3. Key up, no remaining pedal → removed from KeyBuffer and 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.PlayEvent

Placing 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].sentVoice holds the voice that was ultimately triggered downstream after ivls.pass(). This is the voice to reparent, not self.
  • The keys.KeyBuffer must be cleaned up correctly on NoteOff. If keys.active remains ON after a voice is released without pedal, subsequent pedal-down events may incorrectly capture a dead voice.
  • 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