SwiftOCA is a pure Swift implementation of the AES70 control protocol, principally used for remote control of professional audio devices.
The package consists of three libraries:
- SwiftOCA: an OCA controller (client)
- SwiftOCAUI: SwiftUI views for binding to OCA classes (macOS/iOS)
- SwiftOCADevice: an OCA device (server)
All APIs are async-safe and run on macOS, iOS, Linux, and Windows: Apple platforms and Windows use FlyingFox for socket I/O, while Linux uses IORingSwift.
The WebSocket client (WS client) uses Apple's URLSessionWebSocketTask and is therefore available on Apple platforms only. On Windows, datagram (UDP) endpoints are unavailable because FlyingSocks does not implement the sendmsg-based message API there, and Unix-domain (Local) sockets are not yet supported. Mach port transport is macOS-only: it depends on the bootstrap_* service APIs, which are unavailable to sandboxed apps and are rejected by App Store binary validation, so it is compiled out on iOS (including Mac Catalyst) and the other embedded Apple platforms.
- Device discovery:
OcaConnectionBrokerdiscovers AES70 devices via DNS-SD/Bonjour (usingNetServiceBrowseron Apple platforms, orlibdns_sdon Linux), with support for TCP, UDP, and WebSocket service types. Devices can also be registered manually for direct connection without DNS-SD. - WebSocket transport:
OcaFlyingFoxConnectionprovides client-side WebSocket connectivity on Apple platforms usingURLSessionWebSocketTask. - OCP.2 (AES70-4): the JSON protocol is a per-connection option (
OcaConnectionOptions(controlProtocol: .ocp2)) over TCP, WebSocket and the local loopback; the broker discovers_ocajson._tcpand_ocajsonws._tcpservices. See Documentation/OCP2.md. - Mach port transport:
Ocp1MachPortConnectionprovides fast local IPC between processes on macOS using Mach ports. - Property observation:
@OcaPropertyand@OcaBoundedPropertywrappers expose property changes asAsyncSequencestreams, enabling reactive UI updates. - JSON serialization: read the full state of any remote object or block tree as a JSON-compatible dictionary via
jsonObject. - Automatic reconnection: optionally reconnect when a connection drops or a device's IP address changes via mDNS, with configurable options for subscription refresh and object cache retention.
- Full AES70 device implementation: host actuators, sensors, blocks, matrices, managers, and agents.
@OcaDeviceProperty: property wrapper that manages local state and notifies connected controllers on changes.- Block and matrix containers:
OcaBlockandOcaMatrixfor organizing objects into hierarchical or grid-based topologies. - JSON serialization/deserialization: persist and restore device state via
serialize/deserializeand the parameter dataset API. - Multiple transport endpoints: run TCP, UDP, WebSocket, Unix domain socket, and Mach port (macOS) endpoints concurrently.
- OCP.2 (AES70-4) endpoints: pass
controlProtocol: .ocp2to the TCP or WebSocket endpoint to serve JSON controllers; they advertise the_ocajsonservice types. - TLS-secured TCP (
ocasec): PSK (AES70 baseline) and X.509 certificate credentials on both Apple and Linux, with optional mTLS and TLS 1.3 external PSK. See Documentation/TLS.md.
- Automatic view dispatch: the
OcaViewprotocol andOcaDetailViewselect specialized views based on OCA class type. - Pre-built actuator views: gain slider (log-scaled), mute toggle, polarity switch, pan/balance knob, boolean and float actuators.
- Sensor views: level meter with PPM ballistics (color-coded bar graph), identification sensor, and generic sensor displays.
- Block navigation: drill-down sidebar for hierarchical blocks; grid layout for leaf blocks; matrix navigation support.
- Bonjour discovery view: ready-made device browser view for listing and connecting to discovered devices.
- Class model import:
OcaXMIModelreads the AES70-2 class model's XMI and describes its control classes and datatypes with the class manager's descriptors, so a device's own descriptions can be checked against the model. - Class model export:
OcaXMIExportwrites descriptors back as XMI in the same shape, whichOcaXMIModelreads back as written. A UML tool can read its UML section; it is not an Enterprise Architect project. - Serving the model (
SwiftOCAXMIDevice):classManager.serveModel(on: endpoint)serves the device's class model at/aes70/model.xmion anOcaFlyingFoxDeviceEndpointand sets the class manager's ModelURL to its absolute URL,http://<host>:<port>/aes70/model.xmi. The host defaults to the system's host name, with.localadded when it has no domain; the port is the endpoint's, once it is listening.
Sample code can be found in Examples:
- OCADevice — a sample AES70 device with a gain control, boolean actuator matrix, and multiple transport endpoints.
- OCABrowser — a macOS SwiftUI app that discovers devices via Bonjour and provides a navigable block browser with specialized control views.
- OCABrokerTest — a command-line tool that discovers devices and auto-connects as they appear.
- Scripts/ocp2-nc.sh — a shell script that drives an OCP.2 (AES70-4) device with
nc, for poking at one without a controller.
ocacli is a command-line OCA controller that is implemented using SwiftOCA. SwiftOCA is also compatible with third-party OCA controllers such as AES70Explorer.
A Flutter wrapper is available here.
A connection can be constructed from a hostname (or IP literal) and port. The name
is resolved on each connect attempt — getaddrinfo for the socket backends, or
natively by Network.framework (with Happy Eyeballs across the A/AAAA records) for
OcaNWConnection — so a device whose address changes, or that is not yet
reachable, is picked up automatically by the reconnection machinery:
import SwiftOCA
let connection = try await OcaTCPConnection(
host: "mixer.local",
port: 65000,
options: OcaConnectionOptions(flags: [.automaticReconnect])
)
try await connection.connect()Alternatively, construct a connection from a resolved socket address (deviceAddress)
when you already have one — for example from an OcaConnectionBroker discovery event.
Given a connected connection, recursively print the role path of every object:
for actionObject in try await connection.rootBlock.resolveActionObjectsRecursive()
.compactMap({ $0.memberObject as? OcaOwnable }) {
try? await print("- \(actionObject.rolePathString)")
}Subscribe to a gain property and react to changes:
let gain = try await connection.resolve(object: OcaGain.self, objectNumber: gainONo)
for try await value in gain.$gain {
print("gain changed to \(value) dB")
}Create an AES70 device with a gain control and serve it over TCP:
import SwiftOCADevice
let device = OcaDevice.shared
try await device.initializeDefaultObjects()
let gain = try await OcaGain(
objectNumber: 10020,
role: "Main Gain",
deviceDelegate: device
)
let endpoint = try await OcaFlyingSocksStreamDeviceEndpoint(address: listenAddress)
try await endpoint.run()An OCP.2 device frames one JSON PDU per line, so a single nc is enough to poke
at one — no client library, no framing to get right. Against the sample device's
OCP.2 stream endpoint (port 65003):
$ echo '{"ProtocolVersion":1,"Commands":[{"Handle":1,"TargetONo":1,"MethodID":[3,4]}]}' \
| nc -w 2 localhost 65003
{"ProtocolVersion":1,"Responses":[{"StatusCode":"OK","Parameters":{"Name":"OCA Test"},"Handle":1}]}TargetONo 1 is OcaDeviceManager, MethodID is [DefLevel, Index] (3.4 is
GetDeviceName), and Parameters are keyed by their AES70-2A names.
Examples/Scripts/ocp2-nc.sh goes further: several
commands in one PDU, CommandNRs, keep-alives, and an EV2 subscription that
receives a notification. The WebSocket endpoint negotiates the AES70-OCP.2
subprotocol, so reach that one with websocat rather than nc.
Use OcaConnectionBroker to discover devices on the network and connect automatically:
import SwiftOCA
let broker = await OcaConnectionBroker(
connectionOptions: OcaConnectionOptions(flags: [
.automaticReconnect,
.refreshSubscriptionsOnReconnection,
])
)
for try await event in await broker.events {
switch event.eventType {
case .deviceAdded:
try await broker.connect(device: event.deviceIdentifier)
print("connected to \(event.deviceIdentifier.name)")
case .deviceRemoved:
print("lost \(event.deviceIdentifier.name)")
default:
break
}
}Apache License 2.0. See LICENSE.
Luke Howard lukeh@lukktone.com

