Skip to content

How to Integrate the Modrix Modulation Matrix

Add a fully-featured modulation matrix to an existing IVLS product, connecting modulators (LFOs, envelopes, MIDI CCs, etc.) to your engine box parameters.

Prerequisites

  • A working IVLS product with engine box parameters
  • Python 3.13+
  • pip install modrix

Steps

1. Create the Modrix Python configuration

In your ivls-product.ksp, add a run<< block that imports Modrix, creates the modulation system with any product-specific modulators, registers your system, and emits the generated KSP.

run<<
    from pathlib import Path
    import modrix
    from modrix.models import Modulator, Parameter, ModulationType

    resource_path = Path(base_path).parent / 'Resources' / 'data'

    { Create the Modrix instance with optional extra modulators }
    my_modrix = modrix.create(
        [
            { Add any product-specific modulators here }
            Modulator(
                "MY_MOD",              { internal name }
                "My Custom Mod",       { display name }
                ModulationType.CONTINUOUS,
                False,                 { locked }
                False,                 { controller }
                [
                    Parameter("AMOUNT", "MY_MOD.Amount", 500000, 0, 1000000, "Amount", True, False, True),
                    Parameter("CURVE",  "MY_MOD.Curve",  0, -100, 100,      "Curve",  True, False, True),
                ]
            ),
        ],
        resource_path        { NKA output directory }
    )

    { Register your system so Modrix routes API calls to it }
    my_modrix.add_system('myproduct')

    code = modrix.emit(my_modrix)
>>

modrix.create() does two things: it builds the modulator registry (built-in types like LFO, AHDSR, Velocity, CC, etc. plus your extras) and saves a metadata NKA to resource_path. modrix.emit() renders all the Modrix KSP nodes via Jinja templates.

The built-in modulators include: LFO, AHDSR (Envelope), Stepper, Velocity, Velocity Range, Key Range, Keyswitch, MIDI CC, CC Range, Pitch Bend, Aftertouch, Random, Macro, and XY Pad.

2. Emit the generated code into your product

Immediately after the run<< block, output the rendered KSP:

<:code:>

This injects all Modrix node definitions into your script.

3. Implement the 9 required API functions

Modrix dispatches to your system through a set of API functions. You must implement these for each system you registered with add_system(). The function names follow the pattern yoursystem.method_name().

Create a node for your API implementations:

run<<
    { Define how to extract route dimensions from the routing integer }
    par_route = "MODRIX.ROUTE(routing, 1)"
>>

node MyProduct.ModrixAPI:
    cb Functions:
        { Return the minimum engine value for this routing's parameter }
        function myproduct.get_control_min(routing) -> return
            return := my_box.par[<:par_route:>].engine_min
        end function

        { Return the maximum engine value for this routing's parameter }
        function myproduct.get_control_max(routing) -> return
            return := my_box.par[<:par_route:>].engine_max
        end function

        { Return the current un-modulated base value }
        function myproduct.get_control_base(routing) -> return
            return := my_box.par[<:par_route:>]
        end function

        { Return a display name string for this routing's destination }
        function myproduct.get_routing_name(routing) -> return
            return := my_box.par_names[<:par_route:>]
        end function

        { Return a category string (e.g. "Engine: Filter") }
        function myproduct.get_routing_category(routing) -> return
            return := "Engine"
        end function

        { Apply the computed modulation offset to the parameter }
        function myproduct.modulate(routing, mod_offset)
            if my_box.par[<:par_route:>].mod_offset # mod_offset
                my_box.par[<:par_route:>].mod_offset := mod_offset
            end if
        end function

        { Read the base value from a UI proxy knob into the parameter }
        function myproduct.base_from_proxy(routing, ui_id)
            my_box.read_from_ui(<:par_route:>, ui_id)
        end function

        { Write the current parameter value out to a UI proxy knob }
        function myproduct.base_to_proxy(routing, ui_id)
            my_box.write_to_ui(<:par_route:>, ui_id)
        end function

        { Reset the parameter to its default when the user double-clicks }
        function myproduct.set_base_default(routing, ui_id)
            my_box.apply_par_default(ui_id, <:par_route:>)
        end function

        { Fill the destination browser columns for your system }
        function myproduct.fill_modrix_browser_destinations()
            declare row := 0
            declare par
            for par := 0 to my_box.par.SIZE - 1
                if my_box.par[par].moddable = TRUE
                    modrix.ui.browser.dest_names[1, row]  := my_box.par_names[par]
                    modrix.ui.browser.dest_values[1, row] := par
                    modrix.ui.browser.dest_types[1, row]  := modrix.ui.browser.dest_nav_type.PARAMETER
                    inc(row)
                end if
            end for
        end function
end node

Routings encode the target system, any sub-dimensions (like layer index), and the parameter index. Use MODRIX.ROUTE(routing, N) to extract route components -- slot 0 is the system ID, and subsequent slots are your custom dimensions.

There are also 7 optional API functions that you can override for advanced behavior: routing_created, routing_deleted, dismiss_destination, xy_to_system, get_routing_key, get_routing_compat, and get_routing_thread.

4. Add Modrix.Bundle and your API node to IVLS_NODES

define IVLS_NODES += ..., ...
                     Modrix.Bundle, ...
                     MyProduct.ModrixAPI, ...
                     ...

Modrix.Bundle expands to all internal Modrix nodes: Modrix.Box.Bundle, Modrix.Functions, Modrix.Cache, Modrix.API, Modrix.API.SelfFill, Modrix.Modulators, Modrix.Processing, Modrix.Manipulations, Modrix.Initialize, Modrix.GUI.Bundle, and Modrix.Scrollbars.

5. Add Modrix.Sys.ReceiveInput to your evaluation flow

Register the Modrix input receiver in the flow where voice playback begins, before any nodes that read modulated parameter values:

node MyProduct.Flows:
    cb Flows:
        ivls.register_node(my_play_flow, MyProduct.Spawn)
        ivls.register_node(my_play_flow, Modrix.Sys.ReceiveInput)   { process modulation }
        ivls.register_node(my_play_flow, MyProduct.Gate)
        ivls.register_node(my_play_flow, MyProduct.PlayEvent)
end node

If your product has macro knobs or XY pads, link them to Modrix controllers at init:

node MyProduct.ModrixAPI:
    cb Init:
        modrix.controllers.ctrl_link_add(modrix.controllers.par.macro_1, get_ui_id(MyUI.Macro.0))
        modrix.controllers.ctrl_link_add(modrix.controllers.par.macro_2, get_ui_id(MyUI.Macro.1))
        modrix.controllers.ctrl_link_add(modrix.controllers.par.macro_3, get_ui_id(MyUI.Macro.2))
        modrix.controllers.ctrl_link_add(modrix.controllers.par.macro_4, get_ui_id(MyUI.Macro.3))
        modrix.controllers.push_data_id_to_ctrls()
end node

Verify

Play a note and open the Modrix UI. You should be able to:

  1. Create a new modulator (e.g. LFO)
  2. Route it to one of your engine box parameters
  3. See the parameter value move in real time as the modulator runs
  4. Create routings to multiple parameters from the same modulator
  5. Delete routings and see parameter values snap back to their base

Further reading

  • Modrix module reference for the full API surface
  • Engine Box module reference for how parameters and .mod_offset work
  • Guide: Modrix for conceptual background