Modrix¶
A finished instrument doesn't just play samples -- it lets the performer shape the sound in real time. LFOs modulate filter cutoff. Envelopes control volume swell. MIDI CCs drive vibrato depth. A modulation matrix connects any modulation source to any destination parameter, with configurable depth and routing.
Modrix is a standalone modulation matrix system for IVLS instruments. It provides LFO, AHDSR envelope, CC, velocity, key range, random, stepper, and other modulator types, routable to any engine box parameter through a visual routing interface.
Modrix is a product subsystem, not part of core IVLS. It is integrated into products like Polycore that need deep modulation capabilities.
Python Package Integration¶
Modrix is configured through a Python package that generates both KSP code and NKA metadata. The typical integration has two steps:
1. Create the Modrix Instance¶
create() builds the modulation matrix configuration:
- Combines the built-in modulators (LFO, AHDSR, Stepper, Velocity, Key Range, CC, Pitch Bend, Aftertouch, Random, and others) with any product-specific extra modulators
- Generates engine box metadata (ranges, defaults, moddability flags) for every parameter of every modulator
- Saves the metadata as an NKA file for runtime loading
2. Emit the KSP Code¶
emit() renders the KSP source code from Jinja2 templates, producing all the node definitions, parameter engines, and routing infrastructure that the runtime system needs.
The output is imported into the product's build as generated code.
Modulator Types¶
Modrix ships with a comprehensive set of modulators:
| Type | Description | Key Parameters |
|---|---|---|
| LFO | Low-frequency oscillator | Waveform, Rate, Sync, Phase, Rectify, Width, Quantize |
| AHDSR | Envelope generator | Attack, Hold, Decay, Sustain, Release, Curves |
| Stepper | Step sequencer | Steps, Values, Key Map |
| Velocity | Note velocity mapping | Curve |
| Vel Range | Velocity range gate | Low, High, Continuous, Curve |
| Key Range | Key range mapping | Low, High, Continuous, Curve |
| CC | MIDI controller | CC Number, Curve |
| CC Range | CC range gate | CC Number, Low, High, Continuous, Curve |
| Pitch Bend | Pitch bend wheel | Curve, Quantize |
| Aftertouch | Channel aftertouch | Curve |
| Random | Random value generator | Unipolar, Noise, Curve |
| Keyswitch | Note-triggered switch | Note, Latch, Use Velocity |
| Macro | NKS macro knob | Index, Unipolar, Curve |
| XY Pad | Two-axis pad | Index, Axis, Unipolar, Curve |
Each modulator type is defined as a Modulator dataclass with a list of Parameter dataclasses. Parameters have names, ranges, defaults, and flags for moddability, table display, and labeling.
The System API¶
A product integrates with Modrix by implementing a set of system API functions. These functions tell Modrix how to read from and write to the product's parameter system.
There are 9 required functions that every participating system must implement:
get_control_min(routing)-- return the minimum value for the routed parameterget_control_max(routing)-- return the maximum value for the routed parameterget_control_base(routing)-- return the current unmodulated valuemodulate(routing, mod_offset)-- apply a modulation offset to the parameterbase_from_proxy(routing, ui_id)-- read a parameter value from a proxy UI controlbase_to_proxy(routing, ui_id)-- write a parameter value to a proxy UI controlset_base_default(routing, ui_id)-- reset a parameter to its default via a proxyget_routing_name(routing)-- return the display name for a routing destinationget_routing_category(routing)-- return the category name for a routing destination
And 7 optional functions for extended behavior:
fill_modrix_browser_destinations()-- populate the routing browser UIget_routing_compat(route, a, b)-- check if two routing slots are compatibledismiss_destination(routing)-- handle cleanup when a routing is removedxy_to_system(ui_id, pad_idx)-- sync XY pad values to the systemanalyze_routings()-- called on cache refresh to rebuild routing analysis
Each routing is encoded as a compact integer with fields for the target system, layer, and parameter. The MODRIX.ROUTE(routing, field) macro extracts individual fields.
The Processing Loop¶
At runtime, Modrix processes modulation in a continuous loop:
- Modulator tick -- each active modulator computes its current output value (LFO phase, envelope level, CC value, etc.)
- Routing map -- the system iterates through all active routings, computing the modulation offset for each destination by multiplying the modulator output by the routing depth
- Parameter bind -- for each routing, the
modulate()function is called on the target system, which applies the offset to the parameter's live value
The routing cache is rebuilt whenever routings change. The Modrix.RefreshCache custom callback notifies participating systems to update their internal routing analysis.
Integration Example¶
Here is a condensed view of how Polycore integrates with Modrix:
node Polycore.ModrixAPI:
cb Init:
{ Link NKS macro knobs to Modrix controllers }
modrix.controllers.ctrl_link_add(modrix.controllers.par.macro_1, ...)
modrix.controllers.push_data_id_to_ctrls()
cb Modrix.RefreshCache:
{ Rebuild internal routing analysis }
call polycore.analyze_routings()
cb Functions:
function polycore.get_control_min(routing) -> return
return := plc.layers[0].par[par_route].engine_min
end function
function polycore.modulate(routing, mod_offset)
plc.layers[layer].par[par].mod_offset := mod_offset
end function
{ ... remaining API functions ... }
end nodeThe product defines one node that implements the full system API. Modrix handles the modulator processing, routing management, and UI -- the product only needs to describe how its parameters are accessed.
Modrix is a significant subsystem -- its Python package, KSP templates, and runtime nodes form a self-contained modulation engine. Products that need modulation integrate with it through the system API; products that don't need modulation simply don't include it.