Oscilla Control Plane — Architecture & Usage Guide

Oscilla Dynamic Signal Control
Published signals are shared across the system, so animations can control audio parameters, slider-like interfaces can control animations, and multiple cues can influence each other in any combination. Because values are updated continuously, this also allows feedback systems where motion, sound, and interaction form coupled, dynamic behaviours within the score.
synth(uid:pad, freq: follow(fadeSineFreq.t, 90, 2000), env:{a:4})

o2p(path:fadeSineFreq, trig:touch, osc:1, uid:fadeSineFreq, oscAddr:fadeSineFreq)

Overview

The Oscilla Control Plane enables bidirectional signal flow between cues. Any animation can publish its values as signals, and any synth or audio cue can subscribe to those signals to control its parameters in real-time.

This transforms Oscilla from a trigger-based score system into a dynamic, signal-driven, executable score environment.

Core Concept

┌─────────────────┐                    ┌─────────────────┐
│   O2P Fader     │ ──publishes──────▶ │   ParamBus      │
│   uid: slider1  │    t, x, y, angle  │  (signal store) │
└─────────────────┘                    └────────┬────────┘
                                                │
                                                │ subscribes
                                                ▼
                                       ┌─────────────────┐
                                       │   Synth         │
                                       │   freq: follow(slider1.t)│
                                       └─────────────────┘

Quick Start

1. Create a Controller (O2P Fader)

o2p(path:faderTrack, trig:touch, uid:myFader)

This fader automatically publishes:

2. Bind a Synth Parameter

synth(uid:pad, freq: follow(myFader.t, 200, 800), amp:0.2)

The freq: follow(myFader.t, 200, 800) syntax means:

3. Result

Moving the fader changes the synth's frequency in real-time!


Signal Reference Syntax

A binding is written as a follow() call in the parameter's value.

Basic Binding

param: follow(source.channel)

The signal arrives as 0-1 and is passed straight through.

Binding with Range

param: follow(source.channel, min, max)

Maps the 0-1 signal to the output range, linearly.

Range and Curve

param: follow(source.channel, min, max, curve)
Curve Description
lin Linear (default)
exp2 Quadratic (x²) — slower at start, faster at end
exp3 Cubic (x³) — more pronounced exponential
exp4 Quartic (x⁴) — very steep curve
log Logarithmic — faster at start, slower at end

Default Value

param: follow(source.channel, min, max, default)
param: follow(source.channel, min, max, default, curve)

The fourth argument is read by type: a number is a default, a name is a curve, and both may appear in either order. With a default, the fader initialises to the position producing that output. Without one, presets and localStorage control the initial state.

Seeing what is connected. The connections view (topbar button) draws every binding in the score as a patch cord, and shows every port each object has — including the free ones you could connect to. The demo-connections demo is built to be read with it open.

One Animation Driving Another

Any animation that publishes can drive any parameter that binds, including another animation's speed. A rotate() publishes its angle as .norm (0-1 per revolution); a rotate() also accepts a bound dur:

rotate(uid:r1, dir:1, dur:6, trig:auto)
rotate(uid:r2, dir:1, dur: follow(r1.norm, 8, 0.5), trig:auto)

r1 turns steadily. r2's revolution takes 8 seconds while r1 is at the start of its turn and half a second by the end of it — so r2 breathes, slow to fast and back, once per turn of r1. Note the range runs high to low: a long duration is a slow rotation, so 8, 0.5 reads as "gets faster".

Neither cue names the other's type. r1 could be replaced by a fader, a controlXY handle or an incoming OSC address without touching r2.

A driven object is still a source. r2 publishes its own angle while following r1, so a third object can follow r2:

scale(uid:box, sx: follow(r2.norm, 0.6, 1.8), sy: follow(r2.norm, 0.6, 1.8))

giving r1 → r2 → box. There is no special status for being both; a signal is a signal.

Loop counts are infinite by default. loop:1 means once, so a continuous spin simply does not name loop at all.

When a Binding Names Something That Does Not Exist

A follow() whose source has no matching uid, or whose channel is misspelt, cannot work — and used to say nothing at all. The score loaded cleanly and the binding simply never happened.

Both are now reported when the score loads, with the cue that wrote them:

[refs] 1 unresolved reference, 1 suspect — these do nothing at all
  ✗ signal binding source: "nosuchfader.t" matches no element
      in: synth(uid:pad, freq: follow(nosuchfader.t, 90, 2000), …)
  ? signal binding source: "fader.tt" — unknown channel ".tt"

The second is the subtler failure: the source is real, so nothing looks wrong, but .tt is not a channel anything publishes and the binding listens to a path nobody writes. Channel names are checked against what the cue types actually publish; an unknown one is a warning rather than an error, because an ext() extension may publish channels Oscilla cannot know about.

The same references appear in the connections view as dotted red cords ending in an open ring, so a broken connection is visible in the score as well as in the console.

Binding to Incoming OSC

param: follow(/pedal/1, 0, 1)

A source beginning with / is an OSC address rather than a uid. Everything else — range, curve, default — works the same.

Syntax Summary

Format Example Description
follow(source.channel) follow(fader.t) Basic binding (0-1)
follow(source.channel, min, max) follow(fader.t, 200, 2000) With range, linear
follow(source.channel, min, max, curve) follow(fader.t, 0, 4800, exp3) With range and curve
follow(source.channel, min, max, default) follow(fader.t, 200, 2000, 440) With range and default
follow(source.channel, min, max, default, curve) follow(fader.t, 0, 4800, 60, exp3) Everything
follow(/osc/address, min, max) follow(/pedal/1, 0, 1) Incoming OSC

A note on the older syntaxes. Two forms preceded this one. The parser accepted fader.t-200-2000-440-exp3, where the arguments ran together and were told apart by type; much of the documentation instead showed fader.t[200,2000], which the parser never accepted at all — it parsed to nothing and the binding silently did not happen. Neither form is recognised any more. [...] now means only what it means everywhere else in the DSL: an array literal.

Examples

DSL Meaning
freq: follow(slider.t) Freq follows slider position (0-1)
freq: follow(slider.t, 200, 2000) Freq mapped to 200-2000 Hz (linear)
freq: follow(slider.t, 200, 2000, 440) Freq 200-2000 Hz, starts at 440 Hz
rotspeed: follow(knob.t, 0, 4800, exp3) Rotation speed with cubic curve
rotspeed: follow(knob.t, 0, 4800, 60, exp3) Rotation speed, starts at 60°/s, cubic curve
amp: follow(fader.y, 0, 0.5) Amplitude mapped to 0-0.5
pan: follow(knob.x, -1, 1) Pan mapped to -1 to 1 — a negative minimum needs no special casing
dur: follow(speed.t, 120, 1) Duration 120s (left) to 1s (right) — inverted for speed control

map() — when the source is too slow, or too narrow

follow(src, min, max) assumes the source sweeps the whole 0–1. Often it does not, and then the binding barely moves. transport.t is the clearest case: it spans the entire piece, so across one screen of playhead travel it changes by a fraction and whatever it drives looks static.

map() is follow() with the input range spelled out — you say which part of the source to use, and that slice is stretched across the full output:

map(source, inMin, inMax, outMin, outMax [, default] [, curve])
freq: follow(fx.t, 90, 2000)              # 0-1 of the source → 90..2000
freq: map(fx.t, 0, 1, 90, 2000)         # identical, just written out
freq: map(fx.t, 0.3, 0.35, 90, 2000)    # a 5% slice → the whole range

follow() was called link(). The old spelling still parses and always will — there are scores carrying it — but everything here uses the new one. The rename is worth the churn because follow() names a behaviour: this parameter tracks that signal continuously, for as long as both exist. That is exactly what distinguishes it from fade(from:, to:), which samples its endpoints once, at trigger. A noun hid that difference; a verb shows it.

Arithmetic — mul() and add()

A binding can read more than one source:

sx:   mul(follow(adc.amp, 0, 2), follow(lfo.norm, 0.5, 1))
freq: add(follow(pad.y, 200, 800), follow(adc.pitch, 0, 200))
sx:   mul(follow(adc.amp, 0, 2), 0.5)

Each term carries its own range, because a term is simply a follow() or a map() — the thing that already knows how to read a source. That is what makes it compose: anything you can bind to a parameter, you can multiply by anything else you can bind to a parameter, and a plain number is a term too. They nest, so mul(add(a, b), c) is fine.

The result is clamped to the target's own range. Each term is inside its range, but a product is not: two terms topping out at 4 and 3 give 12, which would put a scale somewhere absurd.

A term that has not published yet is skipped rather than counted as zero — the identity is 1 for mul and 0 for add — so one silent source does not flatten the whole expression.

Each term draws its own cord in the connections view, and a broken term is reported at load like any other reference.

Arithmetic on two constants is refused: mul(2, 3) is a number, not a binding, and making a cord that can never move would be worse than leaving the value alone.


So follow is map with the input range assumed 0–1. Everything else is the same: the same sources, the same curves, the same cords in the connections view — map() produces exactly the same binding, it just also carries the window.

Input outside the window pins to the nearer end. A window is a window, not an extrapolation: below inMin you get outMin, above inMax you get outMax, and it moves only in between.

That is what makes a slow source usable. Window it to the stretch you care about and it spends its whole range there:

# sweeps its full range only while the playhead crosses one section
sx: map(transport.t, 0.605, 0.67, 0.2, 3)

map() reads a leading slash as an OSC address, exactly as follow() does:

cutoff: map(/pedal/1, 0.2, 0.8, 200, 8000, exp2)

Published Signals by Cue Type

A published signal is addressed uid.channel — the uid you gave the cue, then the channel. There is no cue-type prefix: fader1.t, not o2p:fader1.t.

Cue Signal Meaning
rotate {uid}.angle degrees, 0-360
rotate {uid}.rad radians
rotate {uid}.norm 0-1 per revolution
rotate {uid}.vel angular speed, degrees/s
rotate {uid}.tvel turns per second
rotate trig:{uid} fires a cue it passes over
scale {uid}.sx x scale factor
scale {uid}.sy y scale factor
scale {uid}.uniform mean of the two
scale trig:{uid} fires a cue it passes over
color {uid}.hNorm hue, 0-1
color {uid}.sNorm saturation, 0-1
color {uid}.lNorm lightness, 0-1
fade {uid}.opacity 0-1
o2p {uid}.t how far along the path, 0-1
o2p {uid}.x x position, 0-1
o2p {uid}.y y position, 0-1
o2p {uid}.angle heading in degrees
o2p {uid}.p rotation handle, 0-1
o2p {uid}.vel speed, score units/s
o2p {uid}.vx {uid}.vy signed velocity components, score units/s
o2p {uid}.tvel path lengths (laps) per second — the rate of t
o2p trig:{uid} fires a cue it passes over
oscCtrl {uid}.t position along the lane, 0-1
oscCtrl {uid}.v lane value, 0-1
controlXY {uid}.x pad x, 0-1
controlXY {uid}.y pad y, 0-1
controlXY {uid}.p handle rotation, 0-1
image {uid}.index which plate, 0-based
image {uid}.t position through the folder, 0-1
trans {uid}.t progress through the whole waypoint list, 0-1
trans {uid}.legT progress along the current leg, 0-1
trans {uid}.x x across the travel, 0-1
trans {uid}.y y across the travel, 0-1
trans {uid}.vel speed, score units/s
trans {uid}.vx {uid}.vy signed velocity components, score units/s
trans {uid}.tvel the rate of t: whole waypoint lists per second
trans trig:{uid} fires a cue it passes over
drag {uid}.x where it sits across the score, 0-1 (clamped)
drag {uid}.y and down it, 0-1 (clamped)
drag {uid}.vel speed, score units/s
drag {uid}.vx {uid}.vy signed velocity components, score units/s
drag trig:{uid} fires a cue it passes over
transport transport.t the playhead through the score, 0-1
transport transport.elapsed seconds into the score
transport transport.speed the transport's speed multiplier
stopwatch {uid}.elapsed seconds since it started
stopwatch {uid}.t through its hold, 0-1
stopwatch {uid}.remaining seconds left of the hold
video {uid}.time playback position in seconds
video {uid}.t through the file, 0-1
metro {uid}.beat which beat of the bar, 1-based
metro {uid}.phase through the bar, 0-1
metro {uid}.bar bars since it started
audio adc.amp level
audio adc.peak peak level
audio adc.centroid spectral centroid
audio adc.onset onset detection
audio adc.low low band
audio adc.mid mid band
audio adc.high high band
audio adc.pitch detected pitch in Hz
audio adc.note detected note number
audio adc.pitchConf pitch confidence, 0-1

Two of these have no element behind them. transport is the score's own playhead — follow(transport.t, …) works in any score, with no cue to declare. mic is the audio analyser, published under that fixed uid.

Velocity

Everything that moves — o2p, trans, drag, rotate — also publishes how fast it is moving. vel is the speed in the units the object actually moves in: score (SVG user) units per second, or degrees per second for a rotation. vx/vy are its signed components. A stopped object reads 0 about a tenth of a second after its last movement, so a released fader or a paused animation does not freeze at its last speed.

vel is not 0-1, so a plain follow(obj.vel) would pin at 1. Use map() and name the window you care about — the vals overlay mode shows the raw v: number on an o2p so you can pick it:

amp: map(fader.vel, 0, 400, 0, 1)        # silent at rest, full at 400 units/s

tvel is the same speed measured against the path instead of the score: the rate of t, in path lengths per second — laps per second for an o2p, turns per second for a rotate, waypoint lists per second for a trans. It depends only on the geometry, never on dur:, so four cyclists on four lanes of different lengths but each "one lap" can share one mapping:

display(v: map(cyc1.tvel, 0, 1, 0, 1440), fmt:"{v} km/h", dec:0)   # 400 m lap: 1 lap/s = 1440 km/h
cutoff: map(wheel.tvel, 0, 2, 200, 4000)                             # up to two turns a second

A drag() has no path to be a fraction of, so it has no tvel.


Sinks: sending a signal out, showing a signal

A binding normally lands on a parameter. Two cues exist only to take signals somewhere else. Both are always on — registered when the score loads, never on the playhead — and both accept any signal, not just the ones they were written for.

oscOut() — signals → OSC

oscOut(addr:/obj/vel, v: map(fader.vel, 0, 500, 0, 1))
oscOut(addr:/obj, x: follow(a.x), y: follow(a.y), laps: follow(a.tvel), uid:s1)

Every parameter except addr: and uid: becomes one argument of the message, in the order written; the second example sends /oscilla/obj [x y laps]. A follow()/map() is followed and the message re-sent on every change, so the range and curve of the binding are the range and curve on the wire; a plain number is a constant argument. Addresses follow the same rule as everywhere else: /oscilla/<addr>, unless addr: starts with /.

display() — signals → text in the score

display(v: map(fader.vel, 0, 500, 0, 100), fmt:"speed {v} %", dec:0)
display(x: follow(pad.x), y: follow(pad.y))                  → "0.42 0.77"

Put it on a <text> element and its content is rewritten in place — the font, size and position stay whatever they are in Inkscape. On any other element a <text> is added at its top-left corner. Every parameter except fmt:, dec: and uid: is a named value; fmt: places them as {name}, and without one they are joined with spaces. dec: is the number of decimals (default 1).

Give either a uid: and the connections view draws its cords like any other target.


Bindable Parameters

Anything below takes a follow() in place of a number.

Cue Parameter Meaning
rotate dur: seconds per revolution
scale sx: x scale factor
scale sy: y scale factor
scale dur: seconds per step
color dur: seconds per cycle
fade from: start opacity, sampled at trigger
fade to: end opacity, sampled at trigger
fade dur: seconds, sampled at trigger
o2p dur: seconds for one traverse
o2p rotspeed: handle rotation speed
synth freq: pitch in Hz
synth amp: level, 0-1
synth pan: -1 to 1
synth cutoff: filter frequency in Hz
synth q: filter resonance
image opacity: 0-1
image index: pick the plate by position in the folder
text speed: scroll speed in px/s, sampled at trigger
ui opacity: 0-1, sampled at trigger
ui scale: scale factor, sampled
ui rotate: degrees, sampled
ui x: pixels, sampled
ui y: pixels, sampled
ui dur: seconds of transition, sampled
video opacity: 0-1
video speed: playback rate

dur: on rotate(), scale(), color() and fade(), and from:/to: on fade(), are sampled when the cue fires rather than followed: the tween is built once from those numbers, so there is nothing for a later value to change. Everything else follows the signal continuously.

A synth also binds detune, and audio cues bind amp, pan and pitch/rate; effects within a synth bind delayTime, feedback and mix.


Architecture

Module Overview

┌─────────────────────────────────────────────────────────────────┐
│                     control/paramBus.js                         │
│  Central signal store with pub/sub                              │
│  • set(path, value) — publish a signal                         │
│  • get(path) — read current value                              │
│  • subscribe(path, callback) — react to changes                │
└─────────────────────────────────────────────────────────────────┘
                              ▲
                              │
        ┌─────────────────────┴─────────────────────┐
        │                                           │
        ▼                                           ▼
┌───────────────────┐                   ┌───────────────────┐
│ control/          │                   │ Animation Modules │
│ paramBinding.js   │                   │                   │
│                   │                   │ • publish() call  │
│ • bindParam()     │ ◀──── used by ────│   in update loop  │
│ • publish()       │                   │                   │
│ • applyCurve()    │                   └───────────────────┘
│ • mapRange()      │
└───────────────────┘
        │
        │ used by
        ▼
┌───────────────────┐
│ cues/             │
│ cueParamBinding.js│
│                   │
│ • setupSpeedBinding│
│ • setupParamBinding│
│ • fader defaults  │
└───────────────────┘

File Descriptions

File Purpose
control/paramBus.js Central signal store with pub/sub
control/paramBinding.js bindParam(), publish(), applyCurve(), mapRange()
parser/parserSignalRef.js Parser extension for signal ref syntax
cues/cueParamBinding.js Cue-level binding helpers, fader defaults

Integration Guide

Adding Signal Publishing to a Cue

To make any animation publish signals, add one line to its update callback:

import { publish } from '../control/paramBinding.js';

// In your animation's update callback:
update: () => {
    // ... existing animation logic ...

    // ADD THIS LINE:
    publish("o2p", cfg.uid, { t: pathT, x: normX, y: normY, angle });
}

Example: O2P Animation

Location: cues/o2p.js, in startContinuousO2P()

// BEFORE (existing code):
emitO2POsc({ cfg, uid: cfg.uid, path, point, pathT });

// AFTER (add this line):
import { publish } from '../control/paramBinding.js';
// ... then in update callback:
emitO2POsc({ cfg, uid: cfg.uid, path, point, pathT });
publish("o2p", cfg.uid, { t: globalT, x: normX, y: normY, angle });

Note: You'll need to calculate normX and normY from the path bbox:

const bbox = path.getBBox();
const normX = (point.x - bbox.x) / bbox.width;
const normY = (point.y - bbox.y) / bbox.height;

Example: Rotate Animation

Location: cues/rotate.js, in handleRotateContinuous()

import { publish } from '../control/paramBinding.js';

update: () => {
    // ... existing code ...
    const angle = getCurrentAngle(animEl, 0);

    // ADD THIS:
    publish("rotate", cfg.uid, { angle: angle });
}

Example: Scale Animation

Location: cues/scale.js, in handleScaleContinuous()

import { publish } from '../control/paramBinding.js';

update: () => {
    // ... existing code ...
    const sx = parseFloat(tr.match(/scale\(([^,]+),/)?.[1] || 1);
    const sy = parseFloat(tr.match(/,\s*([^)]+)\)/)?.[1] || sx);
    
    // ADD THIS:
    publish("scale", cfg.uid, { sx, sy, uniform: (sx + sy) / 2 });
}

Adding Signal Binding to a Cue

To make parameters bindable in a cue, use bindParam():

import { bindParam, isSignalRef } from '../control/paramBinding.js';

function startMyCue(params) {
    const unbinders = [];

    // Bind frequency - works with both static and signal ref
    const freqBinding = bindParam(
        params.freq,
        (hz) => oscillator.frequency.setTargetAtTime(hz, 0, 0.02),
        { min: 20, max: 20000, default: 440 }
    );
    unbinders.push(freqBinding.unbind);

    // Use initial value
    oscillator.frequency.value = freqBinding.value;

    // ... later, on cleanup:
    unbinders.forEach(fn => fn());
}

Example: Synth Integration

Location: cues/synth.js, in startSynthVoice()

import { bindParam } from '../control/paramBinding.js';

function startSynthVoice(uid, ast, cueElement, opts) {
    const ctx = sharedAudioCtx;
    const params = extractParams(ast);
    const unbinders = [];
    
    // Frequency binding
    const freqBinding = bindParam(
        params.freq,
        (hz) => {
            if (voice.source?.kind === 'osc') {
                voice.source.node.frequency.setTargetAtTime(hz, ctx.currentTime, 0.02);
            }
        },
        { min: 20, max: 20000, default: 440 }
    );
    unbinders.push(freqBinding.unbind);
    
    // Amplitude binding
    const ampBinding = bindParam(
        params.amp,
        (amp) => {
            voice.graph.gain.gain.setTargetAtTime(amp, ctx.currentTime, 0.02);
        },
        { min: 0, max: 0.5, default: 0.1 }
    );
    unbinders.push(ampBinding.unbind);
    
    // ... create voice with initial values ...
    const freqHz = freqBinding.value;
    const amp = ampBinding.value;
    
    // Store unbinders on voice for cleanup
    voice._unbinders = unbinders;
}

function cleanupVoice(uid, voice) {
    // Unbind all signal subscriptions
    voice._unbinders?.forEach(fn => fn());
    // ... rest of cleanup ...
}

Parser Integration

Modifying the Parser

The parser needs to recognize signal references in parameter values. Add this to the value extraction logic:

Location: parser/parser.js, in cstToAst() synth/audio sections

import { maybeConvertToSignalRef } from './parserSignalRef.js';

// When extracting a parameter value:
let val = extractRawValue(valueNode);
val = maybeConvertToSignalRef(val, paramName);

What the Parser Outputs

Input DSL:

synth(uid:pad, freq: follow(fader1.t, 200, 800, 440, exp2), amp:0.2)

Output AST:

{
    type: "cueSynth",
    args: [
        { type: "uid", value: "pad" },
        {
            type: "freq",
            value: {
                type: "signalRef",
                source: "fader1",
                channel: "t",
                range: [200, 800],
                default: 440,
                curve: "exp2"
            }
        },
        { type: "amp", value: 0.2 }
    ]
}

Note: Fields default and curve are only present when specified in the DSL.


Debugging

Enable Debug Mode

// In browser console:
oscillaParamBus.setDebugMode(true);

This logs all signal changes.

Inspect Current Signals

// List all signals:
oscillaParamBus.list();

// List signals from a specific source:
oscillaParamBus.list("o2p:");

// Get current value:
oscillaParamBus.get("o2p:fader1.t");

// Get snapshot of all values:
oscillaParamBus.snapshot();

Test Signal Publishing

// Manually publish a signal:
oscillaParamBus.set("o2p:test.t", 0.5);

// Watch a signal:
const unsub = oscillaParamBus.subscribe("o2p:test.t", (value, path) => {
    console.log(`${path} = ${value}`);
});

// Later: unsub() to stop watching

Common Patterns

XY Pad → Synth

o2p(path:xyPad, trig:touch, uid:xy)
synth(uid:pad, freq: follow(xy.x, 200, 2000), amp: follow(xy.y, 0, 0.5))

Rotation → Filter

rotate(dur:4, loop:0, uid:wheel)
synth(uid:drone, freq:220, cutoff: follow(wheel.norm, 200, 4000))

Speed Control with Curve

o2p(path:speedFader, trig:touch, uid:speed)
rotate(dur: follow(speed.t, 120, 1), rotspeed: follow(speed.t, 0, 4800, exp3), uid:spinner)

Using exp3 curve gives finer control at lower speeds.

Multiple Controllers → One Synth

o2p(path:freqSlider, trig:touch, uid:fSlider)
o2p(path:ampSlider, trig:touch, uid:aSlider)
synth(uid:lead, freq: follow(fSlider.t, 100, 1000), amp: follow(aSlider.t, 0, 0.3))

One Controller → Multiple Synths

o2p(path:masterFader, trig:touch, uid:master)
synth(uid:bass, freq: follow(master.t, 50, 200), amp:0.2)
synth(uid:mid, freq: follow(master.t, 200, 800), amp:0.15)
synth(uid:high, freq: follow(master.t, 800, 4000), amp:0.1)

Fader with Default Position

o2p(path:volumeFader, trig:touch, uid:vol)
synth(uid:pad, amp: follow(vol.t, 0, 1, 0.5))

Fader starts at 0.5 (middle). Without -0.5, the fader position is restored from presets/localStorage.


Limitations & Notes

  1. Signal Publishing Rate: Signals are published at ~60fps to avoid overwhelming the system.

  2. Binding Lifecycle: Bindings are automatically cleaned up when the cue stops (if you call the unbind functions).

  3. No Circular Dependencies: The system doesn't prevent circular signal routing. Avoid creating feedback loops unless intentional.

  4. Static Parameters: Some parameters can't be changed after cue start (e.g., wave type, audio src). These will use the initial value even if bound.

  5. Signal Range: All signals are assumed to be in the 0-1 range. Use the -min-max syntax to map to your desired output range.

  6. Default Values & Presets: When no default is specified in the signal ref, the fader's initial position is controlled by presets or localStorage. This allows the same score to have different initial states. When a default IS specified, the fader will always start at that position.

  7. Curve Inversion: When setting a default value with a curve (e.g., exp3), the fader position is calculated by inverting the curve so that the output equals the default value.


Summary Checklist

To Make an Animation Publish Signals:

  1. ✅ Import publish from control/paramBinding.js
  2. ✅ Add publish("type", uid, { channel: value }) to update callback
  3. ✅ Done!

To Make a Cue Accept Signal Bindings:

  1. ✅ Import bindParam from control/paramBinding.js
  2. ✅ Wrap parameter initialization with bindParam()
  3. ✅ Store unbind functions for cleanup
  4. ✅ Call unbind functions when cue stops
  5. ✅ Update parser to recognize signal refs (one-time)

Version History

Tip: use ← → or ↑ ↓ to navigate the docs