Oscilla Control Plane — Architecture & Usage Guide
synth(uid:pad, freq: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:slider1.t│
└─────────────────┘
Quick Start
1. Create a Controller (O2P Fader)
o2p(path:faderTrack, trig:touch, uid:myFader)
This fader automatically publishes:
o2p:myFader.t— position along path (0-1)o2p:myFader.x— normalized X position (0-1)o2p:myFader.y— normalized Y position (0-1)o2p:myFader.angle— tangent angle in degrees
2. Bind a Synth Parameter
synth(uid:pad, freq:myFader.t-200-800, amp:0.2)
The freq:myFader.t-200-800 syntax means:
- Subscribe to signal
o2p:myFader.t - Map the 0-1 value to 200-800 Hz (linear)
3. Result
Moving the fader changes the synth's frequency in real-time!
Signal Reference Syntax
Signal references use hyphen-separated segments to specify range, default, and curve options.
Basic Binding
param:source.channel
- param — The parameter to control (freq, amp, pan, etc.)
- source — The uid of the publishing cue
- channel — Which signal to subscribe to (t, x, y, angle, etc.)
Binding with Range
param:source.channel-min-max
Maps the signal (assumed 0-1) to the specified output range using linear interpolation.
Binding with Range and Curve
param:source.channel-min-max-curve
Maps the signal to the output range using a non-linear curve. Available curves:
| 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 |
sqrt |
Square root — faster at start |
Binding with Default Value
param:source.channel-min-max-default
param:source.channel-min-max-default-curve
When a default is specified, the fader will initialize to the position that produces that output value. When no default is specified, presets and localStorage control the initial state.
Syntax Summary
| Format | Example | Description |
|---|---|---|
source.channel |
fader.t |
Basic binding (0-1) |
source.channel-min-max |
fader.t-200-2000 |
With range, linear |
source.channel-min-max-curve |
fader.t-0-4800-exp3 |
With range and curve (no default) |
source.channel-min-max-default |
fader.t-200-2000-440 |
With range and default |
source.channel-min-max-default-curve |
fader.t-0-4800-60-exp3 |
Full syntax |
Examples
| DSL | Meaning |
|---|---|
freq:slider.t |
Freq follows slider position (0-1) |
freq:slider.t-200-2000 |
Freq mapped to 200-2000 Hz (linear) |
freq:slider.t-200-2000-440 |
Freq 200-2000 Hz, starts at 440 Hz |
rotspeed:knob.t-0-4800-exp3 |
Rotation speed with cubic curve |
rotspeed:knob.t-0-4800-60-exp3 |
Rotation speed, starts at 60°/s, cubic curve |
amp:fader.y-0-0.5 |
Amplitude mapped to 0-0.5 |
pan:knob.x--1-1 |
Pan mapped to -1 to 1 (note double hyphen for negative min) |
dur:speed.t-120-1 |
Duration 120s (left) to 1s (right) — inverted for speed control |
Published Signals by Cue Type
O2P Animation
| Signal | Description | Range |
|---|---|---|
o2p:{uid}.t |
Position along path | 0-1 |
o2p:{uid}.x |
Normalized X in frame | 0-1 |
o2p:{uid}.y |
Normalized Y in frame | 0-1 |
o2p:{uid}.angle |
Tangent angle | degrees |
Rotate Animation
| Signal | Description | Range |
|---|---|---|
rotate:{uid}.angle |
Current angle | 0-360 |
rotate:{uid}.rad |
Angle in radians | 0-2π |
rotate:{uid}.norm |
Normalized angle | 0-1 |
Scale Animation
| Signal | Description | Range |
|---|---|---|
scale:{uid}.sx |
Scale X factor | varies |
scale:{uid}.sy |
Scale Y factor | varies |
scale:{uid}.uniform |
Average scale | varies |
Bindable Parameters
Synth
| Parameter | Default Range | Description |
|---|---|---|
freq / frequency |
20-20000 | Oscillator frequency (Hz) |
amp / amplitude |
0-1 | Output amplitude |
pan |
-1 to 1 | Stereo position |
cutoff / filterFreq |
20-20000 | Filter cutoff (Hz) |
q / resonance |
0.1-30 | Filter resonance |
detune |
-1200 to 1200 | Detune in cents |
Audio / AudioPool / AudioImpulse
| Parameter | Default Range | Description |
|---|---|---|
amp |
0-1 | Playback amplitude |
pan |
-1 to 1 | Stereo position |
pitch / rate |
0.25-4 | Playback rate |
Effects (within synth)
| Parameter | Default Range | Description |
|---|---|---|
delayTime |
0-2 | Delay time (seconds) |
feedback / fb |
0-0.99 | Delay feedback |
mix / wet |
0-1 | Effect mix level |
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: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:xy.x-200-2000, amp:xy.y-0-0.5)
Rotation → Filter
rotate(dur:4, loop:0, uid:wheel)
synth(uid:drone, freq:220, cutoff:wheel.norm-200-4000)
Speed Control with Curve
o2p(path:speedFader, trig:touch, uid:speed)
rotate(dur:speed.t-120-1, rotspeed: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:fSlider.t-100-1000, amp:aSlider.t-0-0.3)
One Controller → Multiple Synths
o2p(path:masterFader, trig:touch, uid:master)
synth(uid:bass, freq:master.t-50-200, amp:0.2)
synth(uid:mid, freq:master.t-200-800, amp:0.15)
synth(uid:high, freq:master.t-800-4000, amp:0.1)
Fader with Default Position
o2p(path:volumeFader, trig:touch, uid:vol)
synth(uid:pad, amp: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
-
Signal Publishing Rate: Signals are published at ~60fps to avoid overwhelming the system.
-
Binding Lifecycle: Bindings are automatically cleaned up when the cue stops (if you call the unbind functions).
-
No Circular Dependencies: The system doesn't prevent circular signal routing. Avoid creating feedback loops unless intentional.
-
Static Parameters: Some parameters can't be changed after cue start (e.g.,
wavetype, audiosrc). These will use the initial value even if bound. -
Signal Range: All signals are assumed to be in the 0-1 range. Use the
-min-maxsyntax to map to your desired output range. -
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.
-
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:
- ✅ Import
publishfromcontrol/paramBinding.js - ✅ Add
publish("type", uid, { channel: value })to update callback - ✅ Done!
To Make a Cue Accept Signal Bindings:
- ✅ Import
bindParamfromcontrol/paramBinding.js - ✅ Wrap parameter initialization with
bindParam() - ✅ Store unbind functions for cleanup
- ✅ Call unbind functions when cue stops
- ✅ Update parser to recognize signal refs (one-time)
Version History
-
v1.1 — Extended signal reference syntax
- Hyphen-separated syntax:
source.channel-min-max-default-curve - Non-linear curves:
exp2,exp3,exp4,log,sqrt - Optional default values (presets/localStorage control initial state when omitted)
- Fader default positioning respects curve for correct output mapping
- Hyphen-separated syntax:
-
v1.0 — Initial control plane implementation
- ParamBus signal store
- bindParam/publish helpers
- Signal reference syntax support
Tip: use ← → or ↑ ↓ to navigate the docs