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 nodeRoutings 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 node6. Link macro controllers (optional)¶
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 nodeVerify¶
Play a note and open the Modrix UI. You should be able to:
- Create a new modulator (e.g. LFO)
- Route it to one of your engine box parameters
- See the parameter value move in real time as the modulator runs
- Create routings to multiple parameters from the same modulator
- 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_offsetwork - Guide: Modrix for conceptual background