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.

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:

  1. CreateLink.create(src, dst) constructs a Link instance and immediately invokes install() to compile the forwarding callback.
  2. 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 in self.callback.
  3. Trigger – When the source updates, Link.trigger(value, ctx) executes the compiled callback, applying any Scaler transformations, 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 in nallely/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

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:

Summary

  • Link.create(src, dst) in nallely/core/links.py is the primary factory for establishing connections between any source feed and destination parameter.
  • The bind() helper on feed objects provides a convenient wrapper around Link.create() for standard use cases.
  • Scaler objects enable automatic range conversion and are stored in self.chain during link initialization.
  • Runtime flags (bouncy, muted, velocity) modify link behavior during the trigger() phase without requiring recompilation.
  • Direct Link instantiation and manual install() calls support custom source types and specialized callback logic.

Frequently Asked Questions

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().

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →