Voices¶
IVLS is an acronym which stands for the Interactive Voice Logic System. It offers a dedicated pattern flexible enough for a variety of powerful playback logics, including guitar fretting algorithms, articulation matrices, arpeggiators, step sequencers, and more.
In order to keep these sophisticated systems organized and maintainable, IVLS proposes that we should build large systems out of small, well-defined parts. It provides a mechanism for connecting independent features together.
In order for these connections to work, the framework needs a way to pass information between nodes during playback. This inter-node messaging protocol is referred to as the Voice.
A Voice is a data object that represents a note event traveling through your instrument. When a key is pressed, the framework creates a Voice, fills it with MIDI data (the note number, velocity, MIDI channel), and sends it into a flow -- the pipeline of nodes that defines your instrument's playback logic. As the Voice moves through the flow, each node reads its fields, modifies them, and passes it along.
Here's the analogy: if nodes are stations on a conveyor belt and the flow is the belt itself, then the Voice is the object on the belt. It carries all the information each station needs to do its job.
What's Inside a Voice¶
A Voice carries fields that describe the current state of the note. The most important ones to know first:
Voice[self].note-- the MIDI note number (0-127)Voice[self].vel-- the velocity (0-127)Voice[self].event-- the Kontakt event ID (set after playback triggers)Voice[self].volume,Voice[self].pan,Voice[self].tune-- playback modifiersVoice[self].delay-- delay before the voice plays (in microseconds or ticks)Voice[self].duration-- how long the voice should play before auto-releasingVoice[self].thread-- which layer or thread this voice belongs toVoice[self].rr-- the round robin position
Here's a simple example that reads a voice's fields:
cb NoteOn:
message(f'Note: <Voice[self].note>, Velocity: <Voice[self].vel>')There are more fields beyond these -- flow, stage, config, midi_ch, offset, and several internal ones. You'll encounter them as you go deeper. For now, the fields above cover the vast majority of what you'll work with.
self -- The Current Voice¶
You may have noticed the self keyword in the examples above. Inside cb NoteOn, cb NoteOff, and cb NotePass, the variable self always refers to the current Voice -- the one that arrived at this node in the flow. You don't create it or manage it. The framework hands it to you.
You read fields with the bracket syntax:
cb NotePass:
{ Read the note and velocity }
declare n := Voice[self].note
declare v := Voice[self].velAnd you write fields the same way:
cb NotePass:
{ Override the velocity to maximum }
Voice[self].vel := 127This is the fundamental pattern of IVLS programming: read the Voice, decide what to do, write the Voice.
How Voices Move Through Flows¶
A Voice enters a flow at stage 0 and visits each node in sequence. You'll learn more about flows on the next page, but for now, the key idea is that inside a node, the developer controls what happens to the Voice by calling one of a few commands:
ivls.pass()-- forward the voice to the next node in the flow. This is the most common action.ivls.play(vo)-- send a new voice into the flow (for spawning additional sounds like layers, harmonies, or release noises).- Do nothing -- the voice stops at this node. This is the case for terminal nodes like PlayEvent, or for
cb NotePassnodes where the voice advances automatically.
We'll see concrete examples of each shortly.
NoteOn, NoteOff, and NotePass -- Three Ways to Process a Voice¶
When a voice arrives at a node in the flow, the framework needs to run your code. But different nodes need different behavior. Some just modify a field and move on instantly. Others need to wait, create additional voices, or respond when the key is released. IVLS provides three callbacks to cover these cases.
cb NotePass¶
cb NotePass runs when a voice arrives, processes it synchronously, and the voice automatically advances to the next node. No waiting, no NoteOff, no async behavior. The voice passes through instantly.
This is the simplest and cheapest option. Use it for nodes that just read or modify voice fields without needing to wait:
{ Transpose all notes up one octave }
node MyProduct.Modify.Transpose:
cb NotePass:
Voice[self].note := Voice[self].note + 12
end nodeThe voice arrives, the note is transposed, and it immediately continues to the next node in the flow. No lifecycle management needed.
cb NoteOn¶
cb NoteOn runs when a voice arrives at this node during a key press. Unlike NotePass, the node is asynchronous -- it can wait, create copies, spawn children, and manage timing. The voice stays alive as long as the NoteOn code is running. When the key is released, the voice receives a NoteOff.
{ Play a note for exactly 500ms, then release it }
node MyProduct.Play.TimedNote:
cb NoteOn:
ivls.pass()
ivls.wait_ms(500)
ivls.release_voice(self)
cb NoteOff:
{ Clean up when the voice is released }
end nodeBecause NoteOn is async, the ivls.wait_ms(500) call pauses execution for half a second without blocking other voices. When the wait completes, the voice is explicitly released.
cb NoteOff¶
cb NoteOff runs on the same voice when the key is released (or when the voice is explicitly released via ivls.release_voice()). This is where you handle release behavior: play a release sample, start a fade, clean up state. The voice is still alive during NoteOff execution.
NoteOff is always paired with NoteOn -- you can't have a NoteOff without a NoteOn.
The Key Rule¶
Start with NotePass unless you need async behavior or NoteOff handling. NotePass is cheaper, simpler, and sufficient for any node that just reads or modifies voice fields. You only need NoteOn when your node needs to wait, manage timing, or respond to release.
The Voice as an Object¶
A Voice isn't just a message -- it's a pool-allocated object with a lifecycle. The framework manages a pool of Voice slots. When a note is pressed, a Voice is allocated from the pool. When the note is fully released and all processing is complete, the Voice is returned to the pool.
You access Voice fields through the bracket syntax: Voice[reference].field. The reference is an integer -- a slot index into the pool. self is just the reference for the current voice.
This means you can store voice references in variables and access other voices:
declare my_vo := ivls.new_voice(self)
Voice[my_vo].note := Voice[self].note + 7
ivls.play(my_vo)Here, my_vo holds a reference to a different voice in the pool. You can read and write its fields exactly like self, because the bracket syntax works with any valid voice reference.
Creating Voices¶
When you need more than one sound from a single key press -- harmonies, layers, release noises -- you create additional voices. IVLS provides two functions for this:
ivls.new_voice(self)-- creates a copy of the current voice. The copy shares the parent's modulation context (VMC).ivls.new_formal_voice(self)-- creates a copy with its own independent modulation context. Use this when the new voice needs independent fades, volume control, or tuning.
After creating a voice, modify its fields and send it into the flow with ivls.play():
cb NoteOn:
{ Create a harmony voice one fifth up }
declare harmony := ivls.new_voice(self)
Voice[harmony].note := Voice[self].note + 7
ivls.play(harmony)
{ Continue the original voice }
ivls.pass()The difference between the two functions matters when you get to modulation. A voice created with ivls.new_voice() shares its parent's fades and modulation -- if the parent fades out, the child fades with it. A voice created with ivls.new_formal_voice() gets its own independent modulation context, so it can fade, pan, and tune independently. We'll cover this in detail in Unit 6.
Here's a real-world example from a production instrument. This node spawns a separate voice for each enabled layer, giving each one its own modulation context and thread assignment:
cb NoteOn:
declare l
for l := 0 to MAX_LAYERS
if _true(plc.layers[l].enable)
declare new_vo := ivls.new_formal_voice(self)
Voice[new_vo].thread := l
ivls.play(new_vo)
end if
end forEach layer gets its own voice, its own thread, and its own modulation -- all from a single key press.
The Voice Tree¶
Every voice created from another voice becomes its child. This forms a tree:
Key Press (parent voice)
+-- Layer A voice (child)
+-- Layer B voice (child)
+-- Layer C voice (child)
When a parent voice is released, all children with auto_release = TRUE are released automatically. This is the default -- you get clean, cascading cleanup without manual bookkeeping. Press a key, spawn five layer voices, and when you release the key, all five are cleaned up for you.
Internally, the tree uses four pointers: vo_parent, vo_child, vo_left (sibling), and vo_right (sibling). This is a doubly-linked structure -- removing a voice reconnects its siblings automatically, making deletion O(1).
The voice tree is the foundation for the ownership system covered in Unit 5. For now, the important takeaway is that IVLS handles cleanup automatically through the parent-child relationship.
The Pinned State -- How the Framework Preserves Voice Data¶
When the framework copies a voice (via ivls.pass(), ivls.new_voice(), or ivls.new_formal_voice()), it creates a snapshot of the voice's fields at that moment. The copy starts with identical field values but is a separate object -- changes to one don't affect the other.
This is a subtle but important concept. Consider what happens when a node transposes a voice:
cb NoteOn:
{ self.note is currently 60 (Middle C) }
{ Create a copy -- harmony.note is also 60 }
declare harmony := ivls.new_voice(self)
{ Modify the copy -- harmony.note is now 67 }
Voice[harmony].note := Voice[self].note + 7
{ self.note is still 60 -- the copy didn't affect it }
ivls.play(harmony)
ivls.pass()After this code runs, two voices continue through the flow: the original at note 60, and the harmony at note 67. They are independent objects with independent fields.
This "pinning" is critical because nodes in a flow modify the voice as it passes through. If a node changes Voice[self].note, that change is visible to all subsequent nodes in the flow. But the original voice (before the copy) retains its original values. This is how IVLS maintains clean state boundaries between different branches of a voice tree -- each branch works with its own snapshot, unaffected by what other branches do.
Where Voices Go From Here¶
The Voice is the central concept in IVLS, and you'll work with it constantly. Everything else in the playback system builds on it:
- Flows (next page) define the pipeline that voices travel through -- how nodes are ordered and connected.
- Playing Sounds covers how voices ultimately trigger Kontakt events via PlayEvent.
- Unit 5 covers advanced voice mechanics: custom fields (
vo_field), the voice tree in depth, runtimes, and ownership. - Unit 6 covers modulation: VMCs, modbits, and fades -- the modulation system that parallels the voice tree.
For now, remember the core pattern: a key press creates a Voice, the Voice travels through a flow, and each node reads, modifies, or copies the Voice to do its work.