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

> Learn to use scalers and auto-scaling in nallely-midi for efficient signal mapping. Translate values between ranges with linear or logarithmic curves and dynamic range detection.

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

---

**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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/parameter_instances.py) and [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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.

### Link Integration and Chain Execution

The `Link` class in [`nallely/core/links.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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.

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

```python

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

```python

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

```python
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`](https://github.com/dr-schlange/nallely-midi/blob/main/tests/test_scalers.py) – demonstrates linear scaling with fixed bounds.*

### Auto‑Scaling MIDI Pitch‑Bend to CV

```python
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`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/parameter_instances.py) – auto flag logic.*

### Pad Velocity to Virtual Parameter with Auto‑Scaling

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

```python

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

### How does the Link class apply scalers during signal routing?

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