Skip to content

Multi-Dimensional Boxes


A basic engine box gives you one set of parameters -- a volume knob, an attack slider, a mode selector -- and it works great for a single-layer instrument. But real instruments are rarely that simple.

Consider a synthesizer with four sound layers. Each layer needs its own volume, pan, tuning, filter cutoff, and envelope settings. You can't create four separate engine boxes with duplicate parameters -- the boilerplate alone would be unmanageable, and the framework has better tools for this.

This is where multi-dimensional engine boxes come in. Instead of a flat list of parameters, you organize them across multiple axes -- layer and parameter, or key and parameter. The dimension system gives you a structured data grid; one engine holds all four layers' worth of parameters in a single declaration.

From One Dimension to Two


In the previous chapter, a single-dimension engine box looked like this:

define my_engine.NUM_DIMS := 1
define my_engine.DIMS     := par
define my_engine.MULTS    := par
define my_engine.SIZES    := 8

One dimension (par), one axis to address. my_engine.par[my_engine.par.VOLUME] gives you the volume value, and that's the whole story.

Now imagine four sound layers, each with its own volume, pan, and tuning. You need a second dimension -- the layer axis:

define layer_engine.NUM_DIMS := 2
define layer_engine.DIMS     := layer, par
define layer_engine.MULTS    := layer
define layer_engine.SIZES    := 4, 8

NUM_DIMS := 2 tells the engine it has two dimensions.

DIMS := layer, par names them. The par dimension is always required -- it's the parameter axis. The layer dimension is the new one you're adding.

MULTS := layer lists all non-par dimensions. The engine uses this to know which axes represent "copies" of the same parameter set. With one extra dimension, MULTS is just layer. If you had layers and articulations, it might be layer, artic.

SIZES := 4, 8 gives the size of each dimension, in the order they appear in DIMS. Four layers, up to eight parameters each.

Declaring the Dimensions


Each dimension needs its members declared:

define layer_engine.layer.MEMBERS := A, B, C, D

define layer_engine.par.MEMBERS := ...
    volume, ...
    pan, ...
    tune

define layer_engine.par.LONGNAMES := ...
    "Volume", ...
    "Pan", ...
    "Tune"

The layer dimension gets named members (A, B, C, D) that become integer constants after initialization. The par dimension works the same way as in a single-dimension engine.

Creating a Multi-Dimensional Engine


Instead of Data.CreateSingleDim(), you use Data.CreateMultiDim():

node LayerEngine:
    cb Init:
        Data.InitializeDimension(layer_engine, layer)
        Data.InitializeDimension(layer_engine, par)
        Data.CreateMultiDim(layer_engine)

You must initialize each dimension with Data.InitializeDimension() before creating the engine. After this, you have a 2D parameter space: four layers, each with its own volume, pan, and tune.

Addressing Multi-Dimensional Parameters


With two dimensions, addressing takes two indices -- the layer and the parameter:

{ Read Layer B's volume }
declare vol := layer_engine[layer_engine.layer.B].par[layer_engine.par.VOLUME]

{ Set Layer C's pan }
layer_engine[layer_engine.layer.C].par[layer_engine.par.PAN] := 500000

The bracket syntax [layer] selects the layer dimension, and .par[par_index] selects the parameter. This is the same par property from single-dimension engines, just with an extra dimension in front.

Response Functions with Extra Dimensions


In the par_cb.response and par_disp.response functions, the layer argument is provided automatically:

function layer_engine.par_cb.response(layer, par, value)
    select par
        case layer_engine.par.VOLUME
            { Apply volume to this layer's groups }
        case layer_engine.par.TUNE
            { Apply tuning to this layer's groups }
    end select
end function

The function signature now includes layer before par -- matching the dimension order from DIMS. The engine calls this function once per parameter per layer during reload, and once per change during interaction.

The display function works the same way:

function layer_engine.par_disp.response(layer, par, value, par_prefix_str, par_value_str)
    par_prefix_str := f'Layer < char_up[layer] >'

    select par
        case layer_engine.par.VOLUME
            par_value_str := f'< math.print_decimal(value, 4, 1) > dB'
        case layer_engine.par.PAN
            par_value_str := f'< value > %'
        case layer_engine.par.TUNE
            par_value_str := f'< value > st'
    end select
end function

When the user adjusts a parameter on Layer B, the display shows "Layer B: -6.0 dB". The par_prefix_str provides context; the par_value_str provides the formatted value. Together they tell the user exactly what they're adjusting.

The Meta File for Multi-Dimensional Engines


Setting ranges and defaults follows the same pattern as single-dimension engines, but defaults are set per-layer:

node LayerEngine.Meta:
    cb Init:
        declare l

        layer_engine.set_range(layer_engine.par.VOLUME, 0, 1000000)
        layer_engine.set_range(layer_engine.par.PAN,    0, 1000000)
        layer_engine.set_range(layer_engine.par.TUNE, -24,      24)

        for l := 0 to 3
            layer_engine[l].par[layer_engine.par.VOLUME].default := 500000
            layer_engine[l].par[layer_engine.par.PAN].default    := 500000
            layer_engine[l].par[layer_engine.par.TUNE].default   := 0
        end for
end node

set_range() is called once per parameter -- ranges apply across all layers. Defaults, however, are set per-layer, since you might want Layer A enabled by default but Layers B through D disabled, or different starting volumes for each layer.

Showing Multi-Dimensional Data Through the UI


You now have four layers of data. But on screen, you typically have one set of knobs -- a volume knob, a pan knob, and a tune knob, not twelve. When the user selects a different layer, those same knobs need to show and edit that layer's values.

The engine box's direct .ctrl_id binding can't express this kind of conditional routing. The next unit on GUI covers multiplex -- the manual routing system that connects shared UI controls to the correct data slot based on a selector like a layer tab, a key selection, or a mode toggle.


Multi-dimensional engines let you scale your parameter management across layers, articulations, keys, or any other axis without duplicating engine boxes. The dimension system handles the data grid. The response functions receive the extra dimension arguments automatically. You declare once, address by index, and the framework handles the rest.