Skip to content

Configuration

Every command-line flag, every environment variable and every config.toml key the standalone application reads, with what each is for and which one wins when two of them disagree. What the app does once it is open is in Running the app.

ritmolux --help prints the flag roster and exits — that is the authority on what this binary accepts, and a test holds it in step with the scanners, so a flag that exists is a flag --help names. This page says what each flag is for, which is the part a roster line has no room for, and a test holds the page in step with the roster.

An argument no flag claims is a startup error that names it and the nearest spelling: ritmolux --ocs 127.0.0.1:9000 exits rather than starting a visualizer that publishes no telemetry.

The flags

FlagValueWhat it is for
--helpPrint the flag roster and exit
--consoleOpen the operator console at launch, on a display other than the show’s
--list-devicesEnumerate audio capture endpoints and exit (Windows-only)
--list-adaptersEnumerate graphics adapters and exit, from both rosters
--schemaPrint the preset schema as JSON on stdout and exit
--eventsReport as JSON lines on stderr, for a parent process
--previewstdout | stdout@WxHMirror the windowed show to a parent process, as a fixed-size copy
--inputloopback | line-inWhere audio comes from (Windows-only)
--device"<friendly name>"Which capture endpoint to open
--tierfloor | richPin the quality tier instead of letting the engine pick
--osc<host:port>Publish analyzer telemetry as OSC over UDP, and turn the sink on
--control<host:port>Listen for studio control messages as OSC over UDP, and turn the listener on
--soak[path]Write a long-run frame-time trace; bare, a default path
--downbeat-log[path]Write the per-beat downbeat decomposition; bare, a default path
--streamRun headless and publish every frame to a sink
--sinkspout | stdoutWhere the published frames go, default spout. Needs --stream
--sizeWxHPublished frame size; default 1280x720 on --sink spout, 640x360 on --sink stdout. Needs --stream
--fps<n>Published frame rate; default 60 on --sink spout, 30 on --sink stdout. Needs --stream
--sender<name>The published Spout sender name, default ritmolux. Needs --stream
--frames<n>Stop after this many frames. Needs --stream
--gpu<name|index>Which graphics adapter to render on — the window and --stream both
--preset<name>Hold one scene and disable rotation

A flag marked Needs --stream passed without it is a startup error naming both flags, rather than silence.

The ones that need a paragraph

--help writes to stdout and creates no window, no GPU device and no capture client, so a script can probe the flag surface without starting a show.

--events turns on the structured report a parent process reads (the control protocol). One JSON object per line on standard error: hello with this build’s version, the schema hash and the control port actually bound; preset and roster as the show moves; preset_error and preset_warning with the file, the message and — when the TOML parser gives a position — the line and column; health once a second while frames are being drawn; and pong answering a ctl/ping.

Every event line begins with { and no human diagnostic does, so a parent splits the two on the first byte and needs no framing. The flag is purely additive: without it standard error carries exactly the lines it always did, and with it those same lines are still there, unchanged, beside the events.

--preview stdout mirrors a windowed run’s frames to whatever spawned it, as raw 8-bit frames announced by the same stream event --stream --sink stdout announces. It is how a studio watches the show that is actually on the projector, rather than a second headless one.

The mirror is a fixed-size copy, and the size is the mirror’s own. --preview stdout sends 640x360; --preview stdout@WIDTHxHEIGHT asks for something else. The show keeps the window’s resolution and the mirror is a scaled copy of it, letterboxed so a window of any shape keeps its aspect. The size is answered once, before the first frame, and a resize, a maximize or a fullscreen toggle does not move it — which is what lets a reader cut the byte stream into frames by a number it was told at the start. That is the one place --preview and --sink stdout differ in kind: the latter is an exact feed at the size named on the command line, for ffmpeg, and it is unchanged by any of this.

Off by default, and the default matters: with the flag absent nothing is allocated, no frame is copied twice, and the show draws exactly what it drew before the flag existed. With it on, the frame is routed through the operator console’s own intermediate, blitted into the mirror’s fixed target, copied to a staging buffer, and read back one frame late with a non-blocking poll — so the display loop never waits for it.

It drops rather than stalls. A reader slower than the show loses frames and the show carries on; the count of what was dropped is written to diagnostics.log beside the frame-time figures, so the cost of the mirror is measurable rather than assumed. That is the opposite of the headless --sink stdout policy, which blocks — a headless loop has no present deadline and a windowed one does. See Capturing.

--schema answers the other question a program asks before it starts driving the player: what a preset may contain. It prints one JSON object on stdout — every system and engine stage with its parameters (name, default, range, and the line that says what each does), every structural table with its keys and the closed rosters they draw from, the expression grammar’s identifier rosters, and a hash of all of it. Like --help it creates nothing and exits, and it moves no files on the way.

The grammar object carries three arrays — variables, functions and constants — which is what an expression editor colours from. variables is what the parser’s identifier lookup accepts, so the reserved [latch] placeholders are absent: an author reaches a latch through the name they declared for it, and a preset’s own latch names are in the preset rather than here.

The parameter half is rendered from the same declarations the reference table in the preset library’s README is, so the two cannot disagree. The hash changes when and only when the document does: a studio that cached the schema compares it against the one the player reports on startup to know whether its panels are stale.

--console is a presence flag with no value: it turns the console on for this run and never off, and it does not write itself into config.toml (the same shape --input, --device and --osc follow). [console] enabled = true is the persistent form, and the C hotkey and the settings menu’s Console row are the same path — no two of the four can disagree about whether the console is open.

--list-adapters prints both rosters: the one the renderer selects through and the one the Spout sender selects through. They are separate enumerations and are not assumed to agree on order, so both are printed with their own indices.

--gpu <name|index> works for both the window and --stream. On a machine with one GPU you will never need it; on a hybrid laptop it is the difference between a picture and nothing. A Spout sender shares a D3D11 texture by handle and the receiver opens it on its own device, which works only when both are the same physical GPU — and Windows hands a plain console process the power-saving GPU while the receiving application runs on the discrete one. One flag moves both halves: the renderer and the sender each resolve the name against their own roster. Unset, --stream’s renderer asks for the high-performance adapter and the sender follows it by name, printing what both resolved to.

The window’s unset behaviour is deliberately different: it asks for whatever the graphics layer picks for the surface, which is what it has always asked for, so no published frame-time figure moves because this flag arrived. On a hybrid laptop that default is the power-saving GPU, and --gpu <name|index> is how you move the window onto the discrete one; the startup line in diagnostics.log names the adapter and says whether a flag pinned it. A named adapter that cannot drive the window is a startup error rather than a quiet fall-back to another GPU.

--preset <name> takes the preset’s display nameClifford, Rose Window — as the browse overlay and --preset’s own error listing spell it, not the .toml filename, so most of them need quoting. An unknown name is a startup error that lists the roster, and no window opens. Hotkeys still browse, so this pins where a run starts and turns the dwell timer off.

--input loopback|line-in overrides [input] mode. loopback taps whatever the system is playing; line-in captures an input endpoint (an audio interface, a mixer feed). A value that is neither is a usage error and the app exits, the same way a bad --tier does. The input also moves while the app is running — the settings menu’s Input mode and Input device rows swap the capture stream in place and write the choice to config.toml, so --input pins the launch rather than the session. Expect a brief hitch on the swap (the old stream is stopped and the new one opened synchronously), and, when the two endpoints negotiate different sample rates, a second or two of re-adaptation while the level tracking rebuilds.

--device "<friendly name>" overrides [input] device. Copy a name out of --list-devices; a substring is enough. The two flags override independently--device alone keeps the configured mode, --input alone keeps the configured device name. A name that matches no active endpoint of the selected mode is not an error: capture falls back to that mode’s default endpoint and says so on stderr, because the interface being unplugged is a fact about the world rather than a typo in the flag. Giving no name at all — a trailing --device, or --device=is an error: an empty value selects the default endpoint, which is the opposite of what naming a device asks for. What happens when an endpoint disappears mid-show is in Running the app.

--stream runs headless as a live video source and publishes every frame as a Spout sender, for TouchDesigner (or any Spout receiver) on the same machine. No window, no swapchain, no codec. Windows-only, and present only in a build with the spout feature — the release ritmolux.exe has it; a plain cargo build does not, and --stream there fails with a named error saying so. Presets rotate on the operator config’s dwell timer exactly as they do in the window (rotation is on here even where [rotate] auto is off, since a headless source has nobody to press Space). Ctrl-C stops it and prints the run’s frames, wall clock and scene clock. See Headless capture and video for the TouchDesigner side.

--tier floor|rich pins the quality tier. Unpinned, the app starts on rich and a frame-time governor demotes it to floor once if the display’s frame budget is not being held. A pin is never demoted, so this is also how you keep rich on a machine a transient stall demoted. The tier also moves while the app is running — see Quality tiers.

--osc <host:port> both aims the sink and turns it on, so enabled = false in config.toml cannot veto a target typed for this run. Off unless you ask for it. A target that will not resolve is a usage error and the app exits — the same way a bad --tier does — whereas a stale target in config.toml degrades to no sink and says so, because a config file must not stop a show. Sends are non-blocking and dropped on failure: a broken link costs the telemetry, never a frame, and the app prints one line when it starts failing and one when it recovers rather than a line per frame.

Environment variables

VariableValueWhat it is for
RLX_PRESET_DIRa directoryRead presets from here instead of the seeded per-user directory; edits to *.toml there hot-reload live
RLX_TIERfloor | richThe same pin as --tier, for a one-off run

RLX_PRESET_DIR is read by the headless shot CLI as well as by the app, so a capture and a live run resolve the same library.

config.toml

A small per-user file under the same directory the presets live in — %APPDATA%\Ritmolux\ on Windows. It is read once at startup and written back whenever a hotkey or a settings row changes a choice, so a stage setup survives a restart.

Every key is optional. A missing file, a missing section and an unknown extra key all degrade to the built-in default rather than failing, so the file below is a complete listing rather than something you have to write.

[output]

Which display the show opens on, and whether it opens fullscreen.

KeyDefaultWhat it means
display0Target monitor index — the fallback when no display_name matches
display_nameunsetPreferred monitor identity, matched by name before the raw index. Unset means “use the index”
fullscreenfalseOpen borderless-fullscreen on the target display; windowed otherwise

The name is tried before the index because the window system’s monitor ordering can shift across a boot or a hotplug, so a stored index alone may point at the wrong screen.

[input]

Where audio comes from. Windows-only; the macOS path taps system audio and takes no endpoint choice.

KeyDefaultWhat it means
mode"loopback""loopback" taps a render device (what the system is playing); "line-in" captures an input device
device"default"Friendly device name to capture. "default", or a name matching no active endpoint, falls back to the selected mode’s default endpoint

[rotate]

The scene director’s auto-rotate policy.

KeyDefaultWhat it means
autofalseAuto-rotate on the dwell timer; manual-only (Space) when off
min_dwell_secs20Never rotate sooner than this many seconds after the last change
max_dwell_secs90Always rotate by this many seconds, even through a steady passage
track_changetrueLet the track-change novelty signal nudge rotation in on the same dwell

Hold one scene by default. Out of the box the app stays on a single scene until you opt in — the A hotkey, or auto = true here. Manual Space works either way. When auto is on the defaults favour a mostly-predictable, timer-led rotation: a steady passage holds to the 90 s cap and never rotates before 20 s. An energy drop can land a change early, but only well past the minimum dwell (a softened gate, around 37.5 s at these defaults), so it cannot flip scenes every few seconds.

[quality]

KeyDefaultWhat it means
tier"auto""auto" lets the engine resolve rich and demote it if the frame time says so; "floor" and "rich" pin it

Precedence, highest first: --tier, then RLX_TIER, then [quality] tier, then auto. A pin made from inside the app ([, ], or the settings menu) is written here, so it survives a restart — and the two above still win at the next launch.

[hud]

The furniture the shell paints over the show. Separate from [output] because it is about what is painted, not about which screen the window opens on.

KeyDefaultWhat it means
preset_nametrueDraw the active preset’s name in the top-left corner. Even when on, the name yields to a menu and to the F3 panel — this is “never show it”, not “show it always”
now_playingtrueAnnounce the current track in the lower-left corner when it changes. Off means no track ever reaches the visualizer, not a banner drawn transparent

[osc]

The lighting telemetry sink. Off by default, like every optional sink: a user who runs no lighting rig must not have a socket bound or a datagram leaving their machine because they installed the app.

KeyDefaultWhat it means
enabledfalsePublish telemetry
target"127.0.0.1:9000"Where to send, as host:port. Inert until enabled (or --osc) turns the sink on
rate_hz60Datagram sets per second; 0 means every rendered frame, whatever the frame rate

--osc overrides target and turns the sink on, and leaves rate_hz to the file — the one key it has no spelling for.

[control]

The studio control-in listener — the socket a studio, a lighting console or a MIDI bridge drives this player from (the control protocol). Off by default, and loopback by default, and those are two separate promises: off means a machine that was never asked to be driven binds no port at all, and loopback means one that was asked, and named no host, is reachable only from itself.

KeyDefaultWhat it means
enabledfalseListen for control messages
listen"127.0.0.1:9001"Where to listen, as host:port. Inert until enabled (or --control) turns the listener on

--control overrides listen and turns the listener on, exactly as --osc does for the sink. The port is one above [osc] target’s so the two OSC surfaces read as a pair and a machine running both needs neither moved.

Binding anywhere but loopback is a decision, not a default. Anything that can reach the port can move a parameter on the running show, which is the whole feature and also the whole of the exposure — there is no authentication, and OSC has no channel to carry one. On a venue network, leave it on 127.0.0.1 and run the studio on the same machine.

[console]

The operator console’s second window. Off by default, for the reason every optional surface here is: a config that has never heard of a console produces exactly one window, one surface, no intermediate render target and no extra copy per frame.

KeyDefaultWhat it means
enabledfalseOpen the console at launch
display_nameunsetPreferred monitor identity for the console, matched by name before the index. Unset means “use the index”
display1Fallback monitor index when no display_name matches
frame_latency1The console swapchain’s desired_maximum_frame_latency, clamped to 1..=3. At 1 the surface holds a single in-flight image, so acquiring the next one waits for its own previous present to retire
present_every_n1Present the console every Nth output frame. At 1 every frame; at 2 the console’s readout updates at half rate, which is visible on the transport strip and the preview

Neither of the last two needs touching on a machine that keeps up. They exist so the console’s cost can be measured rather than argued about; the defaults above are the shipped ones, and what they cost in practice is in Running the app.

display defaults to 1, not 0: a console’s whole point is to be on a display other than the show’s, and the show defaults to 0. On a single-monitor machine this falls back to the only monitor there is, which is the correct degrade rather than a failure.

A complete file

Every key at its default. display_name in [output] and [console] is absent because “unset” is its default and the file format has no spelling for one.

[output]
display = 0
fullscreen = false
[input]
mode = "loopback"
device = "default"
[rotate]
auto = false
min_dwell_secs = 20
max_dwell_secs = 90
track_change = true
[quality]
tier = "auto"
[hud]
preset_name = true
now_playing = true
[osc]
enabled = false
target = "127.0.0.1:9000"
rate_hz = 60
[control]
enabled = false
listen = "127.0.0.1:9001"
[console]
enabled = false
display = 1
frame_latency = 1
present_every_n = 1

Precedence

A flag beats an environment variable beats the file, and nothing beats a change made inside the running app for that session.

SettingHighestLowest
Quality tier--tierRLX_TIER[quality] tierauto
Preset directoryRLX_PRESET_DIRthe seeded per-user directory
Input mode--input[input] modeloopback
Input device--device[input] devicethe mode’s default endpoint
OSC target--osc[osc] target
OSC on/off--osc (on)[osc] enabledoff
Control address--control[control] listen
Control on/off--control (on)[control] enabledoff
Console on/off--console (on)[console] enabledoff

--input, --device, --osc, --control and --console pin a run and never write themselves into config.toml; the file is the persistent form. There is no environment variable for the input selection, because an input is a property of a rig and already persists to the config.

OSC addresses

--osc <host:port>, or [osc] enabled = true, publishes the analyzer’s telemetry as OSC over UDP so a lighting console or a bridge can follow the music.

The address space is versioned in the addresses, so a later signal is additive under the same /rlx/v1 prefix and a mapping you have already bound keeps working across it. One argument per address, so a console binds a parameter to an address rather than to a position inside a message.

AddressTypeWhat it carries
/rlx/v1/level/bassfBass excitation, 01, normalized against its own running peak — not a magnitude. It reads 1.0 whenever this moment is the loudest bass of the last few seconds, which on four-on-the-floor material is every kick. See the note below the table before binding it to a dimmer
/rlx/v1/level/midfMid excitation, same normalization and same ceiling behaviour
/rlx/v1/level/trebfTreble excitation, same normalization and same ceiling behaviour
/rlx/v1/level/onsetfSpectral-flux onset envelope, same normalization and same ceiling behaviour
/rlx/v1/level/rmsfBroadband RMS of the waveform trace — un-normalized, unlike the four above it, because the trace it comes from deliberately is. Map it with a gain in the console
/rlx/v1/raw/bassfRaw mean magnitude in the bass band — the absolute twin of level/bass, and the one to bind when you want loudness. Unnormalized, so it does move with input gain and is not portable across tracks
/rlx/v1/raw/midfRaw mean magnitude, mid
/rlx/v1/raw/trebfRaw mean magnitude, treble
/rlx/v1/raw/onsetfRaw spectral-flux envelope
/rlx/v1/beat/triggeri1 on a frame an onset fired, 0 otherwise — the discrete event
/rlx/v1/beat/indexiMonotone count of onset detections. Not a musical beat count — the detector fires 1.2x–2.3x per beat depending on material, so no fixed multiplier turns it into bars. Useful as a ratchet, not as a meter
/rlx/v1/beat/phasefBeat phase in [0, 1): 0 on each beat, ramping to the next
/rlx/v1/tempofTempo estimate in BPM, 0 until the tracker warms. Expect a warm-up of tens of seconds before it settles
/rlx/v1/presetsThe active preset’s name

level/* touches its ceiling by design, and no input gain moves it. Each of the four is divided by its own slowly-decaying running peak — instant attack, a 2.5 s release (ADR-0049) — so the value it publishes is raw / peak, an excitation relative to this track’s recent past rather than a loudness. Two consequences are worth knowing before a fixture is bound to one:

  • 1.0 is a normal reading, not a clip and not “loud”. It means only this is the loudest bass since the peak last released. At 120 BPM the peak is re-adopted every 0.5 s against a 2.5 s release, so on periodic material the term spends most of its life near the top of its range and reaches 1.0 once per kick. A look shaped as glow + depth * level/bass therefore sits at glow + depth on every kick and reads flat in the room — that is the term behaving as specified, not a saturated input.
  • Turning the mic or the interface down changes it by exactly nothing. raw / peak is scale-invariant: halve the input and both terms halve. Gain portability is the entire purpose of the normalization — > 0.5 is meant to mean the same thing on every track at every gain — so an input-gain control is provably the wrong lever for a pinned level/*. The right lever is the binding: reach for raw/*, which is the absolute twin and is published beside every one of them, and scale it yourself.

Telemetry rides the rendered frame, so it stops when the window is hidden and the preset name lags a switch by one frame. Nothing here is a musical timebase you can drive a sequencer from — it is a level feed for lights.

The root moved in this release: /lmv/v1 became /rlx/v1. There is no transition period and no dual-emit, and OSC has no error channel — so a binding left on the old root stops firing and reports nothing, which looks exactly like a fixture that happens not to be moving. Re-point every address by hand, and keep the old show file until each one is confirmed against a playing track. /v1 did not move, because no payload, type tag, address suffix, vocabulary or send cadence changed: re-point the root, change nothing else, and the mapping is correct.

macOS

Loopback capture is implemented — the standalone taps system audio through ScreenCaptureKit, so it needs macOS 13+ and the Screen Recording permission (that API will not run an audio-only stream, so the capture carries a throwaway 2x2 px stub video alongside the audio). Grant it, then relaunch — the app does not pick the permission up mid-run.

The caveat is that this path has never run on Apple hardware: CI compiles it on every push, but no runner can play audio or drive a real Metal adapter, so the first live run is also its validation. A window with visuals but no reaction to music means capture did not start, not a crash — launch from Terminal to see the reason.

Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.