Effect Plugin Guide
Effect plugins generate visual lighting patterns. They receive a buffer of LEDs and write RGB colors to it each frame.
Directory Structure
plugins/effect.my_effect/
├── manifest.json # Metadata + parameter definitions
├── main.lua # Entry script with lifecycle hooks
├── lib/ # Optional Lua modules
├── locales/ # Optional i18n files
│ ├── en-US.json
│ └── zh-CN.json
└── data/ # Runtime data directory
Lifecycle
User selects effect → Instantiate Lua environment
→ plugin.on_init()
→ plugin.on_params(params_table) ← Parameters changed
→ [Render loop]
plugin.on_tick(elapsed, buffer, width, height)
→ plugin.on_shutdown()
Lifecycle Hooks
on_init()
Called once when the effect is instantiated.
function plugin.on_init()
-- Initialize state, precompute lookup tables, etc.
end
on_params(params)
Called whenever the user changes a parameter value. Also called once initially.
params— Table of key/value pairs matching your manifestparams
function plugin.on_params(p)
if p.speed then speed = p.speed end
if p.color then
base_r = p.color[1]
base_g = p.color[2]
base_b = p.color[3]
end
end
on_tick(elapsed, buffer, width, height)
Called every frame. Write RGB colors to the buffer.
elapsed— Seconds since the effect started (float, continuously increasing)buffer— LED color buffer (userdata, 1-indexed)width— Layout width (for 2D effects)height— Layout height (for 2D effects)
function plugin.on_tick(elapsed, buffer, width, height)
local count = buffer:len()
for i = 1, count do
local hue = (elapsed * 60 + (i - 1) / count * 360) % 360
buffer:set_hsv(i, hue, 1.0, 1.0)
end
end
on_shutdown()
Called when the effect is removed. Clean up any resources.
function plugin.on_shutdown()
-- Cleanup
end
Buffer API
The buffer object provides methods for setting LED colors:
| Method | Description |
|---|---|
buffer:len() | Returns the number of LEDs |
buffer:set(i, r, g, b) | Set LED color by RGB (1-indexed, 0–255) |
buffer:set_hsv(i, h, s, v) | Set LED color by HSV (h: 0–360, s/v: 0.0–1.0) |
Host Utilities
Color Conversion
local r, g, b = host.hsv_to_rgb(hue, saturation, value)
-- hue: 0-360, saturation: 0.0-1.0, value: 0.0-1.0
-- returns: r, g, b (0-255)
Logging
host.log("Debug message")
print("Also works for logging")
Screen Capture
Effects can capture screen content for ambient lighting. Requires the "screen:capture" permission.
{
"permissions": ["screen:capture"]
}
-- List available displays
local displays = screen.list_displays()
-- Returns: {index, name, width, height, is_hdr}
-- Capture screen (downscaled to specified resolution)
local frame = screen.capture(64, 36)
-- Returns: {width, height, pixels=[0xRRGGBB, ...]}
-- Returns nil on failure
The user selects which display to capture via the UI. The special params __screen_index and __screen_region are injected by Core.
Audio Analysis
Effects can react to audio. Requires the "audio:capture" permission.
{
"permissions": ["audio:capture"]
}
-- Get FFT data
local data = audio.capture(32) -- 32 frequency bins
-- Returns: {amplitude, bins=[0.0..1.0, ...]}
-- amplitude: overall audio level
-- bins: frequency magnitude values (low to high)
Media Album Art
Effects can use the currently playing media's album art. Requires the "media:album_art" permission.
Currently only supported on Windows. Returns nil on other platforms.
{
"permissions": ["media:album_art"]
}
local art = media.album_art(64, 64)
-- Returns: {width, height, pixels=[0xRRGGBB, ...]}
-- Returns nil if no media is playing or track has no cover
if art then
local pixel = art.pixels[1]
local r = (pixel >> 16) & 0xFF
local g = (pixel >> 8) & 0xFF
local b = pixel & 0xFF
end
Core caches the album art internally and updates it only when the track changes. Calling media.album_art() in every on_tick frame is safe and introduces no I/O overhead — no need to cache it yourself.
Parameter Types in Action
Slider
{"key": "speed", "kind": "slider", "min": 0, "max": 10, "step": 0.5, "default": 2}
function plugin.on_params(p)
if p.speed then speed = p.speed end -- number
end
Select
{
"key": "mode",
"kind": "select",
"options": [{"label": "Wave", "value": 0}, {"label": "Pulse", "value": 1}],
"default": 0
}
function plugin.on_params(p)
if p.mode then mode = p.mode end -- number (value field)
end
Toggle
{"key": "reverse", "kind": "toggle", "default": false}
function plugin.on_params(p)
if p.reverse ~= nil then reverse = p.reverse end -- boolean
end
Color
{"key": "color", "kind": "color", "default": "#FF0000"}
function plugin.on_params(p)
if p.color then
r, g, b = p.color[1], p.color[2], p.color[3] -- 0-255 each
end
end
MultiColor
{
"key": "colors",
"kind": "multi-color",
"default": ["#FF0000", "#00FF00"],
"minCount": 2,
"maxCount": 8
}
function plugin.on_params(p)
if p.colors then
-- Array of {r, g, b} tables
palette = p.colors
end
end
Complete Example: Plasma Effect
local plugin = {}
local speed = 1.0
local scale = 3.0
function plugin.on_init() end
function plugin.on_params(p)
if p.speed then speed = p.speed end
if p.scale then scale = p.scale end
end
function plugin.on_tick(elapsed, buffer, width, height)
local count = buffer:len()
local t = elapsed * speed
for i = 1, count do
local x = (i - 1) / count * scale
local v1 = math.sin(x * 10 + t)
local v2 = math.sin(x * 10 * math.sin(t / 2) + t)
local v3 = math.sin(x * 10 + math.sin(t / 3) * 2)
local v = (v1 + v2 + v3) / 3
local hue = ((v + 1) / 2 * 360) % 360
buffer:set_hsv(i, hue, 1.0, 0.8)
end
end
function plugin.on_shutdown() end
return plugin
See the Effect API Reference for the complete API.