uvi-script
Musical event scripting with Lua
Loading...
Searching...
No Matches
MIDI Event Generation

Overview

Beyond receiving MIDI events through callbacks like onNote and onController, uvi-script can also generate outgoing MIDI events. This is useful for building arpeggiators, chord generators, MIDI processors, and automation scripts.

There are two approaches:

  • Helper functions — concise, dedicated functions for each event type
  • postEvent() — low-level, allows full control over all event fields

Helper Functions

These convenience functions wrap postEvent with the correct event type. All accept optional channel and input parameters for routing to specific Parts.

Control Change

Send a MIDI CC message:

function onNote(e)
-- send mod wheel (CC1) value 100 on the same channel
controlChange(1, 100, e.channel)
end
-- reset all controllers on note release
function onRelease(e)
for cc = 1, 119 do
end
end
void onNote(table e)
event callback that will receive all incoming note-on events if defined.
void onRelease(table e)
event callback executed whenever a note off message is received.
function postEvent(e, delta)
send a script event back to the script engine event queue.
Definition api.lua:857
function controlChange(cc, val, ch, inp)
sends a ControlChange event.
Definition api.lua:1067

The loop stops at CC 119: controllers 120-127 are channel-mode messages (all sound off, reset all controllers, all notes off, omni/mono/poly mode), not ordinary controllers. The engine acts on them — CC 120 cuts every playing voice immediately and CC 123 releases every held note — so they must not be sent as part of a controller reset.

See also
controlChange, onController

Pitch Bend

Send a pitch bend message. The value is normalized to [-1.0; 1.0]:

-- pitch bend ramp on each note
function onNote(e)
spawn(function()
for i = 0, 100 do
pitchBend(i / 100) -- ramp from 0 to +1
wait(10)
end
pitchBend(0) -- reset
end)
end
function pitchBend(b, ch, inp)
sends a PitchBend event.
Definition api.lua:1098
function wait(ms)
suspend the current thread callback execution for the given number of milliseconds.
Definition wrapper.lua:39
function spawn(fun,...)
Launch a function in a separate parallel execution thread (deferred execution)
Definition api.lua:1201
See also
pitchBend, onPitchBend

Program Change

Send a program change message:

-- switch program based on velocity range
function onNote(e)
if e.velocity > 100 then
programChange(1) -- high velocity: program 1
else
programChange(0) -- low velocity: program 0
end
end
function programChange(val, ch, inp)
sends a ProgramChange event.
Definition api.lua:1083
See also
programChange, onProgramChange

Aftertouch

Send channel aftertouch or polyphonic aftertouch:

-- convert velocity to channel aftertouch
function onNote(e)
afterTouch(e.velocity)
end
-- per-note pressure from velocity
function onNote(e)
polyAfterTouch(e.velocity, e.note)
end
function afterTouch(v, ch, inp)
sends an AfterTouch event.
Definition api.lua:1113
function polyAfterTouch(v, n, ch, inp)
sends a PolyAfterTouch event.
Definition api.lua:1129
See also
afterTouch, polyAfterTouch, onAfterTouch, onPolyAfterTouch

Modifying Incoming Events

Instead of generating new events, you can modify incoming events before forwarding them with postEvent. This is the most efficient way to transform MIDI data:

-- Remap CC1 (mod wheel) to CC11 (expression)
function onController(e)
if e.controller == 1 then
e.controller = 11
end
end
void onController(table e)
event callback that will receive all incoming control-change events when defined.

You can modify any field of the event table before posting it:

-- Transpose all notes up one octave
function onNote(e)
e.note = e.note + 12
end
function onRelease(e)
e.note = e.note + 12
end
Note
When modifying onNote events, remember to apply the same transformation in onRelease so the engine can match the note-off to the correct voice.

RPN, NRPN and Data Entry CCs

A few controller numbers are consumed before they reach the script: CC 6 and CC 38 (data entry MSB/LSB), CC 98 and CC 99 (NRPN LSB/MSB), and CC 100 and CC 101 (RPN LSB/MSB) do not trigger onController (or onEvent) and do not update getCC.

To receive them as plain CC events, opt in at script load time:

enable_rpn_as_cc(true) -- receive CC 100/101
enable_nrpn_as_cc(true) -- receive CC 98/99
enable_data_entry_as_cc(true) -- receive CC 6/38

Once enabled, the MSB/LSB state machine is the script's responsibility: decode the parameter number from CC 99/98 (NRPN) or CC 101/100 (RPN), then the value from CC 6/38, inside onController.

Using postEvent Directly

The helper functions are shortcuts for postEvent. You can achieve the same result (and more) by constructing event tables directly:

-- these two are equivalent:
postEvent({type = Event.ControlChange, controller = 1, value = 64})
Event types.
Definition Engine.cpp:723

Using postEvent directly gives access to additional fields like layer, plus an optional second argument delta (timing offset in ms):

-- send CC to a specific layer with a 10ms delay
type = Event.ControlChange,
controller = 1,
value = 100,
layer = 2
}, 10)

Raw MIDI with postMidiEvent

For playback of MIDI files or raw MIDI data, use postMidiEvent with MidiEvent objects. The class lives in the uvi table, and the constructor takes the channel before the event type: uvi.MidiEvent(timestamp, channel, type, byte1, byte2). The Event type constants are global and need no prefix.

Note
MidiEvent objects use the raw 0-based MIDI channel in [0;15], unlike the event tables passed to callbacks and postEvent, which use 1-based channels in [1;16] (with 0 accepted by postEvent as omni). Channel 1 in an event table is channel 0 here.
-- note-on: channel 0, note 60, velocity 100
local event = uvi.MidiEvent(0, 0, Event.NoteOn, 60, 100)
void postMidiEvent(MidiEvent e)
send a MidiEvent directly to the script engine event queue.
Note
Events posted via postMidiEvent don't have voice IDs. For advanced voice tracking, use playNote and releaseVoice instead.
See also
MIDI, Events, Event Callbacks, Asynchronous Operations, Porting from Kontakt (KSP)