Skip to content

Stl.PlayEvent

Stl.PlayEvent and Stl.PlayEvent.Template are the standard library nodes responsible for firing Kontakt note events from within the IVLS voice system. Every product that triggers audio uses one of these as its event-trigger node.

Why This Exists

Firing a Kontakt note event correctly requires more than calling play_note(). The note needs volume, pan, and tuning applied from the voice's fields. Groups must be allowed or blocked. The VMC must register the new sound voice. A minimum-hold guard must prevent near-zero-duration events from being silent. Stl.PlayEvent.Template packages all of this into a well-tested base, leaving products to customize only what they need to.

Key Concepts

The from Inheritance Pattern

Products never instantiate Stl.PlayEvent.Template directly. Instead they declare a product-specific node that inherits from it:

node MyProduct.PlayEvent from Stl.PlayEvent.Template:
    cb EventArgs:
        // customize event.note, event.vel, event.offset, event.duration

    cb EventGroups:
        // set event.groups[grp] := TRUE for each allowed group

    cb EventDismiss:
        // cleanup before note_off fires
end node

Stl.PlayEvent is the concrete default implementation. Inherit from Stl.PlayEvent.Template (not Stl.PlayEvent) when you need custom group logic.

Virtual Callbacks

Callback Called when Purpose
EventArgs Before play_note() Override event.note, event.vel, event.offset, event.duration
EventGroups After play_note(), before group-allow write Set event.groups[grp] := TRUE for each allowed group
EventDismiss On NoteOff, before note_off() Release resources, cancel modbits, cleanup

The event_vo local holds the voice index at the point these callbacks fire.

NoteOn Internal Flow

EventArgs
  → play_note() → Voice[self].event
  → change_vol / change_pan / change_tune  (from voice fields)
  → set_event_par(event, EVENT_PAR_3, ENGINE_UPTIME)  (minimum-hold guard)
  → reset event.groups[] to FALSE for all slots
  → EventGroups
  → set_event_par_arr(..., EVENT_PAR_ALLOW_GROUP, ...) for every group
  → VMC.register_sound_voice(self)
  → ivls.pass()  (if more nodes remain in the flow)[optional] VMC.deregister_sound_voice(self)  (if freeze_mod_on_play = TRUE)

NoteOff Internal Flow

EventDismiss
  → minimum-hold guard: if ENGINE_UPTIME <= stored play time, wait
  → note_off(Voice[self].event)
  → VMC.deregister_sound_voice(self)

The minimum-hold guard prevents premature note-offs when the host sends a NoteOff with near-zero latency after NoteOn, which would otherwise cause Kontakt to not trigger the sample at all.

freeze_mod_on_play

When freeze_mod_on_play = TRUE, the node calls VMC.deregister_sound_voice immediately after ivls.pass() on NoteOn rather than waiting for NoteOff. This freezes VMC-driven volume modulation at the instant of trigger — useful for one-shot samples where re-evaluation after trigger is unwanted.

Positioning in a Flow

Stl.PlayEvent (or its product subclass) is always the terminal node in a note-on flow:

Polycore.Spawn.Layers
  → Polycore.Modify.VelVolume
  → Stl.RoundRobins
  → Stl.Pedals
  → Polycore.PlayEvent          ← play_note() happens here

Example: Polycore Implementation

Polycore handles drum-kit sounds (fixed trigger note) and menu sounds (note cycling within a variation span):

node Polycore.PlayEvent from Stl.PlayEvent.Template:
    cb EventArgs:
        declare trigger_note := plc.get_sound_trigger_info(sound_id, plc.trigger.NOTE)
        if trigger_note # -1
            event.note := trigger_note       // oneshot: use fixed note
        else
            declare playback_span := plc.get_sound_trigger_info(sound_id, plc.trigger.VARIATIONS)
            if playback_span > 0
                declare modulo_buffer := int(ceil(float(root) / float(playback_span))) * playback_span
                event.note := (event.note - root + modulo_buffer) mod playback_span + range_start
            end if
        end if

    cb EventGroups:
        event.groups[plc.layers.get_layer_sound_group(thread)] := TRUE
        redirect_output(Voice[event_vo].event, OUTPUT_TYPE_BUS_OUT, thread)
end node

Connections to Other Parts of IVLS

PlayEvent sits at the boundary between the voice pipeline and the Kontakt audio engine. Before it: stl-round-robins writes the RR position, stl-pedals reparents the voice if a pedal is held. After it: the vmc system registers the sound voice so modulation fades and VMC modbits can reach the playing sample. stl-legato nodes set Voice[self].legato.* fields that are read by downstream nodes.

Patterns and Caveats

  • EventGroups is called after play_note(). The event ID is already set when EventGroups fires, so set_event_par_arr calls inside it apply to the correct event.
  • Stl.Pedals must appear before PlayEvent in the flow. Pedals uses NodeEnv[nenv].sentVoice to reparent the voice that was triggered downstream. If Pedals comes after PlayEvent, sentVoice is stale.
  • The event.groups[] array has capacity 10,000 to match Kontakt's maximum group count. Products only need to set the slots they care about; all others remain FALSE.
  • stl-round-robins — RR position is written before PlayEvent runs
  • stl-pedals — must precede PlayEvent in the flow
  • stl-modulation-fades — VMC modbits affecting volume are activated by VMC.register_sound_voice inside PlayEvent
  • vmc — register_sound_voice and EMCache are set up here