Lua Scripting Reference¶
MAGDA hosts user Lua 5.4 scripts to drive MIDI controllers and automate the DAW. Every script runs in a sandboxed runtime — no io, os, package, or require; just Lua's standard string, math, table, and coroutine libraries plus the magda.* API.
API stability
The magda.* surface is unstable in 0.7. Method names, argument shapes, and which fields are exposed on snapshot tables are likely to change in 0.8 as more controllers are scripted against it. Pin a script to a release if you want it to keep working.
For an end-to-end overview of how to load scripts and the footer indicator, see Controllers — Lua Controller Scripts.
Script Lifecycle¶
A script is a plain .lua file. Top-level code runs once when the script is loaded; after that, MAGDA dispatches events to three optional callback functions:
| Callback | When it fires | Arguments |
|---|---|---|
on_load() |
Once after the script is loaded and the magda table is installed |
none |
on_midi(event) |
For every incoming MIDI event matching the script's assigned input port (or every event on every input, if the port is left blank) | the event table — see below |
on_tick(dt) |
~30 Hz | dt — seconds since the last tick (number) |
All three are called on the message thread. MIDI input arrives on the audio thread but is hopped to the message thread before on_midi fires, so the entire magda.* API is safe to call from anywhere.
function on_load()
magda.log.info("script loaded, MAGDA " .. magda.app.version())
end
function on_midi(event)
-- ...
end
function on_tick(dt)
-- e.g. blink an LED every half second
end
MIDI Event Shape¶
The event table passed to on_midi has these fields:
| Field | Type | Description |
|---|---|---|
type |
string | "cc", "note_on", "note_off", "pitch_bend", "aftertouch", "program_change", "sysex", or "other" |
channel |
integer | MIDI channel 1..16 (0 for sysex — no channel) |
number |
integer | CC number, note number, program number, or 0 (pitch bend, channel-pressure aftertouch, sysex) |
value |
integer | CC value 0..127, velocity 0..127, pitch-bend −8192..8191, aftertouch pressure 0..127 |
bytes |
array of integers | Sysex only — the payload bytes, 1-indexed, not including F0/F7 framing |
port |
string | Display name of the originating MIDI input |
API Reference¶
Every function below lives under the global magda table. All are message-thread safe.
magda.log¶
| Function | Description |
|---|---|
magda.log.info(msg) |
Log at info level (visible in MAGDA's debug console) |
magda.log.warn(msg) |
Log at warn level |
magda.log.error(msg) |
Log at error level |
magda.app¶
| Function | Returns | Description |
|---|---|---|
magda.app.version() |
string | MAGDA version (e.g. "0.7.0") |
magda.transport¶
| Function | Returns | Description |
|---|---|---|
magda.transport.play() |
— | Start playback |
magda.transport.stop() |
— | Stop playback |
magda.transport.set_recording(enabled) |
— | Set the record-arm state of the transport |
magda.transport.is_playing() |
boolean | |
magda.transport.is_recording() |
boolean | |
magda.transport.is_loop_enabled() |
boolean | |
magda.transport.set_loop_enabled(enabled) |
— | |
magda.transport.position_beats() |
number | Current playhead position, in beats |
magda.transport.set_position_beats(beats) |
— | Move the playhead |
magda.project¶
| Function | Returns | Description |
|---|---|---|
magda.project.info() |
table | { name, file_path, tempo, time_sig_num, time_sig_den, sample_rate, loop_enabled } |
magda.selection¶
| Function | Returns | Description |
|---|---|---|
magda.selection.track() |
integer or nil | Selected track id |
magda.selection.clip() |
integer or nil | Selected clip id |
magda.selection.clips() |
array of integers | All selected clip ids (order unspecified) |
magda.selection.has_notes() |
boolean | A note selection is active inside a MIDI editor |
magda.selection.note_clip() |
integer or nil | Clip id the note selection lives in |
magda.selection.note_indices() |
array of integers | Selected note indices within that clip |
magda.selection.select_track(id) |
— | |
magda.selection.select_tracks({id, id, ...}) |
— | Multi-select |
magda.selection.select_clip(id) |
— | |
magda.selection.select_clips({id, id, ...}) |
— | Multi-select |
magda.selection.clear_notes() |
— | Clear note selection |
magda.tracks¶
| Function | Returns | Description |
|---|---|---|
magda.tracks.create(name [, type]) |
integer | Track id; type is "audio" (default), "group", "aux", "master", or "multi_out" |
magda.tracks.delete(id) |
— | |
magda.tracks.count() |
integer | |
magda.tracks.list() |
array of tables | Each table: { id, name, type, volume, pan, muted, soloed, record_armed, frozen } |
magda.tracks.get(id) |
table or nil | Same shape as the list entries |
magda.tracks.set_name(id, name) |
— | |
magda.tracks.set_volume(id, value) |
— | Linear gain 0..1 |
magda.tracks.set_pan(id, value) |
— | -1..1 |
magda.tracks.set_muted(id, bool) |
— | |
magda.tracks.set_soloed(id, bool) |
— |
magda.clips¶
| Function | Returns | Description |
|---|---|---|
magda.clips.create_midi(track_id, start_beats, length_beats) |
integer | New MIDI clip id |
magda.clips.delete(id) |
— | |
magda.clips.list_on_track(track_id) |
array of integers | Clip ids on that track |
magda.clips.list_arrangement() |
array of tables | Each table: { id, track_id, name, start_beats, length_beats } |
magda.clips.set_name(id, name) |
— | |
magda.clips.set_groove(id, template_name) |
— | Apply a groove template by name |
magda.clips.colour(id) |
table or nil | { r, g, b } with each component 0..127 (7-bit, sysex-ready) |
magda.session¶
| Function | Returns | Description |
|---|---|---|
magda.session.launch_clip(clip_id) |
— | Launch a session clip |
magda.session.stop_clip(clip_id) |
— | |
magda.session.stop_track(track_id) |
— | Stop the currently-active session clip on a track |
magda.session.stop_all() |
— | |
magda.session.launch_scene(scene_index) |
— | 0-based; tracks with empty slots in that row have their active clip stopped (matches the UI scene-button click) |
magda.session.active_clip_on_track(track_id) |
integer or nil | Currently-playing session clip on the track |
magda.session.clip_in_slot(track_id, scene_index) |
integer or nil | Clip in a specific slot, or nil if empty |
magda.session.clip_play_state(clip_id) |
string | "stopped", "queued", or "playing" |
magda.session.set_view(scene_offset [, scene_count]) |
— | Publish the controller's visible scene window so MAGDA's UI can highlight it |
magda.focused¶
The "focused" device is the device currently shown in the parameter view. Profiles with focused.macro.* resolvers and the AI panel both read it.
| Function | Returns | Description |
|---|---|---|
magda.focused.has_focus() |
boolean | A device is focused |
magda.focused.name() |
string | Display name of the focused device |
magda.focused.macro_name(index) |
string | Name of macro index (0..7) |
magda.focused.macro_value(index) |
number | Current value 0..1 |
magda.focused.set_macro(index, value) |
— | Write a macro value 0..1 |
magda.focused.cycle_device(direction) |
— | Move focus through the chain — +1 next, -1 previous |
magda.focused.auto_map() |
— | Engage automap so the controller's profile drives the focused device's macros |
magda.focused.clear_auto_map() |
— | Disengage automap |
magda.midi¶
For driving feedback (motorised faders, LED rings, screens) back to the controller. The port argument is the display name of a MIDI output — pass "@default" (or the empty string) to use the script's assigned output port.
| Function | Returns | Description |
|---|---|---|
magda.midi.send(port, status, data1 [, data2]) |
boolean | Raw channel-voice message; returns true if the port was found |
magda.midi.send_cc(port, channel, number, value) |
boolean | |
magda.midi.send_note_on(port, channel, note, velocity) |
boolean | |
magda.midi.send_note_off(port, channel, note) |
boolean | |
magda.midi.send_sysex(port, {byte, byte, ...}) |
boolean | F0/F7 framing is added by the binding — pass payload bytes only |
magda.midi.outputs() |
array of strings | Names of every connected MIDI output port |
magda.midi.default_output() |
string | The script's currently-assigned output port |
Examples¶
Sustain pedal launches the selected session clip¶
function on_midi(e)
if e.type ~= "cc" or e.number ~= 64 then return end
local track = magda.selection.track()
if not track then return end
if e.value >= 64 then
local clip = magda.selection.clip()
if clip then magda.session.launch_clip(clip) end
else
magda.session.stop_track(track)
end
end
Eight knobs map to track volumes¶
function on_midi(e)
if e.type ~= "cc" then return end
if e.number < 1 or e.number > 8 then return end
magda.tracks.set_volume(e.number, e.value / 127.0)
end
Light the play button while the transport is rolling¶
local PLAY_NOTE = 60
local was_playing = false
function on_tick(_dt)
local playing = magda.transport.is_playing()
if playing == was_playing then return end
was_playing = playing
if playing then
magda.midi.send_note_on("@default", 1, PLAY_NOTE, 127)
else
magda.midi.send_note_off("@default", 1, PLAY_NOTE)
end
end
Bundled Examples¶
Working scripts ship in the examples/scripts/ folder of the repo:
foot-pedal.lua— sustain-pedal launches the selected session clip8-knobs.lua— eight CCs drive eight track volumeslaunchpad.lua— Novation Launchpad clip launcher with LED feedbacklaunchkey_mini_mk4.lua— full Launchkey Mini MK4 surface (transport, pads, knobs, sceneswitching)
Copy any of them into your scripts folder (see Controllers — Scripts Folder) and load via the Controllers dialog.