How to Create Custom Parameter Linking with Nallely-MIDI’s Link Entity System
Use the Link class in nallely/core/links.py to connect any source feed to any destination parameter through a three-stage compilation process that creates, installs, and triggers callbacks between MIDI controls, virtual CVs, or custom scalers.
The dr-schlange/nallely-midi framework provides a robust entity system for routing control data between virtual and MIDI devices. Understanding how to create custom parameter linking allows you to wire any source—whether a MIDI CC, virtual CV output, or sensor value—to any compatible destination with optional transformations and runtime behavior flags.
The Three-Stage Link Architecture
Every parameter link in Nallely-MIDI follows a strict compilation pipeline defined in nallely/core/links.py. The Link class orchestrates this process through three distinct stages:
- Create –
Link.create(src, dst)constructs aLinkinstance and immediately invokesinstall()to compile the forwarding callback. - Install – The system selects the appropriate callback via a dispatch matrix (
_install_<src>__<dst>) that binds the source to the device’s link registry and stores a callable inself.callback. - Trigger – When the source updates,
Link.trigger(value, ctx)executes the compiled callback, applying anyScalertransformations, custom velocity values, and bouncy feedback logic before reaching the destination.
This architecture ensures type-safe routing between heterogeneous control surfaces while maintaining minimal runtime overhead.
Basic Parameter Linking with bind()
All feed objects expose a bind(target) helper method that internally delegates to Link.create(). The most common feed types include:
ParameterInstance– Virtual CV outputs or inputs defined innallely/core/parameter_instances.py.Int– Concrete MIDI CC controls.PadOrKey– MIDI note and velocity inputs.
To link a virtual CV output to a MIDI CC destination, call bind() on the source parameter:
# my_virtual_device is a VirtualDevice subclass
# my_midi_device is a MidiDevice instance
my_virtual_device.output_cv.bind(my_midi_device.modulation)
In this example, output_cv is a ParameterInstance acting as the source, while modulation is an Int representing a MIDI CC. The bind method automatically registers the link with the device’s internal registry.
Transforming Values with Scalers
When source and destination use incompatible value ranges, insert a Scaler between them. The scale() method—available on ParameterInstance and other feed types—returns a Scaler object that stores to_min, to_max, and interpolation method parameters.
During link initialization, Link.__post_init__ detects the Scaler and stores it in self.chain. The scaler transforms values during the trigger phase before the callback executes:
# Scale 0-1 CV to MIDI 0-127 range
scaled = my_virtual_device.output_cv.scale(min=0, max=127, as_int=True)
scaled.bind(my_midi_device.modulation)
This pattern ensures precise value mapping without manual conversion logic in your application code.
Customizing Runtime Behavior
The Link class accepts three optional flags that modify execution at runtime. These properties are consulted during trigger() in nallely/core/links.py:
bouncy=True– After the destination receives the value, the source receives immediate feedback. Useful for visual indicators or bidirectional synchronization.muted=True– Suppresses value forwarding entirely; the link remains registered but inactive.velocity=int– Forces a specific MIDI velocity for note-type sources, overriding the velocity carried in the thread context.
Set these flags after creating the link:
link = my_virtual_device.output_cv.bind(my_midi_device.modulation)
link.bouncy = True
link.velocity = 100
Advanced Custom Link Construction
For non-standard source types or bespoke routing logic, instantiate Link directly instead of using bind(). This approach requires manually calling install() to compile the callback via the dispatch matrix:
from nallely.core.links import Link
# Custom source: raw sensor integer with required attributes
class SensorValue(int):
def __init__(self, val):
super().__init__()
self.device = my_midi_device
self.parameter = my_midi_device.modulation.parameter
sensor = SensorValue(0)
dest = my_virtual_device.input_cv # ParameterInstance destination
# Manual construction
link = Link(sensor, dest, bouncy=True, velocity=64)
link.install() # Compiles _install_Int__ParameterInstance callback
sensor.bind(dest) # Registers with device registry
Because Link.__post_init__ recognizes the Int-like structure of SensorValue, it automatically selects the appropriate _install_Int__ParameterInstance method from the dispatch matrix. You may also override link.callback after installation for completely custom behavior.
Key Implementation Files
The link entity system spans three core modules:
nallely/core/links.py– Contains theLinkclass,create()factory,install()dispatch logic, andtrigger()runtime method.nallely/core/parameter_instances.py– ImplementsParameterInstance.bind(), thescale()method, and concrete control classes likeIntandPadOrKey.nallely/core/virtual_device.py– Exposes defaultoutput_cvparameters and forwards binding calls to the link system.
Summary
Link.create(src, dst)innallely/core/links.pyis the primary factory for establishing connections between any source feed and destination parameter.- The
bind()helper on feed objects provides a convenient wrapper aroundLink.create()for standard use cases. Scalerobjects enable automatic range conversion and are stored inself.chainduring link initialization.- Runtime flags (
bouncy,muted,velocity) modify link behavior during thetrigger()phase without requiring recompilation. - Direct
Linkinstantiation and manualinstall()calls support custom source types and specialized callback logic.
Frequently Asked Questions
What is the difference between using bind() and instantiating Link directly?
The bind() method is a convenience wrapper on feed objects that calls Link.create() and returns the link instance. Instantiating Link directly gives you control over the construction process, allowing custom source types that don’t inherit from standard feeds, and requires explicitly calling install() to compile the callback.
How do I prevent feedback loops when linking bidirectional controls?
Enable the bouncy=True flag on your link. This setting allows the destination to send values back to the source without creating infinite recursion, as the framework tracks the origin context during trigger() execution to distinguish between user input and automated feedback.
Can I chain multiple scalers between a source and destination?
The current architecture supports a single Scaler in self.chain. To apply multiple transformations, nest scaler calls or create a custom Scaler subclass that combines operations, then pass that composite object as the source to Link.create().
How do I temporarily disable a link without removing it from the device registry?
Set link.muted = True. The link remains active in the registry and continues receiving trigger events, but the trigger() method returns early without forwarding values. Set muted = False to resume normal operation without recompiling the callback.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →