Skip to content

RT Scripting: Per-Step Modulation (Pass 2)

Pass 1 plays notes with a fixed velocity. This pass extends the script so that every step drives four parameters independently — velocity, detune, filter envelope depth, and wavetable position — each value isolated per voice so overlapping steps never interfere with each other.

The naive approach — changing the filter cutoff knob when a step fires — breaks down immediately once notes overlap. When step 2 fires while step 1 is still ringing, setting the filter globally clobbers step 1’s sound mid-note. The same problem applies to wavetable position and filter envelope depth.

The solution is per-voice modulation: each note carries its own modulation values that only affect that note’s voice, independent of any other note playing at the same time. This is how hardware envelopes and LFOs work — they are always per-voice — and the same model applies here.

The platform provides two mechanisms for per-voice modulation from a script:

Predefined targetssys_default_target_uids.sourceVolume and sys_default_target_uids.sourceTune — are always available without any setup. They apply directly to the note’s volume and pitch respectively. This instrument uses sourceTune for per-step detune. Velocity is handled separately: passed directly as the MIDI velocity argument to sys_create_note_event.

Script modulators are explicit modulator components added to the layer, just like an envelope or LFO. The script feeds them arbitrary values per-note via sys_create_custom_control_event. The two script modulators added in Sound Design Pass 2 cover wavetable position and filter envelope intensity.

At the top of step_sequencer.script, expand the UID section to include the two script modulator UIDs copied in Sound Design Pass 2:

-- list of referenced UIDs (replace with your own from the editor):
local processorIdWaveTablePos = "<copy from your editor>"
local processorIdFltEnvIntensity = "<copy from your editor>"
local mainZoneMapId = "<copy from your editor>" -- <== already pasted before

Insert the three helper functions after the local variable declarations and before startNote:

function rescaleVelocity(veloMod)
local minValue = 64
local maxValue = 127
return (maxValue - minValue) * veloMod + minValue
end
function rescaleDetune(tuneMod)
local minValue = -1
local maxValue = 1
return (maxValue - minValue) * tuneMod + minValue
end
function rescaleFilter(filterMod)
local minValue = 0.2
local maxValue = 1
return (maxValue - minValue) * filterMod + minValue
end

Each step stores its parameters as normalised 0.0–1.0 values — convenient for the UI and for serialisation. The rescale helpers convert these to the range each target expects. Velocity has a floor of 64: even the quietest step remains audible. The filter floor of 0.2 matches the intensity value set in Sound Design Pass 1. Rather than one generic rescale function, three named functions are used — explicit per-parameter ranges are easier to read than a shared helper with arguments.

Replace the existing startNote function with the full per-step version:

function startNote(curStep, offset, length)
-- muted steps have a note value of -1:
if curStep.note < 0 then return end
local eventId = sys_create_note_event(
offset,
0,
curStep.note + keyWithinOctave,
rescaleVelocity(curStep.velocity),
length)
-- emit a tune ramp: start at zero (no change), then ramp to the mod value
-- over a 16th note length:
local lengthOneSixteenth = sys_get_quarter_note_length() / 4
sys_create_custom_control_event(
offset,
eventId,
sys_default_target_uids.sourceTune,
0)
sys_create_custom_control_event(
offset + lengthOneSixteenth,
eventId,
sys_default_target_uids.sourceTune,
rescaleDetune(curStep.tune))
-- set the filter envelope intensity for this step:
sys_create_custom_control_event(
offset,
eventId,
processorIdFltEnvIntensity,
rescaleFilter(curStep.filter))
-- set the wavetable position for this step:
sys_create_custom_control_event(
offset,
eventId,
processorIdWaveTablePos,
curStep.wtPos) -- no rescaling: full 0–1 range
sys_force_note_event_to_zone_map(eventId, mainZoneMapId)
end

Velocity is passed directly as the MIDI velocity argument to sys_create_note_event via rescaleVelocity — no custom control event needed.

Detune uses the predefined sourceTune target and is shaped as a two-point ramp: a breakpoint at 0 at the note’s start, then the target value one sixteenth note later. The short glide-in is what produces the characteristic 303 portamento feel without any additional processor.

Filter envelope intensity and wavetable position are each set with a single breakpoint at the note’s offset, targeting processorIdFltEnvIntensity and processorIdWaveTablePos respectively. Wavetable position uses the stored value directly — the full 0–1 range maps naturally to the WTPos knob with no rescaling.

  • UI controls for per-step velocity, detune, filter, and wavetable position (Pass 2)