Overview
uvi-script provides per-voice control over tuning, volume, panning, and fading. Each voice represents a single playing note and has a unique ID that can be used to manipulate it individually.
This allows for sophisticated voice-level effects like vibrato, tremolo, detuning, crossfading, and dynamic expression that operate independently on each note.
Voice Lifecycle
Understanding the voice lifecycle is essential for effective voice manipulation:
- Voice Creation - A voice is created via playNote or postEvent, which returns a unique voice ID
- Voice Manipulation - The voice can be modified using changeTune, changePan, changeVolume
- Voice Fading - The voice can be faded using fadein, fadeout, or fade
- Voice Release - The voice ends via releaseVoice or automatic release when the note ends
Creating Voices
-- Method 1: Forward incoming event and get voice ID
-- Method 2: Create new voice with explicit parameters
local id2 =
playNote(e.note, e.velocity, -1) -- -1 = sustain until released
-- Method 3: Create voice with specific layer and channel
local id3 =
playNote(60, 100, 1000, 1, 0) -- C4, vel 100, 1s duration, layer 1
end
void onNote(table e)
event callback that will receive all incoming note-on events if defined.
function playNote(note, vel, duration, layer, channel, input, vol, pan, tune, slice, oscIndex)
helper function to generate a note event.
Definition api.lua:942
function postEvent(e, delta)
send a script event back to the script engine event queue.
Definition api.lua:853
Tracking Voice IDs
Always store voice IDs if you plan to manipulate them later:
-- Later, manipulate this specific voice
end
function wait(ms)
suspend the current thread callback execution for the given number of samples.
Definition wrapper.lua:39
function changeTune(voiceId, shift, relative, immediate)
change the tuning of specific voice in (fractionnal) semitones.
Definition api.lua:488
Managing Multiple Active Voices
Most real-world scripts need to track several playing voices simultaneously. The standard pattern is a table keyed by note number:
local voices = {} -- voices[note] = voiceId
voices[e.note] = id
-- now you can manipulate this specific voice later
changePan(
id, (e.note / 127) * 2 - 1) -- spread by pitch
end
voices[e.note] = nil
end
void onRelease(table e)
event callback executed whenever a note off message is received.
function changePan(voiceId, pan, relative, immediate)
changes the pan position of a specific note event.
Definition api.lua:515
This pattern is essential for effects that need to act on all active voices, such as pitch bend applied from a controller:
local voices = {}
end
voices[e.note] = nil
end
for note, id in pairs(voices) do
end
end
void onPitchBend(table e)
event callback that will receive all incoming pitch-bend events if defined.
Voice Parameters
Tuning Control
Use changeTune to modify the pitch of a voice in semitones:
-- Bend pitch up by 2 semitones (whole step)
-- Bend pitch down by 1 semitone
-- Detune by 10 cents
end
Volume Control
Use changeVolume to modify the volume of a voice:
-- Set volume to 50%
-- Set volume to 100%
-- Mute the voice
end
function changeVolume(voiceId, gain, relative, immediate)
changes a voice's volume.
Definition api.lua:530
Volume range: 0.0 (silent) to 1.0 (full volume)
For decibel-based control, use changeVolumedB instead. This is often more intuitive for mixing tasks where you think in dB attenuation:
-- Attenuate by 6 dB (roughly half perceived loudness)
-- Boost by 3 dB
end
function changeVolumedB(voiceId, dBGain, relative, immediate)
change the volume of specific voice in decibels.
Definition api.lua:557
Panning Control
Use changePan to modify the stereo position of a voice:
-- Pan hard left
-- Pan center
-- Pan hard right
-- Pan slightly right
end
Pan range: -1.0 (hard left) to 1.0 (hard right), 0.0 is center
Fading Voices
Control voice envelope with fade functions:
-- Fade in over 500ms
-- Later,
fade out over 1000ms and stop voice
fadeout(
id, 1000, true) -- true = release voice after
fade
end
function fadeout(voiceId, duration, killVoice, reset, layer)
starts a volume fade-out.
Definition api.lua:609
function fade(voiceId, targetValue, duration, layer)
starts a volume fade.
Definition api.lua:662
function fadein(voiceId, duration, reset, layer)
starts a volume fade-in for a specific voice.
Definition api.lua:643
Common Use Cases
Detuning for Richness
Create a richer sound by layering slightly detuned copies:
-- Play root note
local id1 =
playNote(e.note, e.velocity, -1)
-- Add slightly detuned copies for chorus effect
local id2 =
playNote(e.note, e.velocity, -1)
local id3 =
playNote(e.note, e.velocity, -1)
-- Optional: Pan them for width
end
Dynamic Vibrato
Apply vibrato that intensifies over time:
local vibratoRate =
Knob(
"Vibrato Rate", 5, 0, 10)
local vibratoDepth =
Knob("Vibrato Depth", 0.1, 0, 0.5)
local elapsed = 0
-- Vibrato depth increases over time
local depthEnvelope = math.min(elapsed / 1000, 1.0)
local phase = 2 * math.pi * vibratoRate.value * elapsed / 1000
local modulation = vibratoDepth.value * depthEnvelope * math.sin(phase)
elapsed = elapsed + 5
end
end
Knob widget.
Definition ui.cpp:1520
function isNoteHeld()
return true is the note that created this callback is still held.
Definition api.lua:821
Tremolo Effect
Create rhythmic volume modulation:
local tremoloRate =
Knob(
"Tremolo Rate", 5, 0, 20)
local tremoloDepth =
Knob("Tremolo Depth", 0.5, 0, 1)
local phase = 2 * math.pi * tremoloRate.value *
getTime() / 1000
local modulation = 1 - (tremoloDepth.value * (1 - math.cos(phase)) / 2)
end
end
function getTime()
get the number of milliseconds elapsed since the script engine start.
Definition api.lua:751
Auto-Panning
Create movement in the stereo field:
local panRate =
Knob(
"Pan Rate", 0.5, 0, 5)
local phase = 2 * math.pi * panRate.value *
getTime() / 1000
local pan = math.sin(phase)
end
end
Voice Crossfading
Smoothly transition between voices:
-- Play two different layers
-- Start with layer 1 only
-- Crossfade from layer 1 to layer 2 over 2 seconds
for i = 0, 100 do
local mix = i / 100
end
end
A layer of sounds.
Definition Engine.cpp:255
Pitch Bend Effect
Create smooth pitch glides:
-- Bend from -2 semitones to 0 over 500ms
for i = 0, 100 do
local bend = -2.0 + (2.0 * i / 100)
changeTune(
id, bend,
false,
false) -- Smooth interpolation
end
end
Unison/Ensemble Effect
Create a thick unison sound with multiple detuned voices:
local unisonVoices = 5
local unisonDetune =
Knob(
"Detune", 0.1, 0, 0.5)
local unisonSpread =
Knob("Stereo Spread", 0.5, 0, 1)
for i = 1, unisonVoices do
local
id =
playNote(e.note, e.velocity, -1)
-- Detune each voice differently
local detune = (i - (unisonVoices + 1) / 2) * unisonDetune.value / unisonVoices
-- Spread across stereo field
local pan = ((i - 1) / (unisonVoices - 1) - 0.5) * 2 * unisonSpread.value
end
end
Best Practices
- Always Store Voice IDs
- If you plan to manipulate a voice later, store its ID:
-- GOOD: Store ID for later use
-- BAD: Can't manipulate the voice later
-- How do we change
this voice now?
ScriptProcessor & this
Reference to the current ScriptProcessor instance.
Definition Engine.cpp:404
- Clean Up Voice Data
- Remove stored voice data when voices end:
voiceData[id] = {startTime =
getTime()}
-- Clean up
voiceData[id] = nil
end
function waitForRelease()
suspend the current thread callback execution until the note that created this callback is released e...
Definition wrapper.lua:47
- Limit Voice Count
- Be mindful of CPU usage when creating many voices:
-- GOOD: Reasonable voice count
for i = 1, 5 do
end
end
-- POTENTIALLY BAD: Too many voices
for i = 1, 50 do -- May cause CPU issues
end
end
Sample Start Offset
Use setSampleOffset to change where playback begins within a sample, specified in milliseconds. This is applied to an already-playing voice, letting you skip into the middle of a sample on the fly.
-- Random start offset (0–80 ms) for natural variation on repeated notes
local offset = math.random() * 80
end
function setSampleOffset(voiceId, value)
changes a sample starting point in milliseconds.
Definition api.lua:569
Script Modulation
The voice functions (changeTune, changeVolume, changePan) give per-voice control but only over three fixed parameters. MIDI CC can modulate any parameter in the engine but is global — all voices receive the same value. Poly aftertouch is per-voice but limited to a single pressure dimension.
sendScriptModulation combines the best of both: per-voice targeting with access to any modulatable parameter in the engine (filter cutoff, resonance, effect mix, oscillator gain, etc.), plus built-in linear ramps with float precision. Think of it as a polyphonic CC with no 7-bit resolution limit.
Setup
In Falcon, add a Script Event Modulation source to the modulation list of a keygroup, then connect it to the parameter you want to control. Each source has an Event Id (0–127) that you reference from script. You can create multiple independent sources with different Event Ids to control several parameters at once.
API
Use sendScriptModulation to send a value to a modulation source:
- id — modulation source index (must match the Event Id configured in Falcon)
- targetValue — destination value, [0,1] unipolar or [-1,1] bipolar depending on the source configuration
- rampTime — interpolation time in ms (default 20 ms, avoids zipper noise)
- voiceId — optional; pass a voice ID for per-voice modulation, or omit to broadcast to all voices
Use sendScriptModulation2 for piece-wise linear ramps where you need an explicit start value. Parameters are the same, with an additional startValue before targetValue.
Use Cases
Per-voice velocity expression — drive any parameter per-voice at note-on, like poly aftertouch but not limited to pressure:
-- Per-voice filter cutoff driven by velocity (source 0 → filter Freq)
local velNorm = e.velocity / 127
end
function sendScriptModulation(id, targetValue, rampTime, voiceId)
send a script modulation event to control a Script Modulation source.
Definition api.lua:428
MPE-style per-voice control — route MPE dimensions (Y, Z) to per-voice modulation destinations. Each voice responds independently:
-- MPE Y (CC74 / Brightness) → per-voice filter modulation (source 1)
-- MPE Z (aftertouch) → per-voice dynamics (source 2)
if e.controller == 74 then
local value = e.value / 127
end
end
end
void onController(table e)
event callback that will receive all incoming control-change events when defined.
void onAfterTouch(table e)
event callback that will receive all incoming channel after-touch events if defined.
Broadcast knob control — like a MIDI CC but with float precision and configurable ramp time. One call modulates all active voices:
--
Knob controlling effect depth (source 3 → effect mix)
fxDepthKnob.
changed = function(self)
end
Script-built envelopes with sendScriptModulation2 — define an explicit start value for piece-wise linear ramps. Useful for per-voice attack transients or custom envelopes that go beyond what the built-in modulators offer:
-- Per-voice swell: ramp source 0 from 0 to 1 over 500 ms on note-on
end
function sendScriptModulation2(id, startValue, targetValue, rampTime, voiceId)
send a script modulation event to control a Script Modulation source.
Definition api.lua:458
Comparison
| Target | Per-Voice | Precision | Ramp Control |
| changeTune / changeVolume / changePan | 3 fixed params | Yes | Float | Immediate or smoothed |
| MIDI CC | Any parameter | No (global) | 7-bit (128 steps) | Fixed exponential smoothing |
| Poly Aftertouch | Any parameter | Yes (by note) | 7-bit | Fixed exponential smoothing |
| sendScriptModulation | Any parameter | Yes (by voice ID) | Float | Configurable linear ramp (ms) |
See Also
- See also
- Voice Manipulation - Voice Manipulation manipulation functions
-
playNote - Create a new voice
-
postEvent - Forward event and get voice ID
-
changeTune - Modify voice tuning
-
changeVolume - Modify voice volume (linear)
-
changeVolumedB - Modify voice volume (decibels)
-
changePan - Modify voice panning
-
setSampleOffset - Set sample playback start position
-
sendScriptModulation - Drive a script modulation source
-
sendScriptModulation2 - Drive a script modulation source with explicit start value
-
fadein - Fade voice in
-
fadeout - Fade voice out
-
releaseVoice - Release a voice
-
isNoteHeld - Check if note is still held
-
Threading and Timing - Threading and timing guide
-
Examples Gallery - Examples gallery