The Usual Suspects

Digitally Preserving Classic Synthesizers through Emulation

Lua Scripting for Skins

Gearmulator supports Lua scripting in plugin skins, giving skin authors the power to create dynamic, interactive UI elements without writing C++ code. Using Lua, you can read and write synthesizer parameters, react to parameter changes in real time, and manipulate the UI layout and appearance on the fly.

This feature is aimed at skin designers who want to go beyond static layouts and build things like custom visualizations, conditional UI logic, animated controls, or parameter-driven display elements.

Gearmulator uses Lua 5.4 embedded in the RmlUi skin framework. If you’re new to RmlUi, check the official RmlUi documentation for details on RML elements, RCSS properties, data bindings, and events.

Quick Start

Add a <script> block in your RML file’s <head> section to define Lua functions, then call them from event handlers on your elements:

<rml>
  <head>
    <link type="text/rcss" href="myskin.rcss"/>
    <script>
      function onBodyLoad()
        local val = params.get("Osc1Pitch")
        Log.Message(Log.logtype.info, "Osc1Pitch = " .. val)
      end
    </script>
  </head>
  <body onload="onBodyLoad()">
    ...
  </body>
</rml>

Script Execution Timing

Understanding when scripts run is important for writing correct Lua code:

Global Variables

These globals are available in all Lua scripts and event handlers:

Global Type Description
document ElementDocument The current RML document. Use for DOM access like document:GetElementById().
params table Parameter API for reading/writing synth parameters and subscribing to changes.
Log table RmlUi logging. Use Log.Message(Log.logtype.info, "message").
rmlui table RmlUi core API (contexts, font loading, etc.).

Parameter API

The params table provides full access to the synthesizer’s parameters. The part argument is optional for all functions — when omitted, the currently selected part is used.

Reading Values

-- Get raw integer value (current part)
local value = params.get("Osc1Pitch")

-- Get for a specific part
local value = params.get("Osc1Pitch", 3)

-- Get for explicit "current" part
local value = params.get("Osc1Pitch", "current")

-- Get formatted display text
local text = params.getText("Osc1Pitch")
local text = params.getText("Osc1Pitch", 3)

Writing Values

-- Set value on current part
params.set("Osc1Pitch", 64)

-- Set value on specific part
params.set("Osc1Pitch", 64, 3)

Parameter Info

local info = params.getInfo("Osc1Pitch")
-- info.min          (integer) minimum value
-- info.max          (integer) maximum value
-- info.name         (string)  internal parameter name
-- info.displayName  (string)  human-readable name
-- info.defaultValue (integer) default value
-- info.isBool       (boolean) true if parameter is a toggle
-- info.isBipolar    (boolean) true if parameter is bipolar

Subscribing to Changes

-- Subscribe to changes on current part (auto-rebinds when part changes)
local id = params.onChange("Osc1Pitch", function(value, text)
  -- value: raw integer
  -- text: formatted display string
end)

-- Subscribe to changes on a specific part
local id = params.onChange("Osc1Pitch", function(value, text)
  -- ...
end, 3)

-- Unsubscribe
params.removeListener(id)

When subscribing with the current part (the default), the listener automatically rebinds to the new part’s parameter when the current part changes. If the value differs between the old and new part, the callback fires immediately with the new value.

Current Part

-- Get current part number (0-based)
local part = params.getCurrentPart()

-- Subscribe to part changes
local id = params.onPartChanged(function(newPart)
  -- newPart: 0-based part number
end)

DOM Manipulation

The document global provides access to the RmlUi DOM. You can find elements, modify styles, toggle classes, and build dynamic content:

-- Find elements
local elem = document:GetElementById("myElement")

-- Read/write styles
elem.style.width = "100px"
elem.style.height = "50px"
elem.style["background-color"] = "#ff0000"

-- Read computed size (in px, available after layout)
local w = elem.offset_width
local h = elem.offset_height

-- Add/remove CSS classes
elem:SetClass("active", true)
elem:SetClass("hidden", false)

-- Read/write attributes
local val = elem:GetAttribute("id")
elem:SetAttribute("data-value", "42")

-- Read/write inner RML
elem.inner_rml = '<div class="child">Hello</div>'

-- Navigate the tree
local parent = elem.parent_node
local first = elem.first_child
local doc = elem.owner_document

Logging

Log.Message(Log.logtype.info, "informational message")
Log.Message(Log.logtype.warning, "warning message")
Log.Message(Log.logtype.error, "error message")

Log levels: always, error, warning, info, debug.

Example: Arpeggiator User Pattern Visualization

This example creates a bar graph showing the arpeggiator step pattern for the Virus TI, with bar width controlled by step length and height by step velocity. It demonstrates the key patterns for building parameter-driven visualizations.

Styles and script in <head>:

<style>
  .arp-bar {
    position: absolute;
    bottom: 0px;
    background-color: #ffffffdd;
  }
  .arp-bar-even { background-color: #ccddffcc; }
  .arp-bar-odd  { background-color: #ddccffcc; }
  .arp-bar-inactive { background-color: #aaaaaa55; }
</style>
<script>
  local STEPS = 32
  local bars = {}
  local container = nil

  function updateArpBar(step)
    local bar = bars[step]
    if bar == nil or container == nil then return end

    local cw = container.offset_width
    local ch = container.offset_height
    if cw <= 0 or ch <= 0 then return end
    local stepMaxW = cw / STEPS

    local length   = params.get("Step " .. step .. " Length")
    local velocity = params.get("Step " .. step .. " Velocity")
    local bitfield = params.get("Step " .. step .. " Bitfield")
    local patLen   = params.get("Arpeggiator/UserPatternLength")

    local active = (step - 1) <= patLen and bitfield > 0

    bar.style.left   = math.floor((step - 1) * stepMaxW) .. "px"
    bar.style.width  = math.floor((length / 127) * stepMaxW) .. "px"
    bar.style.height = math.floor((velocity / 127) * ch) .. "px"

    bar:SetClass("arp-bar-inactive", not active)
  end

  function updateAllArpBars()
    for i = 1, STEPS do updateArpBar(i) end
  end

  function initArp()
    container = document:GetElementById("LuaArpUserGraphics")
    for i = 1, STEPS do
      bars[i] = document:GetElementById("arp-bar-" .. i)
    end
    for i = 1, STEPS do
      local step = i
      params.onChange("Step " .. step .. " Length",    function() updateArpBar(step) end)
      params.onChange("Step " .. step .. " Velocity",  function() updateArpBar(step) end)
      params.onChange("Step " .. step .. " Bitfield",  function() updateArpBar(step) end)
    end
    params.onChange("Arpeggiator/UserPatternLength", function() updateAllArpBars() end)
  end
</script>

Container and bars in <body>:

<body onload="initArp()">
  ...
  <div id="LuaArpUserGraphics" onshow="updateAllArpBars()"
       style="width: 1921dp; height: 476dp; position: relative; overflow: hidden;">
    <div id="arp-bar-1"  class="arp-bar arp-bar-odd"/>
    <div id="arp-bar-2"  class="arp-bar arp-bar-even"/>
    <!-- ... bars 3-31 ... -->
    <div id="arp-bar-32" class="arp-bar arp-bar-even"/>
  </div>
  ...
</body>

Key patterns:

Debugging

RmlUi Debugger

Gearmulator includes the RmlUi visual debugger. To enable it, open the plugin’s settings and enable it in the Skin section. The debugger overlay will appear immediately.

The debugger provides:

Debug Logging from Lua

Use Log.Message to write to the RmlUi event log:

Log.Message(Log.logtype.info, "debug: value = " .. tostring(myVar))

These messages appear in the debugger’s Event Log panel. Lua errors (syntax errors, runtime errors, timeouts) are also logged there automatically with full stack tracebacks.

Safety

Sandboxed Standard Libraries

Only safe Lua standard libraries are loaded: base, coroutine, table, string, math, utf8. The following are deliberately excluded:

Library Reason
io Filesystem access
os Shell commands, file operations
package Loading arbitrary native code via require
debug Internal state inspection/modification

Execution Timeout

Scripts are limited to 3 seconds of execution time. If a script exceeds this limit (e.g., an accidental infinite loop), it is terminated with an error message and the plugin continues working normally. The timeout applies to all Lua execution: inline scripts, external scripts, event handlers, and parameter callbacks.

Error Handling

All Lua errors are caught via protected calls. Errors are logged as warnings through the RmlUi logging system and never crash the plugin or DAW. A full stack traceback is included in the error message.

Adding Lua Files to Skins

Place .lua files in the skin folder alongside .rml, .rcss, and image files. They are automatically compiled into the plugin’s binary data and can be loaded from RML:

<script src="myskin.lua"></script>

This lets you keep your Lua code in separate files for better organization, especially for larger scripts.