Skip to content

Engine Box

The Engine Box (renamed from "Data Module" in January 2025) is IVLS's parameter management layer. It provides automatic bidirectional UI synchronization, serialization, modulation targeting, and callback dispatch for a named set of parameters across one or more dimensions.

Every interactive feature in a product should be backed by an Engine Box. The box is the single source of truth for parameter values; widgets are merely views into it.

Why Engine Boxes Exist

Without a centralized parameter system, products manually sync knob positions, save/restore preset values, and fire callbacks on every widget interaction. This code is repetitive and error-prone. The Engine Box handles all of it: move a knob and the par updates; change a par in code and the knob updates; load a preset and all pars restore silently. The product only needs to implement what happens in response to a change.

Key Concepts

The Three-File Pattern

Current best practice splits each data domain across three files. Polycore and Sylvan Phrases both use this layout:

plc-layers-data.ksp    ← box declaration, par_cb, par_disp
plc-layers-meta.ksp    ← ranges, defaults, moddable flags
plc-layers-ui.ksp      ← ctrl_id bindings, multiplex

This separation means adding parameters never touches display or binding code. Meta values and UI binds can be regenerated independently (even from a Python build step).

File 1: Box Declaration and Callbacks

Declare dimensions and members at file scope, initialize them in cb Init, and implement par_cb.response and par_disp.response in cb Functions.

For a multi-dimension box (Polycore layers):

define plc.layers.NUM_DIMS := 2
define plc.layers.DIMS     := layer, par
define plc.layers.MULTS    := layer
define plc.layers.SIZES    := 8, 128       // oversized for snapshot compat

define plc.layers.layer.MEMBERS := A, B, C, D
define plc.layers.par.MEMBERS   := enable, sound_id, volume, pan, amp_a, amp_d, ...

The two response functions drive all behavior:

  • par_cb.response fires when a linked widget is moved, when a par is set in code, or when a knob alias glides through entries. It does not fire on DAW reload or snapshot load.
  • par_disp.response provides the human-readable label for the Kontakt info area and NKS display. Always set both par_prefix_str (the slot label) and par_value_str (the formatted value).
function plc.layers.par_cb.response(layer, par, value)
    select par
        case plc.layers.par.ENABLE
            call uir.update()
            purge.execute()
        case plc.layers.par.SOUND_ID
            plc.layers[layer].family_id.raw := plc.get_sound_trigger_info(value, plc.trigger.FAMILY)
            purge.execute()
        case plc.layers.par.VOLUME
            plc.group_update_par(layer, plc.layers.par.VOLUME)
    end select
end function

For a single-dimension box (Sylvan global pars), the same structure applies but Data.CreateSingleDim is used instead of Data.CreateMultiDim.

File 2: Ranges, Defaults, Moddable Flags

function plc.layers.par_ranges()
    plc.layers.set_range(plc.layers.par.VOLUME,      0, 1000000)
    plc.layers.set_range(plc.layers.par.COARSE_TUNE, -24, 24)
    plc.layers.set_range(plc.layers.par.AMP_A,       0, 1000000)
    // ...
end function

function plc.layers.par_moddable()
    plc.layers[l].par[plc.layers.par.VOLUME].moddable := TRUE
    plc.layers[l].par[plc.layers.par.SOUND_ID].moddable := FALSE
    // ...
end function

File 3: Widget Binding and Multiplex

Each parameter gets a ctrl_id assignment. Use Jinja loops for repeated structures:

cb Init:
    <! for l in range(0, 4) !>
        plc.layers[<:l:>].volume.ctrl_id := get_ui_id(plc.ui.MainPage.Rack.<:l:>.Volume)
        plc.layers[<:l:>].amp_a.ctrl_id  := get_ui_id(plc.ui.LayerPage.<:l:>.Sound.EnvVolA)
    <! endfor !>

cb PostReload:
    call plc.layers.push_data_id_to_ctrls()
    call plc.layers.update_ctrls()

Always call push_data_id_to_ctrls() and update_ctrls() in cb PostReload so widgets reflect the loaded state after every preset change.

Bidirectional Sync

Setting a par in code automatically updates any linked widget. Moving a widget automatically updates the par and fires par_cb.response. No manual sync is needed for parameters with ctrl_id set.

Multiplex

Multiplex is used when a single physical widget shows data for whichever layer, key, or slot is currently selected. Two override functions control routing:

  • multiplex.to_ui(dims..., par) — called when the engine needs to push a value to a shared widget
  • multiplex.to_engine(ui_id) — called when a shared widget fires

Register shared widgets with listen_widget(ui_id) and parameters with listen_par(par_id), then call register_multiplex(). The search(my_knobs_array, ui_id) pattern identifies which slot's knob was moved.

Oversizing Dimensions for Snapshot Compatibility

Box dimension SIZES should always be larger than the current product's parameter count. Polycore declares SIZES := 8, 128 for a product with 4 layers and ~60 active parameters. Sylvan declares SIZES := 16 for 5 globals.

If a snapshot is loaded where the stored length differs from the declared length, IVLS rejects the load and logs an error. Oversizing prevents this when the product grows. Rule of thumb: round up to the next power of two, or to 128 for parameter dimensions.

Connections to Other Parts of IVLS

Engine Boxes wire directly to ui-routines (via call uir.update() in par_cb.response), ui-callbacks (the box handles widget binding so most on ui_control() blocks are unnecessary), purge (via purge.execute() when layer enable or sound ID changes), and serialization (the box participates in snapshot save/restore through Data.PCCB).

Patterns and Caveats

  • par_cb.response does not fire on snapshot load. For post-load work, use cb PostReload or cb Reload.
  • The version system (get_current_version, check_version.response) exists for schema migrations. As long as you only append parameters to the end of MEMBERS, version can stay at 0 indefinitely.
  • Data IDs must be globally unique per product. Two boxes with the same ID cause silent data corruption in snapshots.
  • UI.NULL is the correct value for ctrl_id when a parameter has no linked widget. Do not leave it unset or use raw -1.
  • ui-routines — named update functions that respond to par changes
  • ui-callbacks — widget binding and the on ui_control() pattern
  • serialization — NKA save/load and the snapshot type 3 gotcha
  • purge — purge.execute() is commonly called from par_cb.response