Skip to content

The Voice Tree


In the previous page, we created child voices with ivls.new_voice() and ivls.play(). But we didn't talk about what happens when those voices are released. If a key is pressed and spawns four layer voices, what happens when the key is released? Do you have to manually track and release each one?

No. IVLS handles this through the voice tree -- a hierarchical structure that connects every voice to its parent and its children. When a parent is released, all of its children with auto_release enabled are released automatically.

How the Tree Works


Every voice has four relationship pointers:

  • vo_parent -- the voice that spawned this one
  • vo_child -- the first (most recent) child of this voice
  • vo_left -- the previous sibling (an older child of the same parent)
  • vo_right -- the next sibling (a newer child of the same parent)

When you call ivls.play(new_vo), the new voice is inserted as a child of self. If self already has children, the new voice becomes the head of the sibling list, pushing existing children to the right.

Consider a key press that spawns three layer voices:

     [Key Voice]
     /    |    \
  [L1]  [L2]  [L3]

Key Voice is the parent. L1, L2, and L3 are siblings linked by their vo_left and vo_right pointers. The parent's vo_child points to the most recently added child.

Auto-Release


Every voice has an auto_release field, which defaults to TRUE. When a parent voice is released (either by a key-up event or by ivls.release_voice()), the framework automatically releases all children whose auto_release is TRUE.

This cascades: if a child has its own children, they're released too, and so on down the tree. A single key release propagates through the entire branch.

This is exactly what makes the fan-out pattern from the previous page work cleanly. You spawn multiple voices from a key press, and when the key is released, they all get cleaned up automatically -- no manual tracking required.

Releasing Voices Manually


Sometimes you need to release voices explicitly:

ivls.release_voice(vo) releases a specific voice. If that voice has auto_release children, they're released too:

ivls.release_voice(some_vo)

ivls.release_voice_children(vo) releases all auto_release children of a voice without releasing the voice itself:

ivls.release_voice_children(self)

This is useful when you want to clear a voice's children and replace them with new ones -- for example, when a legato transition replaces the current sound layer but keeps the parent voice alive.

Iterating Children


The for_each_child macro lets you loop over all direct children of a voice:

for_each_child(self)
    { iter_child is the current child voice }
    if Voice[iter_child].my_layer = target_layer
        ivls.release_voice(iter_child)
    end if
end_for_each_child()

Inside the loop, iter_child holds the reference to the current child. The macro handles the sibling traversal safely -- you can even release children during iteration, because the next sibling is cached before the loop body executes.

Why the Tree Matters


The voice tree isn't just a convenience for cleanup. It's fundamental to how complex instruments work.

Consider a legato instrument: when you play a new note while holding the previous one, the instrument needs to crossfade from the old sound to the new one. The old note's voice tree might look like this:

     [Note C4]
     /       \
  [Layer A]  [Layer B]

When legato transitions to D4, the instrument releases the C4 voice. Because Layer A and Layer B are children with auto_release, they're released too -- their sounds fade out naturally. The new D4 voice spawns its own layers. The tree structure means the instrument doesn't need to manually track which specific layer voices belong to which note.

The same principle applies to any hierarchical sound design: drum kit voices with sub-layers, ensemble instruments with per-section voices, or phrase engines with overlapping patterns.

Advanced Tree Operations


IVLS provides additional tree manipulation functions for more complex scenarios: ivls.move_children() transfers all children from one parent to another, and ivls.prune_branch_to_voice() walks up the tree to find the highest releasable ancestor. These are advanced tools used in legato systems and voice management -- they'll be covered in later units.


The voice tree gives your instrument automatic lifecycle management. By structuring voices as parent-child relationships, you get clean, predictable cleanup without manual bookkeeping. The next page covers runtimes -- a mechanism that can override the tree's auto-release behavior when you need voices to outlive their parents.