Creating and Copying Voices¶
Until now, voices have been created automatically when keys are pressed. The framework handles the creation, populates the note and velocity fields, and sends the voice into the appropriate flow based on your keymaps. You haven't had to think about where voices come from.
But many instruments need to create voices programmatically. A multi-layer instrument needs to spawn one voice per active layer when a single key is pressed. A release-noise engine needs to create a new voice when a key is released. A legato instrument might need to duplicate a voice, modify its note, and send it into a different flow.
IVLS gives you direct control over voice creation and routing.
Copying the Current Voice¶
The most common operation is creating a copy of the current voice. ivls.new_voice() does this:
declare new_vo := ivls.new_voice(self)This creates a new voice that's a copy of self -- same note, same velocity, same voice fields, same flow and stage. The new voice is not sent anywhere yet; it's just an allocated copy sitting in memory, waiting for you to modify it and send it on its way.
You'll typically modify the copy before sending it:
declare new_vo := ivls.new_voice(self)
Voice[new_vo].note := Voice[self].note + 12 { Transpose up an octave }
ivls.play(new_vo)ivls.new_formal_voice¶
ivls.new_formal_voice() also creates a copy, but with one important difference: it creates a new VMC (Voice Modulation Container) for the copy:
declare new_vo := ivls.new_formal_voice(self)A VMC is the container that holds modulation data -- fades, volume adjustments, and other real-time modifications. When you use ivls.new_voice(), the copy shares its parent's VMC, meaning modulation applied to one affects the other. When you use ivls.new_formal_voice(), the copy gets its own independent VMC.
Use ivls.new_formal_voice() when the new voice needs independent modulation -- for example, when spawning layer voices that each need their own volume fade. We'll cover VMCs in detail in Unit 6.
Sending Voices with ivls.play¶
Once you've created and configured a new voice, you send it into the flow with ivls.play():
declare new_vo := ivls.new_voice(self)
Voice[new_vo].my_layer := 2
ivls.play(new_vo)ivls.play() fires the new voice as a child of the current voice (self). The new voice continues from the next stage in the flow -- it doesn't re-enter the current node.
The Fan-Out Pattern¶
A common pattern in multi-layer instruments is fan-out: one incoming voice spawns multiple child voices, one per active layer. Here's how Polycore's Spawn.Layers node does it:
node Polycore.Spawn.Layers:
cb NoteOn:
declare l
for l := 0 to 3
if _true(plc.layers[l].enable) and _false(plc.layers[l].mute)
declare new_vo := ivls.new_formal_voice(self)
Voice[new_vo].thread := l
Voice[new_vo].plc.sound_id := plc.layers[l].sound_id
ivls.play(new_vo)
end if
end for
end nodeFor each enabled layer, the node creates a formal voice (with independent modulation), sets the layer's thread and sound ID on it, and plays it. The original voice (self) stays in place as the parent. Each child voice continues through the rest of the flow independently.
One-Shot Voices¶
Sometimes you want a voice to play for a fixed duration and then release itself automatically. ivls.play.oneshot() handles this:
declare release_vo := ivls.new_voice(self)
ivls.play.oneshot(release_vo, 500000) { Play for 500ms }The second argument is the duration in microseconds. The voice will auto-release when the duration expires. This is useful for release noises, one-shot sound effects, or any sound with a predetermined length.
Forwarding the Current Voice with ivls.pass¶
ivls.pass() is different from creating a new voice. Instead of spawning a copy, it forwards the current voice to the next node in the flow:
node MyModifier:
cb NoteOn:
Voice[self].vel := math.clamp(Voice[self].vel * 2, 1, 127)
ivls.pass()
end nodeThink of ivls.pass() as saying "I'm done with this voice, send it along." The voice continues to the next stage in the flow. If you don't call ivls.pass() and don't call ivls.play(), the voice stops at your node -- the chain ends there.
The distinction matters:
ivls.play(vo)sends a new voice (one you created withivls.new_voice()) as a child of the current voiceivls.pass()forwards the current voice to the next stage
Most nodes that modify a voice and pass it along use ivls.pass(). Nodes that spawn multiple voices use ivls.play() in a loop.
Combining pass and play¶
You can use both in the same node. For example, a node that passes the original voice through and spawns an additional harmony voice:
node AddHarmony:
cb NoteOn:
{ Pass the original voice through }
ivls.pass()
{ Also create a harmony }
declare harmony := ivls.new_voice(self)
Voice[harmony].note := Voice[self].note + 7
ivls.play(harmony)
end nodeVoice creation is one of the most powerful tools in IVLS. By copying, modifying, and routing voices programmatically, you can build instruments that generate complex multi-voice textures from a single key press. The next pages will cover how these voices relate to each other through the voice tree, and how that tree structure enables automatic cleanup when notes are released.