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.
Why Per-Voice Modulation
Section titled “Why Per-Voice Modulation”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 targets — sys_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.
Update the UID Section
Section titled “Update the UID Section”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 beforeAdd the Rescale Helpers
Section titled “Add the Rescale Helpers”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 + minValueend
function rescaleDetune(tuneMod) local minValue = -1 local maxValue = 1 return (maxValue - minValue) * tuneMod + minValueend
function rescaleFilter(filterMod) local minValue = 0.2 local maxValue = 1 return (maxValue - minValue) * filterMod + minValueendEach 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.
Update startNote
Section titled “Update startNote”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)endHow It Works
Section titled “How It Works”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.
Outline
Section titled “Outline”- UI controls for per-step velocity, detune, filter, and wavetable position (Pass 2)