Skip to content

Voices

A Voice is a pool-allocated struct that represents a note event in flight through an instrument. When a MIDI key is pressed, IVLS allocates a voice, fills it with the incoming MIDI data, assigns it to a flow, and dispatches it through that flow's nodes one stage at a time. When the key is released, the voice receives a NoteOff dispatch and is eventually returned to the pool.

Voices carry all state a node chain needs to do its work: MIDI data, timing information, routing references, modulation handles, and any product-specific fields you add. A voice is not a sound — it is the data envelope that travels through the pipeline and eventually tells a play node what to trigger.

Why Voices Exist

Before the voice system, products managed note state with arrays indexed by note number or event ID. This broke down quickly with polyphony, layering, legato, and pedal logic. A voice is a named, routable, tree-structured unit of state that makes all of these things composable without special-casing.

The pool allocation model means voices have O(1) allocation and deallocation, no garbage collection, and a fixed memory footprint regardless of how many notes arrive.

Key Base Fields

Every voice has these fields from the SDK's core schema:

Field Default Description
input -1 VOICE_ON or VOICE_OFF — current dispatch type
vo_parent / vo_child / vo_left / vo_right -1 Tree links
auto_release TRUE Release when parent is released
vmc -1 VMC reference
note -1 MIDI note number (0–127)
vel -1 MIDI velocity (0–127)
flow 0 Current flow index
stage -1 Current stage within the flow
thread 0 Layer/thread index
volume / pan / tune 0 Volume (mb), pan, tune (cents × 1000)
event -1 Kontakt event ID, set after play_note() fires
rr 0 Round-robin position
note_on_time 0 ENGINE_UPTIME at note-on

Access any field with Voice[self].field_name inside a callback, or Voice[vo].field_name when operating on a specific voice reference.

Adding Custom Fields: vo_field

vo_field is the current syntax for extending the Voice struct with product-specific fields. The preprocessor expands it into member declarations, default values, and a validator — all three at once.

vo_field field_name = default_value if (validation_expression)

Declare vo_field in cb Init or cb ICB:

node MyProduct.Feature:
    cb ICB:
        vo_field tky.release.vol_offset = -100000 if (tky.release.vol_offset <= 1)
        vo_field plc.sound_id = -1 if ((search(plc.sound_ids, plc.sound_id) # -1) or (plc.sound_id = -1))
        vo_field lkh.on_action = -1 if (lkh.on_action >= -1)
end node

Then use the field anywhere in a voice callback:

cb NotePass:
    Voice[self].plc.sound_id := mf.pick_sound()

The vo_field macro generates Voice.ADD_MEMBERS and Voice.ADD_INIT entries — these are the accumulation points that the SDK merges into the final Voice schema.

The Voice Tree

Voices form a tree through four link fields: vo_parent, vo_child, vo_left, vo_right. This quadruple-linked structure makes deletion O(1) — removing a voice reconnects its siblings. When a parent voice is released, all children with auto_release = TRUE are released in a cascade.

The vmc tree mirrors this structure but is managed by reference counting — a VMC stays alive as long as any voice still references it, giving the developer a mechanism to keep modulation data alive beyond a note's natural release point.

Iterating children:

for_each_child(parent_vo)
    // iter_child = current child voice index
    // sibling = next sibling (pre-cached so deletion is safe)
    ivls.release_voice(iter_child)
end_for_each_child()

for_each_child pre-caches the next sibling before entering the loop body, so you can safely release iter_child without losing your place.

Voice Dispatch Loop

When a voice enters a flow, the SDK's Voice.CB taskfunc processes it stage by stage:

  1. If voice.delay > 0, wait according to delay_type before starting
  2. For each stage while not exhausted and voice not invalid:
  3. If the stage is a FLOW redirect, reflow immediately and continue
  4. If input_type = VOICE_OFF, run NoteOff, then delete the voice
  5. If the node is NotePass, run inline in the current taskfunc
  6. If the node is NoteOn, launch a new async taskfunc
  7. If ivls.pass() was called, self := sentVoice and advance stage
  8. When the flow is exhausted, the voice is done

For NotePass nodes the voice stays in the same taskfunc. For NoteOn nodes a new taskfunc is launched — this is why NoteOn can wait and NotePass cannot.

Creating Voices

// Copy a template voice — inherits all fields
declare new_vo := ivls.new_voice(tpl)

// Copy + allocate a new child VMC
declare formal_vo := ivls.new_formal_voice(tpl)

// Copy + replace VMC + fire as a new child in the current nenv context
ivls.clone_voice(vo, nenv)

ivls.new_voice is the cheapest option. Use ivls.new_formal_voice when the new voice needs its own modulation scope. Use ivls.clone_voice to immediately launch a copy as a child. After creating, set fields and call ivls.play(vo) to launch:

cb NoteOn:
    declare rel_vo := ivls.new_voice(self)
    Voice[rel_vo].note := Voice[self].note
    ivls.reflow_voice(rel_vo, mf.release_flow)
    ivls.play(rel_vo)
    ivls.pass()

Manipulating the Voice Tree

ivls.release_voice(vo)                    // trigger NoteOff dispatch
ivls.release_voice_children(vo)           // release all auto_release children
ivls.unparent_voice(vo)                   // re-parent to root (omni) voice
ivls.reparent_voice_to_voice(vo, parent)  // move under a specific parent
ivls.move_children(old_parent, new_parent) // adopt children, then release old
ivls.prune_branch_to_voice(vo)            // delete sole-parent ancestors up to vo
ivls.add_child_to_parent(vo, parent_vo)   // insert as first child

ivls.move_children is standard in legato transitions: the new note voice adopts the children of the old note voice, keeping sounds alive, then the old voice is released.

Runtimes

Runtimes let a system take ownership of voices to prevent auto-release when a parent is released. The sustain pedal uses this pattern: it claims active voices so physical key release does not kill them.

declare r := ivls.create_runtime()
ivls.add_voice_to_runtime(r, vo)   // sets auto_release := FALSE
// ... later ...
ivls.release_runtime(r)            // release all voices in runtime and destroy it
ivls.restore_parent(vo)            // remove from runtime and restore auto_release

Up to 1024 runtimes can be active concurrently. Each runtime holds a FIFO of voice references.

Time Waits

Available in NoteOn callbacks (not NotePass):

ivls.wait_ms(ms)          // safe — checks path_on, fires PathCancellation if voice died
ivls.wait_us(micros)
ivls.wait_ticks(ticks)

ivls.wait_ms.unsafe(ms)   // skips path_on check
ivls.wait_us.unsafe(micros)
ivls.wait_ticks.unsafe(ticks)

Safe variants are the default. Use unsafe only when you own the voice's lifetime and have already confirmed it cannot die during the wait — for example, inside a modulation loop on a voice you control exclusively.

How It Connects to Other Parts of IVLS

Voices traverse flows. Their modulation scope is managed by the vmc tree. The keymap system determines which flow a voice enters when a key is pressed. Product-specific behavior is added by declaring vo_field entries and writing to those fields in nodes.

Patterns and Caveats

self is valid only inside a voice-flow callback. It is the Voice index for the currently processing voice. Outside a node callback, always use explicit voice references (Voice[vo].field).

auto_release defaults to TRUE. Child voices created from a parent will be released when the parent is released unless auto_release is set to FALSE or the voice is moved to a runtime.

ivls.move_children is the legato voice-handoff primitive. When a new note takes over from an old one, move the old note's playing children to the new note voice before releasing the old one.

Do not ivls.release_voice inside for_each_child unless you use iter_child. The macro pre-caches the sibling, so releasing iter_child is safe. Releasing some other voice during the loop can corrupt the traversal.

  • flows — the routing pipelines voices traverse
  • vmc — persistent modulation containers that mirror the voice tree
  • nodes — the processing units voices pass through