# How to Create Custom Parameter Linking with Nallely-MIDI’s Link Entity System

> Learn to create custom parameter linking with Nallely-MIDI's Link Entity System. Connect MIDI controls, virtual CVs, or custom scalers using a three-stage compilation process. Get started today.

- Repository: [dr-schlange/nallely-midi](https://github.com/dr-schlange/nallely-midi)
- Tags: how-to-guide
- Published: 2026-02-28

---

**Use the `Link` class in [`nallely/core/links.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/links.py). The `Link` class orchestrates this process through three distinct stages:

1. **Create** – `Link.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`](https://github.com/dr-schlange/nallely-midi/blob/main/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:

```python

# 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:

```python

# 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`](https://github.com/dr-schlange/nallely-midi/blob/main/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:

```python
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:

```python
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`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/links.py)** – Contains the `Link` class, `create()` factory, `install()` dispatch logic, and `trigger()` runtime method.
- **[`nallely/core/parameter_instances.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/parameter_instances.py)** – Implements `ParameterInstance.bind()`, the `scale()` method, and concrete control classes like `Int` and `PadOrKey`.
- **[`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py)** – Exposes default `output_cv` parameters and forwards binding calls to the link system.

## Summary

- **`Link.create(src, dst)`** in [`nallely/core/links.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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

### 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.