uvi-script
Musical event scripting with Lua
Loading...
Searching...
No Matches
Voice Manipulation

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:

  1. Voice Creation - A voice is created via playNote or postEvent, which returns a unique voice ID
  2. Voice Manipulation - The voice can be modified using changeTune, changePan, changeVolume
  3. Voice Fading - The voice can be faded using fadein, fadeout, or fade
  4. Voice Release - The voice ends via releaseVoice or automatic release when the note ends

Creating Voices

function onNote(e)
-- Method 1: Forward incoming event and get voice ID
local id1 = postEvent(e)
-- 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:

function onNote(e)
local voiceId = postEvent(e) -- Store the ID
-- Later, manipulate this specific voice
wait(500)
changeTune(voiceId, 0.5) -- Bend up 50 cents
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
function onNote(e)
local id = postEvent(e)
voices[e.note] = id
-- now you can manipulate this specific voice later
changePan(id, (e.note / 127) * 2 - 1) -- spread by pitch
end
function onRelease(e)
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 = {}
function onNote(e)
voices[e.note] = postEvent(e)
end
function onRelease(e)
voices[e.note] = nil
end
function onPitchBend(e)
for note, id in pairs(voices) do
changeTune(id, e.bend * 2) -- ±2 semitone range
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:

function onNote(e)
local id = postEvent(e)
-- Bend pitch up by 2 semitones (whole step)
changeTune(id, 2.0)
-- Bend pitch down by 1 semitone
changeTune(id, -1.0)
-- Detune by 10 cents
changeTune(id, 0.1)
end

Volume Control

Use changeVolume to modify the volume of a voice:

function onNote(e)
local id = postEvent(e)
-- Set volume to 50%
changeVolume(id, 0.5)
-- Set volume to 100%
changeVolume(id, 1.0)
-- Mute the voice
changeVolume(id, 0.0)
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:

function onNote(e)
local id = postEvent(e)
-- 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:

function onNote(e)
local id = postEvent(e)
-- Pan hard left
changePan(id, -1.0)
-- Pan center
changePan(id, 0.0)
-- Pan hard right
changePan(id, 1.0)
-- Pan slightly right
changePan(id, 0.3)
end

Pan range: -1.0 (hard left) to 1.0 (hard right), 0.0 is center

Fading Voices

Control voice envelope with fade functions:

function onNote(e)
local id = postEvent(e)
-- Fade in over 500ms
fadein(id, 500)
-- Later, fade out over 1000ms and stop voice
wait(2000)
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:

function onNote(e)
-- 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)
changeTune(id2, 0.05) -- +5 cents
changeTune(id3, -0.05) -- -5 cents
-- Optional: Pan them for width
changePan(id1, 0.0) -- Center
changePan(id2, -0.3) -- Slightly left
changePan(id3, 0.3) -- Slightly right
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)
function onNote(e)
local id = postEvent(e)
local elapsed = 0
while isNoteHeld() do
-- 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)
changeTune(id, modulation)
wait(5)
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)
function onNote(e)
local id = postEvent(e)
while isNoteHeld() do
local phase = 2 * math.pi * tremoloRate.value * getTime() / 1000
local modulation = 1 - (tremoloDepth.value * (1 - math.cos(phase)) / 2)
changeVolume(id, modulation)
wait(5)
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)
function onNote(e)
local id = postEvent(e)
while isNoteHeld() do
local phase = 2 * math.pi * panRate.value * getTime() / 1000
local pan = math.sin(phase)
changePan(id, pan)
wait(10)
end
end

Voice Crossfading

Smoothly transition between voices:

function onNote(e)
-- Play two different layers
local voice1 = playNote(e.note, e.velocity, -1, 1) -- Layer 1
local voice2 = playNote(e.note, e.velocity, -1, 2) -- Layer 2
-- Start with layer 1 only
changeVolume(voice2, 0.0)
-- Crossfade from layer 1 to layer 2 over 2 seconds
for i = 0, 100 do
local mix = i / 100
changeVolume(voice1, 1.0 - mix)
changeVolume(voice2, mix)
wait(20)
end
end
A layer of sounds.
Definition Engine.cpp:255

Pitch Bend Effect

Create smooth pitch glides:

function onNote(e)
local id = postEvent(e)
-- 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
wait(5)
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)
function onNote(e)
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
changeTune(id, detune)
-- Spread across stereo field
local pan = ((i - 1) / (unisonVoices - 1) - 0.5) * 2 * unisonSpread.value
changePan(id, pan)
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
local id = postEvent(e)
wait(1000)
changeTune(id, 0.5)
-- 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:
function onNote(e)
local id = postEvent(e)
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
function onNote(e)
for i = 1, 5 do
playNote(e.note, e.velocity, -1)
end
end
-- POTENTIALLY BAD: Too many voices
function onNote(e)
for i = 1, 50 do -- May cause CPU issues
playNote(e.note, e.velocity, -1)
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.

function onNote(e)
local id = postEvent(e)
-- Random start offset (0–80 ms) for natural variation on repeated notes
local offset = math.random() * 80
setSampleOffset(id, offset)
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)
function onNote(e)
local id = postEvent(e)
local velNorm = e.velocity / 127
sendScriptModulation(0, velNorm, 0, id) -- immediate, per-voice
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)
function onController(e)
if e.controller == 74 then
local value = e.value / 127
sendScriptModulation(1, value, 15, e.id) -- per-voice, 15ms ramp
end
end
function onAfterTouch(e)
sendScriptModulation(2, e.value / 127, 12, e.id) -- per-voice, 12ms ramp
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)
sendScriptModulation(3, self.value, 20) -- broadcast, 20ms ramp
end
function changed
callback function used by child widgets to be notified of changes
Definition ui.cpp:872

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
function onNote(e)
local id = postEvent(e)
sendScriptModulation2(0, 0.0, 1.0, 500, id)
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