Overview
uvi-script uses cooperative multithreading based on Lua coroutines. Each event callback runs as a lightweight micro-thread that can be suspended and resumed without blocking other threads or the audio processing.
This threading model enables complex, time-based behaviors while maintaining real-time performance guarantees.
Threading Model
Event Callbacks as Threads
Every event callback (onNote, onRelease, onController, etc.) executes as an independent thread:
-- Each note creates a new thread
-- This loop runs independently for each note
local lfo = math.sin(2 * math.pi * 5 *
getTime() / 1000) -- 5 Hz vibrato
end
end
function getTime()
get the number of milliseconds elapsed since the script engine start.
Definition api.lua:740
function isNoteHeld()
return true is the note that created this callback is still held.
Definition api.lua:810
void onNote(table e)
event callback that will receive all incoming note-on events if defined.
function postEvent(e, delta)
send a script event back to the script engine event queue.
Definition api.lua:842
ScriptProcessor & this
Reference to the current ScriptProcessor instance.
Definition Engine.cpp:404
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
When multiple notes are played, each runs its own thread in parallel, maintaining separate state and execution flow.
Cooperative Scheduling
Threads are cooperative, meaning they must explicitly yield control using wait, waitBeat, or other waiting functions. A thread runs until it yields, and no other thread can execute in the meantime.
In practice this means every long-running loop needs a yield point:
-- BAD: infinite loop without yielding — blocks all note processing
-- missing
wait() here: nothing else can
run!
end
end
-- GOOD: yield on every iteration
wait(10) -- yield every 10 ms
end
end
function run(fun,...)
launch a function in a separate parallel execution thread.
Definition api.lua:1191
Thread-Local Variables
Use local variables within callbacks to maintain per-thread state:
local elapsed = 0 -- Separate for each note
elapsed = elapsed + 10
print("Note " .. e.note .. " held for " .. elapsed .. "ms")
end
end
Global variables are shared across all threads:
local noteCount = 0 -- Shared across all threads
noteCount = noteCount + 1
print("Total notes played: " .. noteCount)
end
Waiting Strategies
Wait in Milliseconds
Use wait to suspend execution for a precise number of milliseconds:
wait(1000) -- Wait exactly 1 second
end
function playNote(note, vel, duration, layer, channel, input, vol, pan, tune, slice, oscIndex)
helper function to generate a note event.
Definition api.lua:931
The wait function is sample-accurate and maintains precise timing even under CPU load.
Wait in Musical Time
Use waitBeat to suspend execution for a tempo-synchronized duration:
-- Create a tempo-synced arpeggio
waitBeat(0.25) -- Wait one sixteenth note
end
function waitBeat(beat)
Suspend execution for a tempo-synchronized duration in beats.
Definition conversions.lua:142
Common beat values:
waitBeat(0.25) - Sixteenth note
waitBeat(0.5) - Eighth note
waitBeat(1.0) - Quarter note (one beat)
waitBeat(2.0) - Half note
waitBeat(4.0) - Whole note
Wait for Note Release
Use waitForRelease to suspend until the triggering note is released:
-- Wait for release, then play a release sample
playNote(e.note - 12, 50, 200) -- Play low release sound
end
function waitForRelease()
suspend the current thread callback execution until the note that created this callback is released e...
Definition wrapper.lua:47
Waiting for Conditions
Use isNoteHeld in loops to create conditional waiting:
-- Vibrato that only plays while note is held
local modulation = 0.1 * math.sin(2 * math.pi * 5 *
getTime() / 1000)
end
end
Parallel Execution
Spawning New Threads
Use spawn to create new threads that run in parallel with the current one:
-- Spawn a vibrato thread that runs independently
local duration = 0
local modulation = 0.1 * math.sin(2 * math.pi * 5 * duration)
duration = duration + 0.005
end
end)
-- This callback completes immediately while vibrato continues
end
function spawn(fun,...)
Launch a function in a separate parallel execution thread (deferred execution)
Definition api.lua:1165
The spawned thread executes at the end of the current instant (callback cycle), so both the calling code and spawned function share the same initial timestamp.
Running Threads Immediately
Use run to execute a function immediately in a new thread:
-- Run starts the thread immediately
for i = 1, 4 do
playNote(e.note + i * 12, e.velocity, 100)
end
end)
end
Difference between spawn and run functions:
spawn() - Defers execution until end of current instant
run() - Starts execution immediately
Coordinating Multiple Threads
Create complex behaviors by spawning multiple coordinated threads:
-- Thread 1: Vibrato
local vib = 0.1 * math.sin(2 * math.pi * 5 *
getTime() / 1000)
end
end)
-- Thread 2: Volume swell
for vol = 0, 1, 0.01 do
end
end)
-- Thread 3: Panning oscillation
local pan = math.sin(2 * math.pi * 0.5 *
getTime() / 1000)
end
end)
end
function changeVolume(voiceId, gain, relative, immediate)
changes a voice's volume.
Definition api.lua:519
function changePan(voiceId, pan, relative, immediate)
changes the pan position of a specific note event.
Definition api.lua:504
Time Query Functions
Getting Current Time
Use getTime to retrieve the current sample-accurate time in milliseconds:
local elapsed =
getTime() - startTime
print("Elapsed: " .. elapsed .. "ms") -- Will print ~1000ms
end
Getting Musical Context
Query musical timing information:
local tempo =
getTempo() -- Current tempo in BPM
local timeSig = getTimeSig() -- Time signature (e.g., "4/4")
print("Tempo: " .. tempo .. " BPM")
print("Beat: " .. beat)
print("Time sig: " .. timeSig)
end
function getBeatDuration()
return the duration of a beat in ms.
Definition api.lua:696
function getBeatTime()
get song position in beats.
Definition api.lua:748
function getTempo()
return the current tempo.
Definition api.lua:712
Common Timing Patterns
Tempo-Synced Arpeggio
local notes = {0, 4, 7, 12} -- Major chord
local step = 0
local interval = notes[(step % #notes) + 1]
playNote(e.note + interval, e.velocity, 100)
step = step + 1
end
end
Delay Effect
-- Spawn delayed echoes
for i = 1, 4 do
wait(250 * i) -- Each echo 250ms apart
local vel = e.velocity * (0.7 ^ i) -- Decay velocity
end
end)
end
Tremolo Effect
local tremoloFreq =
Knob(
"Tremolo Rate", 5, 0, 20)
local tremoloDepth =
Knob("Tremolo Depth", 0.5, 0, 1)
local phase = 2 * math.pi * tremoloFreq.value *
getTime() / 1000
local modulation = 1 - (tremoloDepth.value * (1 - math.cos(phase)) / 2)
end
end)
end
Knob widget.
Definition ui.cpp:1424
Rhythmic Gate
local gateOpen = true
if gateOpen then
else
end
gateOpen = not gateOpen
waitBeat(0.25) -- Toggle every sixteenth note
end
end)
end
Cancelling a Running Loop
When a new note should restart a sequencer or arpeggiator, you need to stop the previous loop cleanly. The standard pattern uses a shared counter: each loop checks whether its ID is still current before continuing.
local seqId = 0
seqId = seqId + 1 -- invalidate any running loop
local myId = seqId -- capture current ID
local step = 0
while myId == seqId do -- stop if a new note started
playNote(e.note + step, e.velocity, 200)
step = (step + 1) % 8
end
end
function onRelease(e)
seqId = seqId + 1 -- also stop on release
end
This is safer than relying on isNoteHeld alone, because it handles the case where a new note arrives before the previous one is released (legato playing).
Musical Context Queries
Beyond basic time queries, the scripting API provides functions to inspect musical context, note state, and MIDI controller values. These are essential for building responsive instruments that react to performance in real time.
Duration Queries
Use getBarDuration and getBeatDuration to retrieve the current bar and beat durations in milliseconds. These update automatically when the host tempo changes. getSamplingRate returns the audio engine sample rate in Hz.
local barMs =
getBarDuration() -- Full bar in ms (e.g. 2000 at 120 BPM, 4/4)
-- Compute a dotted-eighth delay time from the current tempo
local dottedEighth = beatMs * 0.75
print("Dotted eighth = " .. dottedEighth .. " ms")
end
function getBarDuration()
return the duration of a bar in ms.
Definition api.lua:688
function getSamplingRate()
return the current audio sampling rate.
Definition api.lua:704
Beat Position
getBeatTime returns the beat position reported by the host transport. It reflects the DAW playhead and jumps when the user relocates the cursor.
getRunningBeatTime uses an internal monotonic clock that keeps counting even when the host transport is stopped. This makes it suitable for free-running sequencers or arpeggios that should continue regardless of transport state.
-- Host-synced position — follows the DAW playhead
-- Internal monotonic position — never resets or jumps
print("Host beat: " .. hostBeat .. " Running beat: " .. runBeat)
end
function getRunningBeatTime()
get song position in beats.
Definition api.lua:757
Note and Key State
getNoteDuration returns the number of milliseconds elapsed since the last note-on for a given MIDI note number. isKeyDown returns true if a specific note is currently held, and isOctaveKeyDown returns true if any octave of that note class is held (e.g. any C).
-- Velocity-sensitive release: short notes get a staccato sample,
-- long notes get a sustain-release sample
if held < 200 then
-- Short press — trigger staccato release layer
else
-- Long press — trigger sustain release layer
playNote(e.note, math.min(e.velocity, 80), 400)
end
end
-- Check if the octave below is also held (e.g. for split behaviour)
print("Another octave of
this note is held")
end
end
function isOctaveKeyDown(note)
test if a note is down ignoring octave.
Definition api.lua:796
function getNoteDuration(note)
return the number of ms since the last note-on for the given note.
Definition api.lua:765
void onRelease(table e)
event callback executed whenever a note off message is received.
MIDI State
getCC returns the last received value (0–127) for any MIDI continuous controller number. This lets scripts read controller state at any time, not only inside onController.
local vibratoDepth = 0
-- Read mod wheel (CC 1) to scale vibrato depth
local modWheel =
getCC(1) / 127
local vib = modWheel * 0.5 * math.sin(2 * math.pi * 5 *
getTime() / 1000)
end
end)
end
function getCC(cc)
Retrieve the last control change value received for the given cc number.
Definition api.lua:731
Best Practices
- Always Yield in Loops
- Never create infinite loops without yielding:
-- BAD: will freeze the script — no yield point
end
end
-- GOOD: yields every 10 ms
end
end
- Use Tempo-Synced Timing for Musical Events
- For musical timing, prefer waitBeat over wait functions:
-- GOOD: Stays in sync with tempo changes
waitBeat(1.0) -- Always one beat, regardless of tempo
end
-- LESS IDEAL: Fixed time, doesn't adapt to tempo
wait(500) -- Always 500ms, tempo-independent
end
- Store Voice IDs for Later Manipulation
- Always store voice IDs returned by playNote or postEvent functions:
local
id =
postEvent(e) -- Store the voice ID
changeTune(
id, 0.5) -- Can modify
this specific voice
end)
end
- Use Local Variables for Thread State
- Keep per-thread state in local variables:
local elapsed = 0 -- Unique to this thread
elapsed = elapsed + 10
-- Use elapsed...
end
end
Transport State
The onTransport callback is called whenever the host transport state changes (play/stop). This is useful for synchronizing sequencers or arpeggios with the host:
if playing then
print("Transport started")
else
print("Transport stopped")
end
end
void onTransport(bool playing)
event callback that is called when the host transport bar state changes.
See Also
- See also
- Time and Threading - Time and Threading manipulation functions
-
wait - Suspend execution for milliseconds
-
waitBeat - Suspend execution for musical beats
-
waitForRelease - Wait until note is released
-
spawn - Create new thread (deferred)
-
run - Create new thread (immediate)
-
getTime - Get current time
-
getTempo - Get current tempo
-
getBeatTime - Get current beat position
-
isNoteHeld - Check if note is still held
-
getBarDuration - Get bar duration in milliseconds
-
getBeatDuration - Get beat duration in milliseconds
-
getSamplingRate - Get audio sample rate
-
getRunningBeatTime - Get monotonic beat position
-
getNoteDuration - Get time since note-on for a note
-
isKeyDown - Check if a specific note is held
-
isOctaveKeyDown - Check if any octave of a note is held
-
getCC - Get last value of a MIDI CC
-
Examples Gallery - Examples gallery