GitHub - PADL/SwiftOCA: Swift AES70/OCA client/server · GitHub
Skip to content
PADLPublic

About

Swift AES70/OCA client/server

Topics

Resources

Stars

26 stars

Watchers

6 watching

Forks

Repository files navigation

SwiftOCA

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:

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.

Platform TCP UDP WS client WS server Local Mach
macOS ✅ ✅ ✅ ✅ ✅ ✅
iOS ✅ ✅ ✅ ✅ ✅ ❌
Linux ✅ ✅ ❌ ✅ ✅ ❌
Windows ✅ ❌ ❌ ✅ ❌ ❌

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.

Features

Controller (SwiftOCA)

  • Device discovery: OcaConnectionBroker discovers AES70 devices via DNS-SD/Bonjour (using NetServiceBrowser on Apple platforms, or libdns_sd on Linux), with support for TCP, UDP, and WebSocket service types. Devices can also be registered manually for direct connection without DNS-SD.
  • WebSocket transport: OcaFlyingFoxConnection provides client-side WebSocket connectivity on Apple platforms using URLSessionWebSocketTask.
  • 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._tcp and _ocajsonws._tcp services. See Documentation/OCP2.md.
  • Mach port transport: Ocp1MachPortConnection provides fast local IPC between processes on macOS using Mach ports.
  • Property observation: @OcaProperty and @OcaBoundedProperty wrappers expose property changes as AsyncSequence streams, 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.

Device (SwiftOCADevice)

  • 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: OcaBlock and OcaMatrix for organizing objects into hierarchical or grid-based topologies.
  • JSON serialization/deserialization: persist and restore device state via serialize/deserialize and 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: .ocp2 to the TCP or WebSocket endpoint to serve JSON controllers; they advertise the _ocajson service 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.

SwiftOCAUI

  • Automatic view dispatch: the OcaView protocol and OcaDetailView select 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.

SwiftOCAXMI

  • Class model import: OcaXMIModel reads 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: OcaXMIExport writes descriptors back as XMI in the same shape, which OcaXMIModel reads 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.xmi on an OcaFlyingFoxDeviceEndpoint and 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 .local added when it has no domain; the port is the endpoint's, once it is listening.

Examples

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.

OCABrowser

Connecting to a device

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.

Walking the device tree

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)")
}

Observing property changes

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")
}

Hosting a device

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()

Talking to a device with nc

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.

Discovering devices with OcaConnectionBroker

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
  }
}

License

Apache License 2.0. See LICENSE.

Luke Howard lukeh@lukktone.com

About

Swift AES70/OCA client/server

Topics

Resources

Stars

26 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages