How to Use Scalers and Auto‑Scaling for Signal Mapping in Nallely

Scalers in nallely-midi translate values between numeric ranges using linear or logarithmic curves, with an optional auto‑detection flag that reads the source's current range at runtime when min/max bounds are omitted.

Signal mapping in the nallely-midi framework relies on scalers to bridge incompatible numeric ranges between virtual devices. These components automatically convert incoming values—such as LFO speeds or MIDI pitch‑bend data—into target ranges suitable for synthesizer parameters. Understanding how to configure manual versus auto‑scaling unlocks dynamic routing capabilities without hard‑coding source boundaries.

The Three Layers of the Scaling Pipeline

The scaling architecture spans three distinct areas that handle conversion logic, convenience instantiation, and signal routing.

Scaler Class Implementation

The core conversion engine lives in nallely/core/scaler.py. The Scaler class implements convert(), convert_lin(), and convert_log() methods that apply mathematical transformations between ranges. Each instance stores an auto boolean flag that determines whether the source range should be detected automatically from the underlying data reference or use fixed bounds.

Convenience Methods on Parameters and Devices

Every ParameterInstance and VirtualDevice exposes a .scale() method defined in nallely/core/parameter_instances.py and nallely/core/virtual_device.py. These methods instantiate a Scaler with the correct data reference attached. They automatically set the auto flag using the logic auto = (min is None and max is None), allowing you to toggle auto‑scaling simply by omitting bounds.

The Link class in nallely/core/links.py serves as the routing glue between sources and destinations. When Link.create() detects that the source feeder is a Scaler instance, it stores that scaler in Link.chain. During each trigger cycle, the link executes value = self.chain(value, ctx) before calling the destination callback, ensuring scaled values reach the target parameter.

Creating Manual and Auto‑Scaling Mappings

You create scalers by calling .scale() on any parameter instance or virtual device, choosing between fixed mappings or runtime auto‑detection.

Fixed Range Scaling

Supply explicit min and max values to create a deterministic mapping. This approach sets auto = False and always applies the same linear or logarithmic formula regardless of the source's current output range.

from nallely.lfos import LFO

lfo = LFO()

# Map LFO speed (0-10) to CV range 0-127 as integers

speed_scaler = lfo.scale(0, 127, as_int=True)
lfo.speed_cv = speed_scaler

The method parameter defaults to "lin" for linear interpolation. For exponential curves suitable for frequency control, pass method="log".

Auto‑Scaling for Dynamic Sources

Omit both bounds to enable auto‑scaling, which forces the Scaler to read self.data.range during each trigger cycle. This is essential when the source range changes at runtime, such as with MIDI pitch‑bend wheels or host‑automated parameters.


# Auto-detect source range, map to 0-127

auto_scaler = lfo.scale(None, None)  # auto = True

Auto‑scaling ensures that if a device later modifies its minimum or maximum output values, the scaler automatically adapts without code changes.

Wiring Scalers into the Signal Chain

When you assign a scaler to a parameter using the = operator, nallely-midi internally creates a Link that checks whether the source is a Scaler instance. If so, the link stores the scaler in Link.chain and rewrites the underlying source to the original data feed.

During each update cycle, Link.trigger() executes the following sequence:

  1. Retrieves the raw value from the source parameter
  2. Applies value = self.chain(value, ctx) if a scaler exists
  3. Passes the converted value to self.callback(value, ctx) for the destination

This architecture keeps scaling logic decoupled from destination parameters while ensuring zero‑overhead when no conversion is required.

Linear vs. Logarithmic Scaling Methods

The Scaler class supports two mathematical modes controlled via the method parameter:

  • "lin" (default): Applies linear interpolation using convert_lin(), suitable for linear parameters like velocity or standard CV control voltages.
  • "log": Applies logarithmic scaling via convert_log(), which maps values exponentially. This is critical for frequency parameters where perceptual pitch scales logarithmically.

# Linear scaling for general CV

linear = lfo.scale(0, 100, method="lin")

# Logarithmic scaling for frequency range 20Hz-2000Hz

logarithmic = lfo.scale(20, 2000, method="log")

The logarithmic implementation in nallely/core/scaler.py (lines 87‑102) handles the mathematical transformation while respecting the as_int flag for integer quantization.

Complete Usage Examples

Mapping LFO Speed to a MIDI CC Range

from nallely.lfos import LFO

lfo = LFO()

# Scale speed (0-10) to MIDI CC range (0-127) as integers

lfo.speed_cv = lfo.scale(0, 127, as_int=True)

# Verify the link configuration

link = list(lfo.links_registry.values())[0]
assert link.chain is not None
assert link.chain.to_min == 0
assert link.chain.to_max == 127
assert link.chain.auto is False

Source: tests/test_scalers.py – demonstrates linear scaling with fixed bounds.

Auto‑Scaling MIDI Pitch‑Bend to CV

from nallely.core.midi_device import MidiDevice

midi_device = MidiDevice(name="keyboard")

# Auto-detect pitchwheel range, map to 0-127 CV

midi_device.pitchwheel_cv = midi_device.pitchwheel_cv.scale(0, 127)

Source: nallely/core/parameter_instances.py – auto flag logic.

Pad Velocity to Virtual Parameter with Auto‑Scaling

from nallely.core.virtual_device import VirtualDevice

class MySynth(VirtualDevice):
    volume_cv = VirtualParameter("volume", range=(0, 1))

synth = MySynth()

# Map pad velocity (0-127) to volume (0-1) with auto-detection

synth.volume_cv = synth.modules.pad36.velocity.scale(None, None)

Because both min and max are None, the scaler reads the Pad's range property (0‑127) at each trigger and maps it to the target range (0‑1) dynamically.

Switching Between Scaling Curves


# Linear mapping for standard control

lin_cv = lfo.scale(0, 100, method="lin")

# Logarithmic mapping for frequency control

log_cv = lfo.scale(20, 20000, method="log")

Summary

  • Scalers translate numeric ranges using the Scaler class in nallely/core/scaler.py, supporting both linear and logarithmic curves.
  • Auto‑scaling activates when you omit min and max arguments, setting auto = True and reading the source's current range at runtime.
  • Integration happens through Link.chain in nallely/core/links.py, which automatically detects scaler instances and applies them during the trigger cycle.
  • Convenience methods on ParameterInstance and VirtualDevice handle scaler instantiation and flag configuration via the .scale() method.
  • Serialization preserves scaler configuration—including the auto flag—in JSON format via Link.scaler_as_dict() for UI persistence.

Frequently Asked Questions

What is the difference between manual and auto‑scaling in nallely-midi?

Manual scaling requires explicit min and max arguments to scale(), creating a fixed mathematical mapping with auto = False. Auto‑scaling omits these bounds, setting auto = True, which causes the Scaler to query self.data.range during each trigger cycle to adapt to runtime changes in the source device's output range.

When you assign a scaler to a destination parameter, Link.create() detects if the source is a Scaler instance and stores it in Link.chain. During each update, Link.trigger() executes value = self.chain(value, ctx) before passing the result to the destination callback, ensuring the scaler transforms values in the signal path.

Can I use logarithmic scaling for frequency parameters?

Yes. Pass method="log" to the .scale() method to enable logarithmic conversion via convert_log() in nallely/core/scaler.py. This applies exponential scaling suitable for frequency ranges (e.g., 20Hz‑20kHz) where perceptual pitch follows a logarithmic curve.

Where is the auto flag stored and how is it serialized?

The auto boolean is stored as an instance attribute on the Scaler class (self.auto). When links are serialized, Link.scaler_as_dict() exports the flag alongside to_min, to_max, and method in a JSON object, allowing UI interfaces and persistence layers to reconstruct the exact scaling behavior including auto‑detection settings.

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 →