How to Implement Cross-Device Parameter Binding with Value Conversion in Nallely

To implement cross-device parameter binding with value conversion in Nallely, retrieve ParameterInstance objects from your VirtualDevice instances, optionally apply a Scaler via the .scale() method to handle range conversion, and invoke .bind() on the source to create a Link that automatically handles linear/logarithmic mapping and conversion policies.

Nallely treats every controllable knob, slider, pad, or CV output as a VirtualParameter living on a VirtualDevice. When you need to synchronize parameters across different hardware or virtual instruments in the dr-schlange/nallely-midi ecosystem, the library provides a declarative binding system that handles value conversion automatically through Scaler objects and conversion policies.

Understanding the Core Architecture

Before implementing bindings, you must understand four key classes that orchestrate the data flow between devices.

VirtualParameter and Conversion Policies

In nallely/core/virtual_device.py, the VirtualParameter class defines the schema for any controllable value. Each parameter can store a conversion_policy that dictates how incoming values should be coerced before reaching the destination hardware.

The supported policies are:

  • "round" – Casts the value to the nearest integer
  • ">0" – Clamps negative values to 0 (useful for gate-type CV)
  • "!=0" – Converts any non-zero value to 1 (binary on/off)

These policies are stored on the VirtualParameter and automatically consulted by the scaling engine during value transmission.

ParameterInstance and the Bind Method

When you access a parameter through a device attribute (e.g., device.speed_cv), Nallely returns a ParameterInstance defined in nallely/core/parameter_instances.py. This wrapper exposes the .bind() method, which delegates to Link.create(self, target) to establish the connection.

The Link class in nallely/core/links.py wires the source to the destination. If the source and target ranges differ, Nallely automatically inserts a Scaler into the link chain. The Scaler class in nallely/core/scaler.py implements:

  • Linear mapping (method="lin") – Direct proportion mapping between intervals
  • Logarithmic mapping (method="log") – Exponential curve mapping for frequency-based parameters
  • Policy enforcement – Applies the destination's conversion_policy inside methods like convert_lin()

Step-by-Step Implementation Guide

Follow these steps to bind parameters across devices with automatic value conversion.

1. Create or Retrieve Device Instances

Instantiate your virtual or physical devices. Every CV output or input is automatically wrapped as a ParameterInstance.

from nallely import LFO, Synth

lfo = LFO()      # Virtual LFO with speed_cv (0-127)

synth = Synth()  # Virtual synth with filter_cv (0-127)

2. Select Source and Destination Parameters

Access the parameters through the device attributes. These return ParameterInstance objects capable of binding.

src_param = lfo.speed_cv      # ParameterInstance

dst_param = synth.filter_cv   # ParameterInstance

3. Configure Conversion Policies (Optional)

If the destination requires specific value coercion, set the policy on the underlying VirtualParameter before binding.


# Ensure the synth receives only integer values

dst_param.parameter.conversion_policy = "round"

4. Create the Binding with Scaling

Call .bind() on the source parameter. If the ranges differ, pass a scaled view of the destination using .scale().


# Bind with automatic linear scaling (0-127 → 0-127)

src_param.bind(dst_param.scale(to_min=0, to_max=127, method="lin", as_int=True))

The .scale() method returns a Scaler object that Link.create() inserts into the processing chain. When the LFO speed changes, the value flows through Scaler.convert_lin(), applies the rounding policy, and arrives at the synth's filter.

Advanced Value Conversion Methods

When binding parameters with different value ranges or scaling curves, explicitly configure the Scaler behavior.

Linear vs. Logarithmic Scaling

Use method="log" for frequency-based parameters to achieve perceptually smooth transitions across wide ranges.


# Map LFO speed (0-127) to synth cutoff (20-20000 Hz) logarithmically

lfo.speed_cv.bind(
    synth.cutoff_cv.scale(to_min=20, to_max=20000, method="log", as_int=False)
)

The convert_lin() and equivalent logarithmic methods in nallely/core/scaler.py handle the mathematical transformation while respecting the as_int flag and any active conversion policies.

Integer vs. Float Output

Set as_int=True when the destination expects MIDI CC values or discrete steps. Set as_int=False for continuous CV control.


# Integer output for MIDI compatibility

src_param.bind(dst_param.scale(to_min=0, to_max=127, method="lin", as_int=True))

Remote Binding via the Trevor API

For UI-driven or script-based binding over WebSocket, use the Trevor API exposed in nallely/trevor/trevor_api.py. This allows cross-device binding without direct Python object access.

from nallely.trevor import TrevorClient

trevor = TrevorClient("ws://localhost:3000")
trevor.bind(
    src_device="my_lfo", src_parameter="speed_cv",
    dst_device="my_synth", dst_parameter="filter_cv",
    conversion="lin",    # Optional: "lin" or "log"

    as_int=True        # Request integer output

)

The bind() function in the Trevor API mirrors the local implementation, automatically creating the Link and inserting a Scaler when conversion parameters are provided.

Summary

  • Architecture: Nallely uses VirtualParameter definitions, ParameterInstance wrappers, Link connections, and Scaler conversions to route values between devices.
  • File Locations: Core logic resides in nallely/core/virtual_device.py (policies), nallely/core/parameter_instances.py (binding entry point), nallely/core/scaler.py (value conversion), and nallely/core/links.py (connection management).
  • Implementation Pattern: Retrieve instances via device attributes, optionally configure conversion_policy on the destination, then call source.bind(destination.scale(...)) to establish the connection with automatic value mapping.
  • Scaling Options: Choose method="lin" for linear mapping, method="log" for exponential curves, and as_int to control numeric precision.
  • Remote Access: The Trevor WebSocket API in nallely/trevor/trevor_api.py exposes identical binding capabilities for external scripts and UI components.

Frequently Asked Questions

What is the difference between a VirtualParameter and a ParameterInstance?

A VirtualParameter defines the specification, range, and conversion policy for a control (defined in nallely/core/virtual_device.py). A ParameterInstance is a runtime wrapper that represents that parameter on a specific device instance and provides the .bind() method (implemented in nallely/core/parameter_instances.py). You interact with instances to create links; you configure the underlying VirtualParameter to set conversion policies.

How does Nallely handle mismatched value ranges between devices?

When you pass a scaled view of the destination parameter via .scale(to_min, to_max, method), Nallely automatically inserts a Scaler object into the Link chain. The Scaler intercepts values from the source, applies linear or logarithmic transformation via convert_lin() or equivalent methods, and delivers the converted value to the destination. This happens transparently whenever the source value changes.

Can I apply multiple conversion policies to a single parameter?

No, each VirtualParameter supports a single conversion_policy string (either "round", ">0", or "!=0"). However, you can combine this policy with custom scaling logic by pre-processing values in event callbacks or by chaining multiple virtual devices. The policy is applied automatically by the Scaler during the final conversion step before the value reaches the hardware.

Does the Trevor API support the same conversion features as direct Python binding?

Yes. The trevor.bind() function in nallely/trevor/trevor_api.py accepts conversion (specifying "lin" or "log") and as_int parameters, mirroring the functionality of the local ParameterInstance.bind() and .scale() methods. The WebSocket server creates the equivalent Link and Scaler objects on the backend, ensuring consistent behavior whether you bind programmatically or through the remote UI.

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 →