Skip to content

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.LAYERS

Threads 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 node

Key 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 node

Access 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 node

5. 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 node

6. 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 node

This ensures that disabling a layer also unloads its samples from RAM.

Verify

  1. Enable multiple layers and play a note -- you should hear all active layers sounding simultaneously
  2. Mute/solo individual layers -- only the expected layers should play
  3. Adjust per-layer parameters (volume, pan, tune) -- each layer should respond independently
  4. 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