Transport
| Port | Who listens | What for | |
|---|---|---|---|
| sclang | 57120 | SuperCollider | Everything in this protocol. The engine sends here. |
| scsynth | 57110 | The audio server | Direct /c_set control-bus writes only. Nothing in this protocol uses it yet. |
| engine | 9000 | The game engine | The 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.
State changes are idempotent
Section titled “State changes are idempotent”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.
Packet size
Section titled “Packet size”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.
Bundles
Section titled “Bundles”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.
If sensor data ever outgrows sclang
Section titled “If sensor data ever outgrows sclang”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:
- One array per frame, never one message per hold.
- Split the array across addresses as the wall grows toward the packet budget.
- Write straight to scsynth, bypassing sclang: the engine sends
/c_set <busIndex> <value>to port57110, and SynthDefs read the bus withIn.kr(busIndex). Document the bus allocation in thecontrolBusesblock ofschema/protocol.yaml— an undocumented bus index is exactly the kind of magic number this project exists to prevent.