OSC Input
Oscilla accepts incoming OSC messages on a UDP port (default 57121). Incoming values feed directly into the same ParamBus that drives all other control signals — so any parameter already bindable via a signal ref can be driven from an external system with no additional DSL.
Server Setup
The OSC input port is opened automatically when the server starts.
node server.js # listens on 57121 (default)
node server.js --osc-in 57200 # custom port
node server.js --osc-in 57200 --osc-out 57120
Two Patterns
1. Signal Ref Mode — /uid/channel value
The simplest and most flexible approach. Send a two-segment OSC address matching any signal ref uid and channel already in your score. The value lands on the agnostic ParamBus path and drives all subscriptions immediately — no DSL changes needed.
SuperCollider:
~oscilla = NetAddr("127.0.0.1", 57121);
// drive a scale animation bound to sx:myFader.t
~oscilla.sendMsg("/myFader/t", 1.5);
// drive a synth bound to freq:pitchCtrl.t-200-2000
~oscilla.sendMsg("/pitchCtrl/t", 0.75);
Score DSL — unchanged from any other control source:
scale(uid:myEl, sx:myFader.t)
synth(uid:pad, freq:pitchCtrl.t-200-2000)
The OSC address /uid/channel maps directly to the signal ref uid.channel. SC sends the value you want — no min/max mapping happens in Oscilla.
2. Direct Param Set — /oscilla/uid/param value
Target a specific registered cue parameter by uid and param name. Useful for imperative control where you want to set a known cue property directly.
// set the amplitude of audio cue "mySound"
~oscilla.sendMsg("/oscilla/mySound/amp", 0.6);
// set rotation speed of "spinner"
~oscilla.sendMsg("/oscilla/spinner/rotspeed", 90);
Alternatively, use the message-style form (address + OSC args):
~oscilla.sendMsg("/oscilla/set", "mySound", "amp", 0.6);
Triggering Animations via OSC
Any armed animation cue (rotate, scale, o2p, etc.) can be triggered by an OSC message instead of a click or playhead event. Add trig:osc and oscAddr: to the cue DSL.
rotate(dur:2, uid:myEl, init:armed, trig:osc, oscAddr:/trigger/myEl)
The element appears at ghost opacity immediately (armed, waiting). When the matching OSC message arrives, the animation starts. Subsequent messages pause and resume, matching the same state machine as click triggering.
SuperCollider:
// start / pause / resume the rotation
~oscilla.sendMsg("/trigger/myEl");
oscAddr accepts any OSC address. The argument value is ignored — the message itself is the trigger.
Signal Flow
SuperCollider / Max / PD / TouchOSC
│
│ UDP port 57121
▼
server.js oscPort.on("message")
│
│ WebSocket broadcast
▼
socket.js handleOSCInMessage()
│
├─ /uid/channel value ──────────▶ ParamBus: uid.channel
│ │
└─ /oscilla/uid/param value ────▶ routeControl(uid, param)
│
▼
signal refs, bindings,
animations, synths, audio
Note: addresses with more than two segments store under an osc: prefix
in ParamBus and are not matched by standard signal refs.
Quick Reference
| OSC Address Pattern | Effect | Score DSL |
|---|---|---|
/uid/channel |
Writes to uid.channel in ParamBus |
no changes needed |
/oscilla/uid/param |
Sets param on registered cue directly | — |
/oscilla/set uid param val |
As above, message-style | — |
| any address (trigger) | Fires animation trigger | trig:osc, oscAddr:/addr |
SuperCollider Examples
Continuous scale from a SinOsc
(
var oscilla = NetAddr("127.0.0.1", 57121);
{
var lfo = SinOsc.kr(0.3).range(0.5, 2.0);
SendReply.kr(Impulse.kr(30), "/myFader/t", lfo);
}.play;
OSCdef(\relay, { |msg|
oscilla.sendMsg(msg[2], msg[3]);
}, "/myFader/t");
)
Score: scale(uid:circle, sx:myFader.t)
Envelope-driven synth parameter
(
var oscilla = NetAddr("127.0.0.1", 57121);
{
var env = EnvGen.kr(Env.perc(0.01, 2.0), Impulse.kr(0.5));
SendReply.kr(Impulse.kr(30), "/ampCtrl/t", env);
}.play;
OSCdef(\ampRelay, { |msg|
oscilla.sendMsg(msg[2], msg[3]);
}, "/ampCtrl/t");
)
Score: synth(uid:pad, freq:440, amp:ampCtrl.t-0-0.4)
One-shot trigger from a pattern
(
var oscilla = NetAddr("127.0.0.1", 57121);
Pbind(
\dur, Pseq([1, 0.5, 1.5], inf),
\callback, Pfunc({ oscilla.sendMsg("/trigger/myEl") })
).play;
)
Score: rotate(dur:1, uid:myEl, init:armed, trig:osc, oscAddr:/trigger/myEl)
Debugging
Inspect incoming OSC values in the browser console:
// watch all OSC-driven signals landing in ParamBus
oscillaParamBus.setDebugMode(true);
// check the current value of a specific signal
oscillaParamBus.get("myFader.t");
// list all signals currently in the bus
oscillaParamBus.list();
The oscilla:osc-in window event fires for every incoming message:
window.addEventListener("oscilla:osc-in", ({ detail: { address, args } }) => {
console.log(address, args);
});
Audio Latency Compensation
When OSC drives real-time visuals that are synchronised to audio (e.g. a circle that pulses on each note attack), the visual update arrives before the sound because the audio engine (JACK, PipeWire, etc.) introduces a hardware buffer delay typically between 20 ms and 150 ms.
Oscilla compensates by delaying every incoming OSC value at the
controlRouter level before it reaches ParamBus — so all subscribers
(scale, rotate, colour, synths, etc.) are delayed automatically without
touching individual cues.
Setting global latency
In Preferences → UI → Audio Latency (ms) (range 0–300, step 5).
This sets window.audioLatencyMs which the router reads on every OSC
message. The default is 0 (no delay); set it to match your measured
audio latency.
// Console: override without reopening preferences
window.audioLatencyMs = 80;
Measure your latency: in SuperCollider Server.default.latency gives the
server scheduling latency (typically 0.05–0.2 s); your total system latency
also includes the hardware buffer: s.options.blockSize / s.sampleRate.
How it works
controlRouter.js — the single entry point for all incoming OSC — wraps
ParamBus.set() in a setTimeout(fn, window.audioLatencyMs) when the
value is non-zero:
SC sends OSC → controlRouter → [setTimeout Nms] → ParamBus.set()
│
all subscribers notified
(scale, rotate, colour…)
Because the delay is applied at the router rather than at each animation cue, every visual driven by OSC is delayed uniformly with no per-cue configuration needed.
Per-project default
Set audioLatencyMs in the project preferences.json to bake in a
project-specific default that loads automatically:
{ "audioLatencyMs": 100 }
Tip: use ← → or ↑ ↓ to navigate the docs