LFOs

rustjay-engine provides 8 LFO slots. Each slot is an independent oscillator that can be assigned to any declared parameter at runtime — from the Modulation tab (desktop) or the Modulation web panel (headless).

Configuration

Each slot exposes:

FieldDescription
EnabledToggle the slot on/off without losing its settings
WaveformShape of the oscillator (see table below)
TargetWhich parameter this slot drives; built-in or any effect-declared parameter
DepthModulation amplitude — scales the [-1, 1] oscillator output
Tempo SyncOn: rate is expressed as a beat division; Off: rate in Hz
DivisionBeat subdivision when tempo sync is on (1/16 through 8 beats)
Rate (Hz)Oscillator frequency when tempo sync is off
Phase OffsetStarting phase in degrees (0–360) — useful for quadrature pairs

Waveforms

WaveformShapeOutput range
SineSmooth sinusoidal[-1, 1]
TriangleLinear V-shape[-1, 1]
RampRising linear ramp, instant reset[-1, 1]
SawFalling linear ramp, instant reset[-1, 1]
SquareInstant high/low[-1, 1]

Depth scales the [-1, 1] oscillator output before it is added to the parameter's base value. A depth of 0.5 means the LFO swings ±0.5 units from whatever the base value is set to.

Beat-sync mode

When Tempo Sync is on, the division field replaces the rate field. The engine converts it using effective_bpm():

cycle_duration_seconds = (60 / bpm) * beats_per_cycle

At 120 BPM, a 1/4 division (0.25 beats) runs at 8 Hz; a 1 beat division runs at 2 Hz; a 4 beat division runs at 0.5 Hz.

The LFO phase advances freely based on wall-clock delta time — it does not directly track beat_phase. When using Ableton Link or ProDJ Link, the phase snaps to the quantum boundary on each beat crossing to stay musically in phase. With audio beat detection or tap tempo, the phase resets freely and may drift relative to the actual beat.

Tap tempo

On headless Pi setups without Ableton Link, use Tap Tempo in the Modulation web panel to set the BPM manually. Tap twice for an immediate estimate; subsequent taps refine the average over up to 8 intervals. The BPM display updates after each tap.

Tap tempo writes to audio.bpm, which effective_bpm() returns when the active sync source is Audio.

Reading LFO values in code

When you call engine.get_param(id), LFO modulation is already included — you don't need to read the LFO state directly.

If you need the raw oscillator output (e.g. to drive something outside the parameter system):

#![allow(unused)]
fn main() {
fn build_uniforms(&self, s: &MyState, engine: &EngineState) -> MyUniforms {
    let lfo_0 = engine.lfo.bank.lfos[0].output; // f32 in [-amplitude, amplitude]
    let lfo_1 = engine.lfo.bank.lfos[1].output;
    MyUniforms {
        wobble: lfo_0,
        // ...
    }
}
}

output is in [-amplitude, amplitude] — it is the raw waveform value multiplied by depth.

Phase continuity on config update

When an LFO's configuration is changed via the web Modulation panel, the engine preserves the current phase, output, and last_beat_phase values from the existing slot before applying the new config. A running LFO does not snap back to phase 0 when you adjust its waveform or depth mid-cycle.

Targeting effect-declared parameters

LFO targets fall into two groups:

Built-in targetsHueShift, Saturation, Brightness. These modulate the HSB colour correction layer common to all effects.

Custom targets — any parameter declared by the effect via ParameterDescriptor. In the web Modulation panel, all current effect parameters appear in the Target dropdown under their category name (e.g. Flux / Flow Scale). Internally, these are stored as LfoTarget::Custom("flow_scale") using the bare parameter ID — the category prefix is stripped before storage.

Multiple LFOs on one parameter

If two or more enabled slots target the same parameter, their outputs are summed before being applied. The summed modulation is still clamped to the parameter's [min, max] range.