Skip to content

Transport

PortWho listensWhat for
sclang57120SuperColliderEverything in this protocol. The engine sends here.
scsynth57110The audio serverDirect /c_set control-bus writes only. Nothing in this protocol uses it yet.
engine9000The game engineThe sc → engine half: handshake, /sample/loaded, /sample/ended, /mode/synth/step and the rest.

57120 is sclang’s default listen port, so it needs no configuration on the SuperCollider side. 9000 is a convention, not a requirement — if it collides with something in the engine, change it in schema/protocol.yaml and regenerate; both sides pick it up.

Everything is plain UDP. There is no TCP fallback and no OSC-over-anything-else.

UDP is lossy, and that shapes every message

Section titled “UDP is lossy, and that shapes every message”

No delivery guarantee, no ordering guarantee, no duplicate suppression. On a quiet wired LAN you will lose almost nothing, which is precisely the danger: the protocol has to survive the one packet a day that does go missing, and you will not be able to reproduce that packet on demand.

Two rules follow, and they are not negotiable design preferences — they are the reason the message list looks the way it does.

Continuous data is absolute, never a delta

Section titled “Continuous data is absolute, never a delta”

Send the whole arpeggiator pattern, /mode/synth/notes 0 2 4. Never send “add note 4”.

A dropped absolute value is invisible: the next change carries the truth and the state heals itself. A dropped delta is permanent: a lost “remove note” leaves a note stuck in the pattern until someone presses Reset, and the bug shows up as “the synth sometimes plays a note nobody is holding”, which is close to impossible to diagnose.

Sending /mode/start twice must be safe. Sending /audio/master/volume 0.8 five times must be indistinguishable from sending it once.

This is what makes recovery cheap. When SuperCollider restarts it asks for /sys/sync and the engine re-sends every message marked rate: state — with no bookkeeping about what the receiver already knows, because re-sending something it already has costs nothing.

Where a state change genuinely must land — loading a mode, which allocates buffers — the message declares an explicit acknowledgement rather than hoping. /mode/load is acknowledged by /mode/loaded. The engine retries until the ack arrives.

Keep every packet under 1400 bytes.

The local network’s MTU is 1500 bytes, and IP fragmentation of UDP is a reliability cliff rather than a gentle slope: if any one fragment is lost the entire datagram is discarded, so a 1600-byte packet is far more than twice as likely to go missing as an 800-byte one. 1400 leaves headroom for headers and for any VPN or tunnel that appears later.

The largest messages are /wall/config (44 bytes) and /mode/synth/notes (at most 8 ints). Nothing in 0.2.0 comes near the limit — which is deliberate: no use case needs per-hold pressure, so there is no per-hold stream. If one is added, the validator checks its size against the budget on every build.

Use an OSC bundle when several messages are only meaningful together — they arrive as one datagram, so they cannot be split or reordered.

Do not bundle unrelated messages just to save packets. A bundle is all-or-nothing: one oversized bundle loses everything inside it, where five separate packets would have lost one.

Any message that must be bundled with another says so in its bundleWith field, and the site renders it.

sclang’s OSC responders all run on the single language thread. That thread also runs your scheduling, your GUI and everything else you write, so it is the first thing to saturate.

0.2.0 has no high-rate sensor data — the busiest streams are ten shape positions a second per Chase zone and twenty objective-progress frames a second in Arcade. If continuous per-hold data is ever needed, the escalation path is:

  1. One array per frame, never one message per hold.
  2. Split the array across addresses as the wall grows toward the packet budget.
  3. Write straight to scsynth, bypassing sclang: the engine sends /c_set <busIndex> <value> to port 57110, and SynthDefs read the bus with In.kr(busIndex). Document the bus allocation in the controlBuses block of schema/protocol.yaml — an undocumented bus index is exactly the kind of magic number this project exists to prevent.