Room
A room lets several machines draw one work together. Two Macs on the same network find each other by the room's name alone. There is no server to run, no address to type, and nothing to configure. Values, @Param parameters, and one agreed clock travel between them. That is what makes a row of screens one work rather than several copies of it. Rooms live in a separate library, so the drawing core stays free of MultipeerConnectivity. Add import OllinRoom alongside import Ollin to reach it.
import Ollin
import OllinRoom
final class Wall: Sketch {
let room = Room(named: "wall")
override func setup() {
extend(room)
}
override func draw() {
background(.black)
// The room's clock, so every machine turns together.
let angle = room.time
drawCircle(540 + cos(angle) * 300, 540 + sin(angle) * 300, 60)
}
}
Open that sketch on a second Mac and the two are in the same room. It still runs on a single machine, which is then the room's only seat.
Contents
- Joining a room - the name, the seat, the passcode
- Sending and reading values - the three ways to read, as with OSC and MIDI
- Sharing parameters - one person adjusts a parameter for the whole room
- The clock everyone agrees on - why
room.timeand nottime - Splitting one work across screens - seats
- The network story - who can join, and what the system asks
- Trying it - the two examples
Joining a room
let room = Room(named: "wall") // the name is the whole address
let room = Room(named: "wall", seat: 1) // this machine draws slice 1
let room = Room(named: "wall", as: "left projector") // a name people can read
let room = Room(named: "gallery", passcode: "cempoalli")
extend(room) opens the room, and that is all a sketch needs. You can also open a room by hand with start() and close it with stop(). A sketch does that when it trades values but shares no parameters. A room closed by stop() leaves the others at once. So does a room dropped when a live reload builds a fresh sketch.
Who is here:
room.peerName // what this machine goes by
room.peers // the others, by name
room.everyone // all of them, this machine included
room.isAlone // nobody else yet
room.sketchName(of: peer) // what that machine is running
room.lastError // what went wrong, when something did
Arrivals and departures drain as events, so a sketch can respond to them:
for machine in room.arrivals() { print("\(machine) joined") }
for machine in room.departures() { print("\(machine) left") }
Sending and reading values
Send anything you want the others to know under a key:
room.send("beat", 1.0) // a number
room.send("count", 7) // a whole number
room.send("word", "hola") // text
room.send("on", true) // a flag
room.send("at", Vector2(400, 200)) // a point
room.send("ink", Color.red) // a color
room.send("raw", data) // bytes, for anything else
You read those values in the same three ways OSC and MIDI offer.
The latest value, for anything continuous:
let beat = room.number("beat", default: 0)
let pointer = room.point("at", default: Vector2(0, 0))
Everything that arrived, for events where each one counts:
for message in room.messages() {
if message.key == "note" { play(message.int ?? 0) }
}
int rounds a number toward zero, and reads nil for one that is not finite or does not fit in an Int.
Bound to a parameter, so another machine drives a @Param the way an external fader does:
room.bind("dial", to: $radius) // 0...1 into the parameter's own range
room.bind("dial", to: $radius, from: 0...127) // or another range
Send a value the quick way when you send it every frame, because the next value matters more than one that goes missing:
room.send("pointer", mouse, reliable: false)
Sharing parameters
A shared parameter lets you tune the whole room instead of one machine:
override func setup() {
extend(room)
room.share("speed", "hue") // these parameters travel
room.shareAll() // or every @Param on the sketch
}
Every machine that shares a parameter both sends and follows it, so you can set it from whichever machine you stand at. If two people adjust one parameter at the same moment, the room's clock settles it and the later change wins everywhere. A parameter that nobody shares stays on its own machine.
Values from another machine land on the main thread between frames, before draw(). That is where the inspector's own edits land too. They travel as the same persisted payloads, so a parameter accepts and clamps what the inspector would.
A machine that joins later receives the shared parameters as they stand. It then comes up showing the room's values rather than its own defaults.
Sharing needs extend(room), because Ollin reads and applies the parameters on the frame boundary.
The clock everyone agrees on
Each machine starts its own clock when its sketch starts. Two machines running one sketch are then out of step by the difference between their start times, and an audience sees that difference. room.time is the room's own clock, so read it for anything that moves:
let angle = room.time * speed // in step everywhere
let angle = time * speed // this machine's own clock, drifting from the rest
One machine owns the clock, and the others ask it for the time a few times a second. The owner is the machine whose name sorts first, so every machine picks the same one with no election. Each estimate is a RoomClockSample, and it allows for the time the answer spent on the wire, which it carries as roundTrip. Ollin believes the quickest of several answers, because a slow answer is a delayed one.
room.ownsClock // whether this machine keeps the clock
room.clockError // how far room time can be off, in seconds, nil before the first answer
Between two sketches on one Mac, the first answer lands within a quarter second of joining. The two clocks then agree to within a tenth of a millisecond, which is the finest the measurement could see. Before that first answer, room.time is this machine's own clock and clockError is nil. A sketch that must not start early can wait for the first answer.
When the machine that owns the clock leaves, the room picks the next owner. The room keeps the time it already had, rather than starting again from the new owner's own start. Across a real handover, the measured step was a tenth of a millisecond.
Splitting one work across screens
A wall of screens draws one sketch several times, and each machine slides its own part into view:
override func draw() {
let wall = width * Double(room.seatCount)
withState {
translate(-Double(room.seat) * width, 0)
drawWholeSketch(across: wall)
}
}
room.seat counts from zero, and room.seatCount says how many slices there are. Ask for a fixed seat when the machines stand in a known order (Room(named: "wall", seat: 1)). A machine that restarts then comes back to the same slice. Ask for no seat and the room hands seats out in name order, which is enough when the slices are interchangeable.
The network story
The room name is the only thing needed to join, so anyone on the same network who knows it can join. Pass a passcode where that matters. The passcode never travels in the clear, only a hash of it does, and the connection itself is encrypted. Room is a studio and venue tool, like the remote surface. It is fine on your own Wi-Fi or a show network, and not something to leave open on a hostile one.
The machines reach each other over whatever the system has. That can be the same Wi-Fi, a cable, or the direct radio link the system sets up when there is no network at all. The first time a sketch opens a room, the system asks for permission to use the local network. A sketch run from a terminal inherits the terminal's answer, the same way screen capture does.
The name a machine goes by is its computer's name plus a few characters. Two sketches on one Mac are therefore two members rather than one. Pass as: for a name an operator can read on a screen.
Room sits over a seam called RoomTransport, whose only job is to carry bytes to a named peer. A sketch or a test that wants two rooms inside one process supplies its own transport. The loopback example does exactly that.
Trying it
The RoomCanvas example (Examples/Integration/RoomCanvas) is the full one. Beads travel along a wall as wide as the room has seats, and every parameter travels with them. A readout shows the seat, who else is in the room, and how well the clocks agree. Open it on two Macs on the same network.
swift run --package-path Examples Example-Integration-RoomCanvas
The RoomLoopback example (Examples/Integration/RoomLoopback) needs no second machine. Two rooms inside one sketch trade values over a transport that never leaves the process. The left panel sends, and the right one draws only what arrived. Hold the space bar to break the connection.
swift run --package-path Examples Example-Integration-RoomLoopback
See @Param for what a shared parameter can declare. See Remote for the other way a second device reaches a sketch, with one machine drawing and a phone tuning it.