How to Create a Multi-Layer Instrument¶
Set up an instrument that plays multiple sound layers simultaneously, with per-layer parameters and independent voice processing.
Prerequisites¶
- A working IVLS product with single-layer playback
- Multiple sample groups in Kontakt, one per layer
- Understanding of flows and voice fields
Steps¶
1. Define layer count and thread mapping¶
Set ivls.THREADS to the number of layers. Each layer gets its own thread, allowing independent voice processing per layer:
define Polycore.LAYERS := 4
define ivls.THREADS := Polycore.LAYERSThreads are a core IVLS concept -- each voice belongs to a thread, and nodes can read thread to know which layer a voice belongs to.
2. Create the Spawn node¶
The Spawn node runs on each note-on and creates one voice per active layer. Each voice gets a different thread value corresponding to its layer:
node MyProduct.Spawn.Layers:
cb NoteOn:
declare l
{ Check for solos }
declare solos_active := FALSE
for l := 0 to MyProduct.LAYERS - 1
solos_active := solos_active .or. my.layers[l].solo
end for
for l := 0 to MyProduct.LAYERS - 1
{ Check if this layer should play }
declare legal := FALSE
if _true(my.layers[l].enable) ...
and _false(my.layers[l].mute) ...
and (_false(solos_active) or _true(my.layers[l].solo))
legal := TRUE
end if
if _true(legal)
{ Create a new voice and assign it to this layer's thread }
declare new_vo := ivls.new_formal_voice(self)
Voice[new_vo].thread := l
{ Copy layer-specific data to the voice }
Voice[new_vo].my.sound_id := my.layers[l].sound_id
ivls.play(new_vo)
end if
end for
end nodeKey points:
- ivls.new_formal_voice(self) creates a voice that inherits properties from the parent
- Voice[new_vo].thread := l assigns the voice to the layer's thread -- all downstream nodes can read thread to apply per-layer behavior
- Each voice is dispatched with ivls.play(new_vo) and continues through the flow independently
3. Create a 2D engine box for per-layer parameters¶
Use a multi-dimensional engine box where the first dimension is the layer and the second is the parameter index. This gives each layer its own independent set of controls:
{ Engine box dimensions }
define my.layers.NUM_DIMS := 2
define my.layers.DIMS := layer, par
define my.layers.MULTS := layer
define my.layers.SIZES := 8, 128
{ Layer names }
define my.layers.layer.MEMBERS := A, B, C, D
{ Parameter names }
define my.layers.par.MEMBERS := ...
enable, ...
sound_id, ...
volume, ...
pan, ...
mute, ...
solo, ...
coarse_tune, ...
fine_tune
node MyProduct.LayerData:
cb Init:
Data.InitializeDimension(my.layers, layer)
Data.InitializeDimension(my.layers, par)
Data.CreateMultiDim(my.layers)
cb Reload:
Data.PCCB(my.layers)
cb UICBS:
Data.UICBS(my.layers)
cb Functions:
Data.Functions(my.layers)
end nodeAccess layer parameters anywhere using my.layers[layer_index].parameter_name. For example, my.layers[0].volume is Layer A's volume.
4. Register Spawn before existing flow nodes¶
Place the Spawn node at the beginning of your flow, before any per-voice processing. The spawned voices continue through the remaining nodes:
node MyProduct.Flows:
cb Flows:
define FLOWS += my.playback.base_path, my.playback.sound_path
{ Spawn layer voices first }
ivls.register_node(my.playback.base_path, MyProduct.Spawn.Layers)
ivls.register_node(my.playback.base_path, Modrix.Sys.ReceiveInput)
ivls.register_node(my.playback.base_path, Stl.Pedals)
ivls.register_node(my.playback.base_path, MyProduct.Divert.PlayModes)
{ Sound output path -- voices arrive here with thread already set }
ivls.register_node(my.playback.sound_path, MyProduct.Modify.VelVolume)
ivls.register_node(my.playback.sound_path, MyProduct.PlayEvent)
end node5. Use thread in PlayEvent for group selection¶
In your PlayEvent node, use thread to select the correct sample group per layer:
node MyProduct.PlayEvent from Stl.PlayEvent.Template:
cb EventGroups:
{ Select the group based on the layer (thread) }
event.groups[my.layers.get_layer_sound_group(thread)] := TRUE
{ Route audio output to per-layer bus }
redirect_output(Voice[event_vo].event, OUTPUT_TYPE_BUS_OUT, thread)
end node6. Add purge per layer¶
Create a purge node that enables/disables sample groups based on which layers are active:
node MyProduct.LayerPurge:
cb Init:
declare purge_layer
declare purge_layer_state
{ Conservative purge: all groups off by default }
purge.default_group_state := FALSE
cb Purge:
for purge_layer := 0 to MyProduct.LAYERS - 1
purge_layer_state := my.layers[purge_layer].enable
purge.group_states[my.layers.get_layer_sound_group(purge_layer)] := purge_layer_state
end for
end nodeThis ensures that disabling a layer also unloads its samples from RAM.
Verify¶
- Enable multiple layers and play a note -- you should hear all active layers sounding simultaneously
- Mute/solo individual layers -- only the expected layers should play
- Adjust per-layer parameters (volume, pan, tune) -- each layer should respond independently
- Disable a layer and check Kontakt's memory usage -- the purged layer's samples should be unloaded
Further reading¶
- Guide: Multi-Dim Engine Boxes for the full 2D box API
- Guide: Threads for how thread assignment drives per-layer behavior
- Guide: Purge for sample memory management