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, multiplexThis 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.responsefires 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.responseprovides the human-readable label for the Kontakt info area and NKS display. Always set bothpar_prefix_str(the slot label) andpar_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 functionFor 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 functionFile 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 widgetmultiplex.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.responsedoes not fire on snapshot load. For post-load work, usecb PostReloadorcb Reload.- The version system (
get_current_version,check_version.response) exists for schema migrations. As long as you only append parameters to the end ofMEMBERS, 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.NULLis the correct value forctrl_idwhen a parameter has no linked widget. Do not leave it unset or use raw-1.
Related¶
- 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