Oscilla OSC System — Developer Guide
What is OSC in Oscilla?
OSC (Open Sound Control) is how Oscilla talks to external audio engines — SuperCollider, Pure Data, Max/MSP, etc. The browser client doesn't produce sound directly. Instead, cues in the SVG score generate OSC messages that travel:
SVG cue → oscillaOSCClient → WebSocket → Node server → UDP OSC → audio engine
Incoming control messages (e.g. from a hardware controller routed through the server) travel the reverse path.
Architecture Overview
┌─────────────────────────────────────────────────────┐
│ Cue modules (osc, oscCtrl, audio, synth, etc.) │
│ Each calls sendOSC() when the playhead or user │
│ triggers them │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ oscillaOSCClient.js (system/oscillaOSCClient.js) │
│ ─ Single gateway for ALL outbound OSC │
│ ─ Mute control with UI sync │
│ ─ Incoming dispatch + handler registration │
│ ─ Address normalisation utilities │
│ ─ window.oscillaOSC for console debugging │
└──────────┬──────────────────────┬───────────────────┘
│ outbound │ inbound
▼ │
┌──────────────────┐ │
│ WebSocket │◄─────────────┘
│ (socket.js) │
└──────────────────┘
│
▼
┌──────────────────┐
│ Node server │──► UDP OSC to audio engine
└──────────────────┘
Key Files
| File | Location | Role |
|---|---|---|
oscillaOSCClient.js |
system/ |
Central gateway — all OSC in/out passes through here |
osc.js |
cues/ |
Discrete osc(...) cue handler + createOscOverlay() for visual HUD |
oscCtrl.js |
cues/ |
Continuous control lanes — follows SVG path shape as playhead moves |
socket.js |
system/ |
WebSocket transport — routes incoming osc_in / osc_control to dispatchOSC() |
controlRouter.js |
control/ |
Routes control values to animation targets + ParamBus |
paramBus.js |
control/ |
Global signal store with pub/sub — the nervous system for cross-cue modulation |
Cue modules that send OSC
These all import sendOSC from oscillaOSCClient.js:
audio/audioShared.js,audio/audioPool.js—osc_audio_trigger/osc_audio_stop/osc_audio_pool/osc_audio_impulsesynth.js— voice life cycle (osc_synth: start / step / update / stop)o2p.js— path position + vertex events (osc_value)rotate.js—osc_rotate;scale.js—osc_scale;fade.js—osc_fadetrans.js— waypoint progress + arrivals (osc_value)controlXY.js/controlXYPresets.js— multitouch XY pad values (osc_value)oscCtrl.js— control lanes (osc_control)osc.js,button.js— discrete events and toggle state (osc_value)cueDispatcher.js(notation branch) — note events (osc_trigger)metro.js— metronome beats (oscilla/metro, has its own 50ms throttle)
The resulting wire format (addresses and argument order) is documented for users on OSC Output.
Sending OSC
All outbound OSC goes through one function:
import { sendOSC } from "../system/oscillaOSCClient.js";
sendOSC({
type: "osc_value", // message type (server uses this for routing)
addr: "voice/pitch", // OSC address (without leading /oscilla/)
args: [0.5, 0.8], // values
timestamp: Date.now()
});
sendOSC() handles mute checking, UI preview updates, and WebSocket dispatch. You never need to touch window.socket directly for OSC.
There's also a convenience form:
import { send } from "../system/oscillaOSCClient.js";
send("voice/pitch", 0.5, 0.8);
// Equivalent to sendOSC with type "osc_value"
Receiving OSC
Incoming OSC arrives via WebSocket as osc_in or osc_control messages. socket.js hands them to oscillaOSCClient.dispatchOSC(), which does three things:
- Fires registered handlers — any module can listen for specific addresses
- Stores in ParamBus — at path
osc:<normalised-address>for modulation use - Routes through controlRouter — for
/oscilla/setand/oscilla/<uid>/<param>patterns
Registering a handler
import { onAddress } from "../system/oscillaOSCClient.js";
// Exact match
const unsub = onAddress("fader1", (args, address) => {
console.log("fader1 value:", args[0]);
});
// Wildcard (prefix match)
const unsub2 = onAddress("mixer/*", (args, address) => {
console.log(`${address}:`, args);
});
// Clean up when done
unsub();
OSC Mute
Global mute prevents all outbound OSC without stopping cue evaluation or overlays. The mute button (#osc-mute-btn) is wired in UIBindings.js and delegates to:
import { toggleMuted, setMuted, isMuted } from "../system/oscillaOSCClient.js";
toggleMuted(); // flip state
setMuted(true); // explicit
isMuted(); // query
window.oscMuted stays synced for backward compatibility. When muted, the UI preview box shows [muted] prefix but still displays what would have been sent.
ParamBus Integration
Every signal in Oscilla flows through ParamBus (control/paramBus.js). Signal paths follow the pattern:
<source>:<id>.<channel>
Examples:
o2p:sliderA.t— traversal position of an O2P animationrotate:orb1.angle— rotation angle in degreesosc:fader1— external OSC input value
Incoming OSC is automatically stored at osc:<address>. You can subscribe to any signal for cross-cue modulation:
import * as ParamBus from "../control/paramBus.js";
ParamBus.subscribe("osc:fader1", (value, path, meta) => {
// React to external fader changes
});
Control Router
controlRouter.js sits between signals and targets. It provides:
routeControl(uid, param, value)— update a target + ParamBushandleOSCIn(address, args)— route an incoming OSC message onto the bus
(publishSignal() and addModulation() were removed: nothing called them, and
their output landed in a cue: namespace that nothing subscribed to.)
The router does NOT send OSC itself — that's intentional to prevent feedback loops.
Visual Overlays
osc.js exports createOscOverlay() which provides the HUD display attached to cue elements. This is purely visual and completely independent of the sending path. Every cue module imports it separately:
import { createOscOverlay } from "./osc.js";
const overlay = createOscOverlay({
anchorEl: svgElement,
label: "/voice/pitch",
mode: "auto"
});
overlay.update("val:0.500");
overlay.position();
overlay.destroy();
Message Types
Common type values the server expects:
| type | Source | Server address |
|---|---|---|
osc_value |
osc.js, o2p.js, trans.js, controlXY.js, button.js |
/oscilla/<addr> with the client's args |
osc_trigger |
notation (cueDispatcher.js) |
addr verbatim if it starts with /, else /oscilla/<addr>; default args [1] |
osc_control |
oscCtrl.js |
/oscilla/control/<addr> v t |
osc_rotate |
rotate.js |
/oscilla/rotate/<uid> deg rad norm |
osc_scale |
scale.js |
/oscilla/scale/<uid> sx sy |
osc_fade |
fade.js |
/oscilla/fade/<uid> value |
osc_obj2path |
legacy o2p | /oscilla/o2p/<uid> nx ny na |
osc_synth |
synth.js |
/oscilla/synth/<uid> state freq amp dur wave |
oscilla/metro |
metro.js |
/oscilla/metro/<uid> beat bpm |
osc_audio_trigger / osc_audio_stop |
audio/audioShared.js |
/oscilla/audio/trigger file vol loop / /oscilla/audio/stop file fadeOut |
osc_audio_pool / osc_audio_impulse |
audio/audioPool.js |
`/oscilla/audio/pool |
Any addr given with osc_rotate, osc_scale, osc_fade, osc_synth or the audio types replaces the <type>/<uid> part. Types without a case in the server's WebSocket switch are silently dropped — add one there when introducing a new sender.
Debugging
Open the browser console and use window.oscillaOSC:
window.oscillaOSC.setDebugMode(true); // log all in/out
window.oscillaOSC.isMuted(); // check mute state
window.oscillaOSC.getLastMessage(); // inspect last sent payload
For the full signal state:
window.oscillaParamBus.snapshot("osc:"); // all incoming OSC values
window.oscillaParamBus.snapshot(); // everything
Tip: use ← → or ↑ ↓ to navigate the docs