Ollin

OSCQuery

OSCQuery is how a control app finds out what a sketch takes without anyone typing an address. The sketch publishes its @Param parameters as a tree, and an app that speaks the protocol (TouchOSC, Chataigne, ossia score, VDMX, Vezér) reads the tree, builds a control for each parameter with the right range, and sends values back over plain OSC. One extension does all of it. The support lives in OllinOSC, so add import OllinOSC beside import Ollin to reach it.

import Ollin
import OllinOSC

final class Wall: Sketch {
    @Param(20...400, group: "Shape") var radius = 120.0
    @Param(group: "Shape") var spin = true
    @Param var tint = Color.orange

    override func setup() {
        extend(OSCQueryServer())
    }

    override func draw() {
        background(.black)
        // radius, spin, and tint are live from any app that found the sketch
    }
}

When the sketch launches it prints one line, OSCQuery: http://your-mac.local:9000 (OSC on port 9000). An app on the same network lists the sketch by name; a browser at that address shows the tree.

Contents

Serving the namespace

OSCQueryServer(port: Int = 9000, name: String? = nil)
OSCQueryServer(receiver: OSCReceiver, port: Int? = nil, name: String? = nil)
var receiver: OSCReceiver
var name: String?
var boundPort: Int?
var url: String?
var advertises: Bool
var isRunning: Bool
var unavailableReason: String?
func namespace() -> OSCQueryNode
func start()
func stop()

isRunning is true while the listener is up. When it is not, unavailableReason says why in a sentence worth drawing: a port another program holds, a bind the system refused, the OSC port that would not open. It follows the listener's own state, so it says so the moment the system does, and it clears when the listener comes up. start() serves again after stop(), or tries the port again after a start that failed; setup calls it for you the first time.

Register an OSCQueryServer with extend(...) in setup(). It discovers the sketch's parameters and opens two ports at one number. The namespace answers over HTTP on TCP, and the values arrive over OSC on UDP, at the same number. Both are advertised on the local network under the sketch's type name, or the name you give it. The service types are _oscjson._tcp and _osc._udp, so an app that browses for either finds the sketch. Pass port: 0 to let the system pick, and read the numbers back from boundPort and receiver.boundPort.

The second initializer serves the namespace beside a receiver the sketch already reads. Its own messages() and number(_:) keep working for any address outside the tree, and the parameters take theirs. The namespace then listens on the receiver's port number unless you name another.

Values land on the main thread between frames, through the same control the inspector's own drag uses. A parameter's smoothing and clamping apply exactly as they do there. The tree the server hands out is rebuilt every quarter second, so a client that asks again sees what the sketch changed on its own.

Anyone on the network who has the address can move the parameters while the server is up. Treat it as a studio and venue tool. advertises = false keeps the ports open and the name to yourself, and stop() closes both; a reload that builds a fresh sketch releases them on its own.

What a client sees

A parameter becomes a node at /name, or at /Group/name when it declares a group. The group's container is named after it. The characters an OSC address cannot carry, a space and the reserved #, *, ?, [, ], {, and }, become one underscore each, so a group called Look & Feel serves under /Look_Feel. The container's DESCRIPTION still says the real name. Every leaf carries the protocol's core attributes. FULL_PATH is its address, TYPE the OSC type tags of what it takes, ACCESS is 3 for readable and writable, VALUE the current value, RANGE its bounds, and DESCRIPTION the parameter's label. What each kind becomes:

ParameterTYPEVALUERANGE
Double (and Tempo)fthe numberMIN and MAX from the declared range
Intithe numberMIN and MAX
BoolTtrue or falsenone
an enum or a ParamChoices catalogsthe selected option's labelVALS, the option labels in order
Colorr#RRGGBBAAnone
Vector2ffx, yone MIN/MAX pair each
Vector3fffx, y, zone pair each
Rectangleffffx, y, width, heightone pair each
Insetsfffftop, right, bottom, leftthe edge range, four times
ClosedRange<Double>fflower, upperthe outer range, twice
Stringsthe textnone
Palette or Rampa containerone r child per color: /inks/0, /inks/1, and so onnone
a list of numbers ([Double], [Int])a containerone f child per bar: /accents/0, /accents/1, and so oneach child's MIN and MAX
ResponseCurvea containerone ff child per point, its place and its height0 to 1, twice

A strip of colors is served as a container of color methods rather than one node. A client that knows nothing about palettes still gets a color well per stop. Writing a child replaces that one color and leaves the stop where it sits. A list and a curve are served the same way, so a fader per bar drives a step pattern and an XY pad per point shapes a curve. A point sent past a neighbor stops just short of it, as a drag in the graph does. A hidden parameter, one whose show rule currently fails, stays in the tree and keeps taking values, as it keeps following an OSC binding.

?HOST_INFO answers with the server's NAME, the EXTENSIONS it speaks (ACCESS, VALUE, RANGE, and DESCRIPTION; the WebSocket LISTEN stream is not offered, so a client polls for changes), and OSC_PORT with OSC_TRANSPORT set to UDP. In code that answer is an OSCQueryHostInfo, with name, extensions, oscPort, and oscTransport.

Writing a value

A client sets a parameter by sending an OSC message to the address the tree names, on the OSC port, with arguments of the node's type. Values travel in the parameter's own units, the range the tree advertised. So /Shape/radius 240.0 sets the radius to 240, and 1000 is clamped to the top of the range. The server reads generously. A number arrives as any numeric tag, an i for an f or the other way round. A menu takes its option's label as a string or its index as a number. A toggle takes T, F, or a number, where nonzero is on. A color takes the r type or a hex string, #RRGGBB or #RRGGBBAA. A message with too few arguments, or one whose kind does not fit, is ignored. The OSC 1.1 r type is new in OSCArgument for this, as .color(Color), four bytes on the wire.

  /Shape/radius   240.0            a Double
  /Shape/count    12               an Int
  /Shape/style    "Rings"          a menu, by label (or 1, by index)
  /Motion/spin    F                a Bool
  /Motion/pivot   0.25 0.75        a Vector2
  /Color/tint     r:#3399FFFF      a Color, or "#3399FF"
  /Color/inks/1   r:#FFFFFFFF      one color of a palette

Reading the tree yourself

The whole tree is the JSON at the root address, and any node answers alone at its path. Add ?VALUE, ?TYPE, ?RANGE, ?ACCESS, ?DESCRIPTION, or ?CONTENTS to read one attribute. A node without that attribute answers 204. An attribute the server does not speak answers 400, and a path that leads nowhere answers 404.

curl http://localhost:9000/                        # the whole tree
curl http://localhost:9000/Shape/radius            # one node
curl 'http://localhost:9000/Shape/radius?VALUE'    # {"VALUE":[180]}
curl 'http://localhost:9000/?HOST_INFO'            # who serves, and the OSC port

Inside the sketch, namespace() returns the same tree as an OSCQueryNode, the value the server hands out. A node has its fullPath, type, access, description, value (an array of OSCQueryValue, one per type tag), range (an array of OSCQueryRange with min, max, or values), and contents when it is a container, which isContainer answers. node(at:) walks to a path and methods lists every leaf in address order. OSCQueryNode is Codable under the protocol's own attribute names, so it also decodes what another OSCQuery server publishes.

Trying it

The OSCQuery example (Examples/Integration/OSCQuery) serves a ring of marks with parameters in three groups. It draws the namespace as served down its left column, so the picture and the listing are the same thing seen twice. Run it, open the address it prints in a browser, and change a value with any OSC sender:

swift run --package-path Examples Example-Integration-OSCQuery
curl 'http://localhost:9000/Shape/radius?VALUE'

With TouchOSC on a phone on the same Wi-Fi, open the connection settings and choose OSCQuery. The sketch appears in the browse list. Pick it and TouchOSC builds a page of controls from the tree. Chataigne and ossia score find it the same way, as an OSCQuery server on the network. Any app that speaks only plain OSC still works: point it at the OSC port and send to the addresses the tree names.


See the OSCQuery example for a sketch that publishes its parameters and draws the tree, and the OSC page for the messages underneath.

↑↓ move ↩ open