This page collects all bundled Lua examples, organized by category. Each script is a self-contained starting point that you can load directly into MachFive and tweak to fit your own patches.
| Note Processing | Voice Effects | Performance & Articulation | Sequencing | MIDI Tools | Asset Loading | UI Helpers
|
|
Chorder
chord harmonizer with presets
|
Ensemble
ensemble with pan and time spread
|
legato
legato with crossfade and retrigger
|
Arpeggiator
classic chord arpeggiator (up / down / updown, octave range)
|
CCFilter
block a specific midi cc
|
IRLoader
hierarchical ir menu with userready guard
|
FX Controls
bind a program insert effect to the script ui
|
|
InvertPitch
mirror notes around a center pitch
|
tremolo
amplitude and pan lfo
|
monoBassLine
monophonic bass synth with sequencer
|
StepSequencer
beat-synced step sequencer with position display
|
CCRedirect
remap a midi cc number to another
|
SampleDropper
load samples via drag and drop
|
PanelSwitcher
tab-based panel switching with main/fx/seq views
|
|
Keyswitch
automatic layer switching via keyswitches
|
Unison
detuned unison voices
|
portamento
portamento with pitch glide
|
|
CCSmooth
smooth incoming cc messages over time
|
|
TemporaryDisplay
flash knob values on labels with auto-revert
|
|
Latch
latch / hold with mid-hold toggle safety
|
vibrato
per-voice pitch vibrato
|
|
|
MidiLearn
midi learn for note assignment
|
|
|
|
quarterTone
quarter-tone keyboard mapping
|
VoiceTracker
track and manipulate active voices
|
|
|
|
|
|
|
TimbreShifting
borrow neighbouring keygroup timbres
|
|
|
|
|
|
|
Note Processing
Chorder
Chord Harmonizer with Presets
Generates chords by playing up to 6 simultaneous notes with configurable pitch shifts and velocity scaling. Ships with several named presets (Major, Fifth, Jazz, Debusian, etc.) selectable from a Menu widget. Worth studying: the preset Menu pattern. The Shift / Velocity knobs are left .persistent = true (default) so the engine round-trips their values across save / load. The preset Menu itself is .persistent = false so the engine does NOT restore it via the normal path — that would fire its .changed callback during the restore and overwrite the just-restored knob values. We round-trip the menu's visual selection ourselves with setValue(index, false) in onLoad, where the second arg false tells the widget NOT to fire .changed. The user's last-picked preset name is shown again, the knobs keep their tweaks, and no side effect re-fires. .persistent, setValue notify flag
Demonstrates: playNote, Knob, Menu, onSave, onLoad,
local shift = {}
local velocity = {}
local presets = {
{"--", {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}},
{"Default", {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}},
{"Debusian", {-16, 1.02}, {3, 0.54}, {18, 0.83}, {-6, 0.74}, {-9, 1.16}, {0, 1}},
{"Film Noir", {-2, 0.83}, {5, 1.25}, {-6, 0.74}, {12, 0.65}, {0, 1}, {0, 1}},
{"Jazz for dummies", {3, 0.54}, {5, 0.88}, {-16, 1}, {-10, 1}, {0, 1}, {0, 1}},
{"Major", {4, 1}, {7, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}},
{"House for to go", {3, 1}, {7, 1}, {-12, 1}, {0, 1}, {0, 1}, {0, 1}},
{"Fifth", {0, 1}, {7, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}},
{"Fourth", {0, 1}, {5, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}},
{"Grandiosa", {-12, 1}, {-24, 1.33}, {12, 1.41}, {7, 1}, {0, 1}, {0, 1}}
}
for i=1,6 do
shift[i] =
Knob(
"Shift"..tostring(i), 0, -36, 36,
true)
end
for i=1,6 do
velocity[i] =
Knob("Velocity_"..tostring(i), 1, 0.01, 2)
end
local presetNames = {}
for i, preset in ipairs(presets) do
presetNames[i] = preset[1]
end
presetMenu =
Menu(
"Presets", presetNames)
presetMenu.
persistent =
false -- engine does not restore it; we
do it ourselves
presetMenu.changed = function(self)
for i=1,6 do
shift[i].value = presets[self.value][i+1][1]
velocity[i].value = presets[self.value][i+1][2]
end
end
presetMenu:changed()
-- Persist only the menu's visual index. The knobs round-trip themselves
-- via .persistent = true. setValue(idx, false) restores the visible
-- selection without firing .changed (which would clobber the knobs).
function onSave()
return { preset = presetMenu.value }
end
function onLoad(data)
if data.preset then
presetMenu:setValue(data.preset, false)
end
end
function onNote(e)
local done = {} -- store already played notes in order to avoid redundancy
for i=1,6 do
if not done[shift[i].value] then
playNote(e.note + shift[i].value, math.min(127, e.velocity*velocity[i].value))
done[shift[i].value] = true;
end
end
end
function onRelease()
-- eat event, release is automatic with playNote
end
Knob widget.
Definition ui.cpp:1520
⬇ Download Chorder.lua
InvertPitch
Mirror Notes Around a Center Pitch
Mirrors incoming MIDI notes around a user-defined center pitch using a Knob widget. Notes equidistant above the center are mapped below it and vice-versa, creating an intervallic inversion effect.
Demonstrates: onNote callback, Knob widget, pitch arithmetic, playNote
CenterPitch =
Knob(
"Center_Pitch", 60, 0, 127,
true)
local center = CenterPitch.value
local delta = e.note-center
local note = center - delta
if note>=0 and note<=127 then
end
end
function onRelease()
-- eat event
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
⬇ Download InvertPitch.lua
Keyswitch
Automatic Layer Switching via Keyswitches
Assigns a range of low MIDI keys as keyswitches that select which layer receives subsequent notes. The keyswitch range is coloured red on the keyboard for visual feedback.
Demonstrates: Program.layers, playNote with layer parameter, setKeyColour, onNote / onRelease
local numLayers = #
Program.
layers -- number of layer in the program
local lastLayerActivated = 1 -- id of the last activated layer
local KSbaseNote = 36 -- MIDI note where keyswitch are located
if e.note >= KSbaseNote and e.note < KSbaseNote + numLayers then -- if the note is one of the keyswitch keys
lastLayerActivated = e.note - KSbaseNote + 1 -- update the activated layer id
else
playNote(e.note, e.velocity, -1, lastLayerActivated) -- not a keyswitch key so we play the note on the activated layer
end
end
-- eat
event as release is done automatically by
playNote
end
for i=KSbaseNote, KSbaseNote+numLayers-1 do
end
A Patch that represents a monotimbral instrument.
Definition Engine.cpp:238
table layers
Layer list for this Program (1-indexed, use Program.layers to get the count)
Definition Engine.cpp:247
void onRelease(table e)
event callback executed whenever a note off message is received.
function setKeyColour(note, colour)
customize the keyboard colours.
Definition ui.lua:50
⬇ Download Keyswitch.lua
Latch
Latch / Hold with mid-hold toggle safety
Latches incoming notes: while Latch is on, played notes keep sounding after key-up. A second key-down on the same note clears it. The Clear button releases everything at once. The point of this example is the bookkeeping pattern that prevents stuck and orphan notes when the user toggles Latch mid-hold:
- The behaviour at note-on (forward immediately vs. forward and hold) depends on the current Latch state.
- The matching note-off must do the right thing for THIS note's press, not for the current Latch state — otherwise toggling Latch while a key is held leaks voices. The fix is to remember the per-note decision in a
latchedAtPress table at the time of the press, then mirror it at release. Any callback whose action depends on a mutable state should follow this pattern.
Demonstrates: OnOffButton, Button, postEvent, releaseVoice, per-note bookkeeping
displayName = "Latch",
tooltip = "When on, notes sustain after key-up"}
displayName = "Clear",
tooltip = "Release every latched note"}
-- For each note, store the voiceId of the playing voice when it was latched.
-- Without this we cannot release latched voices on demand.
local latchedVoice = {} -- note -> voiceId
-- Per-note flag: was Latch on at the moment of the press?
-- The release must mirror that decision regardless of the Latch state now.
local latchedAtPress = {}
local function clearAll()
for note, vid in pairs(latchedVoice) do
latchedVoice[note] = nil
latchedAtPress[note] = nil
end
end
-- Re-pressing a latched note: clear it, do not forward.
if latchedVoice[e.note] then
latchedVoice[e.note] = nil
latchedAtPress[e.note] = nil
return
end
-- New press: always start the voice. We capture its voiceId so we can
-- release it later (either via a re-press, ClearAll, or onRelease).
latchedAtPress[e.note] = LatchOn.value
if LatchOn.value then
latchedVoice[e.note] = vid
end
end
function onRelease(e)
-- Mirror the press: if the press was latched, hold the voice. If the
-- press was a normal pass-through, forward the release so the engine
-- ends the voice cleanly. Note that we never look at LatchOn.value here.
if latchedAtPress[e.note] then
-- The press was latched -> swallow the release; voice keeps sounding.
latchedAtPress[e.note] = nil
return
end
-- The press was unlatched -> release normally.
latchedAtPress[e.note] = nil
end
ClearAll.changed = function(self)
clearAll()
end
-- Turning Latch off mid-hold: release everything currently latched. Notes
-- whose key is still pressed will be released here too, which is the
-- expected musical behaviour ("flush the latch").
LatchOn.changed = function(self)
if not self.value then
clearAll()
end
end
function releaseVoice(voiceId)
release a specific voice by sending it a note off message
Definition api.lua:1019
function postEvent(e, delta)
send a script event back to the script engine event queue.
Definition api.lua:853
void setSize(number w, number h)
set the script UI dimensions explicitly.
⬇ Download Latch.lua
quarterTone
Quarter-Tone Keyboard Mapping
Remaps the standard 12-tone keyboard to a 24-tone quarter-tone scale relative to a selectable root note. Even intervals map directly; odd intervals are detuned by 50 cents using changeTune.
Demonstrates: Menu widget, changeTune, microtonal pitch mapping, modular arithmetic
notes = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"}
notenames={}
for i=1,128 do
notenames[i] = notes[(1+(i-1)%12)] .. (math.floor(i/12) - 2)
end
Root =
Menu("root", notenames)
Root.value = 60+1
local root = Root.value-1
local note = e.note
local velocity = e.velocity
local detune = 0
if (e.note-root)%2 == 0 then -- no detune
note = root + math.floor((e.note-root)/2)
else
if e.note > root then
note = math.floor((e.note-root)/2) + root + 1
detune = -0.5 -- minus 50 cents
elseif e.note < root then
note = math.floor((e.note-root)/2) + root - 1
detune = 0.50 -- plus 50 cents
end
end
if detune ~= 0 then
end
end
function onRelease(e)
-- release is automatic
end
function changeTune(voiceId, shift, relative, immediate)
change the tuning of specific voice in (fractionnal) semitones.
Definition api.lua:488
⬇ Download quarterTone.lua
TimbreShifting
Borrow Neighbouring Keygroup Timbres
Shifts the played note into a neighbouring keygroup and retunes it back to the original pitch, effectively borrowing that keygroup's timbre. The shift amount is controlled by a bipolar Knob.
Demonstrates: playNote, changeTune with absolute flag, timbral manipulation
shiftKnob =
Knob(
"shift", 0, -5, 5,
true)
local shift = shiftKnob.value
local note = e.note + shift
local
id =
playNote(note, e.velocity, -1)
end
function onRelease()
-- eat event release is automatic in
playNote
end
⬇ Download TimbreShifting.lua
Voice Effects
Ensemble
Ensemble with Pan and Time Spread
Similar to Unison but adds per-voice pan spread and staggered onset timing to simulate an ensemble. Each voice is shifted, retuned, panned, and delayed by a small jitter amount.
Demonstrates: playNote, changeTune, changePan, changeVolume, wait
PanSpread =
Knob(
"PanSpread", 1.0, 0, 1)
TimeSpread =
Knob("TimeSpread", 0.5, 0, 1)
shifts = {0, 1, -1, 2, -2}
--shifts = {0, 1, -1, 2, -2, 3, -3}
--shifts = {0, 1, -1, 2, -2, 3, -3, 4, -4}
local panSpread = PanSpread.value
local timeSpread = TimeSpread.value
local numShifts = #shifts
for i=1,numShifts do
local shift = shifts[i]
local note = e.note + shift
local tune = -shift
wait(timeSpread*10) -- 10 ms jitter
end
end
function onRelease()
-- eat event release is automatic in
playNote
end
function wait(ms)
suspend the current thread callback execution for the given number of samples.
Definition wrapper.lua:39
function changeVolume(voiceId, gain, relative, immediate)
changes a voice's volume.
Definition api.lua:530
function changePan(voiceId, pan, relative, immediate)
changes the pan position of a specific note event.
Definition api.lua:515
⬇ Download Ensemble.lua
tremolo
Amplitude and Pan LFO
Modulates volume and pan of each voice with a sine-wave LFO whose frequency scales with MIDI note number. The volume oscillates between 0 and 1 while pan follows a cosine (90-degree phase offset).
Demonstrates: changeVolume, changePan, isNoteHeld, wait, postEvent
lfoFreq =
Knob(
"freq", 4.0, 0, 10) -- 4 Hz
local duration = 0 -- in seconds
local step = 5 -- ms
local freq = lfoFreq.value * e.note/128.0
local volume = 0.5 * ( 1 + math.sin(2 * math.pi * freq * duration))
local pan = math.sin(2 * math.pi * freq * duration + math.pi/2)
duration = duration + step/1000.0
end
end
function isNoteHeld()
return true is the note that created this callback is still held.
Definition api.lua:821
⬇ Download tremolo.lua
Unison
Detuned Unison Voices
Stacks multiple detuned copies of the incoming note. The number of voices and the maximum detune spread are adjustable. Alternating voices are tuned sharp and flat in increasing amounts, and volume is auto-scaled by the square root of the voice count.
Demonstrates: playNote, Knob, changeTune, changeVolume
Voices =
Knob(
"numVoices", 5, 2, 10,
true)
Detune =
Knob("Detune", 10.0, 0, 40) -- cents
local nVoices = Voices.value
local detune = Detune.value
for i=1,nVoices do
local note = e.note
local i2 = math.floor(i/2)
local rest = i%2
local tune = detune * i2 / 100.0
if rest == 1 then
tune = tune * -1.0
end
local
id =
playNote{e.note, e.velocity, vol=1/math.sqrt(nVoices), tune=tune}
end
end
-- eat
event release is automatic in
playNote
end
⬇ Download Unison.lua
vibrato
Per-Voice Pitch Vibrato
Applies a sine-wave pitch vibrato to each voice independently. Frequency and depth are adjustable in real time via Knob widgets. The LFO runs inside the onNote callback using a wait loop gated by isNoteHeld.
Demonstrates: changeTune, isNoteHeld, wait, per-voice LFO, Knob
Freq =
Knob(
"Freq", 4.0, 0, 10) -- 4 Hz
Depth =
Knob(
"Depth", 0.5, 0, 1)
local step = 5 -- ms
local
id =
postEvent(e) -- duration is omitted
local phase = 0
local depth = Depth.value
local freq = Freq.value
local modulation = depth * math.sin(2 * math.pi * phase)
phase = phase + (step/1000.0) * freq
end
end
⬇ Download vibrato.lua
VoiceTracker
Track and Manipulate Active Voices
Maintains a table of all active voice IDs keyed by note number. Applies a velocity-dependent pan spread: low notes left, high notes right. Demonstrates the voice tracking pattern used in many real-world scripts.
Demonstrates: postEvent, changePan, voice ID tracking, onNote / onRelease
local voices = {} -- track active voices: voices[note] = voiceId
voices[e.note] = id
-- pan spread: note 0 = hard left, note 127 = hard right
local pan = (e.note / 127) * 2 - 1
end
voices[e.note] = nil
end
⬇ Download VoiceTracker.lua
Performance & Articulation
legato
Legato with Crossfade and Retrigger
Implements monophonic legato by crossfading between overlapping notes. New notes fade in from a sample offset while the previous note fades out. An optional retrigger mode re-voices the last held note on release.
Demonstrates: postEvent, fadein, fadeout, setSampleOffset, releaseVoice, note stack
local notes = {}
Fade =
Knob(
"fade", 40, 10, 100)
local sampleOffset = 50 -- ms
if #notes > 0 then
local fadetime = Fade.
value
fadeout(notes[#notes].
id, fadetime,
true)
table.insert(notes, e)
else
table.insert(notes, e)
end
end
function onRelease(e)
for i,noteon in ipairs(notes) do
if noteon.note == e.note then
table.remove(notes, i)
local shouldRetrigger = Retrigger.value and #notes > 0 and i > #notes
if shouldRetrigger then
local noteon = notes[#notes]
local
id =
playNote(noteon.note, noteon.velocity)
noteon.id = id
local fadetime = Fade.value
end
break
end
end
end
function fadeout(voiceId, duration, killVoice, reset, layer)
starts a volume fade-out.
Definition api.lua:609
function setSampleOffset(voiceId, value)
changes a sample starting point in milliseconds.
Definition api.lua:569
function fadein(voiceId, duration, reset, layer)
starts a volume fade-in for a specific voice.
Definition api.lua:643
⬇ Download legato.lua
monoBassLine
Monophonic Bass Synth with Sequencer
A complete instrument script featuring oscillator mixing, amplitude ADSR, resonant filter with LFO modulation, and an 8-step pitch sequencer. Demonstrates full UI layout with multiple Panels, Tables, Menus, and Knobs, as well as Mapper and Unit types for automatic value scaling and display.
Demonstrates: Panel, Table, Menu, Knob, Mapper, Unit, waitBeat, setSize, makePerformanceView
--------------------------------------------------------------------------------
-- init direct access to most used engine nodes
--------------------------------------------------------------------------------
local ampEnv =
Program.
layers[1].keygroups[1].modulations[
"Amp. Env"]
local oscillators = keygroup.oscillators
local filter = keygroup.inserts[1]
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local oscPanel =
Panel(
"Oscs")
local oscVolume = oscPanel:
Knob("Osc", 1, 0, 1)
oscVolume.fillColour = "lightgrey"
oscVolume.outlineColour = "orange"
oscVolume.mapper =
Mapper.Cubic
oscVolume.unit =
Unit.LinearGain
oscVolume.changed = function(self)
oscillators[1]:setParameter("Gain", self.value)
end
oscVolume:changed()
local subVolume = oscPanel:
Knob("Sub", 0, 0, 1)
subVolume.fillColour = "lightgrey"
subVolume.outlineColour = "orange"
subVolume.mapper =
Mapper.Cubic
subVolume.unit =
Unit.LinearGain
subVolume.changed = function(self)
oscillators[2]:setParameter("Gain", self.value)
end
subVolume:changed()
local noiseVolume = oscPanel:
Knob("Noise", 0, 0, 1)
noiseVolume.fillColour = "lightgrey"
noiseVolume.outlineColour = "orange"
noiseVolume.mapper =
Mapper.Cubic
noiseVolume.unit =
Unit.LinearGain
noiseVolume.changed = function(self)
oscillators[3]:setParameter("Gain", self.value)
end
noiseVolume:changed()
--------------------------------------------------------------------------------
-- Amplitude ADSR
--------------------------------------------------------------------------------
local adsrPanel =
Panel("AmpEnv")
local attack = adsrPanel:
Knob("Attack", 0.009, 0.009, 1.06)
attack.outlineColour = "magenta"
attack.mapper =
Mapper.Exponential
attack.unit =
Unit.Seconds
attack.changed = function(self)
ampEnv:setParameter("AttackTime", self.value)
end
attack:changed()
local decay = adsrPanel:
Knob("Decay", 0.174, 0.174, 2.477)
decay.outlineColour = "magenta"
decay.mapper =
Mapper.Exponential
decay.unit =
Unit.Seconds
decay.changed = function(self)
ampEnv:setParameter("DecayTime", self.value)
end
decay:changed()
local sustain = adsrPanel:
Knob("Sustain", 1, 0, 1)
sustain.outlineColour = "magenta"
sustain.unit =
Unit.PercentNormalized
sustain.changed = function(self)
ampEnv:setParameter("SustainLevel", self.value)
end
sustain:changed()
local release = adsrPanel:
Knob("Release", 0.05, 0.05, 5.028)
release.outlineColour = "magenta"
release.mapper =
Mapper.Exponential
release.unit =
Unit.Seconds
release.changed = function(self)
ampEnv:setParameter("ReleaseTime", self.value)
end
release:changed()
--------------------------------------------------------------------------------
-- Filter
--------------------------------------------------------------------------------
local filterPanel =
Panel("Filter")
-- widget constructors also accept a single table, mixing positional and named
-- arguments (similar to Python keyword arguments)
local cutoff = filterPanel:
Knob{
"Cutoff", 20000, 20, 20000,
fillColour = "lightgrey",
outlineColour = "yellow",
changed = function(self)
filter:setParameter("Freq", self.value)
end
}
cutoff:changed()
local reso = filterPanel:
Knob("Reso", 0, 0, 1)
reso.fillColour = "lightgrey"
reso.outlineColour = "yellow"
reso.unit =
Unit.PercentNormalized
reso.changed = function(self)
filter:setParameter("Q", self.value)
end
reso:changed()
local lfoToCutoff = filterPanel:
Knob("lfoToCutoff", 0, -1, 1)
lfoToCutoff.fillColour = "lightgrey"
lfoToCutoff.outlineColour = "yellow"
lfoToCutoff.unit =
Unit.PercentNormalized
lfoToCutoff.changed = function(self)
filter:getParameterConnections("Freq")[1]:setParameter("Ratio", self.value)
end
lfoToCutoff:changed()
--------------------------------------------------------------------------------
-- Mini bass line Sequencer
--------------------------------------------------------------------------------
local seqPanel =
Panel("Sequencer")
local resolutions = {0.5, 0.25, 0.125}
local resolutionNames = {"1/8", "1/16", "1/32"}
local numSteps = 8
local steps = seqPanel:
Table(
"pitch", numSteps, 0, -12, 12,
true)
local res = seqPanel:
Menu{
"Resolution", resolutionNames, selected=2}
-- displayText overrides the automatic unit display with a custom string,
-- useful when the value needs domain-specific formatting
local gate = seqPanel:
Knob(
"Gate", 1, 0, 1)
gate.changed = function(self)
self.displayText = string.format("%d%%", self.value * 100)
end
gate:changed()
res.backgroundColour = "black"
res.textColour = "cyan"
res.arrowColour = "grey"
res.outlineColour = "#1fFFFFFF" -- transparent white
local positionTable = seqPanel:Table("steps"..tostring(i), numSteps, 0, 0, 1, true)
positionTable.enabled = false
positionTable.persistent = false
function clearPosition()
for i = 1, numSteps do
positionTable:setValue(i, 0)
end
end
local arpId = 0
local heldNotes = {}
function arpeg(arpId_)
local index = 0
while arpId_ == arpId do
local e = heldNotes[#heldNotes]
local p = resolutions[res.value]
local note = e.note + steps:getValue(index+1)
playNote(note, e.velocity, beat2ms(gate.value*p))
positionTable:setValue((index - 1 + numSteps) % numSteps + 1, 0)
positionTable:setValue((index % numSteps)+1, 1)
index = (index+1) % numSteps
waitBeat(p)
end
end
--------------------------------------------------------------------------------
-- callbacks
--------------------------------------------------------------------------------
function onNote(e)
table.insert(heldNotes, e)
if #heldNotes == 1 then
arpeg(arpId)
end
end
function onRelease(e)
for i,v in ipairs(heldNotes) do
if v.note == e.note then
table.remove(heldNotes, i)
if #heldNotes == 0 then
clearPosition()
arpId = arpId + 1
end
break
end
end
end
--------------------------------------------------------------------------------
-- UI positioning
--------------------------------------------------------------------------------
local margin = 10
oscPanel.x = margin
oscPanel.y = margin
oscPanel.width = 400
oscPanel.height = 60
filterPanel.x = oscPanel.x
filterPanel.y = oscPanel.y + oscPanel.height + margin
filterPanel.width = 400
filterPanel.height = 60
adsrPanel.x = filterPanel.x
adsrPanel.y = filterPanel.y + filterPanel.height + margin
adsrPanel.width = 500
adsrPanel.height = 60
seqPanel.x = adsrPanel.x
seqPanel.y = adsrPanel.y + adsrPanel.height + margin
seqPanel.width = 630
seqPanel.height = 150
steps.y = steps.y + 10
steps.width = 500
steps.height = 130
positionTable.x = steps.x
positionTable.y = steps.y - 10
positionTable.width = steps.width
positionTable.height = 10
res.x = steps.x + steps.width + margin
res.y = steps.y
gate.x = steps.x + steps.width + margin
gate.y = steps.y + 70
setSize(650, 380)
makePerformanceView()
Predefined mapper types.
Definition ui.cpp:630
@ Exponential
Exponential mapper, the parameter's range should be strictly positive.
Definition ui.cpp:642
The Synthesis primitive.
Definition Engine.cpp:301
Panel widget.
Definition ui.cpp:1806
Table widget.
Definition ui.cpp:1327
Predefined unit types.
Definition ui.cpp:688
@ Hertz
display Hz symbol.
Definition ui.cpp:715
⬇ Download monoBassLine.lua
⬇ Download Bundled MonoSynth patch (load in Falcon)
portamento
Portamento with Pitch Glide
Creates a smooth pitch glide between consecutive notes using a custom coroutine-based glide function. The outgoing note glides up while the incoming note glides down, producing a continuous portamento effect.
Demonstrates: changeTune, spawn, fadein, fadeout, playNote
local numNotes = 0
local lastid = -1
local lastnote = 0
Fade =
Knob(
"fade", 100, 1, 500)
function glide(id, from, to, duration, period)
duration = duration or 100 -- 100 ms
period = period or 10 -- 5 ms
local doglide = function()
local value = from
local increment = (to - from) * period / duration
local t = 0
local immediate = true
while t < duration do
immediate = false
value = value + increment
t = t + period
end
end
_spawn(doglide)
end
if numNotes > 0 then
local fadetime = Fade.value
glide(lastid, 0, (e.note-lastnote), fadetime)
fadein(lastid, fadetime, true)
glide(lastid, (lastnote-e.note), 0, fadetime)
lastnote = e.note
else
lastnote = e.note
end
numNotes = numNotes + 1
end
function onRelease(e)
numNotes = math.max(0, numNotes - 1)
end
⬇ Download portamento.lua
Sequencing
Arpeggiator
Classic chord arpeggiator (Up / Down / UpDown, octave range)
A simplified port of the built-in UVI arpeggiator: takes chord input, plays a single time-aligned pattern across the held notes, with the standard modes and an octave-range knob. Architecture worth studying:
- One shared timeline, not one coroutine per note. A single arp coroutine is spawned on the first note-down and torn down on the last note-up via an epoch token (
arpId). Notes added or removed mid-pattern simply update the shared held-notes set; the running loop sees the change on its next step. This is what gives the arp a stable beat grid regardless of how many keys come and go.
- Per-note mirror for safe Enable toggle. When Enable is off the script forwards notes (pass-through). When on it swallows them and feeds the arp. The matching release MUST do whatever the press did, regardless of the current Enable state, otherwise toggling Enable mid-hold leaks stuck or orphan voices. We remember the decision per-note in
forwarded. OnOffButton, Menu, Knob, per-note bookkeeping
Demonstrates: spawn, waitBeat, beat2ms, postEvent, playNote,
local resolutions = {1.0, 0.5, 1.0/3, 0.25, 1.0/6, 0.125}
local resolutionNames = {"1/4", "1/8", "1/4t", "1/16", "1/8t", "1/32"}
local modes = {"Up", "Down", "UpDown"}
------------------------------------------------------------------------
-- UI
------------------------------------------------------------------------
displayName = "Arp On",
tooltip = "Off = pass-through"}
Mode =
Menu{
"Mode", modes,
selected = 1,
displayName = "Mode"}
Speed =
Menu{
"Speed", resolutionNames,
selected = 2,
displayName = "Speed"}
Octaves =
Knob{
"Octaves", 1, 1, 4,
true,
displayName = "Octaves",
tooltip = "Octaves the pattern spans"}
Gate =
Knob{
"Gate", 0.8, 0.05, 1.0,
displayName = "Gate"}
------------------------------------------------------------------------
-- Held-note set
-- Tones are kept sorted ascending by pitch so each step indexes into a
-- stable, musical order regardless of the order the user pressed keys.
------------------------------------------------------------------------
local heldByNote = {}
local held = {}
local function rebuildSorted()
local arr = {}
for _, ev in pairs(heldByNote) do arr[#arr + 1] = ev end
table.sort(arr, function(a, b) return a.note < b.note end)
held = arr
end
------------------------------------------------------------------------
-- Per-note bookkeeping (see header comment).
------------------------------------------------------------------------
local forwarded = {}
------------------------------------------------------------------------
-- Arp coroutine: one shared timeline.
-- The arpId epoch token lets older spawns die cleanly when the chord
-- empties or Enable is turned off.
------------------------------------------------------------------------
local arpId = 0
local function noteForStep(step)
local n = #held
if n == 0 then return nil end
local octs = math.max(1, Octaves.value)
local span = n * octs -- total tones across octaves
local m = Mode.value
local idx, octIdx
if m == 1 then -- Up
local s = step % span
idx, octIdx = (s % n) + 1, math.floor(s / n)
elseif m == 2 then -- Down
local s = (span - 1) - (step % span)
idx, octIdx = (s % n) + 1, math.floor(s / n)
else -- UpDown (palindrome)
local cycle = (span > 1) and (2 * (span - 1)) or 1
local s = step % cycle
if s >= span then s = cycle - s end
idx, octIdx = (s % n) + 1, math.floor(s / n)
end
return held[idx], octIdx
end
local function runArp(myId)
local step = 0
while myId == arpId do
local tone, octIdx = noteForStep(step)
if tone then
local note = tone.note + 12 * octIdx
if note > 127 then note = 127 end
local beats = resolutions[Speed.value]
velocity = tone.velocity,
duration =
beat2ms(beats) * Gate.value}
else
end
step = step + 1
end
end
------------------------------------------------------------------------
-- Callbacks
------------------------------------------------------------------------
if not Enable.value then
forwarded[e.note] = true
return
end
forwarded[e.note] = nil
heldByNote[e.note] = {note = e.note, velocity = e.velocity}
rebuildSorted()
if #held == 1 then -- first key down: start the arp
arpId = arpId + 1
end
end
-- Mirror the press decision regardless of the current Enable state.
if forwarded[e.note] then
forwarded[e.note] = nil
return
end
if heldByNote[e.note] then
heldByNote[e.note] = nil
rebuildSorted()
if #held == 0 then
arpId = arpId + 1 -- last key up: kill the arp
end
end
end
-- Turning Enable off mid-chord silences the arp loop. Notes already
-- forwarded (Enable was off at their press) keep their entry in
-- `forwarded` and will release correctly on key-up.
Enable.changed = function(self)
if not self.value then
arpId = arpId + 1
held = {}
heldByNote = {}
end
end
@ PercentNormalized
display % symbol but actual value is in the 0..1 range
Definition ui.cpp:703
function beat2ms(beat)
Convert beat duration to milliseconds based on the current tempo.
Definition conversions.lua:38
function waitBeat(beat)
Suspend execution for a tempo-synchronized duration in beats.
Definition conversions.lua:142
function spawn(fun,...)
Launch a function in a separate parallel execution thread (deferred execution)
Definition api.lua:1176
void makePerformanceView()
make this script User Interface visible in performance view.
Definition ui.lua:107
⬇ Download Arpeggiator.lua
StepSequencer
Beat-Synced Step Sequencer with Position Display
A simple 8-step pitch sequencer that plays in sync with the host tempo. Uses a Table widget for step editing and a second Table as a visual position indicator. Demonstrates waitBeat, Table widget, and the sequencer pattern found in many factory presets.
Demonstrates: Table, waitBeat, playNote, beat2ms, spawn, Panel
local numSteps = 8
local panel =
Panel(
"Sequencer")
panel.backgroundColour = "3f000000"
local steps = panel:
Table("pitch", numSteps, 0, -12, 12, true)
steps.width = 500
steps.height = 120
-- position indicator (read-only)
local position = panel:
Table("pos", numSteps, 0, 0, 1, true)
position.enabled = false
position.persistent = false
position.width = steps.width
position.height = 10
local resolution = panel:
Menu{
"Resolution", {
"1/8",
"1/16",
"1/32"}, selected = 2}
local resValues = {0.5, 0.25, 0.125} -- in beats
local seqId = 0
seqId = seqId + 1
local myId = seqId
local step = 0
while myId == seqId do
local res = resValues[resolution.value]
local note = e.note + steps:getValue(step + 1)
-- update position display
position:setValue((step - 1 + numSteps) % numSteps + 1, 0)
position:setValue(step + 1, 1)
step = (step + 1) % numSteps
end
end
function onRelease(e)
seqId = seqId + 1 -- stop the running loop
for i = 1, numSteps do position:setValue(i, 0) end
end
⬇ Download StepSequencer.lua
MIDI Tools
CCFilter
Block a Specific MIDI CC
Blocks a user-selected CC from passing through. All other events are forwarded unchanged. Demonstrates selective event filtering with onController.
Demonstrates: onController, postEvent, event filtering, Knob
local blocked =
Knob{
"Blocked CC", 64, 0, 127,
true} -- integer knob
if e.controller ~= blocked.value then
end
end
void onController(table e)
event callback that will receive all incoming control-change events when defined.
⬇ Download CCFilter.lua
CCRedirect
Remap a MIDI CC Number to Another
Remaps incoming CC messages from one controller number to another. Other events pass through unchanged. Demonstrates event property mutation before forwarding.
Demonstrates: onController, postEvent, event mutation, Menu
local source =
Menu{
"Source CC", {
"1 (Mod Wheel)",
"2 (Breath)",
"7 (Volume)",
"11 (Expression)"}}
local target =
Menu{
"Target CC", {
"1 (Mod Wheel)",
"2 (Breath)",
"7 (Volume)",
"11 (Expression)"}, selected = 4}
local ccMap = {1, 2, 7, 11}
if e.controller == ccMap[source.value] then
e.controller = ccMap[target.value] -- mutate the event
end
end
⬇ Download CCRedirect.lua
CCSmooth
Smooth Incoming CC Messages Over Time
Applies exponential smoothing to a MIDI CC, producing gradual transitions instead of abrupt jumps. Demonstrates spawning a background task from onController and generating CC output.
Demonstrates: onController, controlChange, spawn, wait, Knob, Mapper
local targetCC =
Knob{
"CC", 1, 0, 127,
true} -- which CC to smooth
local currentValue = 0
local targetValue = 0
local running = false
function smoothCC()
running = true
while math.abs(currentValue - targetValue) > 0.5 do
currentValue = currentValue + (targetValue - currentValue) * 0.15
end
currentValue = targetValue
running = false
end
if e.controller == targetCC.value then
targetValue = e.value
if not running then
end
else
end
end
@ Cubic
Cubic mapper:
Definition ui.cpp:666
@ Seconds
display s symbol.
Definition ui.cpp:707
function controlChange(cc, val, ch, inp)
sends a ControlChange event.
Definition api.lua:1044
⬇ Download CCSmooth.lua
MidiLearn
MIDI Learn for Note Assignment
Demonstrates the MIDI learn pattern: press a button to enter learn mode, then play a note to assign it. The learned note is displayed and used to transpose incoming notes.
Demonstrates: Button, Label, onNote, setKeyColour, resetKeyColour, MIDI learn pattern
local learnedNote = 60
local learning = false
local learnBtn =
Button{
"Learn"}
learning = true
status.text = "Play a note..."
end
local status =
Label{
"status"}
if learning then
-- assign the played note
learnedNote = e.note
status.text = string.format("Note: %d", learnedNote)
learning = false
else
-- transpose relative to learned note
local offset = e.note - 60
playNote(learnedNote + offset, e.velocity, -1)
end
end
function onRelease(e)
-- eat releases:
playNote handles them via duration -1
end
text label widget.
Definition ui.cpp:1080
string text
text to display on screen
Definition ui.cpp:1088
function resetKeyColour(note)
customize the keyboard colours.
Definition ui.lua:61
⬇ Download MidiLearn.lua
Asset Loading
IRLoader
Hierarchical IR menu with userReady guard
A hierarchical Menu (path-based entries grouped by "/") that loads an impulse response into a SampledReverb insert. The menu's selection is persisted with the preset, so the user's chosen IR survives a save / load round-trip. Why this example shows the userReady flag: When a preset loads, the engine restores every persistent widget to its saved value. For widgets whose .changed callback performs an expensive side effect (here loadImpulse, which hits the disk), we must not let that callback fire during the restore — the IR is already in the patch. The standard guard:
- declare a flag
userReady = false at script load,
- flip it to
true in onInit (which fires AFTER preset state has been fully restored),
- have any side-effecting
.changed early-return when the flag is false. With the guard in place, only real user clicks trigger loadImpulse. The same pattern protects any expensive .changed side effect: loadSample, network calls, rebuilding a big lookup, etc. See writes into other widgets and you want the menu non-persistent. hierarchical menu, userReady guard
Demonstrates: onInit, Menu, Label, loadImpulse,
-- Flip to
true in
onInit, after preset state has been fully restored.
-- Every .changed that performs a destructive side effect must check this.
local userReady = false
-- Hierarchical menu: "/" creates sub-menu levels.
local irList = {
"Halls/Large Hall",
"Halls/Medium Hall",
"Halls/Small Hall",
"Plates/Bright Plate",
"Plates/Dark Plate",
"Rooms/Studio A",
"Rooms/Studio B",
"Rooms/Living Room",
"Springs/Short Spring",
"Springs/Long Spring",
}
local irMenu =
Menu{
"IR", irList,
hierarchical = true,
backgroundColour = "333333",
textColour = "white",
}
local status =
Label{
"status"}
status.
text =
"No IR loaded"
status.textColour = "aaaaaa"
irMenu.changed = function(self)
if not userReady then return end -- preset load: do not re-hit disk
local irName = self.selectedText
local irPath = "impulses/" .. irName .. ".wav"
status.text = "Loading..."
status.textColour = "aaaaaa"
if task.success then
status.text = irName
status.textColour = "00FF88"
else
status.text = "Failed: " .. irName
status.textColour = "FF4444"
end
end)
end
userReady = true
-- We do NOT call irMenu:changed() here. The patch already contains
-- the matching IR; re-running
loadImpulse would be a redundant disk
-- hit on every preset load.
end
table inserts
all InsertEffect for this node
Definition Engine.cpp:243
function loadImpulse(reverb, path, callback)
load and impulse response inside the reverb.
Definition api.lua:327
void onInit()
initial callback that is called just after the script initialisation if the script was successfully a...
Definition IRLoader.lua:87
⬇ Download IRLoader.lua
SampleDropper
Load Samples via Drag and Drop
Creates a drag-and-drop zone that loads dropped audio files into the first oscillator of the current layer. Demonstrates DnDArea and async loadSample with visual feedback.
Demonstrates: DnDArea, loadSample, Program.layers, Label, FileFormat
local status =
Label{
"status"}
status.
text =
"Drop a sample here"
status.align = "centred"
status.textColour = "white"
dnd.bounds = {5, 30, 350, 60}
dnd.backgroundColour = "3fFFFFFF"
dnd.fileDropped = function(self)
status.text = "Loading..."
if task.success then
status.text = osc.sampleInfo.name
else
status.text = "Load failed"
end
end)
end
DnDArea widget.
Definition ui.cpp:1042
function loadSample(oscillator, path, callback)
load a sample inside the oscillator
Definition api.lua:67
⬇ Download SampleDropper.lua
UI Helpers
FX Controls
Bind a Program insert effect to the script UI
Builds a small control panel for the first insert effect on the Program, here a "Drive". The Param* widgets bind straight to the effect's parameters: each control inherits its range, default and unit from the parameter and stays in sync with host automation, modulation and preset changes - no changed callback is needed to drive the effect. ParameterValue adds a non-visual binding, used here to read a parameter's live value from script. The parameter names ("Bypass", "DriveAmount", "Mode") are the internal names of the Drive effect; point Program.inserts[1] at that effect, or swap the names for those of your own FX.
Demonstrates: ParamKnob, ParamOnOffButton, ParamMenu, ParameterValue, Panel
-- the effect we want to control: first insert on the
Program
local panel =
Panel{
"drive"}
panel.
bounds = {20, 20, 420, 150}
panel.backgroundColour = "2a2a2a"
-- No ranges are given: they are inherited from the bound parameter.
-- Bounds are relative to the panel.
bypass.
bounds = {25, 58, 110, 36}
local amount = panel:
ParamKnob(fx,
"DriveAmount") -- 0..1, shown as %
amount.
bounds = {180, 25, 100, 100}
local mode = panel:
ParamMenu(fx,
"Mode") -- enumerated parameter (entries from the param)
mode.
bounds = {290, 54, 110, 44}
--------------------------------------------------------------------------------
-- Non-visual (logical) binding: read the live drive amount from script without
-- placing a control
for it.
ParameterValue draws nothing; it just exposes the
-- parameter through its .
value.
--------------------------------------------------------------------------------
bypass.changed = function(self)
local state = self.value and "bypassed" or "active"
print(string.format("Drive %s (amount = %d%%)", state, math.floor(driveValue.value * 100 + 0.5)))
end
A knob bound to an existing Element parameter.
Definition ui.cpp:1586
A non-visual (logical) binding to an existing Element parameter.
Definition ui.cpp:1759
Value value
the bound parameter's value (read and write)
Definition ui.cpp:1773
void setBackgroundColour(string colour)
set the script background colour.
⬇ Download FXControls.lua
⬇ Download Bundled Drive patch (load in Falcon)
PanelSwitcher
Tab-Based Panel Switching with Main/FX/Seq Views
Demonstrates the standard pattern for building a tabbed interface. Three OnOffButtons act as tab selectors, toggling visibility of three Panel containers. Each panel holds its own set of widgets. This pattern is used extensively in Falcon factory presets.
Demonstrates: OnOffButton, Panel, Knob, Slider, Table, Mapper, Unit
-- Tab buttons
local tabNames = {"Main", "FX", "Seq"}
local tabButtons = {}
local tabPanels = {}
local contentY = 30
local contentH = 100
for i = 1, #tabNames do
bounds = {(i - 1) * 80, 0, 78, 25},
backgroundColourOff = "333333",
backgroundColourOn = "FF8800",
textColourOff = "aaaaaa",
textColourOn = "ffffff",
persistent = false
}
tabPanels[i] =
Panel{bounds = {0, contentY, 500, contentH},
backgroundColour = "2f000000"
}
end
-- Main panel: basic voice controls
-- FX panel: filter controls
-- Seq panel: step sequencer
tabPanels[3]:
Table(
"Steps", 8, 0, -12, 12,
true)
tabPanels[3]:
Knob{
"Rate", 0.25, 0.0625, 1}
-- Tab switching: deselect all others, show only the active panel
for i = 1, #tabButtons do
for j = 1, #tabButtons do
tabButtons[j]:setValue(j == i, false) -- deselect others without triggering callback
tabPanels[j].visible = (j == i) -- show only matching panel
end
end
end
tabButtons[1]:changed() -- show Main tab by default
setSize(500, contentY + contentH + 5)
Horizontal or vertical slider widget.
Definition ui.cpp:1452
@ Pan
display -1;1 pan value type
Definition ui.cpp:731
@ SemiTones
display semitones symbol
Definition ui.cpp:739
@ LinearGain
display dB symbol but with normalized gain
Definition ui.cpp:723
⬇ Download PanelSwitcher.lua
TemporaryDisplay
Flash Knob Values on Labels with Auto-Revert
Shows a reusable pattern for temporarily displaying a knob's value on a label, then reverting to the default name after a delay. Uses spawn + wait since widget callbacks are not coroutines and cannot call wait() directly. A counter ensures only the last update reverts, handling rapid knob turns.
Demonstrates: Label, Knob, Panel, Mapper, Unit, spawn, wait
--------------------------------------------------------------------------------
-- Reusable helper: flash a value on a label, revert after delay
--------------------------------------------------------------------------------
local flashCounters = {}
function flashLabel(label, text, defaultText, ms)
label.text = text
flashCounters[label] = (flashCounters[label] or 0) + 1
local myId = flashCounters[label]
if flashCounters[label] == myId then
label.text = defaultText
end
end)
end
--------------------------------------------------------------------------------
-- Layout
--------------------------------------------------------------------------------
local panel =
Panel{
"controls"}
panel.
bounds = {10, 10, 340, 90}
panel.backgroundColour = "2a2a2a"
local margin = 10
local knobSize = 60
local labelH = 16
local function makeKnobWithLabel(parent, name, default, min, max, x, opts)
local label = parent:
Label{name,
bounds = {x, margin + knobSize + 2, knobSize, labelH},
align = "centred", fontSize = 11,
textColour = "888888", backgroundColour = "#00000000"
}
local knob = parent:
Knob{name ..
"_k",
default, min, max,
bounds = {x, margin, knobSize, knobSize},
showLabel = false, showValue = false, showPopupDisplay = false,
fillColour = opts.colour or "555555",
outlineColour = opts.colour or "888888"
}
if opts.
mapper then knob.mapper = opts.mapper end
if opts.unit then knob.unit = opts.unit end
return knob, label, name
end
local volKnob, volLabel, volName = makeKnobWithLabel(panel, "VOL", 0.8, 0, 1, margin,
local cutKnob, cutLabel, cutName = makeKnobWithLabel(panel, "CUTOFF", 20000, 20, 20000, margin + 70 + margin,
local resKnob, resLabel, resName = makeKnobWithLabel(panel, "RES", 0, 0, 1, margin + 2 * (70 + margin),
--------------------------------------------------------------------------------
-- Wire up temporary display
--------------------------------------------------------------------------------
volKnob.changed = function(self)
local dB = self.value > 0 and string.format("%.1f dB", 20 * math.log10(self.value)) or "-inf"
flashLabel(volLabel, dB, volName, 800)
end
cutKnob.changed = function(self)
local text = self.value < 1000
and string.format("%.0f Hz", self.value)
or string.format("%.1f kHz", self.value / 1000)
flashLabel(cutLabel, text, cutName, 800)
end
resKnob.changed = function(self)
flashLabel(resLabel, string.format("%.0f%%", self.value * 100), resName, 800)
end
⬇ Download TemporaryDisplay.lua
See Also