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.
Link Integration and Chain Execution
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:
- Retrieves the raw value from the source parameter
- Applies
value = self.chain(value, ctx)if a scaler exists - 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 usingconvert_lin(), suitable for linear parameters like velocity or standard CV control voltages."log": Applies logarithmic scaling viaconvert_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
Scalerclass innallely/core/scaler.py, supporting both linear and logarithmic curves. - Auto‑scaling activates when you omit
minandmaxarguments, settingauto = Trueand reading the source's currentrangeat runtime. - Integration happens through
Link.chaininnallely/core/links.py, which automatically detects scaler instances and applies them during the trigger cycle. - Convenience methods on
ParameterInstanceandVirtualDevicehandle scaler instantiation and flag configuration via the.scale()method. - Serialization preserves scaler configuration—including the
autoflag—in JSON format viaLink.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. 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →