-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
2,200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))") | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.