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 nodeStl.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 hereExample: 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 nodeConnections 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¶
EventGroupsis called afterplay_note(). The event ID is already set whenEventGroupsfires, soset_event_par_arrcalls inside it apply to the correct event.Stl.Pedalsmust appear beforePlayEventin the flow. Pedals usesNodeEnv[nenv].sentVoiceto reparent the voice that was triggered downstream. If Pedals comes after PlayEvent,sentVoiceis 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 remainFALSE.
Related¶
- 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