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:931
function postEvent(e, delta)
send a script event back to the script engine event queue.
Definition api.lua:842

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:477

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:504

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:519

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:546

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:598
function fade(voiceId, targetValue, duration, layer)
starts a volume fade.
Definition api.lua:651
function fadein(voiceId, duration, reset, layer)
starts a volume fade-in for a specific voice.
Definition api.lua:632

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:1424
function isNoteHeld()
return true is the note that created this callback is still held.
Definition api.lua:810

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:740

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:558

Script Modulation

Script modulation sources are modulation slots in the engine that can be driven entirely from script code. You assign a script modulation source to a destination in the engine UI, then send values from your script. This allows continuous, per-voice or broadcast parameter control — useful for expression, morphing, or any automation that goes beyond simple note-on changes.

Use sendScriptModulation to send a value to a modulation source:

  • id — modulation source index (configured in the engine UI)
  • targetValue — destination value, [0,1] unipolar or [-1,1] bipolar depending on the mapping
  • rampTime — interpolation time in ms (default 20 ms, avoids zipper noise)
  • voiceId — optional; pass a voice ID for per-voice modulation, or nil to broadcast to all voices
-- Broadcast example: map MIDI CC1 (mod wheel) to script modulation source 0
function onController(e)
if e.controller == 1 then
local value = e.value / 127 -- Normalize to 0..1
sendScriptModulation(0, value, 20) -- voiceId omitted = broadcast
end
end
void onController(table e)
event callback that will receive all incoming control-change events when defined.
function sendScriptModulation(id, targetValue, rampTime, voiceId)
send a script modulation event to control a Script Modulation source.
Definition api.lua:417

Use sendScriptModulation2 for piece-wise linear ramps where you need an explicit start value. Parameters are the same, with an additional startValue before targetValue:

-- Per-voice expression: ramp modulation source 1 from 0 to 1 over 500 ms
function onNote(e)
local id = postEvent(e)
sendScriptModulation2(1, 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:447

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