Tutorial

What can UVI scripts do?

UVI introduced a new type of module in its core engine: Event Processors.

Event Processors are like MIDI effects with some extensions that allows to go beyond the limitations of MIDI.

UVI scripts are written using the Lua programming Language and a UVI-specific API.

Lua is a simple yet very efficient and powerful scripting language used in many applications. For general information about learning the Lua language, you can have a look at the Lua Reference Manual and the very well written online book Programming in Lua

This document aims to help to write your first UVI script.

Creating a ScriptProcessor Module

In the Main Edit page, click on the EventProcessors tab -> Add -> ScriptProcessor.

AddScriptProcessor.png

Now open your favourite TextEditor with Lua syntax support (NotePad++, TextWrangler ... etc ) and create an empty text file vibrato.lua.

Click on the load Button in the ScriptProcessor toolbar and locate your script.

load.png

At this point you have an empty but valid script loaded inside the ScriptProcessor. You can now proceed to writing the actual script instructions. Don't forget to click on reload each time you want to test your changes.

reload.png

Event driven programming

UVI script is an event oriented programming interface. The default behaviour is to let all incoming events pass-through unmodified. This behaviour can be customized by writing event callbacks:

Writing your first UVI script

Here we will write a very short vibrato script and analyze it line by line to explain what's happening:

freq = 4 -- vibrato frequency in Hz
depth = 0.1 -- the vibrato depth in semitones
grain = 5 -- vibrato updating period in milliseconds
function onNote(e)
local duration = 0
local id = postEvent(e)
while isNoteHeld() do
local modulation = depth * math.sin(2 * math.pi * freq * duration)
changeTune(id, modulation);
wait(grain)
duration = duration + grain/1000
end
end

Main code chunk

freq = 4 -- vibrato frequency in Hz
depth = 0.1 -- the vibrato depth in semitones
grain = 5 -- vibrato updating period in milliseconds

Here we initialize two global variables freq to define the frequency of the vibrato and grain to define its refresh period. We will use it later during the note callback execution

onNote callback

function onNote(e)
local duration = 0
...
end

Here we write our own onNote callback function to customize its behaviour. We also initialize a local variable (duration) that we will use to measure the time elapsed since we started the callback.

Warning
It's important to declare this variable as being local to this callback, otherwise each time a new note event would be processed, we would reset the duration to 0 for all the simultaneously running callbacks.
It's also important to forward events, otherwise if you just let the callback empty they won't pass through.

Forwarding the note event

function onNote(e)
...
local id = postEvent(e)
...
end

Here we just forward the incoming note event to the output of this script untouched but we store its (voice) id in a local variable that will be needed later own to change this specific voice's pitch.

the vibrato loop

while isNoteHeld() do
local modulation = depth * math.sin(2 * math.pi * freq * duration)
changeTune(id, modulation);
wait(grain)
duration = duration + grain/1000
end

Here every 5 milliseconds, while the note that created this callback is still held:

  1. we compute the modulation value.
  2. we change the voice tuning using the voice id
  3. we put the callback on hold for 5ms
  4. we update the elapsed duration variable

Adding User Interface controls

vibrato.png

here we will add some knobs on the user interface to make the frequency and depth of the vibrato editable:

freq = Knob("Freq", 4.0, 0, 10) -- vibrato frequency in Hz
depth = Knob("Depth", 0.1, 0, 1) -- the vibrato depth in semitones
grain = 5 -- vibrato updating period in milliseconds
function onNote(e)
local duration = 0
local id = postEvent(e)
while isNoteHeld() do
local modulation = depth.value * math.sin(2 * math.pi * freq.value * duration)
changeTune(id, modulation);
wait(grain)
duration = duration + grain/1000
end
end

we only changed 3 lines of code:

freq = Knob("Freq", 4.0, 0, 10) -- vibrato frequency in Hz
depth = Knob("Depth", 0.1, 0, 1) -- the vibrato depth in semitones

we now declare freq and depth to be Knobs instead of scalar variables. freq as a default value of 4Hz and a range of [0;10] while depth has a default initial value of 0.1 with a range of [0;1]

local modulation = depth.value * math.sin(2 * math.pi * freq.value * duration)

freq and depth are now Knob objects, so instead of using them as scalar values, we access their value attribute.

Conclusion

We have implemented with very few lines of code a vibrato script. Although being simple, this example has demonstrated several important concepts:

You can now get further by looking at the reference documentation.