Runtime Bindings

Runtime Bindings

Several runtime features — synth parameters today, lighting later — accept bindings where a config value can be a literal, a direct event-property lookup, or a full expression evaluated against live game state.

This page documents the expression namespace available to those bindings and the rules that apply across every consumer.

Where Bindings Apply

ConsumerBinding attributes
synth { } in on_eventpitch, velocity, any numeric synth parameter

Bindings share a single evaluation surface, so the namespace and syntax below are identical across every consumer.

Binding Forms

A binding can take one of three forms. The form is inferred from the value you write — you don’t declare it explicitly.

1. Literal numbers

pitch    = 440
velocity = 0.8

Literal numeric values are used as-is. This is the fastest path and the default choice when the value doesn’t depend on runtime state.

2. Event-property lookup — from:event.<field>

pitch    = "from:event.pitch"
velocity = "from:event.velocity"

A from:event.<field> string reads the named field directly from the event’s data payload. This is a fast path that bypasses expression evaluation. It’s equivalent to writing event.<field> in an expression, but dedicated syntax keeps the common case cheap.

3. Expressions

pitch    = "220 + var.combo_count * 55"
velocity = "signal.multiball_active ? 1.0 : 0.6"

Any other string is parsed as an expression and evaluated each time the binding is resolved. Expressions are compiled once at config load — invalid expressions surface as config errors, not runtime failures.

Expression Namespace

Inside a binding expression, the following identifiers are available:

IdentifierValue
event.<key>A field from the event’s data payload. Missing fields resolve to 0.
scoreThe current player’s score.
var.<name>The current value of any declared variable. Nonexistent variables are a parse-time error.
signal.<name>1.0 if the named signal is active at the moment of evaluation, 0.0 otherwise.

All values are coerced to numbers for binding resolution — booleans become 0 or 1, strings are rejected at parse time when used in numeric contexts.

Combining the namespace

Anything the expression engine supports is fair game: arithmetic, comparisons, ternaries, min/max, clamping, and the built-in math library.

# Scale pitch by how close the score is to the next award threshold.
pitch = "440 + ((score % 100000) / 100000) * 880"

# Louder bumper zap when the player is in multiball AND above 1M points.
velocity = "(signal.multiball_active && score > 1000000) ? 1.0 : 0.6"

# Cap how much a variable can push the pitch up.
pitch = "220 + min(var.combo_count, 12) * 35"

Evaluation Model

Bindings are resolved at the moment the consumer uses them — for synth, that’s the instant the on_event handler fires and dispatches the patch. State read during evaluation reflects game state at that instant:

If you need a value frozen at a specific earlier point, compute it upstream and store it in a variable.

Backwards Compatibility

Existing configs using only literal numbers and from:event.* bindings continue to work with no changes. The expression form is a pure extension — it’s selected when a binding string is neither a number nor a from:event.* lookup.

Error Handling

ConditionWhen surfaced
Invalid expression syntaxConfig load (file fails validation)
Reference to undeclared var.<name>Config load
Reference to undeclared signal.<name>Config load
Missing event.<key> at runtimeResolves to 0
Expression returns non-numeric valueConfig load (compile-time type error)

Because parse and compile errors surface during config load, a binding that gets past validation will always resolve to a number at runtime.