Interactive Art

OSC Tutorial: Getting Started With Open Sound Control

The protocol every tool in your installation already speaks, and nobody explains. How addresses work, why UDP matters, what to do about discovery, and the five mistakes that eat an afternoon.

Look at the tools we have covered for interactive work — TouchDesigner, Max/MSP, Isadora, QLab, Resolume, VDMX, Notch — and there is one thing all of them do that none of the tutorials covers, because it belongs to none of them.

They all speak OSC.

That is why a piece built from four programs on three machines works at all. So here is the protocol itself, which takes about twenty minutes to understand and will save you from a class of problem that is otherwise mystifying.

What it is

Open Sound Control is a message format for sending named, typed values between programs over a network. It came out of CNMAT at Berkeley in the late 1990s as an answer to MIDI’s limits, and the name is misleading in a useful way: it is not about sound. It is a general-purpose way to send structured control data, and it is used at least as much for video, lighting and sensors as for audio.

A message has two parts:

/stage/light/3/intensity   0.75
^ address pattern          ^ arguments

That is it. A URL-shaped address, and some typed arguments.

Why that beats MIDI for installation work

Four differences that matter in practice.

Resolution. A MIDI control change carries 7 bits — 128 steps. OSC arguments are typically 32-bit floats. For a fader that is the difference between audible stepping and a smooth ramp; for a position value it is the difference between a jittery object and a stable one.

Names instead of numbers. MIDI gives you channel 1, CC 74. OSC gives you /synth/filter/cutoff. Six months later, one of those is self-documenting and the other requires the spreadsheet you did not keep.

Structure. You can send several typed values in one message — /blob 1 0.23 0.81 440.0 — rather than decomposing everything into single bytes.

Networking for free. It runs over IP, so it crosses machines, rooms and buildings on the cabling you already have.

MIDI still wins on hardware ubiquity and on being a physical standard with physical connectors. For software-to-software on a network, OSC is the better tool.

Addresses, and the pattern matching nobody uses

Addresses form a hierarchy, and you should design it on purpose:

/lamp/1/brightness
/lamp/1/colour
/lamp/2/brightness
/sensor/door/open
/sensor/motion/1/active

Receivers can match with wildcards, which is the part most people never discover:

PatternMatches
/lamp/*/brightnessthe brightness of every lamp
/lamp/[12]/brightnesslamps 1 and 2 only
/lamp/{1,3,5}/brightnesslamps 1, 3 and 5
/lamp/?/brightnesssingle-character lamp ids

So one handler can catch a whole family of addresses. Support varies by implementation — test it in your actual tools before designing around it, because plenty of libraries implement literal matching only.

Types

The common type tags:

TagType
i32-bit integer
f32-bit float
sstring
bblob (raw bytes)
T / Ftrue / false
Nnil

The type matters. A receiver expecting a float will often ignore an integer entirely, and you will see nothing with no error anywhere. Sending 1 where 1.0 was wanted is the single most common OSC bug, and it is silent. If a message is not arriving, check the types before you check the network.

It is UDP, and that has consequences

OSC is almost always carried over UDP, and this is the design decision you need to understand rather than work around.

UDP does not guarantee delivery, does not guarantee order, and does not retransmit. In exchange it has no handshake and no head-of-line blocking, so it is fast and never stalls.

For continuous values that is exactly right. If you are sending a fader position 60 times a second, a dropped packet is irrelevant — another one is 16 milliseconds behind it, and you would rather have the new value than a retransmission of the old one.

For events, it is a trap. “Start the show”, “fire cue 12”, “the door opened” — if that packet is lost, nothing tells you. Three defences:

  1. Repeat the message a few times with a tag so the receiver can ignore duplicates.
  2. Make it a state, not an event. Send /show/state running continuously rather than /show/start once. State is self-healing; events are not.
  3. Use TCP if your tools support it, accepting the latency and stall risk.

The general principle: use OSC for the things that repeat, and be defensive about the things that happen once.

Getting two things talking

The minimum you need: the receiver’s IP address and the port it is listening on, agreed between both ends. There is no discovery in the protocol itself — nothing finds anything automatically.

Python, using python-osc:

# send
from pythonosc.udp_client import SimpleUDPClient

client = SimpleUDPClient("192.168.1.42", 9000)
client.send_message("/lamp/1/brightness", 0.75)   # float, deliberately
client.send_message("/blob", [1, 0.23, 0.81, 440.0])
# receive
from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_server import BlockingOSCUDPServer

def on_brightness(address, *args):
    print(address, args)

dispatcher = Dispatcher()
dispatcher.map("/lamp/*/brightness", on_brightness)
dispatcher.set_default_handler(lambda addr, *a: print("UNMATCHED", addr, a))

server = BlockingOSCUDPServer(("0.0.0.0", 9000), dispatcher)
server.serve_forever()

Two details in there are worth copying into every receiver you write. Binding to 0.0.0.0 listens on all interfaces, rather than only loopback — if you bind 127.0.0.1 you will never hear another machine. And setting a default handler that prints unmatched addresses turns “nothing is happening” into a readable log, which is the difference between a two-minute fix and an hour.

The five things that go wrong

In rough order of how often they waste an afternoon:

1. Firewall. macOS and Windows both block inbound UDP by default. Nothing arrives, nothing is logged, both programs look fine. Test on one machine over loopback first to prove your addresses and types, then go across the network, so you are only ever debugging one layer at a time.

2. Type mismatch. Integer sent, float expected, message silently dropped. See above.

3. Port already in use. Two programs cannot listen on the same port on the same machine. If you opened the project twice, or a previous run did not exit, the second receiver binds and hears nothing — or fails in a way you did not notice.

4. Wrong subnet. 192.168.1.x cannot reach 10.0.0.x without routing. Check that both machines are actually on the same network, not merely both “on WiFi”.

5. Broadcast assumptions. Sending to a broadcast address (192.168.1.255) reaches everything on the subnet, which is convenient in a studio and a bad idea in a venue — you will also reach equipment you do not own. Unicast to known addresses for anything you ship.

Design advice for a real piece

Write the address scheme down before you build anything. A short document listing every address, its types and its range. It is the interface contract between your programs, and it is the thing that lets someone else — or you, next year — work out what talks to what.

Namespace by subsystem, not by machine. /lighting/... and /audio/... survive you moving a program to a different computer; /macbook/... does not.

Send state continuously where you can afford it. A receiver that joins late, restarts, or drops a packet converges on its own. This single habit removes most of the fragility people blame on OSC.

Log everything in one place during development. A small receiver that binds a port and prints every message it sees is twenty lines and will tell you more than any tool’s built-in monitor.

Sources