Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pfvernon2 committed Jul 18, 2021
1 parent e8f4271 commit ffb8232
Show file tree
Hide file tree
Showing 8 changed files with 2,200 additions and 0 deletions.
33 changes: 33 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "OSCine",
platforms: [
.macOS(.v11),
.iOS(.v14),
.tvOS(.v14)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "OSCine",
targets: ["OSCine"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "OSCine",
dependencies: []),
.testTarget(
name: "OSCineTests",
dependencies: ["OSCine"]),
]
)
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# OSCine

OSCine is a simple robust Swift implementation of the Open Sound Control protocol version 1.1 which supports UDP and TCP and includes automatic Bonjour browsing.

This package has no external dependencies. It is written for Swift 5.4. All networking and discovery is managed by the Apple Network Framework.

Future versions will likely support Swift 5.5 async operations.

## OSC

OSCine follows the OSC specification closely and uses terminology from the OSC spec heavily throughout. If you are not familar with OSC and its paradigms it is strongly suggested that you review them before proceeding:

#### Overview:
http://cnmat.org/OpenSoundControl/OSC-spec.html

#### The nitty gritty:
http://opensoundcontrol.org/spec-1_0.html
http://opensoundcontrol.org/files/2009-NIME-OSC-1.1.pdf

## Usage

As a user of this package you should expect to interact with the various classes differently based upon your use case: Client or Server.

### Client usage

OSCClientUDP/OSCClientTCP & OSCClientDelegate
OSCMessage & OSCBundle
OSCDataTypes

If you are building an OSC client. You should expect to interact primarly with with one or both of the `OSCClientUDP` and `OSCClientTCP` classes. Additionally both of these classes depend upon the `OSCClientDelegate` class for reporting network connection state change information back to the class managing the client.

To begin sending messages from your client one would typically call `client.connect()` with either a specific address and port or the name of the Bonjour service to connect to a running OSC Server.

IMPORTANT: I have reused the concept of `connect()` from the Apple Network Framework but be aware that UDP is a "connectionless" protocol and this terminology is somewhat misleading. When using a UDP client the `connect()` method simply prepares the network stack to send messages. UDP messages are sent without any validation, or even expectation, of delivery and the OSC protocol also provides no such functionality. If you require assurance of delivery (at the potential expense of slower, or even delayed, delivery) you should use the TCP protocol. If you require timely delivery, and can handle potentially loss of messages, UDP is generally more efficient.

In order to send OSC messages via your client you would expect to use the `OSCMessage` or `OSCBundle` classes. Briefly, a message is fundamental unit of information exchange in OSC and bundles are collections of messages (and other bundles.)

The `OSCMessage` class has two fundamenal properties: The `address pattern`, and the `arguments`.

The address pattern is either a literal representation of the path to which you want to send a message to a `method` or `container` on the server, or it may be a descriptive "wildcard" represenation of the path. For example `/path/to/control` or `/path/*/control`. There are a number of wildcard options which may be combined in a variety of ways.

The arguments are an ordered array of objects conforming to the `OSCDataType` protocol. OSC supports a specific set of data types. These are represted in OSCine as: `OSCInt, OSCFloat, OSCBool, OSCString, OSCBlob, OSCNull, OSCImpulse, and OSCTimeTag`

```
OSCMessage(address: "/test/mixer/*/button*",
arguments: [OSCInt(1), OSCInt(0)])
OSCBundle(timeTag: OSCTimeTag(immediate: true),
bundleElements: [message1, message2, message3, message4, message5])
```

### Server usage

OSCServerUDP/OSCServerTCP & OSCServerDelegate
OSCMethod
OSCMessage
OSCDataTypes

If you are building an OSC server. You should expect to interact primarly with with one or both of the `OSCServerUDP` and `OSCServerTCP` classes. Additionally both of these classes depend upon the `OSCServerDelegate` class for reporting network connection state change information back to the class managing the server.

To begin receiving messages one would typically call `server.listen()` with a specific port and/or the name for your Bonjour service. Note that if you are using Bonjour for your client connection discovery it is advised *not* to specify a specific port. If you do not specify a port the network stack will assign a port randomly and Bonjour will advertise said port for you. In general it is best to not specify specific ports if possible as it adds complexity to your server setup and error handling.

In order to receive OSC messages via your server you will need to implement one or more `OSCMethod` classes. To implement a method you will need to provide an `OSCAddressPattern` and a `handleMessage()` method. Your `handleMessage()` method will be called when a match for the specified `OSCAddressPattern` is received. Unlike client messages which may specify wildcards your `OSCAddressPattern` must be a valid and fully qualified OSC Address Pattern. A trivial OSCMethod implementation might look like:

```
class MyMethod: OSCMethod {
init(address: OSCAddressPattern) throws {
try super.init(addressPattern: address)
}
func handleMessage(_ message: OSCMessage, for match: OSCPatternMatchType) {
print("Received message for: \(message.addressPattern),
"match: \(match)",
"arguments: \(String(describing: message.arguments))")
}
}
```

172 changes: 172 additions & 0 deletions Sources/OSCine/Client.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//
// Client.swift
//
//
// Created by Frank Vernon on 6/30/21.
//

import Foundation
import Network

//MARK: - OSCClientDelegate

protocol OSCClientDelegate: AnyObject {
func connectionStateChange(_ state: NWConnection.State)
}

//MARK: - OSCNetworkClient Protocol

protocol OSCNetworkClient: AnyObject {
var connection: NWConnection? { get set }
var parameters: NWParameters { get set }
var serviceType: String { get set }
var browserTimer: Timer? { get set }

var delegate: OSCClientDelegate? { get set }

func connect(endpoint: NWEndpoint)
func connect(host: String, port: UInt16) throws
func connect(serviceName: String, timeout: TimeInterval?)

func disconnect()

func send(_ packetContents: OSCPacketContents, completion: @escaping (NWError?)->Swift.Void) throws
}

extension OSCNetworkClient {
func connect(endpoint: NWEndpoint) {
connection = NWConnection(to: endpoint, using: parameters)
setupConnection()
}

func connect(host: String, port: UInt16) throws {
guard let port = NWEndpoint.Port(String(port)) else {
throw OSCNetworkingError.invalidNetworkDesignation
}

connection = NWConnection(host: NWEndpoint.Host(host),
port: port,
using: parameters)
setupConnection()
}

func connect(serviceName: String, timeout: TimeInterval?) {
let browser = NWBrowser(for: .bonjour(type: serviceType, domain: nil), using: parameters)
browser.stateUpdateHandler = { [weak self] newState in
switch newState {
case .failed(let error):
self?.delegate?.connectionStateChange(.failed(error))
default:
break
}
}

browser.browseResultsChangedHandler = { [weak self] results, changes in
guard let match = results.firstMatch(serviceName: serviceName) else {
return
}

browser.cancel()
self?.connect(endpoint: match.endpoint)
}

// Start browsing and ask for updates on the main queue.
browser.start(queue: .main)

//start the browser timer if requested
if let timeout = timeout {
browserTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] timer in
browser.cancel()
self?.disconnect()
self?.delegate?.connectionStateChange(.failed(NWError.dns(DNSServiceErrorType(kDNSServiceErr_Timeout))))
}
browserTimer?.tolerance = 0.25
}
}

func disconnect() {
connection?.cancel()
}

func send(_ packetContents: OSCPacketContents, completion: @escaping (NWError?)->Swift.Void) throws {
guard let connection = connection, connection.state == .ready else {
throw OSCNetworkingError.notConnected
}

let packet = try OSCPacket(packetContents: packetContents)
connection.send(content: packet, completion: .contentProcessed( { error in
completion(error)
}))
}

fileprivate func setupConnection() {
connection?.stateUpdateHandler = { [weak self] (newState) in
switch newState {
case .ready:
self?.stopTimer()

case .cancelled, .failed:
self?.stopTimer()
self?.connection = nil

default:
break
}

self?.delegate?.connectionStateChange(newState)
}

connection?.start(queue: .main)
}

fileprivate func stopTimer() {
browserTimer?.invalidate()
browserTimer = nil
}
}

//MARK: - OSCClientUDP

class OSCClientUDP: OSCNetworkClient {
internal var serviceType: String = kOSCServiceTypeUDP
weak var delegate: OSCClientDelegate? = nil
internal var connection: NWConnection? = nil
internal var parameters: NWParameters = {
var params: NWParameters = .udp
params.includePeerToPeer = true
return params
}()
internal var browserTimer: Timer? = nil

deinit {
connection?.cancel()
}
}

//MARK: - OSCClientTCP

class OSCClientTCP: OSCNetworkClient {
internal var serviceType: String = kOSCServiceTypeTCP
weak var delegate: OSCClientDelegate? = nil
internal var connection: NWConnection? = nil
internal var parameters: NWParameters = {
// Customize TCP options to enable keepalives.
let tcpOptions = NWProtocolTCP.Options()
tcpOptions.enableKeepalive = true
tcpOptions.keepaliveIdle = 2
tcpOptions.noDelay = true

var params: NWParameters = NWParameters(tls: nil, tcp: tcpOptions)
params.includePeerToPeer = true

//Insert our SLIP protocol framer at the top of the stack
let SLIPOptions = NWProtocolFramer.Options(definition: SLIPProtocol.definition)
params.defaultProtocolStack.applicationProtocols.insert(SLIPOptions, at: .zero)
return params
}()
internal var browserTimer: Timer? = nil

deinit {
connection?.cancel()
}
}
Loading

0 comments on commit ffb8232

Please sign in to comment.