Modrix¶
Modrix is a self-contained modulation matrix that can be added to any IVLS product. It supplies normalized modulator outputs, routes them to product parameters with depth and curve transforms, and keeps the UI in sync through a processing loop that runs on CC events and on a 5 ms LCB timer. The most complete implementation is in Super Audio Cart 2, which combines Polycore and Modrix.
Why Modrix Exists¶
Standard MIDI modulation (LFO, CC mapping) is hard to extend, impossible to introspect, and disconnected from the instrument's own parameter system. Modrix replaces this with a product-aware matrix: modulators know about the product's parameters, depths are bounded by each parameter's actual range, and the whole system is serialized and managed through the engine-box infrastructure.
Key Concepts¶
The Three Concepts¶
Modulators are sources of normalized signal, outputting floats in [-1.0, 1.0] (bipolar) or [0.0, 1.0] (unipolar). Built-in types: LFO, AHDSR, CC, Velocity, Random, Stepper, Pitch Bend, Mono AT, Key Range, Vel Range, Keyswitch, Macros, XY. Custom modulator types can be added.
Routings connect one modulator to one parameter. Each routing stores a modulator index, depth, curve, bounded mode flag, and a four-integer route vector encoding the target system and parameter.
Systems are participating engines that own parameters. Polycore, Console, and custom product systems are all "systems" in Modrix terms. Each system implements a 16-function API (9 required, 7 optional).
Route Encoding¶
Each routing stores a four-integer vector:
| Index | Meaning |
|---|---|
| 0 | System index (modrix.system.POLYCORE, etc.) |
| 1 | Dimension 1 (e.g. layer index) |
| 2 | Dimension 2 (e.g. parameter index) |
| 3 | Dimension 3 (unused or -1 for most systems) |
"Fake" values are intentional: if the max real layer index is 3, the value Polycore.LAYERS (4) signals "apply to all layers." The system's modulate function must handle this, and the browser must write the same fake value when the user selects "All Layers."
The 16-Function System API¶
Each system must implement 9 required functions and may implement 7 optional functions:
Required (9):
| Function | Purpose |
|---|---|
get_control_min(routing) -> min |
Engine minimum |
get_control_max(routing) -> max |
Engine maximum |
get_control_base(routing) -> base |
Current parameter value |
get_routing_name(routing) -> name |
Display name |
get_routing_category(routing) -> cat |
Display category |
modulate(routing, arg) |
Apply computed offset to the parameter |
base_from_proxy(routing, ui_id) |
Widget value → engine |
base_to_proxy(routing, ui_id) |
Engine value → widget |
set_base_default(routing, ui_id) |
Reset widget to default |
Optional (7):
| Function | Purpose |
|---|---|
routing_created(routing) |
Called when a new routing is created |
routing_deleted(routing) |
Called when a routing is deleted |
dismiss_destination(routing) |
Dismiss/cleanup for a destination |
xy_to_system(routing, x, y) |
XY pad forwarding |
get_routing_key(routing) -> key |
MIDI key scoping |
get_routing_compat(routing) -> compat |
Compatibility check |
get_routing_thread(routing) -> thread |
Thread assignment |
The api.ksp template generates all dispatch wrappers automatically from the Python data model.
The Processing Loop¶
Modrix runs on every CC event and on a 5 ms LCB timer:
cb CC:
call modrix.run_matrix()
cb LCB:
if modrix.cache.active_mods.count > 0
and abs(ENGINE_UPTIME - modrix.last_lcb_run) >= 5
modrix.run_matrix()
modrix.last_lcb_run := ENGINE_UPTIME
end ifThe active_mods.count > 0 guard skips the matrix entirely when nothing is routed. The LCB guard prevents back-to-back runs on fast hosts.
modrix.run_matrix() proceeds in three phases:
- Read modulator values: For every active modulator, call
tick(m)to update phase/state, thenprocess(m) -> outto read normalized output. - Map routings: Routings are pre-sorted by destination. For each destination group, sum ADD-style routings and multiply SCALE-style routings.
- Bind to controls: Compute scaled modulation and apply via
system.modulate().
Bounded Mode¶
In Bounded OFF mode, the offset is applied as a blanket addition clamped to the control's range.
In Bounded ON mode, the offset scales asymmetrically: negative offsets scale against the distance from base to min, positive offsets against the distance from base to max. With a base at 70%, a -100% modulation produces -70%, not -100%, because it scales within the 70-unit left span.
Caching and Invalidation¶
modrix.recache() runs whenever the routing matrix changes. It validates routings, sorts them by destination, builds the active modulator list, and fires __RUN_CB__(Modrix.RefreshCache). The sort-by-destination is what enables O(R) processing instead of O(R×D).
Polycore uses cb Modrix.RefreshCache to re-analyze which parameters are actively routed and clear stale mod_offset values on unrouted parameters, preventing ghost modulation from accumulating when routings are deleted.
Console Sync Pattern¶
When Modrix and Console both control the same parameter, a directional protocol prevents feedback loops:
- Moving a Modrix knob → write to Console knob and run its callback
- Moving a Console knob → silently write to the Modrix engine without triggering par_cb
Python Package¶
Modrix is a pip-installable Python package. Product data is defined in Python and KSP code is generated via Jinja templates:
import modrix
mx = modrix.create([...modulators...], nka_data_dir)
mx.add_system('polycore')
mx.add_system('console')
code = modrix.emit(mx)
modrix.create() builds the data model and writes a _meta.nka file containing engine_min, engine_max, default, moddable, random_min, and random_max for every modulator-parameter pair.
Connections to Other Parts of IVLS¶
Modrix routing objects are pool-allocated using the type-system. Cache lists use the array-backed structures from data-structures. The engine-box provides the parameter ranges and moddable flags that Modrix reads. The vmc system is used for applying modulation to Kontakt parameters on active sound voices.
Patterns and Caveats¶
- The browser must write routing destination values using the same encoding that
modulate()reads. Browser columns are not a separate display system — they write directly into routing storage. - Systems are registered by calling
mx.add_system('name')in Python. Order of registration determines the system constant index. Changing order breaks existing saved routings. - The LFO's rate parameter maps to frequency Hz via an exponential curve spanning sub-0.1 Hz to audio rate. The coefficient range is
RATE_COEF_MIN := -4.5639toRATE_COEF_MAX := 3.9121.
Related¶
- type-system — routing and modulator objects are pool-allocated types
- data-structures — cache lists use array-backed structures
- engine-box — parameter ranges, defaults, and moddable flags