├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ └── build.yml ├── .gitignore ├── .spi.yml ├── .swift-version ├── .swiftformat ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── OSCKit-CI.xcscheme ├── Examples ├── Basic Example │ ├── Basic Example.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Basic Example.xcscheme │ ├── Basic Example │ │ ├── BasicExampleApp.swift │ │ ├── ContentView.swift │ │ ├── OSCManager.swift │ │ └── Support │ │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ │ └── Basic Example.entitlements │ └── README.md ├── Custom Type Example │ ├── Custom Type Example.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Custom Type Example.xcscheme │ ├── Custom Type Example │ │ ├── ContentView.swift │ │ ├── CustomType.swift │ │ ├── CustomTypeExampleApp.swift │ │ ├── OSCManager.swift │ │ └── Support │ │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ │ └── Custom Type Example.entitlements │ └── README.md ├── Method Blocks Example │ ├── Method Blocks Example.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Method Blocks Example.xcscheme │ ├── Method Blocks Example │ │ ├── ContentView.swift │ │ ├── MethodBlocksExampleApp.swift │ │ ├── OSCManager.swift │ │ ├── OSCReceiver.swift │ │ └── Support │ │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ │ └── Method Blocks Example.entitlements │ └── README.md ├── Method IDs Example │ ├── Method IDs Example.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Method IDs Example.xcscheme │ ├── Method IDs Example │ │ ├── ContentView.swift │ │ ├── MethodIDsExampleApp.swift │ │ ├── OSCManager.swift │ │ ├── OSCReceiver.swift │ │ └── Support │ │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ │ └── Method IDs Example.entitlements │ └── README.md ├── README.md └── Socket Example │ ├── README.md │ ├── Socket Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Socket Example.xcscheme │ └── Socket Example │ ├── ContentView.swift │ ├── OSCManager.swift │ ├── SocketExampleApp.swift │ └── Support │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ └── Socket Example.entitlements ├── Images ├── osckit-banner.png └── sandbox-network-connections.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── OSCKit │ ├── API Evolution │ │ ├── OSCKit-API-0.5.0.swift │ │ ├── OSCKit-API-1.0.1.swift │ │ └── OSCKit-API-1.2.0.swift │ ├── OSCClient.swift │ ├── OSCHandlerBlock.swift │ ├── OSCKit.docc │ │ ├── AnyOSCNumberValue.md │ │ ├── Documentation.md │ │ ├── Getting-Started.md │ │ ├── OSC-Address-Pattern-Parsing.md │ │ ├── OSC-Custom-Types.md │ │ ├── OSC-Value-Parsing.md │ │ ├── OSC-Value-Types.md │ │ ├── OSCClient.md │ │ ├── OSCServer.md │ │ ├── OSCSocket.md │ │ ├── Receiving-OSC.md │ │ ├── Resources │ │ │ └── osckit-banner.png │ │ └── Sending-OSC.md │ ├── OSCKit.swift │ ├── OSCServer.swift │ ├── OSCServerProtocol.swift │ ├── OSCServerUDPDelegate.swift │ ├── OSCSocket.swift │ └── OSCTimeTagMode.swift └── OSCKitCore │ ├── API Evolution │ └── OSCKitCore-API-1.2.0.swift │ ├── OSCAddressPattern │ ├── Component │ │ ├── Component Token.swift │ │ ├── Component evaluate.swift │ │ ├── Component parse.swift │ │ └── Component.swift │ ├── OSCAddressPattern Strings.swift │ ├── OSCAddressPattern init.swift │ └── OSCAddressPattern.swift │ ├── OSCAddressSpace │ ├── OSCAddressSpace MethodID.swift │ ├── OSCAddressSpace Node.swift │ ├── OSCAddressSpace Utilities.swift │ └── OSCAddressSpace.swift │ ├── OSCBundle │ ├── OSCBundle Data Extensions.swift │ ├── OSCBundle init.swift │ ├── OSCBundle rawData.swift │ └── OSCBundle.swift │ ├── OSCKitCore.docc │ ├── AnyOSCNumberValue.md │ ├── Documentation.md │ ├── OSC-Address-Pattern-Parsing.md │ ├── OSC-Custom-Types.md │ ├── OSC-Value-Parsing.md │ ├── OSC-Value-Types.md │ └── Resources │ │ └── osckit-banner.png │ ├── OSCKitCore.swift │ ├── OSCMessage │ ├── OSCMessage Data Extensions.swift │ ├── OSCMessage init.swift │ ├── OSCMessage rawData.swift │ └── OSCMessage.swift │ ├── OSCObject │ ├── OSCObject Data Extensions.swift │ ├── OSCObject Static Constructors.swift │ ├── OSCObject.swift │ └── OSCObjectType.swift │ ├── OSCSerialization │ ├── OSCSerialization.swift │ ├── OSCSerializationError.swift │ └── OSCValueTagIdentity.swift │ ├── OSCTimeTag │ ├── OSCTimeTag Codable.swift │ ├── OSCTimeTag Properties.swift │ ├── OSCTimeTag Static Constructors.swift │ ├── OSCTimeTag Utilities.swift │ ├── OSCTimeTag init.swift │ └── OSCTimeTag.swift │ ├── OSCValue │ ├── Core Types │ │ ├── Data (Blob).swift │ │ ├── Float32.swift │ │ ├── Int32.swift │ │ └── String.swift │ ├── Extended Types │ │ ├── Bool.swift │ │ ├── Character.swift │ │ ├── Double.swift │ │ ├── Int64.swift │ │ ├── OSCArrayValue.swift │ │ ├── OSCImpulseValue.swift │ │ ├── OSCMIDIValue.swift │ │ ├── OSCNullValue.swift │ │ ├── OSCStringAltValue.swift │ │ └── OSCTimeTag OSCValue.swift │ ├── Interpolated Types │ │ ├── BinaryFloatingPoint.swift │ │ └── BinaryInteger.swift │ ├── OSCInterpolatedValue.swift │ ├── OSCValue.swift │ └── Opaque Types │ │ ├── AnyOSCNumberValue.swift │ │ └── OSCNumberValueBase.swift │ ├── OSCValueCodable │ ├── OSCDecodeError.swift │ ├── OSCEncodeError.swift │ ├── OSCMessageDecoder.swift │ ├── OSCMessageEncoder.swift │ ├── OSCValueCodable.swift │ ├── OSCValueDecodable.swift │ ├── OSCValueDecoder.swift │ ├── OSCValueDecoderBlock.swift │ ├── OSCValueEncodable.swift │ └── OSCValueEncoderBlock.swift │ ├── OSCValueMaskable │ ├── OSCValueMaskError.swift │ ├── OSCValueMaskable.swift │ └── OSCValues Mask Methods.swift │ ├── OSCValueToken │ ├── OSCValueToken Methods.swift │ └── OSCValueToken.swift │ ├── OSCValues │ ├── OSCValues.swift │ └── Type Mask │ │ ├── OSCValues Type Mask 1 Value.swift │ │ ├── OSCValues Type Mask 10 Values.swift │ │ ├── OSCValues Type Mask 2 Values.swift │ │ ├── OSCValues Type Mask 3 Values.swift │ │ ├── OSCValues Type Mask 4 Values.swift │ │ ├── OSCValues Type Mask 5 Values.swift │ │ ├── OSCValues Type Mask 6 Values.swift │ │ ├── OSCValues Type Mask 7 Values.swift │ │ ├── OSCValues Type Mask 8 Values.swift │ │ ├── OSCValues Type Mask 9 Values.swift │ │ └── OSCValues Type Mask Helpers.swift │ └── Utilities │ ├── Concurrency Extensions.swift │ ├── Date Extensions.swift │ ├── Hex and Binary String.swift │ ├── Optional.swift │ ├── Outsourced │ ├── CharacterSet.swift │ ├── Collections.swift │ ├── Data.swift │ ├── FloatingPoint and Darwin.swift │ ├── Integers.swift │ ├── Protocols.swift │ ├── String and CharacterSet.swift │ └── String and Foundation.swift │ └── String Extensions.swift └── Tests ├── OSCKitCoreTests ├── OSCAddressPattern │ ├── Component │ │ ├── Component Evaluate Tests.swift │ │ ├── Component Parse Tests.swift │ │ └── Component Token Tests.swift │ └── OSCAddressPattern Tests.swift ├── OSCAddressSpace │ ├── OSCAddressSpace Tests.swift │ └── OSCAddressSpace Utilities Tests.swift ├── OSCBundle │ ├── OSCBundle Integrity Tests.swift │ ├── OSCBundle Tests.swift │ └── OSCBundle rawData Tests.swift ├── OSCMessage │ ├── OSCMessage Codable Tests.swift │ ├── OSCMessage Integrity Tests.swift │ ├── OSCMessage Tests.swift │ └── OSCMessage rawData Tests.swift ├── OSCObject │ ├── OSCObject Static Constructors Tests.swift │ ├── OSCObject Tests.swift │ └── OSCObject rawData Tests.swift ├── OSCTimeTag │ ├── OSCTimeTag Static Constructors Tests.swift │ └── OSCTimeTag Tests.swift ├── OSCValue │ ├── Extended Types │ │ ├── OSCArrayValue Tests.swift │ │ ├── OSCImpulseValue Tests.swift │ │ ├── OSCMIDIValue Tests.swift │ │ ├── OSCNullValue Tests.swift │ │ └── OSCStringAltValue Tests.swift │ ├── OSCValue CustomStringConvertible Tests.swift │ └── Opaque Types │ │ └── AnyOSCNumberValue Tests.swift ├── OSCValueCodable │ └── OSCValueDecoder Read Tests.swift ├── OSCValueToken │ ├── OSCValueToken Methods Tests.swift │ ├── OSCValueToken Properties Tests.swift │ └── OSCValueToken Tests.swift ├── OSCValues │ └── Type Mask │ │ └── OSCValues Type Mask Tests.swift └── Test Utilities.swift └── OSCKitTests ├── OSCServer Tests.swift ├── OSCSocket Tests.swift ├── OSCTimeTag OSC 1.0 Tests.swift ├── OSCTimeTag OSC 1.1 Tests.swift ├── Object Access Tests.swift └── Test Utilities.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: orchetect 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report about a reproducible problem. 3 | labels: bug 4 | body: 5 | - type: textarea 6 | id: bug-description 7 | attributes: 8 | label: Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc. 9 | description: "A clear and concise description of the bug and steps to reproduce. Include system details (OS version) and build environment particulars (Xcode version, etc.)." 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature request 4 | url: https://github.com/orchetect/OSCKit/discussions 5 | about: Suggest new features or improvements. 6 | - name: I need help setting up or troubleshooting 7 | url: https://github.com/orchetect/OSCKit/discussions 8 | about: Questions not answered in the documentation, discussions forum, or example projects. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | [Dd]ev/ 3 | 4 | # Xcode 5 | 6 | # macOS 7 | .DS_Store 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | ## SPM support in Xcode 40 | # .swiftpm - for shared CI schemes we need these checked in: 41 | # -> .swiftpm/xcode/package.xcworkspace 42 | # -> .swiftpm/xcode/xcshareddata/xcschemes/*.* 43 | 44 | # Swift Package Manager 45 | # 46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 47 | Packages/ 48 | Package.pins 49 | Package.resolved 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | 58 | Pods/ 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots/**/*.png 77 | fastlane/test_output 78 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: macosSpm 5 | scheme: OSCKit-CI 6 | - platform: macosXcodebuild 7 | scheme: OSCKit-CI 8 | - platform: iOS 9 | scheme: OSCKit-CI 10 | - platform: tvOS 11 | scheme: OSCKit-CI 12 | - platform: watchOS 13 | scheme: NOT-SUPPORTED 14 | - platform: visionOS 15 | scheme: OSCKit-CI 16 | external_links: 17 | documentation: "https://orchetect.github.io/OSCKit/" 18 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.9 -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --acronyms ID,URL,UUID 2 | --allman false 3 | --assetliterals visual-width 4 | --beforemarks 5 | --binarygrouping 8,8 6 | --categorymark "MARK: %c" 7 | --classthreshold 0 8 | --closingparen balanced 9 | --closurevoid remove 10 | --commas inline 11 | --conflictmarkers reject 12 | --decimalgrouping ignore 13 | --elseposition same-line 14 | --emptybraces spaced 15 | --enumthreshold 0 16 | --exponentcase lowercase 17 | --exponentgrouping disabled 18 | --extensionacl on-declarations 19 | --extensionlength 0 20 | --extensionmark "MARK: - %t + %c" 21 | --fractiongrouping disabled 22 | --fragment false 23 | --funcattributes prev-line 24 | --groupedextension "MARK: %c" 25 | --guardelse auto 26 | --header "\n {file}\n OSCKit • https://github.com/orchetect/OSCKit\n © 2020-{year} Steffan Andrews • Licensed under MIT License\n" 27 | --hexgrouping 8,8 28 | --hexliteralcase uppercase 29 | --ifdef no-indent 30 | --importgrouping alpha 31 | --indent 4 32 | --indentcase false 33 | --indentstrings true 34 | --lifecycle 35 | --lineaftermarks true 36 | --linebreaks lf 37 | --markcategories true 38 | --markextensions always 39 | --marktypes always 40 | --maxwidth 140 41 | --modifierorder 42 | --nevertrailing 43 | --nospaceoperators 44 | --nowrapoperators 45 | --octalgrouping 4,8 46 | --operatorfunc spaced 47 | --organizetypes actor,class,enum,struct 48 | --patternlet hoist 49 | --ranges spaced 50 | --redundanttype infer-locals-only 51 | --self remove 52 | --selfrequired 53 | --semicolons inline 54 | --shortoptionals always 55 | --smarttabs enabled 56 | --stripunusedargs always 57 | --structthreshold 0 58 | --tabwidth unspecified 59 | --trailingclosures 60 | --trimwhitespace nonblank-lines 61 | --typeattributes preserve 62 | --typemark "MARK: - %t" 63 | --varattributes preserve 64 | --voidtype void 65 | --wraparguments before-first 66 | --wrapcollections before-first 67 | --wrapconditions after-first 68 | --wrapparameters before-first 69 | --wrapreturntype preserve 70 | --wrapternary before-operators 71 | --wraptypealiases before-first 72 | --xcodeindentation enabled 73 | --yodaswap always 74 | --disable blankLinesAroundMark,consecutiveSpaces,preferKeyPath,redundantParens,sortDeclarations,sortedImports,unusedArguments 75 | --enable blankLinesBetweenImports,blockComments,isEmpty,wrapEnumCases 76 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example.xcodeproj/xcshareddata/xcschemes/Basic Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 16 | 17 | 28 | 30 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/BasicExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicExampleApp.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct BasicExampleApp: App { 11 | @StateObject var oscManager = OSCManager() 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | .environmentObject(oscManager) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKit 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @EnvironmentObject private var oscManager: OSCManager 12 | 13 | var body: some View { 14 | VStack(spacing: 20) { 15 | Text("By default, port 8000 is opened to listen for incoming OSC data. Port 8000 is used to send the test message.") 16 | 17 | Text("Received OSC messages are logged to the console.") 18 | 19 | Button("Send Test OSC Message") { 20 | sendTestOSCMessage() 21 | } 22 | } 23 | .multilineTextAlignment(.center) 24 | .padding() 25 | .frame(maxWidth: 480) 26 | } 27 | 28 | private func sendTestOSCMessage() { 29 | oscManager.send( 30 | .message("/some/address/method", values: ["Test string", 123]), 31 | to: "localhost", // destination IP address or hostname 32 | port: 8000 // standard OSC port but can be changed 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/OSCManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCManager.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC lifecycle and send/receive manager. 11 | final class OSCManager: ObservableObject, Sendable { 12 | private let client = OSCClient() 13 | private let server = OSCServer(port: 8000) 14 | 15 | init() { 16 | start() 17 | } 18 | } 19 | 20 | // MARK: - Lifecycle 21 | 22 | extension OSCManager { 23 | /// Call this once on app launch. 24 | func start() { 25 | // setup client 26 | do { try client.start() } catch { print(error) } 27 | 28 | // setup server 29 | server.setHandler { [weak self] message, timeTag, host, port in 30 | self?.handle(message: message, timeTag: timeTag, host: host, port: port) 31 | } 32 | do { try server.start() } catch { print(error) } 33 | } 34 | 35 | func stop() { 36 | client.stop() 37 | server.stop() 38 | } 39 | } 40 | 41 | // MARK: - Receive 42 | 43 | extension OSCManager { 44 | func handle(message: OSCMessage, timeTag: OSCTimeTag, host: String, port: UInt16) { 45 | print("\(message) with time tag: \(timeTag) from: \(host):\(port)") 46 | } 47 | } 48 | 49 | // MARK: - Send 50 | 51 | extension OSCManager { 52 | func send(_ message: OSCMessage, to host: String, port: UInt16) { 53 | do { 54 | try client.send(message, to: host, port: port) 55 | } catch { 56 | print(error) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/Support/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/Support/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/Support/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Basic Example/Basic Example/Support/Basic Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/Basic Example/README.md: -------------------------------------------------------------------------------- 1 | # OSCKit Basic Example 2 | 3 | This example demonstrates the most essential features of OSCKit. 4 | 5 | It will build for all platforms including macOS, iOS, tvOS and visionOS, and can be run in device simulators. 6 | 7 | ## Entitlements 8 | 9 | If you are adding OSCKit to a macOS project that has the Sandbox entitlement, ensure that the network options are enabled. These entitlement options are already set in the example project. 10 | 11 | ![sandbox-network-connections](../../Images/sandbox-network-connections.png) 12 | 13 | ## Build Note 14 | 15 | > [!TIP] 16 | > 17 | >If Xcode builds but the app does not run, it may be because Xcode is defaulting to the wrong Scheme. Ensure the example app's Scheme is selected then try again. 18 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example.xcodeproj/xcshareddata/xcschemes/Custom Type Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 16 | 17 | 28 | 30 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 57 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKit 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @EnvironmentObject private var oscManager: OSCManager 12 | 13 | var body: some View { 14 | VStack(spacing: 20) { 15 | Text("By default, port 8000 is opened to listen for incoming OSC data. Port 8000 is used to send the test message.") 16 | 17 | Text("Received OSC messages are logged to the console.") 18 | 19 | Button("Send Test OSC Message") { 20 | sendTestOSCMessage() 21 | } 22 | } 23 | .multilineTextAlignment(.center) 24 | .padding() 25 | .frame(maxWidth: 480) 26 | } 27 | 28 | private func sendTestOSCMessage() { 29 | let customType = CustomType(id: Int.random(in: 0 ... 9), name: UUID().uuidString) 30 | 31 | oscManager.send( 32 | .message("/test", values: [customType]), 33 | to: "localhost", // destination IP address or hostname 34 | port: 8000 // standard OSC port but can be changed 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/CustomType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomType.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKitCore 9 | 10 | // NOTE: 11 | // In this basic example, we've chosen to use the underlying OSC Type "blob", which is essentially raw data, as our 12 | // underlying data storage chunk within an OSC message, since our object conforms to Codable and it's easily converted 13 | // to/from Data. 14 | // 15 | // Since we've chosen to use JSONEncoder, and JSON is technically text (string) block, we could have also chosen 16 | // to use the OSC Type "string" as the underlying data storage within an OSC message. There are no particular benefits 17 | // or drawbacks of either choice, and is strictly implementation semantics. 18 | 19 | struct CustomType: Equatable, Hashable, Codable, Sendable { 20 | let id: Int 21 | let name: String 22 | } 23 | 24 | extension CustomType: OSCValue { 25 | // Our underlying type when encoded in OSC will be a blob (raw Data) 26 | static let oscValueToken: OSCValueToken = .blob 27 | } 28 | 29 | extension CustomType: OSCValueCodable { 30 | // Define the custom type's OSC Type Tag as "j". 31 | // Note that this MUST NOT use an existing OSC Type Tag already defined in the OSC spec. 32 | // When registering your custom type with the `OSCSerialization` singleton, it will reject registering a type that 33 | // uses an OSC Type Tag that already exists. 34 | // For a reference of existing OSC Type Tags, see the OSC 1.0 spec online. 35 | static let oscTag: Character = "j" 36 | // atomic indicates that the tag is static and will never change based on its data content 37 | static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 38 | } 39 | 40 | extension CustomType: OSCValueEncodable { 41 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 42 | static let oscEncoding = OSCValueEncodingBlock { value in 43 | // Encode our Codable type instance into raw data 44 | let encoder = JSONEncoder() 45 | let jsonData = try encoder.encode(value) 46 | 47 | // OSCValueEncodingBlock makes it our responsibility to make sure OSB blob (data) is encoded correctly, 48 | // including a 4-byte big-endian Int32 length header and trailing null-byte padding to an alignment of 4 bytes. 49 | // We can ask the Data (blob) encoder to do the work for us: 50 | let blobData = try Data.oscEncoding.block(jsonData).data 51 | return (tag: oscTag, data: blobData) 52 | } 53 | } 54 | 55 | extension CustomType: OSCValueDecodable { 56 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 57 | static let oscDecoding = OSCValueDecodingBlock { dataReader in 58 | let decoder = JSONDecoder() 59 | 60 | // Gets entire data chunk from the OSC blob, stripping the length bytes and null padding suffix 61 | // and returning the actual data content 62 | let data = try dataReader.readBlob() 63 | 64 | // Decode into a new instance of our Codable type 65 | let decoded = try decoder.decode(CustomType.self, from: data) 66 | return decoded 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/CustomTypeExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomTypeExampleApp.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct CustomTypeExampleApp: App { 11 | @StateObject var oscManager = OSCManager() 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | .environmentObject(oscManager) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/OSCManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCManager.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC lifecycle and send/receive manager. 11 | final class OSCManager: ObservableObject, Sendable { 12 | private let client = OSCClient() 13 | private let server = OSCServer(port: 8000) 14 | 15 | init() { 16 | do { 17 | try OSCSerialization.shared.registerType(CustomType.self) 18 | } catch { 19 | print(error.localizedDescription) 20 | } 21 | 22 | start() 23 | } 24 | } 25 | 26 | // MARK: - Lifecycle 27 | 28 | extension OSCManager { 29 | /// Call this once on app launch. 30 | func start() { 31 | // setup client 32 | do { try client.start() } catch { print(error) } 33 | 34 | // setup server 35 | server.setHandler { [weak self] message, timeTag, host, port in 36 | self?.handle(message: message, timeTag: timeTag, host: host, port: port) 37 | } 38 | do { try server.start() } catch { print(error) } 39 | } 40 | 41 | func stop() { 42 | client.stop() 43 | server.stop() 44 | } 45 | } 46 | 47 | // MARK: - Receive 48 | 49 | extension OSCManager { 50 | func handle(message: OSCMessage, timeTag: OSCTimeTag, host: String, port: UInt16) { 51 | do { 52 | let customTypeValue = try message.values.masked(CustomType.self) 53 | 54 | let msg = message.addressPattern.stringValue 55 | let id = customTypeValue.id 56 | let name = customTypeValue.name 57 | print( 58 | "OSC message from \(host):\(port), address \"\(msg)\", CustomType value containing id:\(id) and name:\(name)" 59 | ) 60 | } catch { 61 | print("OSC message received that did not have exactly one value of type CustomType.") 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Send 67 | 68 | extension OSCManager { 69 | func send(_ message: OSCMessage, to host: String, port: UInt16) { 70 | do { 71 | try client.send(message, to: host, port: port) 72 | } catch { 73 | print(error) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/Support/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/Support/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/Support/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/Custom Type Example/Support/Custom Type Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/Custom Type Example/README.md: -------------------------------------------------------------------------------- 1 | # OSCKit Custom Type Example 2 | 3 | This example demonstrates conforming a custom type to become OSC codable as an OSC message value. 4 | 5 | It will build for all platforms including macOS, iOS, tvOS and visionOS, and can be run in device simulators. 6 | 7 | ## Disclaimer 8 | 9 | > [!IMPORTANT] 10 | > 11 | > This is an advanced feature and _**in almost every use case**_, it is not necessary to conform a custom type. 12 | > 13 | > The standard data types available for use defined by the OSC spec cover most common data types for most conceivable use cases. Using data types defined by the OSC spec means 3rd-party software/hardware will recognize them as a native type. 14 | > 15 | > While it is possible to define your own OSC Type, it is discouraged by the OSC spec. Only your own software will understand this OSC Type because you have to provide the encoding and decoding implementation logic. This of course means this type can only be sent and received by software that you create containing this implementation. 3rd-party software/hardware receiving OSC messages containing this custom type will ignore it. 16 | > 17 | > Another consideration is that only compact data types should be conformed as an OSC Type. Since the UDP packet size is the upper limit of the amount of data that can be encoded, be aware that custom types that could potentially encode to raw data that overflows UDP packet size will fail to send on the network. 18 | > 19 | > This example project is provided as a starting point for demonstrating how to conform a custom type as an OSC Type. 20 | > 21 | > The example implementation is the same protocolized interface OSCKit uses under the hood to conform all of its standard OSC Types. To see examples of more advanced implementations, see the [`OSCKitCore/OSCValue`](../../Sources/OSCKitCore/OSCValue) source contents. 22 | 23 | ## Entitlements 24 | 25 | If you are adding OSCKit to a macOS project that has the Sandbox entitlement, ensure that the network options are enabled. These entitlement options are already set in the example project. 26 | 27 | ![sandbox-network-connections](../../Images/sandbox-network-connections.png) 28 | 29 | ## Build Note 30 | 31 | > [!TIP] 32 | > 33 | >If Xcode builds but the app does not run, it may be because Xcode is defaulting to the wrong Scheme. Ensure the example app's Scheme is selected then try again. 34 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example.xcodeproj/xcshareddata/xcschemes/Method Blocks Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 16 | 17 | 28 | 30 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKit 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @EnvironmentObject private var oscManager: OSCManager 12 | 13 | var body: some View { 14 | VStack(spacing: 20) { 15 | Text("By default, port 8000 is opened to listen for incoming OSC data. Port 8000 is used to send the test message.") 16 | 17 | Text("Received OSC messages are logged to the console.") 18 | 19 | Button("Send Test OSC Message A") { 20 | sendTestOSCMessageA() 21 | } 22 | 23 | Button("Send Test OSC Message B") { 24 | sendTestOSCMessageB() 25 | } 26 | 27 | Button("Send Test OSC Message C With Optional Value") { 28 | sendTestOSCMessageCWithOptionalValue() 29 | } 30 | 31 | Button("Send Test OSC Message C With No Optional Value") { 32 | sendTestOSCMessageCWithNoOptionalValue() 33 | } 34 | } 35 | .multilineTextAlignment(.center) 36 | .padding() 37 | .frame(maxWidth: 480) 38 | } 39 | 40 | private func sendTestOSCMessageA() { 41 | let oscMessage = OSCMessage( 42 | "/methodA", 43 | values: ["Test string"] 44 | ) 45 | 46 | oscManager.send(oscMessage, to: "localhost", port: 8000) 47 | } 48 | 49 | private func sendTestOSCMessageB() { 50 | let oscMessage = OSCMessage( 51 | "/some/address/methodB", 52 | values: ["Test string", 123] 53 | ) 54 | 55 | oscManager.send(oscMessage, to: "localhost", port: 8000) 56 | } 57 | 58 | private func sendTestOSCMessageCWithOptionalValue() { 59 | let oscMessage = OSCMessage( 60 | "/some/address/methodC", 61 | values: ["Test string", 123.5] 62 | ) 63 | 64 | oscManager.send(oscMessage, to: "localhost", port: 8000) 65 | } 66 | 67 | private func sendTestOSCMessageCWithNoOptionalValue() { 68 | let oscMessage = OSCMessage( 69 | "/some/address/methodC", 70 | values: ["Test string"] 71 | ) 72 | 73 | oscManager.send(oscMessage, to: "localhost", port: 8000) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/MethodBlocksExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodBlocksExampleApp.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct MethodBlocksExampleApp: App { 11 | @StateObject var oscManager = OSCManager() 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | .environmentObject(oscManager) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/OSCManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCManager.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC lifecycle and send/receive manager. 11 | final class OSCManager: ObservableObject, Sendable { 12 | private let client = OSCClient() 13 | private let server = OSCServer(port: 8000) 14 | private let receiver = OSCReceiver() 15 | 16 | init() { 17 | start() 18 | } 19 | } 20 | 21 | // MARK: - Lifecycle 22 | 23 | extension OSCManager { 24 | /// Call once at app startup. 25 | func start() { 26 | // setup client 27 | do { try client.start() } catch { print(error) } 28 | 29 | // setup server 30 | server.setHandler { [weak self] message, timeTag, host, port in 31 | do { 32 | try self?.receiver.handle(message: message, timeTag: timeTag, host: host, port: port) 33 | } catch { 34 | print(error) 35 | } 36 | } 37 | do { try server.start() } catch { print(error) } 38 | } 39 | 40 | func stop() { 41 | client.stop() 42 | server.stop() 43 | } 44 | } 45 | 46 | // MARK: - Send 47 | 48 | extension OSCManager { 49 | func send( 50 | _ message: OSCMessage, 51 | to host: String, 52 | port: UInt16 53 | ) { 54 | do { 55 | try client.send(message, to: host, port: port) 56 | } catch { 57 | print(error) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/OSCReceiver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCReceiver.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC receiver. 11 | /// Registers local OSC addresses that our app is capable of recognizing and 12 | /// handles received bundles & messages. 13 | final class OSCReceiver: Sendable { 14 | private let addressSpace = OSCAddressSpace() 15 | 16 | public init() { 17 | // Register local OSC method and supply a closure block 18 | addressSpace.register(localAddress: "/methodA") { values, host, port in 19 | guard let str = try? values.masked(String.self) else { return } 20 | print("Received methodA from \(host):\(port) with value: \"\(str)\"") 21 | } 22 | 23 | // Register local OSC method and supply a closure block 24 | addressSpace.register(localAddress: "/some/address/methodB") { values, host, port in 25 | guard let (str, int) = try? values.masked(String.self, Int.self) else { return } 26 | print("Received methodB from \(host):\(port) with values: [\"\(str)\", \(int)]") 27 | } 28 | 29 | // Instead of supplying a closure, it's also possible to forward to a function: 30 | 31 | // Option 1: weak reference (recommended): 32 | addressSpace.register( 33 | localAddress: "/some/address/methodC", 34 | block: { [weak self] values, host, port in 35 | self?.handleMethodC(values: values, host: host, port: port) 36 | } 37 | ) 38 | 39 | // Option 2: strong reference (discouraged): 40 | // addressSpace.register( 41 | // localAddress: "/some/address/methodC", 42 | // block: handleMethodC 43 | // ) 44 | } 45 | 46 | private func handleMethodC(values: OSCValues, host: String, port: UInt16) { 47 | guard let (str, dbl) = try? values.masked(String.self, Double?.self) else { return } 48 | print("Received methodC from \(host):\(port) with values: [\"\(str)\", \(dbl as Any)]") 49 | } 50 | 51 | public func handle(message: OSCMessage, timeTag: OSCTimeTag, host: String, port: UInt16) throws { 52 | // Execute closures for matching methods, and returns the matching method IDs 53 | let methodIDs = addressSpace.dispatch(message: message, host: host, port: port) 54 | 55 | // If no IDs are returned, it means that the OSC message address pattern did not match any 56 | // that were registered 57 | if methodIDs.isEmpty { 58 | print("No method registered for:", message) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/Support/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/Support/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/Support/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/Method Blocks Example/Support/Method Blocks Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/Method Blocks Example/README.md: -------------------------------------------------------------------------------- 1 | # OSCKit Method Blocks Example 2 | 3 | This example demonstrates how to get the most out of OSCKit, including: 4 | 5 | - Taking advantage of `OSCAddressSpace` pattern matching using method closure blocks 6 | - OSC message value validation and strong-typing using `masked()` 7 | 8 | It will build for all platforms including macOS, iOS, tvOS and visionOS, and can be run in device simulators. 9 | 10 | ## Entitlements 11 | 12 | If you are adding OSCKit to a macOS project that has the Sandbox entitlement, ensure that the network options are enabled. These entitlement options are already set in the example project. 13 | 14 | ![sandbox-network-connections](../../Images/sandbox-network-connections.png) 15 | 16 | ## Build Note 17 | 18 | > [!TIP] 19 | > 20 | > If Xcode builds but the app does not run, it may be because Xcode is defaulting to the wrong Scheme. Ensure the example app's Scheme is selected then try again. 21 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example.xcodeproj/xcshareddata/xcschemes/Method IDs Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 16 | 17 | 28 | 30 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKit 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @EnvironmentObject private var oscManager: OSCManager 12 | 13 | var body: some View { 14 | VStack(spacing: 20) { 15 | Text("By default, port 8000 is opened to listen for incoming OSC data. Port 8000 is used to send the test message.") 16 | 17 | Text("Received OSC messages are logged to the console.") 18 | 19 | Button("Send Test OSC Message A") { 20 | sendTestOSCMessageA() 21 | } 22 | 23 | Button("Send Test OSC Message B") { 24 | sendTestOSCMessageB() 25 | } 26 | 27 | Button("Send Test OSC Message C With Optional Value") { 28 | sendTestOSCMessageCWithOptionalValue() 29 | } 30 | 31 | Button("Send Test OSC Message C With No Optional Value") { 32 | sendTestOSCMessageCWithNoOptionalValue() 33 | } 34 | } 35 | .multilineTextAlignment(.center) 36 | .padding() 37 | .frame(maxWidth: 480) 38 | } 39 | 40 | private func sendTestOSCMessageA() { 41 | let oscMessage = OSCMessage( 42 | "/methodA", 43 | values: ["Test string"] 44 | ) 45 | 46 | oscManager.send(oscMessage, to: "localhost", port: 8000) 47 | } 48 | 49 | private func sendTestOSCMessageB() { 50 | let oscMessage = OSCMessage( 51 | "/some/address/methodB", 52 | values: ["Test string", 123] 53 | ) 54 | 55 | oscManager.send(oscMessage, to: "localhost", port: 8000) 56 | } 57 | 58 | private func sendTestOSCMessageCWithOptionalValue() { 59 | let oscMessage = OSCMessage( 60 | "/some/address/methodC", 61 | values: ["Test string", 123.5] 62 | ) 63 | 64 | oscManager.send(oscMessage, to: "localhost", port: 8000) 65 | } 66 | 67 | private func sendTestOSCMessageCWithNoOptionalValue() { 68 | let oscMessage = OSCMessage( 69 | "/some/address/methodC", 70 | values: ["Test string"] 71 | ) 72 | 73 | oscManager.send(oscMessage, to: "localhost", port: 8000) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/MethodIDsExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodIDsExampleApp.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct MethodIDsExampleApp: App { 11 | @StateObject var oscManager = OSCManager() 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | .environmentObject(oscManager) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/OSCManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCManager.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC lifecycle and send/receive manager. 11 | final class OSCManager: ObservableObject, Sendable { 12 | private let client = OSCClient() 13 | private let server = OSCServer(port: 8000) 14 | private let receiver = OSCReceiver() 15 | 16 | init() { 17 | start() 18 | } 19 | } 20 | 21 | // MARK: - Lifecycle 22 | 23 | extension OSCManager { 24 | /// Call once at app startup. 25 | func start() { 26 | // setup client 27 | do { try client.start() } catch { print(error) } 28 | 29 | // setup server 30 | server.setHandler { [weak self] message, timeTag, host, port in 31 | do { 32 | try self?.receiver.handle(message: message, timeTag: timeTag, host: host, port: port) 33 | } catch { 34 | print(error) 35 | } 36 | } 37 | do { try server.start() } catch { print(error) } 38 | } 39 | 40 | func stop() { 41 | client.stop() 42 | server.stop() 43 | } 44 | } 45 | 46 | // MARK: - Send 47 | 48 | extension OSCManager { 49 | func send(_ message: OSCMessage, to host: String, port: UInt16) { 50 | do { 51 | try client.send(message, to: host, port: port) 52 | } catch { 53 | print(error) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/OSCReceiver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCReceiver.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC receiver. 11 | /// Registers local OSC addresses that our app is capable of recognizing and 12 | /// handles received bundles & messages. 13 | final class OSCReceiver: Sendable { 14 | private let addressSpace = OSCAddressSpace() 15 | 16 | private let idMethodA: OSCAddressSpace.MethodID 17 | private let idMethodB: OSCAddressSpace.MethodID 18 | private let idMethodC: OSCAddressSpace.MethodID 19 | 20 | public init() { 21 | // register local OSC methods and store the ID tokens once before receiving OSC messages 22 | idMethodA = addressSpace.register(localAddress: "/methodA") 23 | idMethodB = addressSpace.register(localAddress: "/some/address/methodB") 24 | idMethodC = addressSpace.register(localAddress: "/some/address/methodC") 25 | } 26 | 27 | public func handle(message: OSCMessage, timeTag: OSCTimeTag, host: String, port: UInt16) throws { 28 | let ids = addressSpace.methods(matching: message.addressPattern) 29 | 30 | guard !ids.isEmpty else { 31 | // No matches against any registered local OSC addresses. 32 | print(message, "with time tag:", timeTag) 33 | return 34 | } 35 | 36 | for id in ids { 37 | switch id { 38 | case idMethodA: 39 | let str = try message.values.masked(String.self) 40 | performMethodA(str) 41 | 42 | case idMethodB: 43 | let (str, int) = try message.values.masked(String.self, Int.self) 44 | performMethodB(str, int) 45 | 46 | case idMethodC: 47 | let (str, dbl) = try message.values.masked(String.self, Double?.self) 48 | performMethodC(str, dbl) 49 | 50 | default: 51 | break 52 | } 53 | } 54 | } 55 | 56 | private func performMethodA(_ str: String) { 57 | print("Dispatching methodA with value: \"\(str)\"") 58 | } 59 | 60 | private func performMethodB(_ str: String, _ int: Int) { 61 | print("Dispatching methodB with values: [\"\(str)\", \(int)]") 62 | } 63 | 64 | private func performMethodC(_ str: String, _ dbl: Double?) { 65 | print("Dispatching methodC with values: [\"\(str)\", \(dbl as Any)]") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/Support/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/Support/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/Support/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/Method IDs Example/Support/Method IDs Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/Method IDs Example/README.md: -------------------------------------------------------------------------------- 1 | # OSCKit Method IDs Example 2 | 3 | This example demonstrates how to get the most out of OSCKit, including: 4 | 5 | - Taking advantage of `OSCAddressSpace` pattern matching and using method IDs 6 | - OSC message value validation and strong-typing using `masked()` 7 | 8 | It will build for all platforms including macOS, iOS, tvOS and visionOS, and can be run in device simulators. 9 | 10 | ## Entitlements 11 | 12 | If you are adding OSCKit to a macOS project that has the Sandbox entitlement, ensure that the network options are enabled. These entitlement options are already set in the example project. 13 | 14 | ![sandbox-network-connections](../../Images/sandbox-network-connections.png) 15 | 16 | ## Build Note 17 | 18 | > [!TIP] 19 | > 20 | > If Xcode builds but the app does not run, it may be because Xcode is defaulting to the wrong Scheme. Ensure the example app's Scheme is selected then try again. 21 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | # OSCKit Examples 2 | 3 | These example projects are provided to demonstrate using OSCKit. 4 | 5 | They will build for all platforms including macOS, iOS, tvOS and visionOS, and can be run in device simulators. 6 | 7 | ## Build Note 8 | 9 | If Xcode builds but the app does not run, it may be because Xcode is defaulting to the wrong Scheme. Ensure the example app's Scheme is selected then try again. 10 | -------------------------------------------------------------------------------- /Examples/Socket Example/README.md: -------------------------------------------------------------------------------- 1 | # OSCKit Socket Example 2 | 3 | This example demonstrates OSC communication using the `OSCSocket` class. 4 | 5 | It will build for all platforms including macOS, iOS, tvOS and visionOS, and can be run in device simulators. 6 | 7 | ## Overview 8 | 9 | The `OSCSocket` class internally combines both an OSC server and client sharing the same local UDP port number. What sets it apart from `OSCServer` and `OSCClient` is that it does not require enabling port reuse to accomplish this. It also can conceptually make communicating bidirectionally with a single remote host more intuitive. 10 | 11 | This also fulfils a niche requirement for communicating with OSC devices such as the Behringer X32 & M32 which respond back using the UDP port that they receive OSC messages from. For example: if an OSC message was sent from port 8000 to the X32's port 10023, the X32 will respond by sending OSC messages back to you on port 8000. 12 | 13 | ## Entitlements 14 | 15 | If you are adding OSCKit to a macOS project that has the Sandbox entitlement, ensure that the network options are enabled. These entitlement options are already set in the example project. 16 | 17 | ![sandbox-network-connections](../../Images/sandbox-network-connections.png) 18 | 19 | ## Build Note 20 | 21 | > [!TIP] 22 | > 23 | > If Xcode builds but the app does not run, it may be because Xcode is defaulting to the wrong Scheme. Ensure the example app's Scheme is selected then try again. 24 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example.xcodeproj/xcshareddata/xcschemes/Socket Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 16 | 17 | 28 | 30 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 57 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example/OSCManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCManager.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKit 9 | 10 | /// OSC lifecycle and send/receive manager. 11 | final class OSCManager: ObservableObject { 12 | private var socket: OSCSocket? 13 | 14 | @Published var localPort: UInt16 = 8000 15 | @Published var remoteHost: String = "localhost" 16 | @Published var remotePort: UInt16 = 8000 17 | @Published var isIPv4BroadcastEnabled: Bool = false 18 | @Published private(set) var isStarted: Bool = false 19 | 20 | init() { } 21 | } 22 | 23 | // MARK: - Lifecycle 24 | 25 | extension OSCManager { 26 | /// Call this once on app launch. 27 | func start() { 28 | do { 29 | guard socket == nil else { return } 30 | 31 | let newSocket = OSCSocket( 32 | localPort: localPort, 33 | remoteHost: remoteHost, 34 | remotePort: remotePort, 35 | isIPv4BroadcastEnabled: isIPv4BroadcastEnabled 36 | ) 37 | socket = newSocket 38 | 39 | newSocket.setHandler { message, timeTag, host, port in 40 | print("\(host):\(port) - \(message) with time tag: \(timeTag)") 41 | } 42 | 43 | try newSocket.start() 44 | 45 | isStarted = true 46 | 47 | let lp = newSocket.localPort 48 | let rp = newSocket.remotePort 49 | print("Using local port \(lp) and remote port \(rp) with remote host \(remoteHost).") 50 | } catch { 51 | print("Error while starting OSC socket: \(error)") 52 | } 53 | } 54 | 55 | func stop() { 56 | defer { 57 | isStarted = false 58 | } 59 | socket?.stop() 60 | socket = nil 61 | } 62 | } 63 | 64 | // MARK: - Send 65 | 66 | extension OSCManager { 67 | func send(_ message: OSCMessage) { 68 | do { 69 | try socket?.send(message) 70 | } catch { 71 | print(error) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example/SocketExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketExampleApp.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKit 8 | import SwiftUI 9 | 10 | @main 11 | struct SocketExampleApp: App { 12 | @StateObject var oscManager = OSCManager() 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView() 17 | .environmentObject(oscManager) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example/Support/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example/Support/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example/Support/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Socket Example/Socket Example/Support/Socket Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Images/osckit-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orchetect/OSCKit/e764dd623499181908cb2e54c3d8cc4dd0278e00/Images/osckit-banner.png -------------------------------------------------------------------------------- /Images/sandbox-network-connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orchetect/OSCKit/e764dd623499181908cb2e54c3d8cc4dd0278e00/Images/sandbox-network-connections.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Steffan Andrews - https://github.com/orchetect 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // (be sure to update the .swift-version file when this Swift version changes) 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "OSCKit", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13)], 9 | products: [ 10 | .library( 11 | name: "OSCKit", 12 | targets: ["OSCKit"] 13 | ), 14 | .library( 15 | name: "OSCKitCore", 16 | targets: ["OSCKitCore"] 17 | ) 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/robbiehanson/CocoaAsyncSocket", from: "7.0.0"), 21 | .package(url: "https://github.com/orchetect/SwiftASCII", from: "1.1.5"), 22 | .package(url: "https://github.com/apple/swift-numerics", from: "1.0.2") 23 | ], 24 | targets: [ 25 | .target( 26 | name: "OSCKit", 27 | dependencies: [ 28 | "OSCKitCore", 29 | .product( 30 | name: "CocoaAsyncSocket", 31 | package: "CocoaAsyncSocket", 32 | condition: .when(platforms: [.macOS, .macCatalyst, .iOS, .tvOS, .visionOS, .driverKit]) 33 | ) 34 | ], 35 | swiftSettings: [.define("DEBUG", .when(configuration: .debug))] 36 | ), 37 | .target( 38 | name: "OSCKitCore", 39 | dependencies: ["SwiftASCII"], 40 | swiftSettings: [.define("DEBUG", .when(configuration: .debug))] 41 | ), 42 | .testTarget( 43 | name: "OSCKitTests", 44 | dependencies: ["OSCKit"] 45 | ), 46 | .testTarget( 47 | name: "OSCKitCoreTests", 48 | dependencies: [ 49 | "OSCKitCore", 50 | .product(name: "Numerics", package: "swift-numerics") 51 | ] 52 | ) 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![OSCKit](Images/osckit-banner.png) 2 | 3 | # OSCKit 4 | 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Forchetect%2FOSCKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/orchetect/OSCKit) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Forchetect%2FOSCKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/orchetect/OSCKit) [![Xcode 16](https://img.shields.io/badge/Xcode-16-blue.svg?style=flat)](https://developer.apple.com/swift) [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/orchetect/OSCKit/blob/main/LICENSE) 6 | 7 | Open Sound Control ([OSC](https://opensoundcontrol.stanford.edu)) library for macOS, iOS and tvOS written in Swift. 8 | 9 | - OSC address pattern matching and dispatch 10 | - Convenient OSC message value type masking, validation and strong-typing 11 | - Modular: use the provided UDP network layer by default, or use your own 12 | - Support for custom OSC types 13 | - Supports Swift 6 strict concurrency 14 | - Fully unit tested 15 | - Full DocC documentation 16 | 17 | ## Getting Started 18 | 19 | The library is available as a Swift Package Manager (SPM) package. 20 | 21 | Use the URL `https://github.com/orchetect/OSCKit` when adding the library to a project or Swift package. 22 | 23 | See the [getting started guide](https://orchetect.github.io/OSCKit/documentation/osckit/getting-started) for a detailed walkthrough of how to get the most out of OSCKit. 24 | 25 | The [Examples](Examples) folder also contains projects to quickly get started. 26 | 27 | ## Documentation 28 | 29 | See the [online documentation](https://orchetect.github.io/OSCKit/) or view it in Xcode's documentation browser by selecting the **Product → Build Documentation** menu. 30 | 31 | This includes a getting started guide, links to examples, and troubleshooting tips. 32 | 33 | ## Dependencies 34 | 35 | - [CocoaAsyncSocket](https://github.com/robbiehanson/CocoaAsyncSocket) is used by the `OSCKit` target for network sockets. 36 | - [SwiftASCII](https://github.com/orchetect/SwiftASCII) is used for ASCII string and character formatting and validation. 37 | 38 | ## Author 39 | 40 | Coded by a bunch of 🐹 hamsters in a trenchcoat that calls itself [@orchetect](https://github.com/orchetect). 41 | 42 | ## License 43 | 44 | Licensed under the MIT license. See [LICENSE](LICENSE) for details. 45 | 46 | ## Sponsoring 47 | 48 | If you enjoy using OSCKit and want to contribute to open-source financially, GitHub sponsorship is much appreciated. Feedback and code contributions are also welcome. 49 | 50 | ## Community & Support 51 | 52 | Please do not email maintainers for technical support. Several options are available for questions and feature ideas: 53 | 54 | - Questions and feature ideas can be posted to [Discussions](https://github.com/orchetect/OSCKit/discussions). 55 | - If an issue is a verifiable bug with reproducible steps it may be posted in [Issues](https://github.com/orchetect/OSCKit/issues). 56 | 57 | ## Contributions 58 | 59 | Contributions are welcome. Posting in [Discussions](https://github.com/orchetect/OSCKIt/discussions) first prior to new submitting PRs for features or modifications is encouraged. 60 | -------------------------------------------------------------------------------- /Sources/OSCKit/API Evolution/OSCKit-API-0.5.0.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCKit-API-0.5.0.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if !os(watchOS) 8 | 9 | import Foundation 10 | 11 | extension OSCServer { 12 | @_documentation(visibility: internal) 13 | @available(*, deprecated, renamed: "OSCTimeTagMode") 14 | public typealias TimeTagMode = OSCTimeTagMode 15 | 16 | @_documentation(visibility: internal) 17 | @available(*, deprecated, renamed: "localPort") 18 | public var port: UInt16 { 19 | localPort 20 | } 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/OSCKit/API Evolution/OSCKit-API-1.0.1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCKit-API-1.0.1.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if !os(watchOS) 8 | 9 | import Foundation 10 | 11 | extension OSCSocket { 12 | @_documentation(visibility: internal) 13 | @available( 14 | *, 15 | deprecated, 16 | renamed: "init(localPort:remoteHost:remotePort:timeTagMode:isIPv4BroadcastEnabled:receiveQueue:handler:)" 17 | ) 18 | @_disfavoredOverload 19 | public convenience init( 20 | localPort: UInt16? = nil, 21 | remoteHost: String? = nil, 22 | remotePort: UInt16? = nil, 23 | timeTagMode: OSCTimeTagMode = .ignore, 24 | isIPv4BroadcastEnabled: Bool = false, 25 | isPortReuseEnabled: Bool = false, 26 | handler: OSCHandlerBlock? = nil 27 | ) { 28 | self.init( 29 | localPort: localPort, 30 | remoteHost: remoteHost, 31 | remotePort: remotePort, 32 | timeTagMode: timeTagMode, 33 | isIPv4BroadcastEnabled: isIPv4BroadcastEnabled, 34 | receiveQueue: nil, 35 | handler: handler 36 | ) 37 | } 38 | } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /Sources/OSCKit/API Evolution/OSCKit-API-1.2.0.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCKit-API-1.2.0.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if !os(watchOS) 8 | 9 | import Foundation 10 | 11 | /// Received-message handler closure used by OSCKit socket classes. 12 | @_documentation(visibility: internal) 13 | @available(*, deprecated, renamed: "OSCHandlerBlock") 14 | public typealias LegacyOSCHandlerBlock = @Sendable ( 15 | _ message: OSCMessage, 16 | _ timeTag: OSCTimeTag 17 | ) -> Void 18 | 19 | extension OSCServer { 20 | @_documentation(visibility: internal) 21 | @available( 22 | *, 23 | deprecated, 24 | message: "Handler closure now takes 4 parameters: message, timeTag, host, port." 25 | ) 26 | @_disfavoredOverload 27 | public convenience init( 28 | port: UInt16 = 8000, 29 | timeTagMode: OSCTimeTagMode = .ignore, 30 | receiveQueue: DispatchQueue? = nil, 31 | handler: @escaping LegacyOSCHandlerBlock 32 | ) { 33 | self.init( 34 | port: port, 35 | timeTagMode: timeTagMode, 36 | receiveQueue: receiveQueue, 37 | handler: { message, timeTag, _, _ in handler(message, timeTag) } 38 | ) 39 | } 40 | 41 | @_documentation(visibility: internal) 42 | @available( 43 | *, 44 | deprecated, 45 | message: "Handler closure now takes 4 parameters: message, timeTag, host, port." 46 | ) 47 | @_disfavoredOverload 48 | public func setHandler( 49 | _ handler: @escaping LegacyOSCHandlerBlock 50 | ) { 51 | setHandler { message, timeTag, _, _ in handler(message, timeTag) } 52 | } 53 | } 54 | 55 | extension OSCSocket { 56 | @_documentation(visibility: internal) 57 | @available( 58 | *, 59 | deprecated, 60 | message: "Handler closure now takes 4 parameters: message, timeTag, host, port." 61 | ) 62 | @_disfavoredOverload 63 | public convenience init( 64 | localPort: UInt16? = nil, 65 | remoteHost: String? = nil, 66 | remotePort: UInt16? = nil, 67 | timeTagMode: OSCTimeTagMode = .ignore, 68 | isIPv4BroadcastEnabled: Bool = false, 69 | receiveQueue: DispatchQueue? = nil, 70 | handler: @escaping LegacyOSCHandlerBlock 71 | ) { 72 | self.init( 73 | localPort: localPort, 74 | remoteHost: remoteHost, 75 | remotePort: remotePort, 76 | timeTagMode: timeTagMode, 77 | isIPv4BroadcastEnabled: isIPv4BroadcastEnabled, 78 | receiveQueue: nil, 79 | handler: { message, timeTag, _, _ in handler(message, timeTag) } 80 | ) 81 | } 82 | 83 | @_documentation(visibility: internal) 84 | @available( 85 | *, 86 | deprecated, 87 | message: "Handler closure now takes 4 parameters: message, timeTag, host, port." 88 | ) 89 | @_disfavoredOverload 90 | public func setHandler( 91 | _ handler: @escaping LegacyOSCHandlerBlock 92 | ) { 93 | setHandler { message, timeTag, _, _ in handler(message, timeTag) } 94 | } 95 | } 96 | 97 | #endif 98 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCHandlerBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCHandlerBlock.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKitCore 9 | 10 | /// Received-message handler closure used by OSCKit socket classes. 11 | public typealias OSCHandlerBlock = @Sendable ( 12 | _ message: OSCMessage, 13 | _ timeTag: OSCTimeTag, 14 | _ host: String, 15 | _ port: UInt16 16 | ) -> Void 17 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/AnyOSCNumberValue.md: -------------------------------------------------------------------------------- 1 | # ``OSCKit/AnyOSCNumberValue`` 2 | 3 | @Comment { 4 | // ------------------------------------------------------------------- 5 | // NOTE: This file is duplicated in both OSCKit and OSCKitCore targets. 6 | // Ensure both files are updated when making changes. 7 | // ------------------------------------------------------------------- 8 | // ------------------------------------------------------------------- 9 | // NOTE: The top line of this file must be edited to begin with the 10 | // current target's name. 11 | // ------------------------------------------------------------------- 12 | } 13 | 14 | ## Topics 15 | 16 | - ``OSCNumberValueBase`` 17 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``OSCKit`` 2 | 3 | Open Sound Control (OSC) library for macOS, iOS and tvOS. 4 | 5 | ## Overview 6 | 7 | ![OSCKit](osckit-banner.png) 8 | 9 | - OSC address pattern matching and dispatch 10 | - Convenient OSC message value type masking, validation and strong-typing 11 | - Modular: use the provided UDP network layer by default, or use your own 12 | - Support for custom OSC types 13 | - Supports Swift 6 Concurrency 14 | - Fully unit tested 15 | - Full DocC documentation 16 | 17 | @Comment { 18 | // ------------------------------------------------------------------- 19 | // NOTE: The following is identical between the OSCKit and OSCKitCore 20 | // docc bundles, except that the OSCKit docc adds 'Getting Started' 21 | // and 'I/O' topic sections at the top of the Topics list. 22 | // ------------------------------------------------------------------- 23 | } 24 | 25 | ## Topics 26 | 27 | ### Welcome 28 | - 29 | - 30 | - 31 | - 32 | 33 | ### OSC I/O 34 | 35 | - ``OSCClient`` 36 | - ``OSCServer`` 37 | - ``OSCSocket`` 38 | - ``OSCTimeTagMode`` 39 | - ``OSCHandlerBlock`` 40 | 41 | ### OSC Bundles 42 | 43 | - ``OSCBundle`` 44 | - ``OSCTimeTag`` 45 | 46 | ### OSC Messages 47 | 48 | - ``OSCMessage`` 49 | - 50 | - 51 | - 52 | 53 | ### OSC Data Protocol 54 | 55 | - ``OSCObject`` 56 | - ``OSCObjectType`` 57 | 58 | ### Advanced 59 | 60 | - 61 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/Getting-Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Getting started with using OSCKit in your application. 4 | 5 | ## Import the Library 6 | 7 | Import the library: 8 | 9 | ```swift 10 | import OSCKit 11 | ``` 12 | 13 | Or to import OSCKit without networking I/O in order to implement your own sockets: 14 | 15 | ```swift 16 | import OSCKitCore 17 | ``` 18 | 19 | ## Classes 20 | 21 | - term ``OSCClient``: Send OSC messages. 22 | - term ``OSCServer``: Receive OSC messages. 23 | - term ``OSCSocket``: Send and receive OSC messages using a single local port. 24 | 25 | ## Value Types 26 | 27 | - 28 | 29 | ## Sending and Receiving 30 | 31 | - 32 | - 33 | - 34 | - 35 | 36 | ## Example Code 37 | 38 | The [Examples](https://github.com/orchetect/OSCKit/tree/main/Examples) folder contains projects to quickly get started. 39 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/OSC-Custom-Types.md: -------------------------------------------------------------------------------- 1 | # Custom OSC Value Types 2 | 3 | OSCKit supports custom OSC message value types. 4 | 5 | @Comment { 6 | // ------------------------------------------------------------------- 7 | // NOTE: This file is duplicated in both OSCKit and OSCKitCore targets. 8 | // Ensure both files are updated when making changes. 9 | // ------------------------------------------------------------------- 10 | } 11 | 12 | ## Overview 13 | 14 | Implementing custom OSC value types is possible but is an advanced feature and not recommended unless there are special use cases. 15 | 16 | OSCKit provides built-in support for all standard value types and in nearly all use cases they are sufficient. (See for a full list of value types already supported by OSCKit.) 17 | 18 | Note that custom value types are proprietary and only your software will be able to understand them. 19 | 20 | ## Implementation 21 | 22 | In order for a custom type to be usable as an OSC message value: 23 | 24 | 1. It must conform to the ``OSCValue`` protocol. 25 | - This implies that the type also conform to `Equatable`, `Hashable`, `Sendable`, ``OSCValueCodable`` and ``OSCValueMaskable``. 26 | - See the `OSCKitCustomTypeExample` example project for a sample implementation. 27 | - See the `/Sources/OSCKitCore/OSCValue/` folder for examples of how the protocol adoptions have been implemented internally for existing types. 28 | 2. It must be registered with the global ``OSCSerialization`` singleton. 29 | - Call ``OSCSerialization/registerType(_:)`` and pass `YourType.self` once at app startup. 30 | - This registration informs OSCKit of your type and allows OSC message decoding and compatibility with the `masked()` method. 31 | 32 | ## Topics 33 | 34 | ### Protocols 35 | 36 | - ``OSCValue`` 37 | - ``OSCInterpolatedValue`` 38 | - ``OSCValueCodable`` 39 | - ``OSCValueDecodable`` 40 | - ``OSCValueEncodable`` 41 | - ``OSCValueMaskable`` 42 | 43 | ### Encoding and Decoding 44 | 45 | - ``OSCValueDecoder`` 46 | - ``OSCValueDecoderBlock`` 47 | - ``OSCValueEncoderBlock`` 48 | - ``OSCMessageEncoder`` 49 | 50 | ### Encoders and Decoders 51 | 52 | - ``OSCValueTagIdentity`` 53 | - ``OSCValueAtomicDecoder`` 54 | - ``OSCValueAtomicEncoder`` 55 | - ``OSCValueVariableDecoder`` 56 | - ``OSCValueVariableEncoder`` 57 | - ``OSCValueVariadicDecoder`` 58 | - ``OSCValueVariadicEncoder`` 59 | 60 | ### Errors 61 | 62 | - ``OSCDecodeError`` 63 | - ``OSCEncodeError`` 64 | 65 | ### Serialization 66 | 67 | - ``OSCSerialization`` 68 | - ``OSCSerializationError`` 69 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/OSCClient.md: -------------------------------------------------------------------------------- 1 | # ``OSCKit/OSCClient`` 2 | 3 | ### Setup 4 | 5 | ```swift 6 | let oscClient = OSCClient() 7 | ``` 8 | 9 | ### Sending OSC Messages 10 | 11 | See for details on how to send messages. 12 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/OSCServer.md: -------------------------------------------------------------------------------- 1 | # ``OSCKit/OSCServer`` 2 | 3 | ### Setup 4 | 5 | ```swift 6 | let oscServer: OSCServer 7 | 8 | init() { 9 | oscServer = OSCServer(port: 8000) { [weak self] message, timeTag, host, port in 10 | print("Received \(message)") 11 | } 12 | } 13 | ``` 14 | 15 | ### Receiving OSC Messages 16 | 17 | See for details on how to receive messages. 18 | 19 | ### Notes 20 | 21 | > OSC 1.0 Spec: 22 | > 23 | > With regards OSC Bundle Time Tag: 24 | > 25 | > An OSC server must have access to a representation of the correct current absolute time. OSC 26 | > does not provide any mechanism for clock synchronization. If the time represented by the OSC 27 | > Time Tag is before or equal to the current time, the OSC Server should invoke the methods 28 | > immediately. Otherwise the OSC Time Tag represents a time in the future, and the OSC server 29 | > must store the OSC Bundle until the specified time and then invoke the appropriate OSC 30 | > Methods. When bundles contain other bundles, the OSC Time Tag of the enclosed bundle must be 31 | > greater than or equal to the OSC Time Tag of the enclosing bundle. 32 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/OSCSocket.md: -------------------------------------------------------------------------------- 1 | # ``OSCKit/OSCSocket`` 2 | 3 | ### Setup 4 | 5 | If not specified during initialization, the local port will be randomly assigned by the system. The same port will be used to both listen for incoming events and send outgoing events from. This port may only be configured at the time of initialization. 6 | 7 | The remote port may be omitted, in which case the same port number as the local port will be used. The remote port may be overridden if supplied as a parameter when calling ``OSCSocket/send(_:to:port:)``. 8 | 9 | ```swift 10 | let oscSocket: OSCSocket 11 | 12 | init() { 13 | // This would be a typical setup to interact with a remote 14 | // Behringer X32. Randomly generate a local port, but always 15 | // send messages to remote port 10023. 16 | oscSocket = OSCSocket( 17 | remoteHost: "192.168.0.2", 18 | remotePort: 10023 19 | ) { [weak self] message, timeTag, host, port in 20 | print("Received \(message)") 21 | } 22 | } 23 | ``` 24 | 25 | Similar to ``OSCServer``, an ``OSCSocket`` instance must be started before it can send or receive messages. 26 | 27 | ```swift 28 | try oscSocket.start() 29 | ``` 30 | 31 | ### Sending and Receiving OSC Messages 32 | 33 | See and for details on how to send and receive messages. 34 | 35 | ### Addenda on Sending using OSCSocket 36 | 37 | The ``OSCSocket/send(_:to:port:)`` method has a slightly different behavior on ``OSCSocket`` than it does on ``OSCClient``. 38 | 39 | ```swift 40 | // The `remoteHost` and/or `remotePort` supplied at the time of 41 | // initialization can be used by default: 42 | let msg = OSCMessage("/test") 43 | try oscSocket.send(msg) 44 | 45 | // It is also possible to override the destination host and/or port 46 | // on a per-message basis: 47 | let msg = OSCMessage("/test") 48 | try oscSocket.send(msg, to: "192.168.0.3", port: 8000) 49 | ``` 50 | 51 | ### Notes 52 | 53 | > OSC 1.0 Spec: 54 | > 55 | > With regards OSC Bundle Time Tag: 56 | > 57 | > An OSC server must have access to a representation of the correct current absolute time. OSC 58 | > does not provide any mechanism for clock synchronization. If the time represented by the OSC 59 | > Time Tag is before or equal to the current time, the OSC Server should invoke the methods 60 | > immediately. Otherwise the OSC Time Tag represents a time in the future, and the OSC server 61 | > must store the OSC Bundle until the specified time and then invoke the appropriate OSC 62 | > Methods. When bundles contain other bundles, the OSC Time Tag of the enclosed bundle must be 63 | > greater than or equal to the OSC Time Tag of the enclosing bundle. 64 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/Receiving-OSC.md: -------------------------------------------------------------------------------- 1 | # Receiving OSC 2 | 3 | Receiving OSC messages and bundles. 4 | 5 | ## Overview 6 | 7 | Both ``OSCServer`` and ``OSCSocket`` are capable of receiving messages using the same API. 8 | 9 | If not already set during initialization, you may set the receiver handler using the ``OSCServer/setHandler(_:)`` or ``OSCSocket/setHandler(_:)`` method. 10 | 11 | ```swift 12 | oscServer.setHandler { [weak self] message, timeTag, host, port in 13 | do { 14 | try self?.handle(message: message, host: host, port: port) 15 | } catch { 16 | print(error) 17 | } 18 | } 19 | 20 | private func handle(message: OSCMessage, host: String, port: UInt16) throws { 21 | // handle received messages here 22 | } 23 | ``` 24 | 25 | Then start the server/socket to begin listening for inbound OSC packets. 26 | 27 | ```swift 28 | // call this once, usually during your app's startup 29 | try oscServer.start() 30 | ``` 31 | 32 | If received OSC bundles contain a future time tag and the `OSCServer` is set to `.osc1_0` mode, these bundles will be held in memory automatically and scheduled to be dispatched to the handler at the future time. 33 | 34 | Note that as per the OSC 1.1 proposal, this behavior has largely been deprecated. `OSCServer` will default to `.ignore` and not perform any scheduling unless explicitly set to `.osc1_0` mode. 35 | 36 | ## Topics 37 | 38 | - 39 | - 40 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/Resources/osckit-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orchetect/OSCKit/e764dd623499181908cb2e54c3d8cc4dd0278e00/Sources/OSCKit/OSCKit.docc/Resources/osckit-banner.png -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.docc/Sending-OSC.md: -------------------------------------------------------------------------------- 1 | # Sending OSC 2 | 3 | Sending OSC messages and bundles. 4 | 5 | ## Overview 6 | 7 | Both ``OSCClient`` and ``OSCSocket`` are capable of sending messages using the same API. 8 | 9 | Note that the `send(_:to:port:)` method on ``OSCClient`` is globally thread-safe. 10 | 11 | ### OSC Messages 12 | 13 | To send a single message, construct an ``OSCMessage`` and send it using the client or socket instance. 14 | 15 | ```swift 16 | let msg = OSCMessage("/test", values: ["string", 123]) 17 | 18 | try oscClient.send(msg, to: "192.168.1.2", port: 8000) 19 | ``` 20 | 21 | ### OSC Bundles 22 | 23 | To send multiple OSC messages or nested OSC bundles to the same destination at the same time, pack them in an `OSCBundle` and send it using the client or socket instance. 24 | 25 | ```swift 26 | // Option 1: build elements separately 27 | let msg1 = OSCMessage("/msg1") 28 | let msg2 = OSCMessage("/msg2", values: ["string", 123]) 29 | let bundle = OSCBundle([msg1, msg2]) 30 | 31 | // Option 2: build elements inline 32 | let bundle = OSCBundle([ 33 | .message("/msg1"), 34 | .message("/msg2", values: ["string", 123]) 35 | ]) 36 | 37 | // send the bundle 38 | try oscClient.send(bundle, to: "192.168.1.2", port: 8000) 39 | ``` 40 | 41 | #### Sending with a Future Time Tag 42 | 43 | OSC bundles carry a time tag. If not specified, by default a time tag equivalent to "immediate" is used, which indicates to receivers that they should handle the bundle and the message(s) it contains immediately upon receiving them. 44 | 45 | It is possible to specify a future time tag. When present, a receiver which adheres to the OSC 1.0 spec will hold the bundle in memory and handle it at the future time specified in the time tag. 46 | 47 | ```swift 48 | // by default, bundles use an immediate time tag; these two lines are identical: 49 | OSCBundle([ ... ]) 50 | OSCBundle(timeTag: .immediate(), [ ... ]) 51 | 52 | // specify a non-immediate time tag of the current time 53 | OSCBundle(timeTag: .now(), [ ... ]) 54 | 55 | // 5 seconds in the future 56 | OSCBundle(timeTag: .timeIntervalSinceNow(5.0), [ ... ]) 57 | 58 | // at the specified time as a Date instance 59 | let date = Date( ... ) 60 | OSCBundle(timeTag: .future(date), [ ... ]) 61 | 62 | // a raw time tag can also be supplied 63 | let timeTag: UInt64 = 16535555370123264000 64 | OSCBundle(timeTag: .init(timeTag), [ ... ]) 65 | ``` 66 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCKit.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | @_exported import OSCKitCore 8 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCServerUDPDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCServerUDPDelegate.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if !os(watchOS) 8 | 9 | @preconcurrency import CocoaAsyncSocket 10 | import Foundation 11 | 12 | /// Internal UDP receiver class so as to not expose `GCDAsyncUdpSocketDelegate` methods as public. 13 | final class OSCServerUDPDelegate: NSObject, GCDAsyncUdpSocketDelegate, @unchecked Sendable { // TODO: unchecked 14 | weak var oscServer: _OSCServerProtocol? 15 | 16 | init(oscServer: _OSCServerProtocol? = nil) { 17 | self.oscServer = oscServer 18 | } 19 | 20 | /// CocoaAsyncSocket receive delegate method implementation. 21 | func udpSocket( 22 | _ sock: GCDAsyncUdpSocket, 23 | didReceive data: Data, 24 | fromAddress address: Data, 25 | withFilterContext filterContext: Any? 26 | ) { 27 | guard let oscServer else { return } 28 | 29 | var remoteHost: NSString? = nil 30 | var remotePort: UInt16 = 0 31 | _ = GCDAsyncUdpSocket.getHost(&remoteHost, port: &remotePort, fromAddress: address) 32 | 33 | _handle( 34 | oscServer: oscServer, 35 | data: data, 36 | remoteHost: (remoteHost as String?) ?? "", 37 | remotePort: remotePort 38 | ) 39 | } 40 | 41 | /// Stub required to take `oscServer` as sending. 42 | private func _handle( 43 | oscServer: _OSCServerProtocol, 44 | data: Data, 45 | remoteHost: String, 46 | remotePort: UInt16 47 | ) { 48 | oscServer.receiveQueue.async { 49 | do { 50 | guard let payload = try data.parseOSC() else { return } 51 | oscServer._handle(payload: payload, remoteHost: remoteHost, remotePort: remotePort) 52 | } catch { 53 | #if DEBUG 54 | print("OSC parse error: \(error.localizedDescription)") 55 | #endif 56 | } 57 | } 58 | } 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Sources/OSCKit/OSCTimeTagMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTagMode.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Specifies an OSC server's time tag behavior. 10 | /// 11 | /// Time tag information is not altered; this simply dictates how the server reacts to time tag 12 | /// information. 13 | /// 14 | /// > OSC 1.0 Spec: 15 | /// > 16 | /// > With regards OSC Bundle Time Tag: 17 | /// > 18 | /// > An OSC server must have access to a representation of the correct current absolute time. OSC 19 | /// > does not provide any mechanism for clock synchronization. If the time represented by the OSC 20 | /// > Time Tag is before or equal to the current time, the OSC Server should invoke the methods 21 | /// > immediately. Otherwise the OSC Time Tag represents a time in the future, and the OSC server 22 | /// > must store the OSC Bundle until the specified time and then invoke the appropriate OSC 23 | /// > Methods. When bundles contain other bundles, the OSC Time Tag of the enclosed bundle must be 24 | /// > greater than or equal to the OSC Time Tag of the enclosing bundle. 25 | public enum OSCTimeTagMode { 26 | /// Adopt OSC 1.0 spec behavior where time tags may be used to schedule received OSC bundles to 27 | /// be dispatched at a future time. 28 | case osc1_0 29 | 30 | /// Ignore time tags present in OSC bundles. 31 | /// All received OSC bundles are handled immediately when received and no scheduling will occur. 32 | /// 33 | /// > OSC 1.1 Spec: 34 | /// > 35 | /// > "We also have discovered that the OSC 1.0 semantics are not very useful for the common 36 | /// > case of unidirectional OSC messaging. This is because the sender of OSC messages cannot 37 | /// > know how far ahead in time to schedule OSC messages because it cannot learn of the network 38 | /// > latency statistics seen by the receiver. 39 | /// > 40 | /// > Instead of outlawing [common implementations by the OSC community] or other future 41 | /// > scenarios we have decided to embrace all of them by simply not specifying time tag 42 | /// > semantics at all in OSC 1.1. The specification simply provides a place in the stream for a 43 | /// > time-tag, defines units for it and we still require that the least significant bit is 44 | /// > reserved to mean 'immediately'." 45 | case ignore 46 | } 47 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/API Evolution/OSCKitCore-API-1.2.0.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCKitCore-API-1.2.0.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCAddressSpace { 10 | @_documentation(visibility: internal) 11 | @available(*, deprecated, renamed: "MethodBlock") 12 | public typealias LegacyMethodBlock = @Sendable (_ values: OSCValues) async -> Void 13 | 14 | @_documentation(visibility: internal) 15 | @available( 16 | *, 17 | deprecated, 18 | message: "Handler closure now takes 3 parameters: values, host, port." 19 | ) 20 | @_disfavoredOverload 21 | @discardableResult 22 | public func register( 23 | localAddress address: String, 24 | block: @escaping LegacyMethodBlock 25 | ) -> MethodID { 26 | register(localAddress: address) { values, host, port in 27 | await block(values) 28 | } 29 | } 30 | } 31 | 32 | extension OSCAddressSpace { 33 | @_documentation(visibility: internal) 34 | @available(*, deprecated, renamed: "dispatch(message:host:port:)") 35 | @_disfavoredOverload 36 | @discardableResult 37 | public func dispatch( 38 | _ message: OSCMessage 39 | ) -> [MethodID] { 40 | dispatch(message: message, host: "", port: 0) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCAddressPattern/Component/Component Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Component Token.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCAddressPattern.Component { 10 | enum Token { 11 | /// One or more sequential literal characters. 12 | case literal(String) 13 | 14 | /// `*` 15 | case zeroOrMoreWildcard 16 | 17 | /// `?` 18 | case singleCharWildcard 19 | 20 | /// `[]` bracket expression. 21 | case singleChar(isExclusion: Bool, groups: Set) 22 | 23 | /// `{}` curly-brace expression. 24 | case strings(strings: Set) 25 | } 26 | } 27 | 28 | extension OSCAddressPattern.Component.Token: Equatable { } 29 | 30 | extension OSCAddressPattern.Component.Token: Hashable { } 31 | 32 | extension OSCAddressPattern.Component.Token: Sendable { } 33 | 34 | extension OSCAddressPattern.Component.Token { 35 | enum CharacterGroup: Equatable, Hashable { 36 | /// Single character. 37 | case single(Character) 38 | 39 | /// Contiguous range of ASCII characters. 40 | case asciiRange(start: Character, end: Character) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCAddressPattern/Component/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Component.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCAddressPattern { 10 | /// OSC Address Component. 11 | /// A tokenized pattern of an individual path component in an OSC address pattern. 12 | /// 13 | /// For a detailed discussion on OSC address pattern matching, see the inline documentation for 14 | /// `OSCAddressPattern`. 15 | struct Component { 16 | var tokens: [Token] = [] 17 | 18 | init() { } 19 | } 20 | } 21 | 22 | extension OSCAddressPattern.Component: Equatable { } 23 | 24 | extension OSCAddressPattern.Component: Hashable { } 25 | 26 | extension OSCAddressPattern.Component: Sendable { } 27 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCAddressPattern/OSCAddressPattern Strings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCAddressPattern Strings.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - Address Components 10 | 11 | extension OSCAddressPattern { 12 | /// Returns the address pattern as a string. 13 | public var stringValue: String { 14 | rawAddress 15 | } 16 | 17 | /// Returns the address as individual path components (strings between `/` separators). 18 | public var pathComponents: [Substring] { 19 | rawAddress.oscAddressPathComponents 20 | } 21 | } 22 | 23 | // MARK: - Address Pattern 24 | 25 | extension OSCAddressPattern { 26 | /// Utility: 27 | /// Returns the raw OSC address pattern converted to a tokenized pattern. 28 | var components: [Component] { 29 | pathComponents 30 | .map { Component(string: String($0)) } 31 | } 32 | 33 | /// Returns `true` if the address matches a given local address. 34 | /// Employs address pattern matching if the inbound address contains a pattern. 35 | public func matches(localAddress: String) -> Bool { 36 | let selfPattern = components 37 | guard !selfPattern.isEmpty else { return false } 38 | 39 | let localAddressComponents = localAddress.oscAddressPathComponents 40 | 41 | guard selfPattern.count == localAddressComponents.count else { return false } 42 | 43 | for idx in 0 ..< selfPattern.count { 44 | guard idx < localAddressComponents.count, 45 | selfPattern[idx].evaluate(matching: localAddressComponents[idx]) 46 | else { return false } 47 | } 48 | 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCAddressPattern/OSCAddressPattern init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCAddressPattern init.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | #if compiler(>=6.0) 10 | internal import SwiftASCII // ASCIIString 11 | #else 12 | @_implementationOnly import SwiftASCII // ASCIIString 13 | #endif 14 | 15 | extension OSCAddressPattern { 16 | /// Create an OSC address from a raw `String` address. 17 | /// The string will be converted to valid ASCII, lossily converting or removing invalid 18 | /// non-ASCII characters if necessary. 19 | public init(_ lossy: String) { 20 | let asciiLossy = ASCIIString(lossy) 21 | self.init(ascii: asciiLossy) 22 | } 23 | 24 | /// Internal: 25 | /// Create an OSC address from a raw `ASCIIString` address. 26 | init(ascii address: ASCIIString) { 27 | rawAddress = address.stringValue 28 | rawData = address.rawData 29 | } 30 | 31 | /// Create an OSC address from individual path components. 32 | /// The path component strings will be converted to valid ASCII, lossily converting or removing 33 | /// invalid non-ASCII characters if necessary. 34 | /// Empty path components is equivalent to the address of "/". 35 | public init(pathComponents lossy: S) where S: BidirectionalCollection, 36 | S.Element: StringProtocol 37 | { 38 | let formedAddress = ("/" + lossy.joined(separator: "/")) 39 | self.init(formedAddress) 40 | } 41 | 42 | /// Internal: 43 | /// Create an OSC address from individual path components. 44 | /// The path component strings will be converted to ASCII strings, lossily converting or 45 | /// removing invalid non-ASCII characters if necessary. 46 | /// Empty path components is equivalent to the address of "/". 47 | init(asciiPathComponents: S) where S: BidirectionalCollection, S.Element == ASCIIString { 48 | let ascii = ASCIICharacter("/") + asciiPathComponents.joined(separator: "/") 49 | self.init(ascii: ascii) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCAddressSpace/OSCAddressSpace MethodID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCAddressSpace MethodID.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCAddressSpace { 10 | /// A unique identifier corresponding to an OSC Method that was registered. 11 | public struct MethodID { 12 | let uuid = UUID() 13 | } 14 | } 15 | 16 | extension OSCAddressSpace.MethodID: Equatable { } 17 | 18 | extension OSCAddressSpace.MethodID: Hashable { } 19 | 20 | extension OSCAddressSpace.MethodID: Sendable { } 21 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCBundle/OSCBundle Data Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCBundle Data Extensions.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension Data { 10 | /// A fast function to test if Data() begins with an OSC bundle header 11 | /// (Note: Does NOT do extensive checks to ensure data block isn't malformed) 12 | var appearsToBeOSCBundle: Bool { 13 | starts(with: OSCBundle.header) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCBundle/OSCBundle init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCBundle init.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // NOTE: Overloads that take variadic values were tested, 10 | // however for code consistency and conventional indentation, it is 11 | // undesirable to have variadic parameters. 12 | 13 | extension OSCBundle { 14 | /// OSC Bundle. 15 | public init( 16 | timeTag: OSCTimeTag? = nil, 17 | _ elements: [any OSCObject] = [] 18 | ) { 19 | self.timeTag = timeTag ?? .init(1) 20 | self.elements = elements 21 | _rawData = nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCKitCore.docc/AnyOSCNumberValue.md: -------------------------------------------------------------------------------- 1 | # ``OSCKitCore/AnyOSCNumberValue`` 2 | 3 | @Comment { 4 | // ------------------------------------------------------------------- 5 | // NOTE: This file is duplicated in both OSCKit and OSCKitCore targets. 6 | // Ensure both files are updated when making changes. 7 | // ------------------------------------------------------------------- 8 | // ------------------------------------------------------------------- 9 | // NOTE: The top line of this file must be edited to begin with the 10 | // current target's name. 11 | // ------------------------------------------------------------------- 12 | } 13 | 14 | ## Topics 15 | 16 | - ``OSCNumberValueBase`` 17 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCKitCore.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``OSCKitCore`` 2 | 3 | Open Sound Control (OSC) library for macOS, iOS and tvOS. 4 | 5 | ## Overview 6 | 7 | ![OSCKit](osckit-banner.png) 8 | 9 | - OSC address pattern matching and dispatch 10 | - Convenient OSC message value type masking, validation and strong-typing 11 | - Modular: use the provided UDP network layer by default, or use your own 12 | - Support for custom OSC types 13 | - Supports Swift 6 Concurrency 14 | - Fully unit tested 15 | - Full DocC documentation 16 | 17 | 18 | - Tip: For a Getting Started guide, see the `OSCKit` module documentation. 19 | 20 | @Comment { 21 | // ------------------------------------------------------------------- 22 | // NOTE: The following is identical between the OSCKit and OSCKitCore 23 | // docc bundles, except that the OSCKit docc adds 'Getting Started' 24 | // and 'I/O' topic sections at the top of the Topics list. 25 | // ------------------------------------------------------------------- 26 | } 27 | 28 | ## Topics 29 | 30 | ### OSC Bundles 31 | 32 | - ``OSCBundle`` 33 | - ``OSCTimeTag`` 34 | 35 | ### OSC Messages 36 | 37 | - ``OSCMessage`` 38 | - 39 | - 40 | - 41 | 42 | ### OSC Data Protocol 43 | 44 | - ``OSCObject`` 45 | - ``OSCObjectType`` 46 | 47 | ### Advanced 48 | 49 | - 50 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCKitCore.docc/OSC-Custom-Types.md: -------------------------------------------------------------------------------- 1 | # Custom OSC Value Types 2 | 3 | OSCKit supports custom OSC message value types. 4 | 5 | @Comment { 6 | // ------------------------------------------------------------------- 7 | // NOTE: This file is duplicated in both OSCKit and OSCKitCore targets. 8 | // Ensure both files are updated when making changes. 9 | // ------------------------------------------------------------------- 10 | } 11 | 12 | ## Overview 13 | 14 | Implementing custom OSC value types is possible but is an advanced feature and not recommended unless there are special use cases. 15 | 16 | OSCKit provides built-in support for all standard value types and in nearly all use cases they are sufficient. (See for a full list of value types already supported by OSCKit.) 17 | 18 | Note that custom value types are proprietary and only your software will be able to understand them. 19 | 20 | ## Implementation 21 | 22 | In order for a custom type to be usable as an OSC message value: 23 | 24 | 1. It must conform to the ``OSCValue`` protocol. 25 | - This implies that the type also conform to `Equatable`, `Hashable`, `Sendable`, ``OSCValueCodable`` and ``OSCValueMaskable``. 26 | - See the `OSCKitCustomTypeExample` example project for a sample implementation. 27 | - See the `/Sources/OSCKitCore/OSCValue/` folder for examples of how the protocol adoptions have been implemented internally for existing types. 28 | 2. It must be registered with the global ``OSCSerialization`` singleton. 29 | - Call ``OSCSerialization/registerType(_:)`` and pass `YourType.self` once at app startup. 30 | - This registration informs OSCKit of your type and allows OSC message decoding and compatibility with the `masked()` method. 31 | 32 | ## Topics 33 | 34 | ### Protocols 35 | 36 | - ``OSCValue`` 37 | - ``OSCInterpolatedValue`` 38 | - ``OSCValueCodable`` 39 | - ``OSCValueDecodable`` 40 | - ``OSCValueEncodable`` 41 | - ``OSCValueMaskable`` 42 | 43 | ### Encoding and Decoding 44 | 45 | - ``OSCValueDecoder`` 46 | - ``OSCValueDecoderBlock`` 47 | - ``OSCValueEncoderBlock`` 48 | - ``OSCMessageEncoder`` 49 | 50 | ### Encoders and Decoders 51 | 52 | - ``OSCValueTagIdentity`` 53 | - ``OSCValueAtomicDecoder`` 54 | - ``OSCValueAtomicEncoder`` 55 | - ``OSCValueVariableDecoder`` 56 | - ``OSCValueVariableEncoder`` 57 | - ``OSCValueVariadicDecoder`` 58 | - ``OSCValueVariadicEncoder`` 59 | 60 | ### Errors 61 | 62 | - ``OSCDecodeError`` 63 | - ``OSCEncodeError`` 64 | 65 | ### Serialization 66 | 67 | - ``OSCSerialization`` 68 | - ``OSCSerializationError`` 69 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCKitCore.docc/Resources/osckit-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orchetect/OSCKit/e764dd623499181908cb2e54c3d8cc4dd0278e00/Sources/OSCKitCore/OSCKitCore.docc/Resources/osckit-banner.png -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCKitCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCKitCore.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | // Welcome to OSCKit :) 8 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCMessage/OSCMessage Data Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCMessage Data Extensions.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension Data { 10 | /// A fast test if `Data` appears to be an OSC message. 11 | /// (Note: Does NOT do extensive checks to ensure message isn't malformed.) 12 | @_disfavoredOverload 13 | var appearsToBeOSCMessage: Bool { 14 | // it's possible an OSC address won't start with "/", but it should! 15 | starts(with: OSCMessage.header) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCMessage/OSCMessage init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCMessage init.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | #if compiler(>=6.0) 10 | internal import SwiftASCII // internal inits 11 | #else 12 | @_implementationOnly import SwiftASCII // internal inits 13 | #endif 14 | 15 | // NOTE: Overloads that take variadic values were tested, 16 | // however for code consistency and proper indentation, it is 17 | // undesirable to have variadic parameters. 18 | 19 | extension OSCMessage { 20 | /// Create an OSC message from a raw `String` address pattern and zero or more OSC values 21 | /// (arguments). 22 | /// The address string will be converted to an ASCII string, lossily converting or removing 23 | /// invalid non-ASCII characters if necessary. 24 | @_disfavoredOverload 25 | public init( 26 | _ addressPattern: String, 27 | values: OSCValues = [] 28 | ) { 29 | self.addressPattern = OSCAddressPattern(addressPattern) 30 | self.values = values 31 | _rawData = nil 32 | } 33 | 34 | /// Create an OSC message from an ``OSCAddressPattern`` and zero or more OSC values (arguments). 35 | public init( 36 | _ addressPattern: OSCAddressPattern, 37 | values: OSCValues = [] 38 | ) { 39 | self.addressPattern = addressPattern 40 | self.values = values 41 | _rawData = nil 42 | } 43 | 44 | /// Create an OSC message from OSC address pattern path components and zero or more OSC values 45 | /// (arguments). 46 | /// Empty path components is equivalent to the address of "/". 47 | public init( 48 | addressPattern pathComponents: S, 49 | values: OSCValues = [] 50 | ) where S: BidirectionalCollection, S.Element: StringProtocol { 51 | addressPattern = OSCAddressPattern(pathComponents: pathComponents) 52 | self.values = values 53 | _rawData = nil 54 | } 55 | 56 | // MARK: - SwiftASCII typed 57 | 58 | /// Internal: 59 | /// Create an OSC message from a raw ``ASCIIString`` address pattern and zero or more OSC values 60 | /// (arguments). 61 | init( 62 | asciiAddressPattern address: ASCIIString, 63 | values: OSCValues = [] 64 | ) { 65 | addressPattern = OSCAddressPattern(ascii: address) 66 | self.values = values 67 | _rawData = nil 68 | } 69 | 70 | /// Internal: 71 | /// Create an OSC message from ``ASCIIString`` OSC address pattern path components and zero or 72 | /// more OSC values (arguments). 73 | /// Empty path components is equivalent to the address of "/". 74 | init( 75 | asciiAddressPattern pathComponents: S, 76 | values: OSCValues = [] 77 | ) where S: BidirectionalCollection, S.Element == ASCIIString { 78 | addressPattern = OSCAddressPattern(asciiPathComponents: pathComponents) 79 | self.values = values 80 | _rawData = nil 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCMessage/OSCMessage rawData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCMessage rawData.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - OSCMessage 10 | 11 | /// OSC Message. 12 | extension OSCMessage { 13 | /// Initialize by parsing raw OSC message data bytes. 14 | public init(from rawData: Data) throws { 15 | // cache raw data 16 | _rawData = rawData 17 | 18 | let decoded = try OSCMessageDecoder.decode(rawData: rawData) 19 | 20 | // update public properties 21 | addressPattern = .init(decoded.addressPattern) 22 | values = decoded.values 23 | } 24 | 25 | // (inline docs inherited from OSCObject protocol) 26 | public func rawData() throws -> Data { 27 | // return cached data if struct was originally initialized from raw data 28 | // so we don't needlessly church CPU cycles to generate the data 29 | if let cached = _rawData { 30 | return cached 31 | } 32 | 33 | let encoder = try OSCMessageEncoder( 34 | addressPattern: addressPattern, 35 | values: values 36 | ) 37 | 38 | return encoder.rawOSCMessageData() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCObject/OSCObject Data Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCObject Data Extensions.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension Data { 10 | /// Parses raw data and returns valid OSC objects if data is successfully parsed as OSC. 11 | /// 12 | /// - Throws: An error is thrown if data appears to be an OSC bundle or message but is 13 | /// malformed. 14 | /// 15 | /// Errors thrown will typically be a case of ``OSCDecodeError`` but other errors may be thrown. 16 | /// 17 | /// - Returns: Decoded ``OSCObject``, or `nil` if not an OSC data packet. 18 | public func parseOSC() throws -> (any OSCObject)? { 19 | if appearsToBeOSCBundle { 20 | return try OSCBundle(from: self) 21 | } else if appearsToBeOSCMessage { 22 | return try OSCMessage(from: self) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | /// Test if data appears to be an OSC bundle or OSC message. (Basic validation) 29 | /// 30 | /// - Returns: An ``OSCObjectType`` case if validation succeeds. 31 | public var oscObjectType: OSCObjectType? { 32 | if appearsToBeOSCBundle { 33 | return .bundle 34 | } else if appearsToBeOSCMessage { 35 | return .message 36 | } 37 | 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCObject/OSCObject Static Constructors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCObject Static Constructors.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // NOTE: Overloads that take variadic values were tested, 10 | // however for code consistency and proper indentation, it is 11 | // undesirable to have variadic parameters. 12 | 13 | // MARK: - OSCMessage 14 | 15 | extension OSCObject where Self == OSCMessage { 16 | /// OSC Message. 17 | public static func message( 18 | _ addressPattern: String, 19 | values: OSCValues = [] 20 | ) -> Self { 21 | OSCMessage( 22 | OSCAddressPattern(addressPattern), 23 | values: values 24 | ) 25 | } 26 | 27 | /// OSC Message. 28 | public static func message( 29 | _ addressPattern: OSCAddressPattern, 30 | values: OSCValues = [] 31 | ) -> Self { 32 | OSCMessage( 33 | addressPattern, 34 | values: values 35 | ) 36 | } 37 | } 38 | 39 | // MARK: - OSCBundle 40 | 41 | extension OSCObject where Self == OSCBundle { 42 | /// OSC Bundle. 43 | public static func bundle( 44 | timeTag: OSCTimeTag? = nil, 45 | _ elements: [any OSCObject] = [] 46 | ) -> Self { 47 | OSCBundle( 48 | timeTag: timeTag, 49 | elements 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCObject/OSCObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCObject.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - OSCObject 10 | 11 | /// Protocol adopted by all OSC data objects, namely ``OSCMessage`` and ``OSCBundle``. 12 | public protocol OSCObject: Equatable, Hashable where Self: Sendable { 13 | /// Enum case describing the OSC object type. 14 | static var oscObjectType: OSCObjectType { get } 15 | 16 | /// Returns raw OSC data constructed from the struct's properties. 17 | func rawData() throws -> Data 18 | 19 | /// Initialize by parsing raw OSC packet data bytes. 20 | init(from rawData: Data) throws 21 | } 22 | 23 | extension OSCObject { 24 | /// Enum case describing the OSC object type. 25 | public var oscObjectType: OSCObjectType { Self.oscObjectType } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCObject/OSCObjectType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCObjectType.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Enum describing an OSC message type. 8 | public enum OSCObjectType: Equatable, Hashable, Sendable { 9 | case message 10 | case bundle 11 | } 12 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCSerialization/OSCSerializationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCSerializationError.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Error type returned by ``OSCSerialization`` methods. 10 | public enum OSCSerializationError: LocalizedError { 11 | /// One more static tags associated with the type's tag identity are already registered. 12 | case tagAlreadyRegistered 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .tagAlreadyRegistered: 17 | "Tag already registered" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCSerialization/OSCValueTagIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueTagIdentity.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Declarative description of how an OSC value represents itself with OSC message type tag(s). 8 | public enum OSCValueTagIdentity: Equatable, Hashable { 9 | /// Atomic: 10 | /// The OSC value is identified with a single static OSC-type tag character. 11 | /// 12 | /// Most OSC values are represented in this manner. ie: `i` for `Int32`, `s` for `String`, etc. 13 | case atomic(Character) 14 | 15 | /// Variable: 16 | /// The OSC value occupies a single static OSC-type tag character but it varies depending on the 17 | /// content of the value. 18 | /// All possible type tags must be finite and known at compile time. 19 | /// 20 | /// An example is Boolean (true/false). A single instance of the concrete type (`Bool`) occupies 21 | /// a single tag character but the character may be `T` or `F`. 22 | case variable([Character]) 23 | 24 | /// Variadic: 25 | /// The OSC "value" may be complex in nature and occupies one or more OSC-type tag characters 26 | /// and they are not reasonably known at compile time. This scenario requires manual parsing and 27 | /// validation. Very few implementations will require this pattern. 28 | /// 29 | /// One (perhaps the only) example of a variadic implementation is an OSC value array. It starts 30 | /// with the `[` tag and ends with the `]` tag and may contain zero or more atomic values. It 31 | /// would have a `minCount` of 2 and a `maxCount` of `nil`. Variadic is how OSCKit itself 32 | /// internally implements OSC array encoding and decoding. 33 | case variadic(minCount: Int?, maxCount: Int?) 34 | } 35 | 36 | // MARK: - Sendable 37 | 38 | extension OSCValueTagIdentity: Sendable { } 39 | 40 | // MARK: - Methods 41 | 42 | extension OSCValueTagIdentity { 43 | /// Returns `true` if the identity matches the given tag. 44 | /// If the identity is variadic, `false` is always returned. 45 | public func isEqual(to otherTag: Character) -> Bool { 46 | switch self { 47 | case let .atomic(character): 48 | otherTag == character 49 | case let .variable(array): 50 | array.contains(otherTag) 51 | case .variadic: 52 | false 53 | } 54 | } 55 | 56 | /// Returns static tag(s) of the tag identity. 57 | /// If the identity is variadic, an empty array is always returned since the tags are known only 58 | /// to the type's `OSCValueCodable` implementation. 59 | func staticTags() -> [Character] { 60 | switch self { 61 | case let .atomic(character): 62 | [character] 63 | case let .variable(array): 64 | array 65 | case .variadic: 66 | [] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCTimeTag/OSCTimeTag Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag Codable.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCTimeTag: Codable { 10 | public init(from decoder: Decoder) throws { 11 | let container = try decoder.singleValueContainer() 12 | 13 | let value = try container.decode(UInt64.self) 14 | self.init(value) 15 | } 16 | 17 | public func encode(to encoder: Encoder) throws { 18 | var container = encoder.singleValueContainer() 19 | try container.encode(rawValue) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCTimeTag/OSCTimeTag Properties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag Properties.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCTimeTag { 10 | /// Returns `true` if the Time Tag is considered immediate. 11 | /// (Special raw value of 1). 12 | public var isImmediate: Bool { 13 | rawValue == 1 14 | } 15 | 16 | /// Returns `true` if the Time Tag is a time in the future. 17 | public var isFuture: Bool { 18 | guard !isImmediate else { return false } 19 | return rawValue > OSCTimeTag.now().rawValue 20 | } 21 | 22 | /// Returns the time tag converted to a `Date` instance. 23 | public var date: Date { 24 | isImmediate 25 | ? Date() 26 | : Self.primeEpoch.addingTimeInterval(rawValue.seconds(era: era)) 27 | } 28 | 29 | /// Returns the delta time interval in seconds between _now_ and the Time Tag. 30 | public func timeIntervalSinceNow() -> TimeInterval { 31 | timeInterval(since: Date()) 32 | } 33 | 34 | /// Returns the delta time interval in seconds between an arbitrary `Date` and the Time Tag. 35 | public func timeInterval(since other: Date) -> TimeInterval { 36 | isImmediate 37 | ? 0.0 38 | : date.timeIntervalSince(other) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCTimeTag/OSCTimeTag Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag Utilities.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCTimeTag { 10 | /// Prime epoch (NTP era 0). 11 | static let primeEpoch: Date = DateComponents( 12 | calendar: .current, 13 | timeZone: .current, 14 | year: 1900, 15 | month: 1, 16 | day: 1, 17 | hour: 0, 18 | minute: 0, 19 | second: 0 20 | ).date ?? Date() 21 | 22 | /// Duration in seconds of an NTP era (approx 136.1 years). 23 | static let eraDuration = TimeInterval(UInt32.max) 24 | 25 | /// Returns the current era. 26 | static var currentEra: Int { 27 | Int(Date().timeIntervalSince(primeEpoch) / eraDuration) 28 | } 29 | } 30 | 31 | extension TimeInterval { 32 | /// Returns raw OSC Time Tag value from `TimeInterval` as seconds from 1990. 33 | var oscTimeTag: (timeTag: OSCTimeTag.RawValue, era: Int) { 34 | // early return for negative values or zero 35 | guard self > .zero else { 36 | return (timeTag: 0, era: 0) 37 | } 38 | 39 | // early return for prime epoch period (era 0) 40 | if self <= Double(UInt32.max) { 41 | let timeTag = UInt64(self * 0x1_0000_0000) 42 | return (timeTag: timeTag, era: 0) 43 | } 44 | 45 | // calculate era (rollover count) 46 | let calc = quotientAndRemainder(dividingBy: OSCTimeTag.eraDuration) 47 | 48 | let timeTag = calc.remainder * 0x1_0000_0000 49 | 50 | return (timeTag: UInt64(timeTag), era: Int(calc.remainder)) 51 | } 52 | } 53 | 54 | extension OSCTimeTag.RawValue { 55 | /// Returns total elapsed seconds of the Time Tag since prime epoch. 56 | func seconds(era: Int) -> TimeInterval { 57 | let secs = TimeInterval(self) / 0x1_0000_0000 58 | if era == 0 { 59 | return secs 60 | } else if era == 1 { 61 | return OSCTimeTag.eraDuration + secs 62 | } else { 63 | return (TimeInterval(era) * OSCTimeTag.eraDuration) + secs 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCTimeTag/OSCTimeTag init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag init.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCTimeTag { 10 | /// Initialize from a raw OSC Time Tag value. 11 | /// 12 | /// If the intention is to produce an immediate Time Tag, use ``immediate()`` instead. 13 | public init(_ rawValue: RawValue, era: Int = 0) { 14 | self.era = era 15 | self.rawValue = rawValue 16 | } 17 | 18 | /// Returns a Time Tag representing a time in the future. 19 | /// 20 | /// If the intention is to produce an immediate Time Tag, use ``immediate()`` instead of 21 | /// `init(timeIntervalSinceNow: 0.0)`. 22 | /// 23 | /// Passing `seconds` that is `< 0.0` will produce a Time Tag of ``now()``. 24 | public init(timeIntervalSinceNow seconds: TimeInterval) { 25 | if seconds.isZero { 26 | self = Self.now() 27 | return 28 | } 29 | let futureDate = Date(timeIntervalSinceNow: seconds) 30 | self.init(future: futureDate) 31 | } 32 | 33 | /// Returns a Time Tag formed from total elapsed seconds since 1990 (prime epoch). 34 | public init(timeIntervalSince1900 seconds: TimeInterval) { 35 | let converted = seconds.oscTimeTag 36 | self.init(converted.timeTag, era: converted.era) 37 | } 38 | 39 | /// Returns a Time Tag representing a time in the future. 40 | /// 41 | /// If the intention is to produce an immediate Time Tag, use ``immediate()`` instead of 42 | /// `init(at: Date())`. 43 | /// 44 | /// Passing `Date` that is `< now` will produce a Time Tag of ``now()``. 45 | public init(future futureDate: Date) { 46 | self.init(timeIntervalSince1900: futureDate.timeIntervalSince(Self.primeEpoch)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCTimeTag/OSCTimeTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// OSC Time Tag. 10 | /// 11 | /// A special timestamp used in an OSC bundle. In most situations, it is recommended to ignore this 12 | /// time stamp and use the default value. 13 | /// 14 | /// > OSC 1.0 Spec: 15 | /// > 16 | /// > Time tags are represented by a 64 bit fixed point number. The first 32 bits specify the number 17 | /// > of seconds since midnight on January 1, 1900, and the last 32 bits specify fractional parts of 18 | /// > a second to a precision of about 200 picoseconds. This is the representation used by Internet 19 | /// > NTP timestamps. 20 | /// > 21 | /// > The time tag value consisting of 63 zero bits followed by a one in the least significant bit 22 | /// > (essentially a `UInt64` integer value of 1) is a special case meaning 'immediately.' 23 | public struct OSCTimeTag { 24 | /// NTP time format era number. 25 | /// 26 | /// The OSC Time Tag encoding uses NTPv3 time encoding which specifies 32 bits for seconds and 27 | /// 32 bits for fractional sections. Given that the prime epoch (era 0) is the year 1990, 32 bit 28 | /// seconds storage rolls over approximate every 136 years. Every time this rollover occurs, the 29 | /// era increments by 1. 30 | /// 31 | /// Hence, the period between 1 Jan 1900 and 7 Feb 2036 is considered era 0. Dates after 7 Feb 32 | /// 2036 for the following 136 years are considered era 1. And so on. 33 | /// 34 | /// - Note: See https://www.eecis.udel.edu/~mills/y2k.html for details. 35 | public let era: Int 36 | 37 | /// Raw value type (aka `UInt64`) 38 | public typealias RawValue = UInt64 39 | 40 | /// Raw Time Tag value as encoded in OSC. 41 | public let rawValue: RawValue 42 | } 43 | 44 | // MARK: - Equatable, Hashable 45 | 46 | extension OSCTimeTag: Equatable, Hashable { 47 | // implementation is automatically synthesized by Swift 48 | } 49 | 50 | // MARK: - Sendable 51 | 52 | extension OSCTimeTag: Sendable { } 53 | 54 | // MARK: - CustomStringConvertible 55 | 56 | extension OSCTimeTag: CustomStringConvertible { 57 | public var description: String { 58 | if rawValue == 1 { 59 | return "1 (immediate)" 60 | } 61 | 62 | switch era { 63 | case 0: 64 | return "\(rawValue)" 65 | default: 66 | return "\(rawValue)-era:\(era)" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Core Types/Data (Blob).swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data (Blob).swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @_documentation(visibility: internal) 10 | extension Data: OSCValue { 11 | public static let oscValueToken: OSCValueToken = .blob 12 | } 13 | 14 | @_documentation(visibility: internal) 15 | extension Data: OSCValueCodable { 16 | static let oscTag: Character = "b" 17 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension Data: OSCValueEncodable { 22 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 23 | public static let oscEncoding = OSCValueEncodingBlock { value in 24 | let lengthData = value.count.int32.toData(.bigEndian) 25 | let blobData = OSCMessageEncoder.fourNullBytePadded(value) 26 | 27 | return ( 28 | tag: oscTag, 29 | data: lengthData + blobData 30 | ) 31 | } 32 | } 33 | 34 | @_documentation(visibility: internal) 35 | extension Data: OSCValueDecodable { 36 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 37 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 38 | try decoder.readBlob() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Core Types/Float32.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Float32.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @_documentation(visibility: internal) 10 | extension Float32: OSCValue { 11 | public static let oscValueToken: OSCValueToken = .float32 12 | } 13 | 14 | @_documentation(visibility: internal) 15 | extension Float32: OSCValueCodable { 16 | static let oscTag: Character = "f" 17 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension Float32: OSCValueEncodable { 22 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 23 | public static let oscEncoding = OSCValueEncodingBlock { value in 24 | (tag: oscTag, data: value.toData(.bigEndian)) 25 | } 26 | } 27 | 28 | @_documentation(visibility: internal) 29 | extension Float32: OSCValueDecodable { 30 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 31 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 32 | try decoder.readFloat32() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Core Types/Int32.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int32.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @_documentation(visibility: internal) 10 | extension Int32: OSCValue { 11 | public static let oscValueToken: OSCValueToken = .int32 12 | } 13 | 14 | @_documentation(visibility: internal) 15 | extension Int32: OSCValueCodable { 16 | static let oscTag: Character = "i" 17 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension Int32: OSCValueEncodable { 22 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 23 | public static let oscEncoding = OSCValueEncodingBlock { value in 24 | (tag: oscTag, data: value.toData(.bigEndian)) 25 | } 26 | } 27 | 28 | @_documentation(visibility: internal) 29 | extension Int32: OSCValueDecodable { 30 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 31 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 32 | try decoder.readInt32() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Core Types/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | #if compiler(>=6.0) 10 | internal import SwiftASCII // ASCIIString 11 | #else 12 | @_implementationOnly import SwiftASCII // ASCIIString 13 | #endif 14 | 15 | @_documentation(visibility: internal) 16 | extension String: OSCValue { 17 | public static let oscValueToken: OSCValueToken = .string 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension String: OSCValueCodable { 22 | static let oscTag: Character = "s" 23 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 24 | } 25 | 26 | @_documentation(visibility: internal) 27 | extension String: OSCValueEncodable { 28 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 29 | public static let oscEncoding = OSCValueEncodingBlock { value in 30 | ( 31 | tag: oscTag, 32 | data: OSCMessageEncoder.fourNullBytePadded(value.asciiStringLossy.rawData) 33 | ) 34 | } 35 | } 36 | 37 | @_documentation(visibility: internal) 38 | extension String: OSCValueDecodable { 39 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 40 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 41 | try decoder.read4ByteAlignedNullTerminatedASCIIString() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/Bool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | private let oscTypeTagTrue: Character = "T" 10 | private let oscTypeTagFalse: Character = "F" 11 | 12 | @_documentation(visibility: internal) 13 | extension Bool: OSCValue { 14 | public static let oscValueToken: OSCValueToken = .bool 15 | } 16 | 17 | @_documentation(visibility: internal) 18 | extension Bool: OSCValueCodable { 19 | public static let oscTagIdentity: OSCValueTagIdentity = .variable( 20 | [oscTypeTagTrue, oscTypeTagFalse] 21 | ) 22 | } 23 | 24 | @_documentation(visibility: internal) 25 | extension Bool: OSCValueEncodable { 26 | public typealias OSCValueEncodingBlock = OSCValueVariableEncoder 27 | public static let oscEncoding = OSCValueEncodingBlock { value in 28 | ( 29 | tag: value ? oscTypeTagTrue : oscTypeTagFalse, 30 | data: nil 31 | ) 32 | } 33 | } 34 | 35 | @_documentation(visibility: internal) 36 | extension Bool: OSCValueDecodable { 37 | public typealias OSCValueDecodingBlock = OSCValueVariableDecoder 38 | public static let oscDecoding = OSCValueDecodingBlock { tag, decoder in 39 | switch tag { 40 | case oscTypeTagTrue: 41 | return true 42 | case oscTypeTagFalse: 43 | return false 44 | default: 45 | throw OSCDecodeError.unexpectedType(tag: tag) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/Character.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Character.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | #if compiler(>=6.0) 10 | internal import SwiftASCII // ASCIICharacter 11 | #else 12 | @_implementationOnly import SwiftASCII // ASCIICharacter 13 | #endif 14 | 15 | // MARK: - OSC Encoding 16 | 17 | @_documentation(visibility: internal) 18 | extension Character: OSCValue { 19 | public static let oscValueToken: OSCValueToken = .character 20 | } 21 | 22 | @_documentation(visibility: internal) 23 | extension Character: OSCValueCodable { 24 | static let oscTag: Character = "c" 25 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 26 | } 27 | 28 | @_documentation(visibility: internal) 29 | extension Character: OSCValueEncodable { 30 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 31 | public static let oscEncoding = OSCValueEncodingBlock { value in 32 | ( 33 | tag: oscTag, 34 | data: ASCIICharacter(value) 35 | .asciiValue 36 | .int32 37 | .toData(.bigEndian) 38 | ) 39 | } 40 | } 41 | 42 | @_documentation(visibility: internal) 43 | extension Character: OSCValueDecodable { 44 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 45 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 46 | let asciiCharNum = try decoder.readInt32().int 47 | guard let asciiChar = ASCIICharacter(asciiCharNum) else { 48 | throw OSCDecodeError.malformed( 49 | "Character value couldn't be read. Could not form a Unicode scalar from the value." 50 | ) 51 | } 52 | return asciiChar.characterValue 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/Double.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @_documentation(visibility: internal) 10 | extension Double: OSCValue { 11 | public static let oscValueToken: OSCValueToken = .double 12 | } 13 | 14 | @_documentation(visibility: internal) 15 | extension Double: OSCValueCodable { 16 | static let oscTag: Character = "d" 17 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension Double: OSCValueEncodable { 22 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 23 | public static let oscEncoding = OSCValueEncodingBlock { value in 24 | ( 25 | tag: oscTag, 26 | data: value.toData(.bigEndian) 27 | ) 28 | } 29 | } 30 | 31 | @_documentation(visibility: internal) 32 | extension Double: OSCValueDecodable { 33 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 34 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 35 | try decoder.readDouble() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/Int64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int64.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @_documentation(visibility: internal) 10 | extension Int64: OSCValue { 11 | public static let oscValueToken: OSCValueToken = .int64 12 | } 13 | 14 | @_documentation(visibility: internal) 15 | extension Int64: OSCValueCodable { 16 | static let oscTag: Character = "h" 17 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension Int64: OSCValueEncodable { 22 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 23 | public static let oscEncoding = OSCValueEncodingBlock { value in 24 | ( 25 | tag: oscTag, 26 | data: value.toData(.bigEndian) 27 | ) 28 | } 29 | } 30 | 31 | @_documentation(visibility: internal) 32 | extension Int64: OSCValueDecodable { 33 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 34 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 35 | try decoder.readInt64() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/OSCImpulseValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCImpulseValue.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Impulse OSC value (also known as Infinitum or Bang) as defined by the OSC 1.0 spec. 10 | /// This type carries no data. 11 | public struct OSCImpulseValue { 12 | public init() { } 13 | } 14 | 15 | // MARK: - `any OSCValue` Constructors 16 | 17 | extension OSCValue where Self == OSCImpulseValue { 18 | /// Impulse OSC value (also known as Infinitum or Bang) as defined by the OSC 1.0 spec. 19 | /// This type carries no data. 20 | public static var impulse: Self { 21 | OSCImpulseValue() 22 | } 23 | } 24 | 25 | // MARK: - Equatable, Hashable 26 | 27 | extension OSCImpulseValue: Equatable, Hashable { 28 | // implementation is automatically synthesized by Swift 29 | } 30 | 31 | // MARK: - Sendable 32 | 33 | extension OSCImpulseValue: Sendable { } 34 | 35 | // MARK: - CustomStringConvertible 36 | 37 | extension OSCImpulseValue: CustomStringConvertible { 38 | public var description: String { 39 | "Impulse" 40 | } 41 | } 42 | 43 | // MARK: - Codable 44 | 45 | extension OSCImpulseValue: Codable { } 46 | 47 | // MARK: - OSC Encoding 48 | 49 | @_documentation(visibility: internal) 50 | extension OSCImpulseValue: OSCValue { 51 | public static let oscValueToken: OSCValueToken = .impulse 52 | } 53 | 54 | @_documentation(visibility: internal) 55 | extension OSCImpulseValue: OSCValueCodable { 56 | static let oscTag: Character = "I" 57 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 58 | } 59 | 60 | @_documentation(visibility: internal) 61 | extension OSCImpulseValue: OSCValueEncodable { 62 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 63 | public static let oscEncoding = OSCValueEncodingBlock { value in 64 | (tag: oscTag, data: nil) 65 | } 66 | } 67 | 68 | @_documentation(visibility: internal) 69 | extension OSCImpulseValue: OSCValueDecodable { 70 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 71 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 72 | OSCImpulseValue() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/OSCNullValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCNullValue.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Null OSC value as defined by the OSC 1.0 spec. 10 | /// This type carries no data. 11 | public struct OSCNullValue { 12 | public init() { } 13 | } 14 | 15 | // MARK: - `any OSCValue` Constructors 16 | 17 | extension OSCValue where Self == OSCNullValue { 18 | /// Null OSC value as defined by the OSC 1.0 spec. 19 | /// This type carries no data. 20 | public static var null: Self { 21 | OSCNullValue() 22 | } 23 | } 24 | 25 | // MARK: - Equatable, Hashable 26 | 27 | extension OSCNullValue: Equatable, Hashable { 28 | // implementation is automatically synthesized by Swift 29 | } 30 | 31 | // MARK: - Sendable 32 | 33 | extension OSCNullValue: Sendable { } 34 | 35 | // MARK: - CustomStringConvertible 36 | 37 | extension OSCNullValue: CustomStringConvertible { 38 | public var description: String { 39 | "Null" 40 | } 41 | } 42 | 43 | // MARK: - Codable 44 | 45 | extension OSCNullValue: Codable { } 46 | 47 | // MARK: - OSC Encoding 48 | 49 | @_documentation(visibility: internal) 50 | extension OSCNullValue: OSCValue { 51 | public static let oscValueToken: OSCValueToken = .null 52 | } 53 | 54 | @_documentation(visibility: internal) 55 | extension OSCNullValue: OSCValueCodable { 56 | static let oscTag: Character = "N" 57 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 58 | } 59 | 60 | @_documentation(visibility: internal) 61 | extension OSCNullValue: OSCValueEncodable { 62 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 63 | public static let oscEncoding = OSCValueEncodingBlock { value in 64 | (tag: oscTag, data: nil) 65 | } 66 | } 67 | 68 | @_documentation(visibility: internal) 69 | extension OSCNullValue: OSCValueDecodable { 70 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 71 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 72 | OSCNullValue() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Extended Types/OSCTimeTag OSCValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag OSCValue.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @_documentation(visibility: internal) 10 | extension OSCTimeTag: OSCValue { 11 | public static let oscValueToken: OSCValueToken = .timeTag 12 | } 13 | 14 | @_documentation(visibility: internal) 15 | extension OSCTimeTag: OSCValueCodable { 16 | static let oscTag: Character = "t" 17 | public static let oscTagIdentity: OSCValueTagIdentity = .atomic(oscTag) 18 | } 19 | 20 | @_documentation(visibility: internal) 21 | extension OSCTimeTag: OSCValueEncodable { 22 | public typealias OSCValueEncodingBlock = OSCValueAtomicEncoder 23 | public static let oscEncoding = OSCValueEncodingBlock { value in 24 | ( 25 | tag: oscTag, 26 | data: value.rawValue.toData(.bigEndian) 27 | ) 28 | } 29 | } 30 | 31 | @_documentation(visibility: internal) 32 | extension OSCTimeTag: OSCValueDecodable { 33 | public typealias OSCValueDecodingBlock = OSCValueAtomicDecoder 34 | public static let oscDecoding = OSCValueDecodingBlock { decoder in 35 | let rawValue = try decoder.readUInt64() 36 | return OSCTimeTag(rawValue) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Interpolated Types/BinaryFloatingPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BinaryFloatingPoint.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - BinaryFloatingPoint Default Implementation 10 | 11 | @_documentation(visibility: internal) 12 | extension OSCInterpolatedValue 13 | where Self: BinaryFloatingPoint, 14 | CoreOSCValue: BinaryFloatingPoint, 15 | OSCValueEncodingBlock == OSCValueAtomicEncoder, 16 | OSCValueDecodingBlock == OSCValueAtomicDecoder, 17 | CoreOSCValue.OSCValueEncodingBlock == OSCValueAtomicEncoder, 18 | CoreOSCValue.OSCValueDecodingBlock == OSCValueAtomicDecoder 19 | { 20 | public static var oscEncoding: OSCValueEncodingBlock { OSCValueEncodingBlock { value in 21 | try CoreOSCValue.oscEncoding.block(CoreOSCValue(value)) 22 | } } 23 | 24 | public static var oscDecoding: OSCValueDecodingBlock { OSCValueDecodingBlock { decoder in 25 | try Self(CoreOSCValue.oscDecoding.block(&decoder)) 26 | } } 27 | } 28 | 29 | // `Float` (aka `Float32`) is already a core type 30 | 31 | #if !(arch(i386) || arch(x86_64)) // Float16 won't compile for Intel 32 | @_documentation(visibility: internal) 33 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) 34 | extension Float16: OSCInterpolatedValue { 35 | public typealias CoreOSCValue = Float32 36 | } 37 | #endif 38 | 39 | #if !(arch(arm64) || arch(arm) || os(watchOS)) // Float80 is now removed for ARM 40 | @_documentation(visibility: internal) 41 | extension Float80: OSCInterpolatedValue { 42 | public typealias CoreOSCValue = Double 43 | } 44 | #endif 45 | 46 | #if canImport(CoreGraphics) 47 | import CoreGraphics 48 | 49 | @_documentation(visibility: internal) 50 | extension CGFloat: OSCInterpolatedValue { 51 | public typealias CoreOSCValue = Double 52 | } 53 | #endif 54 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Interpolated Types/BinaryInteger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BinaryInteger.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - BinaryInteger Default Implementation 10 | 11 | @_documentation(visibility: internal) 12 | extension OSCInterpolatedValue 13 | where Self: BinaryInteger, 14 | CoreOSCValue: BinaryInteger, 15 | OSCValueEncodingBlock == OSCValueAtomicEncoder, 16 | OSCValueDecodingBlock == OSCValueAtomicDecoder, 17 | CoreOSCValue.OSCValueEncodingBlock == OSCValueAtomicEncoder, 18 | CoreOSCValue.OSCValueDecodingBlock == OSCValueAtomicDecoder 19 | { 20 | public static var oscEncoding: OSCValueEncodingBlock { OSCValueEncodingBlock { value in 21 | try CoreOSCValue.oscEncoding.block(CoreOSCValue(value)) 22 | } } 23 | 24 | public static var oscDecoding: OSCValueDecodingBlock { OSCValueDecodingBlock { decoder in 25 | try Self(CoreOSCValue.oscDecoding.block(&decoder)) 26 | } } 27 | } 28 | 29 | // MARK: - Individual Conformances 30 | 31 | @_documentation(visibility: internal) 32 | extension Int: OSCInterpolatedValue { 33 | // even though Int64 is a possible OSC type and would be safer, 34 | // Int32 is by far more common and is the preferred interpolation 35 | public typealias CoreOSCValue = Int32 36 | } 37 | 38 | @_documentation(visibility: internal) 39 | extension Int8: OSCInterpolatedValue { 40 | public typealias CoreOSCValue = Int32 41 | } 42 | 43 | @_documentation(visibility: internal) 44 | extension Int16: OSCInterpolatedValue { 45 | public typealias CoreOSCValue = Int32 46 | } 47 | 48 | // `Int32` is already a core type 49 | 50 | // `Int64` is already an extended type 51 | 52 | @_documentation(visibility: internal) 53 | extension UInt: OSCInterpolatedValue { 54 | public typealias CoreOSCValue = Int64 55 | } 56 | 57 | @_documentation(visibility: internal) 58 | extension UInt8: OSCInterpolatedValue { 59 | public typealias CoreOSCValue = Int32 60 | } 61 | 62 | @_documentation(visibility: internal) 63 | extension UInt16: OSCInterpolatedValue { 64 | public typealias CoreOSCValue = Int32 65 | } 66 | 67 | @_documentation(visibility: internal) 68 | extension UInt32: OSCInterpolatedValue { 69 | public typealias CoreOSCValue = Int64 70 | } 71 | 72 | @_documentation(visibility: internal) 73 | extension UInt64: OSCInterpolatedValue { 74 | public typealias CoreOSCValue = Int64 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/OSCInterpolatedValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCInterpolatedValue.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - Interpolated types 10 | 11 | /// Protocol to which all interpolated OSC value types conform. 12 | /// This includes all convenience types that are not themselves core OSC value types. 13 | /// 14 | /// For example, types like `Int8` and `Int16` gain ``OSCValue`` compatibility by way of 15 | /// `OSCInterpolatedValue` conformance: they will be transparently encoded as the most common 16 | /// related OSC core type (which this example would be `Int32`). 17 | public protocol OSCInterpolatedValue: OSCValue 18 | where OSCEncoded == Self, 19 | OSCDecoded == Self 20 | { 21 | associatedtype CoreOSCValue: OSCValue 22 | } 23 | 24 | // MARK: - Default Implementation 25 | 26 | @_documentation(visibility: internal) 27 | extension OSCInterpolatedValue { 28 | public static var oscTagIdentity: OSCValueTagIdentity { 29 | CoreOSCValue.oscTagIdentity 30 | } 31 | 32 | public static var oscValueToken: OSCValueToken { 33 | CoreOSCValue.oscValueToken 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/OSCValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValue.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Protocol to which all compatible OSC value types conform. 8 | /// 9 | /// This includes standard OSC types (`Int32`, `String`, etc.) as well as interpolated types (`Int`, 10 | /// `UInt8`, etc.) and novel OSC types (``OSCImpulseValue``, ``OSCMIDIValue``, etc.). 11 | /// For a full list of types, see the "OSC Value Types" article in the OSCKit target documentation. 12 | public protocol OSCValue: Equatable, Hashable, OSCValueCodable, OSCValueMaskable where Self: Sendable { } 13 | 14 | extension OSCValue { 15 | /// Compare hashes of two ``OSCValue`` instances. 16 | /// 17 | /// - Note: This method is a workaround since the compiler will not allow `any OSCValue` to be 18 | /// seen as conforming to `Equatable`. 19 | func hashMatches(_ other: any OSCValue) -> Bool { 20 | AnyHashable(self) == AnyHashable(other) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValue/Opaque Types/OSCNumberValueBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCNumberValueBase.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Type-erased OSC number value encapsulation. 10 | public enum OSCNumberValueBase { 11 | /// Boolean value. 12 | case bool(Bool) 13 | 14 | /// Integer value. 15 | case int(any(OSCValue & BinaryInteger)) 16 | 17 | /// Floating-point value. 18 | case float(any(OSCValue & BinaryFloatingPoint)) 19 | } 20 | 21 | // MARK: - Equatable 22 | 23 | extension OSCNumberValueBase: Equatable { 24 | public static func == (lhs: OSCNumberValueBase, rhs: OSCNumberValueBase) -> Bool { 25 | lhs.anyHashable() == rhs.anyHashable() 26 | } 27 | } 28 | 29 | // MARK: - Hashable 30 | 31 | extension OSCNumberValueBase: Hashable { 32 | public func hash(into hasher: inout Hasher) { 33 | switch self { 34 | case let .bool(v): 35 | hasher.combine(v) 36 | case let .int(v): 37 | hasher.combine(v) 38 | case let .float(v): 39 | hasher.combine(v) 40 | } 41 | } 42 | } 43 | 44 | extension OSCNumberValueBase { 45 | /// Unwraps the base value and returns it as an `AnyHashable` instance. 46 | public func anyHashable() -> AnyHashable { 47 | switch self { 48 | case let .bool(v): 49 | AnyHashable(v) 50 | case let .int(v): 51 | AnyHashable(v) 52 | case let .float(v): 53 | AnyHashable(v) 54 | } 55 | } 56 | } 57 | 58 | // MARK: - Sendable 59 | 60 | extension OSCNumberValueBase: Sendable { } 61 | 62 | // MARK: - CustomStringConvertible 63 | 64 | extension OSCNumberValueBase: CustomStringConvertible { 65 | public var description: String { 66 | switch self { 67 | case let .bool(v): 68 | "\(v)" 69 | case let .int(v): 70 | "\(v)" 71 | case let .float(v): 72 | "\(v)" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCDecodeError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCDecodeError.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Error type thrown from OSC decode methods. 10 | public enum OSCDecodeError: LocalizedError { 11 | /// Malformed data. `verboseError` contains the specific reason. 12 | case malformed(_ verboseError: String) 13 | 14 | /// An unexpected OSC-value type was encountered in the data. 15 | /// `tag` contains the OSC Type Tag encountered. 16 | case unexpectedType(tag: Character) 17 | 18 | /// Internal inconsistency; decoding logic is in an unexpected state and cannot continue. 19 | /// `verboseError` contains the specific reason. 20 | case internalInconsistency(_ verboseError: String) 21 | 22 | public var errorDescription: String? { 23 | switch self { 24 | case let .malformed(verboseError): "Malformed data: \(verboseError)" 25 | case let .unexpectedType(tag: tag): "Unexpected OSC Type Tag: \(tag)" 26 | case let .internalInconsistency(verboseError): "Internal inconsistency: \(verboseError)" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCEncodeError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCEncodeError.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Error type thrown from OSC encode methods. 10 | public enum OSCEncodeError: LocalizedError { 11 | case general 12 | case unexpectedEncoder 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .general: "General error." 17 | case .unexpectedEncoder: "Unexpected encoder type." 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCValueCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueCodable.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Combined protocol that includes ``OSCValueEncodable`` & ``OSCValueDecodable``. 8 | public protocol OSCValueCodable: OSCValueEncodable & OSCValueDecodable { 9 | /// Declarative description of how an OSC value represents itself with OSC message type tag(s). 10 | static var oscTagIdentity: OSCValueTagIdentity { get } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCValueDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueDecodable.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol requirements for ``OSCValue`` decoding. 10 | public protocol OSCValueDecodable { 11 | associatedtype OSCDecoded: OSCValueDecodable 12 | associatedtype OSCValueDecodingBlock: OSCValueDecoderBlock 13 | where OSCValueDecodingBlock.OSCDecoded == OSCDecoded 14 | 15 | static var oscDecoding: OSCValueDecodingBlock { get } 16 | } 17 | 18 | // MARK: - Default Implementation 19 | 20 | extension OSCValueDecodable { 21 | public typealias OSCDecoded = Self 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCValueDecoderBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueDecoderBlock.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol that ``OSCValue`` decoder block encapsulation objects adopt. 10 | public protocol OSCValueDecoderBlock where Self: Sendable { 11 | associatedtype OSCDecoded: OSCValueDecodable 12 | } 13 | 14 | // MARK: - Decoder Blocks 15 | 16 | /// ``OSCValue`` atomic value decoder block encapsulation. 17 | public struct OSCValueAtomicDecoder: OSCValueDecoderBlock { 18 | public typealias Block = @Sendable ( 19 | _ decoder: inout OSCValueDecoder 20 | ) throws -> OSCDecoded 21 | 22 | public let block: Block 23 | 24 | public init(_ block: @escaping Block) { 25 | self.block = block 26 | } 27 | } 28 | 29 | /// ``OSCValue`` variable value decoder block encapsulation. 30 | public struct OSCValueVariableDecoder: OSCValueDecoderBlock { 31 | public typealias Block = @Sendable ( 32 | _ tag: Character, 33 | _ decoder: inout OSCValueDecoder 34 | ) throws -> OSCDecoded 35 | 36 | public let block: Block 37 | 38 | public init(_ block: @escaping Block) { 39 | self.block = block 40 | } 41 | } 42 | 43 | /// ``OSCValue`` variadic value decoder block encapsulation. 44 | /// 45 | /// Return `nil` if no expected tags are encountered. 46 | /// Only throw an error if at least one expected tag is encountered but any other tags or value data 47 | /// is malformed. 48 | public struct OSCValueVariadicDecoder: OSCValueDecoderBlock { 49 | public typealias Block = @Sendable ( 50 | _ tags: [Character], 51 | _ decoder: inout OSCValueDecoder 52 | ) throws -> (tagCount: Int, value: OSCDecoded)? 53 | 54 | public let block: Block 55 | 56 | public init(_ block: @escaping Block) { 57 | self.block = block 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCValueEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueEncodable.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol requirements for ``OSCValue`` encoding. 10 | public protocol OSCValueEncodable { 11 | associatedtype OSCEncoded: OSCValueEncodable 12 | associatedtype OSCValueEncodingBlock: OSCValueEncoderBlock 13 | where OSCValueEncodingBlock.OSCEncoded == OSCEncoded 14 | 15 | /// Declarative description of how an OSC value represents itself with OSC message type tag(s). 16 | static var oscTagIdentity: OSCValueTagIdentity { get } 17 | static var oscEncoding: OSCValueEncodingBlock { get } 18 | } 19 | 20 | // MARK: - Default Implementation 21 | 22 | extension OSCValueEncodable { 23 | public typealias OSCEncoded = Self 24 | } 25 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueCodable/OSCValueEncoderBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueEncoderBlock.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol that ``OSCValue`` encoder block encapsulation objects adopt. 10 | public protocol OSCValueEncoderBlock where Self: Sendable { 11 | associatedtype OSCEncoded: OSCValueEncodable 12 | } 13 | 14 | // MARK: - Encoder Blocks 15 | 16 | /// ``OSCValue`` atomic value encoder block encapsulation. 17 | public struct OSCValueAtomicEncoder: OSCValueEncoderBlock { 18 | public typealias Block = @Sendable ( 19 | _ value: OSCEncoded 20 | ) throws -> ( 21 | tag: Character, 22 | data: Data? 23 | ) 24 | 25 | public let block: Block 26 | 27 | public init(_ block: @escaping Block) { 28 | self.block = block 29 | } 30 | } 31 | 32 | /// ``OSCValue`` variable value encoder block encapsulation. 33 | public struct OSCValueVariableEncoder: OSCValueEncoderBlock { 34 | public typealias Block = @Sendable ( 35 | _ value: OSCEncoded 36 | ) throws -> ( 37 | tag: Character, 38 | data: Data? 39 | ) 40 | 41 | public let block: Block 42 | 43 | public init(_ block: @escaping Block) { 44 | self.block = block 45 | } 46 | } 47 | 48 | /// ``OSCValue`` variadic value encoder block encapsulation. 49 | public struct OSCValueVariadicEncoder: OSCValueEncoderBlock { 50 | public typealias Block = @Sendable ( 51 | _ value: OSCEncoded 52 | ) throws -> ( 53 | tags: [Character], 54 | data: Data? 55 | ) 56 | 57 | public let block: Block 58 | 59 | public init(_ block: @escaping Block) { 60 | self.block = block 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueMaskable/OSCValueMaskError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueMaskError.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Error thrown by ``OSCValues`` `masked(...)` methods. 10 | public enum OSCValueMaskError: LocalizedError { 11 | case invalidCount 12 | case mismatchedTypes 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .invalidCount: 17 | "Invalid argument count" 18 | case .mismatchedTypes: 19 | "Mismatched types" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueMaskable/OSCValueMaskable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueMaskable.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol which all maskable ``OSCValue`` types conform. 10 | public protocol OSCValueMaskable { 11 | /// Token describing the OSC value's OSC type. 12 | static var oscValueToken: OSCValueToken { get } 13 | } 14 | 15 | extension OSCValueMaskable { 16 | /// Token describing the OSC value's OSC type. 17 | public var oscValueToken: OSCValueToken { Self.oscValueToken } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueMaskable/OSCValues Mask Methods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValues Mask Methods.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCValues { 10 | /// Returns `true` if an array of ``OSCValue`` values matches an expected value type mask (order 11 | /// and type of values). 12 | /// To make any mask value `Optional`, use its `*Optional` variant. 13 | /// 14 | /// Some opaque type(s) are available: 15 | /// - ``OSCValueToken/number`` & ``OSCValueToken/numberOptional``: Matches 16 | /// ``OSCValueToken/int32``, ``OSCValueToken/float32``, ``OSCValueToken/double``, or 17 | /// ``OSCValueToken/int64``. 18 | /// - ``OSCValueToken/numberOrBool`` & ``OSCValueToken/numberOrBoolOptional``: Matches 19 | /// all ``OSCValueToken/number`` types as well as ``OSCValueToken/bool``. 20 | /// 21 | /// - parameter mask: ``OSCValueToken`` array representing a positive mask match. 22 | public func matches( 23 | mask: [OSCValueToken] 24 | ) -> Bool { 25 | // should not contain more values than mask 26 | if count > mask.count { return false } 27 | 28 | var matchCount = 0 29 | 30 | for idx in 0 ..< mask.count { 31 | // can be a concrete type or opaque type 32 | let idxOptional = mask[idx].isOptional 33 | 34 | if indices.contains(idx) { 35 | switch mask[idx] { 36 | case .number, .numberOptional, .numberOrBool, .numberOrBoolOptional: 37 | // pass through to be matched as a core type 38 | break 39 | default: 40 | if self[idx] is (any OSCInterpolatedValue) { 41 | // interpolated values should never match against a core OSC type 42 | return false 43 | } 44 | } 45 | 46 | let token = self[idx].getSelf().oscValueToken 47 | switch token.isBaseType( 48 | matching: mask[idx].baseType, 49 | includingOpaque: true 50 | ) { 51 | case true: 52 | matchCount += 1 53 | continue 54 | 55 | case false: 56 | return false 57 | } 58 | } else { 59 | switch idxOptional { 60 | case true: 61 | matchCount += 1 62 | continue 63 | 64 | case false: 65 | return false 66 | } 67 | } 68 | } 69 | 70 | if mask.count == matchCount { return true } 71 | return false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValueToken/OSCValueToken Methods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValueToken Methods.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCValueToken { 10 | /// Returns `true` if the value's base type masks the supplied ``OSCValueToken``. 11 | /// 12 | /// - parameters: 13 | /// - token: ``OSCValueToken`` to compare to. 14 | /// - includingOpaque: Allows matching against an opaque type if one is passed in `type`. 15 | /// If `false`, only exact matching occurs (ie: ``int32`` == ``int32``; 16 | /// ``number`` == ``number``). 17 | /// If `true`, opaque type matching is allowed (ie: ``int32`` == ``number``). 18 | public func isBaseType( 19 | matching token: OSCValueToken, 20 | includingOpaque: Bool = false 21 | ) -> Bool { 22 | if self == token { return true } 23 | 24 | if includingOpaque { 25 | // handle all opaque types 26 | 27 | switch token { 28 | case .number: 29 | if self == .int32 || 30 | self == .float32 || 31 | self == .int64 || 32 | self == .double 33 | { 34 | return true 35 | } 36 | case .numberOrBool: 37 | if self == .int32 || 38 | self == .float32 || 39 | self == .int64 || 40 | self == .double || 41 | self == .bool 42 | { 43 | return true 44 | } 45 | default: 46 | break 47 | } 48 | } 49 | 50 | return false 51 | } 52 | } 53 | 54 | extension OSCValue { 55 | /// A mechanism to easily return the static type of an `any` ``OSCValue`` instance. 56 | @_disfavoredOverload 57 | func getSelf() -> Self.Type { 58 | Self.self 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValues/OSCValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValues.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public typealias OSCValues = [any OSCValue] 8 | 9 | extension OSCValues { 10 | /// Syntactic convenience to allow the formation of an `any` ``OSCValue`` array by using 11 | /// `OSCValues()` as an initializer. 12 | @_disfavoredOverload 13 | public init(_ values: [any OSCValue]) { 14 | self = values 15 | } 16 | } 17 | 18 | // MARK: - Equatable 19 | 20 | extension OSCValues { 21 | public static func == (lhs: Self, rhs: Self) -> Bool { 22 | guard lhs.count == rhs.count else { return false } 23 | for (lhsIndex, rhsIndex) in zip(lhs.indices, rhs.indices) { 24 | guard lhs[lhsIndex].hashMatches(rhs[rhsIndex]) else { 25 | return false 26 | } 27 | } 28 | return true 29 | } 30 | 31 | public static func != (lhs: Self, rhs: Self) -> Bool { 32 | !(lhs == rhs) 33 | } 34 | } 35 | 36 | // MARK: - Hashable 37 | 38 | extension OSCValues { 39 | public func hash(into hasher: inout Hasher) { 40 | forEach { hasher.combine($0) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/OSCValues/Type Mask/OSCValues Type Mask Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCValues Type Mask Helpers.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension OSCValues { 10 | @usableFromInline 11 | func validateCount(_ validCount: Index) throws { 12 | guard count == validCount else { 13 | throw OSCValueMaskError.invalidCount 14 | } 15 | } 16 | 17 | @usableFromInline 18 | func validateCount(_ validRange: ClosedRange) throws { 19 | guard validRange.contains(count) else { 20 | throw OSCValueMaskError.invalidCount 21 | } 22 | } 23 | 24 | // MARK: - Concrete Values 25 | 26 | @usableFromInline 27 | func unwrapValue( 28 | _: MaskType.Type, 29 | index: Index, 30 | asOptional: Bool = false 31 | ) throws -> MaskType 32 | where MaskType: OSCValueMaskable 33 | { 34 | guard indices.contains(index) else { 35 | throw OSCValueMaskError.invalidCount 36 | } 37 | 38 | switch MaskType.self { 39 | case is Int.Type: 40 | guard let typed = self[index] as? (any BinaryInteger) else { 41 | throw OSCValueMaskError.mismatchedTypes 42 | } 43 | 44 | return Int(typed) as! MaskType // guaranteed 45 | 46 | case is AnyOSCNumberValue.Type: 47 | let sourceValue = self[index] 48 | 49 | switch sourceValue { 50 | case let int as any (OSCValue & BinaryInteger): 51 | return AnyOSCNumberValue(int) as! MaskType // guaranteed 52 | case let float as any (OSCValue & BinaryFloatingPoint): 53 | return AnyOSCNumberValue(float) as! MaskType // guaranteed 54 | case let bool as Bool: 55 | return AnyOSCNumberValue(bool) as! MaskType // guaranteed 56 | default: 57 | throw OSCValueMaskError.mismatchedTypes 58 | } 59 | 60 | default: 61 | guard let typed = self[index] as? MaskType else { 62 | throw OSCValueMaskError.mismatchedTypes 63 | } 64 | 65 | return typed 66 | } 67 | } 68 | 69 | // MARK: - Optionals 70 | 71 | @usableFromInline 72 | func unwrapValue( 73 | _ type: MaskType?.Type, 74 | index: Index 75 | ) throws -> MaskType? 76 | where MaskType: OSCValueMaskable 77 | { 78 | guard indices.contains(index) else { 79 | return nil 80 | } 81 | 82 | return try unwrapValue( 83 | type.Wrapped, 84 | index: index, 85 | asOptional: true 86 | ) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Concurrency Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Concurrency Extensions.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | private let maxSeconds = TimeInterval(UInt64.max / 1_000_000_000) 10 | 11 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 12 | extension Task where Success == Never, Failure == Never { 13 | /// Suspends the current task for at least the given duration in seconds. 14 | package static func sleep(seconds: TimeInterval) async throws { 15 | // safety check: protect again overflow 16 | 17 | let secondsClamped = min(seconds, maxSeconds) 18 | let nanoseconds = UInt64(secondsClamped * 1_000_000_000) 19 | 20 | try await sleep(nanoseconds: nanoseconds) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Date Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date Extensions.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension Date { 10 | /// Returns total seconds elapsed since 1990 (prime epoch, NTP era 0). 11 | @_disfavoredOverload 12 | package var timeIntervalSince1900: TimeInterval { 13 | timeIntervalSince(OSCTimeTag.primeEpoch) 14 | } 15 | 16 | /// Returns the NTP era. 17 | @_disfavoredOverload 18 | package var ntpEra: Int { 19 | Int(timeIntervalSince(OSCTimeTag.primeEpoch) / OSCTimeTag.eraDuration) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | // MARK: - OSCKitOptional 8 | 9 | /// Protocol describing an optional, used to enable extensions on types such as `Type?`. 10 | protocol OSCKitOptional { 11 | associatedtype Wrapped 12 | 13 | /// Semantic workaround used to enable extensions on types such as `Type? 14 | @inlinable 15 | var optional: Wrapped? { get } 16 | } 17 | 18 | extension OSCKitOptional { 19 | /// Same as `Wrapped?.none`. 20 | @inlinable 21 | static var noneValue: Wrapped? { 22 | .none 23 | } 24 | } 25 | 26 | extension Optional: OSCKitOptional { 27 | @inlinable 28 | var optional: Wrapped? { 29 | self 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Outsourced/CharacterSet.swift: -------------------------------------------------------------------------------- 1 | /// ---------------------------------------------- 2 | /// ---------------------------------------------- 3 | /// OTCore/Extensions/Foundation/CharacterSet.swift 4 | /// 5 | /// Borrowed from OTCore 1.4.10 under MIT license. 6 | /// https://github.com/orchetect/OTCore 7 | /// Methods herein are unit tested at their source 8 | /// so no unit tests are necessary. 9 | /// ---------------------------------------------- 10 | /// ---------------------------------------------- 11 | 12 | import Foundation 13 | 14 | extension CharacterSet { 15 | /// Initialize a `CharacterSet` from one or more `Character`. 16 | @_disfavoredOverload 17 | init(_ characters: Character...) { 18 | self.init(characters) 19 | } 20 | 21 | /// Initialize a `CharacterSet` from one or more `Character`. 22 | @_disfavoredOverload 23 | init(_ characters: [Character]) { 24 | self.init() 25 | 26 | for character in characters { 27 | character.unicodeScalars.forEach { insert($0) } 28 | } 29 | } 30 | } 31 | 32 | extension CharacterSet { 33 | /// Returns true if the `CharacterSet` contains the given `Character`. 34 | @_disfavoredOverload 35 | func contains(_ character: Character) -> Bool { 36 | character 37 | .unicodeScalars 38 | .allSatisfy(contains(_:)) 39 | } 40 | } 41 | 42 | extension CharacterSet { 43 | /// Same as `lhs.union(rhs)`. 44 | @_disfavoredOverload 45 | static func + (lhs: Self, rhs: Self) -> Self { 46 | lhs.union(rhs) 47 | } 48 | 49 | /// Same as `lhs.formUnion(rhs)`. 50 | @_disfavoredOverload 51 | static func += (lhs: inout Self, rhs: Self) { 52 | lhs.formUnion(rhs) 53 | } 54 | 55 | /// Same as `lhs.subtracting(rhs)`. 56 | @_disfavoredOverload 57 | static func - (lhs: Self, rhs: Self) -> Self { 58 | lhs.subtracting(rhs) 59 | } 60 | 61 | /// Same as `lhs.subtract(rhs)`. 62 | @_disfavoredOverload 63 | static func -= (lhs: inout Self, rhs: Self) { 64 | lhs.subtract(rhs) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Outsourced/FloatingPoint and Darwin.swift: -------------------------------------------------------------------------------- 1 | /// ---------------------------------------------- 2 | /// ---------------------------------------------- 3 | /// OTCore/Extensions/Darwin/FloatingPoint and Darwin.swift 4 | /// 5 | /// Borrowed from OTCore 1.4.10 under MIT license. 6 | /// https://github.com/orchetect/OTCore 7 | /// Methods herein are unit tested at their source 8 | /// so no unit tests are necessary. 9 | /// ---------------------------------------------- 10 | /// ---------------------------------------------- 11 | 12 | import Darwin 13 | 14 | extension FloatingPoint { 15 | /// Similar to `Int.quotientAndRemainder(dividingBy:)` from the standard Swift library. 16 | func quotientAndRemainder(dividingBy rhs: Self) -> (quotient: Self, remainder: Self) { 17 | let calculation = self / rhs 18 | let integral = trunc(calculation) 19 | let fraction = self - (integral * rhs) 20 | return (quotient: integral, remainder: fraction) 21 | } 22 | 23 | /// Returns both integral part and fractional part. 24 | /// 25 | /// - Note: This method is more computationally efficient than calling both `.integral` and 26 | /// `.fraction` properties separately unless you only require one or the other. 27 | /// 28 | /// This method can result in a non-trivial loss of precision for the fractional part. 29 | @inlinable 30 | var integralAndFraction: (integral: Self, fraction: Self) { 31 | let integral = trunc(self) 32 | let fraction = self - integral 33 | return (integral: integral, fraction: fraction) 34 | } 35 | 36 | /// Returns the integral part (digits before the decimal point) 37 | @inlinable 38 | var integral: Self { 39 | integralAndFraction.integral 40 | } 41 | 42 | /// Returns the fractional part (digits after the decimal point) 43 | /// 44 | /// - Note: this method can result in a non-trivial loss of precision for the fractional part. 45 | @inlinable 46 | var fraction: Self { 47 | integralAndFraction.fraction 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Outsourced/Protocols.swift: -------------------------------------------------------------------------------- 1 | /// ---------------------------------------------- 2 | /// ---------------------------------------------- 3 | /// OTCore/Protocols/Protocols.swift 4 | /// 5 | /// Borrowed from OTCore 1.4.10 under MIT license. 6 | /// https://github.com/orchetect/OTCore 7 | /// Methods herein are unit tested at their source 8 | /// so no unit tests are necessary. 9 | /// ---------------------------------------------- 10 | /// ---------------------------------------------- 11 | 12 | // MARK: - NumberEndianness 13 | 14 | /// Enum describing endianness when stored in data form. 15 | enum NumberEndianness { 16 | case platformDefault 17 | case littleEndian 18 | case bigEndian 19 | } 20 | 21 | #if canImport(CoreFoundation) 22 | import CoreFoundation 23 | 24 | extension NumberEndianness { 25 | /// Returns the current system hardware's byte order endianness. 26 | static let system: NumberEndianness = 27 | CFByteOrderGetCurrent() == CFByteOrderBigEndian.rawValue 28 | ? .bigEndian 29 | : .littleEndian 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Outsourced/String and CharacterSet.swift: -------------------------------------------------------------------------------- 1 | /// ---------------------------------------------- 2 | /// ---------------------------------------------- 3 | /// OTCore/Extensions/Foundation/String and CharacterSet.swift 4 | /// 5 | /// Borrowed from OTCore 1.4.10 under MIT license. 6 | /// https://github.com/orchetect/OTCore 7 | /// Methods herein are unit tested at their source 8 | /// so no unit tests are necessary. 9 | /// ---------------------------------------------- 10 | /// ---------------------------------------------- 11 | 12 | import Foundation 13 | 14 | // MARK: - Character tests 15 | 16 | extension StringProtocol { 17 | /// Returns true if the string is entirely comprised of ASCII characters (0-127). 18 | @inlinable @_disfavoredOverload 19 | var isASCII: Bool { 20 | allSatisfy(\.isASCII) 21 | } 22 | 23 | /// Returns true if all characters in the string are contained in the character set. 24 | @_disfavoredOverload 25 | func isOnly( 26 | _ characterSet: CharacterSet, 27 | _ characterSets: CharacterSet... 28 | ) -> Bool { 29 | let mergedCharacterSet = characterSets.isEmpty 30 | ? characterSet 31 | : characterSets.reduce(into: characterSet, +=) 32 | 33 | return allSatisfy(mergedCharacterSet.contains(_:)) 34 | } 35 | 36 | /// Returns true if all characters in the string are contained in the character set. 37 | @_disfavoredOverload 38 | func isOnly(characters: String) -> Bool { 39 | let characterSet = CharacterSet(charactersIn: characters) 40 | return allSatisfy(characterSet.contains(_:)) 41 | } 42 | 43 | /// Returns true if any character in the string are contained in the character set. 44 | @_disfavoredOverload 45 | func contains( 46 | any characterSet: CharacterSet, 47 | _ characterSets: CharacterSet... 48 | ) -> Bool { 49 | let mergedCharacterSet = characterSets.isEmpty 50 | ? characterSet 51 | : characterSets.reduce(into: characterSet, +=) 52 | 53 | for char in self { 54 | if mergedCharacterSet.contains(char) { return true } 55 | } 56 | 57 | return false 58 | } 59 | 60 | /// Returns true if any character in the string are contained in the character set. 61 | @_disfavoredOverload 62 | func contains(anyCharacters characters: String) -> Bool { 63 | let characterSet = CharacterSet(charactersIn: characters) 64 | 65 | for char in self { 66 | if characterSet.contains(char) { return true } 67 | } 68 | 69 | return false 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/Outsourced/String and Foundation.swift: -------------------------------------------------------------------------------- 1 | /// ---------------------------------------------- 2 | /// ---------------------------------------------- 3 | /// OTCore/Extensions/Foundation/String and Foundation.swift 4 | /// 5 | /// Borrowed from OTCore 1.4.10 under MIT license. 6 | /// https://github.com/orchetect/OTCore 7 | /// Methods herein are unit tested at their source 8 | /// so no unit tests are necessary. 9 | /// ---------------------------------------------- 10 | /// ---------------------------------------------- 11 | 12 | import Foundation 13 | 14 | extension StringProtocol { 15 | /// Convenience function to return a new string with whitespaces and newlines trimmed off start 16 | /// and end. 17 | @inlinable 18 | var trimmed: String { 19 | trimmingCharacters(in: .whitespacesAndNewlines) 20 | } 21 | } 22 | 23 | extension StringProtocol { 24 | var quoted: String { 25 | "\"" + self + "\"" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/OSCKitCore/Utilities/String Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String Extensions.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension String { 10 | /// Returns the address as individual path components (strings between `/` separators). 11 | var oscAddressPathComponents: [Substring] { 12 | guard !isEmpty else { return [] } 13 | 14 | var addressSlice = self[startIndex...] 15 | 16 | if addressSlice.starts(with: "/") { 17 | addressSlice = addressSlice.dropFirst() 18 | } 19 | 20 | if addressSlice.hasSuffix("/") { 21 | addressSlice = addressSlice.dropLast() 22 | } 23 | 24 | if addressSlice.isEmpty { return [] } 25 | 26 | return addressSlice 27 | .split( 28 | separator: "/", 29 | omittingEmptySubsequences: false 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCAddressSpace/OSCAddressSpace Utilities Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCAddressSpace Utilities Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | @testable import OSCKitCore 8 | import Testing 9 | 10 | @Suite struct OSCAddressSpace_Utilities_Tests { 11 | @Test 12 | func nodeValidateName() { 13 | #expect( 14 | !OSCAddressSpace.Node.validate(name: "") 15 | ) 16 | 17 | #expect( 18 | OSCAddressSpace.Node.validate(name: "abcDEF1234") 19 | ) 20 | 21 | #expect( 22 | OSCAddressSpace.Node.validate(name: "abc]p} ,.DEF1234-") 23 | ) 24 | 25 | #expect( 26 | !OSCAddressSpace.Node.validate(name: "abc?d") 27 | ) 28 | 29 | #expect( 30 | !OSCAddressSpace.Node.validate(name: "abc*") 31 | ) 32 | 33 | #expect( 34 | OSCAddressSpace.Node.validate(name: "a bc") 35 | ) 36 | 37 | #expect( 38 | !OSCAddressSpace.Node.validate(name: "abc{d,e}f") 39 | ) 40 | 41 | #expect( 42 | !OSCAddressSpace.Node.validate(name: "abc{d,e}f") 43 | ) 44 | 45 | #expect( 46 | !OSCAddressSpace.Node.validate(name: "/abcDEF1234") 47 | ) 48 | 49 | #expect( 50 | !OSCAddressSpace.Node.validate(name: "abcDEF1234/") 51 | ) 52 | } 53 | 54 | @Test 55 | func nodeValidateNameStrict() { 56 | #expect( 57 | !OSCAddressSpace.Node.validate(name: "", strict: true) 58 | ) 59 | 60 | #expect( 61 | OSCAddressSpace.Node.validate(name: "abcDEF1234", strict: true) 62 | ) 63 | 64 | #expect( 65 | !OSCAddressSpace.Node.validate(name: "abc]p} ,.DEF1234-", strict: true) 66 | ) 67 | 68 | #expect( 69 | !OSCAddressSpace.Node.validate(name: "abc?d", strict: true) 70 | ) 71 | 72 | #expect( 73 | !OSCAddressSpace.Node.validate(name: "abc*", strict: true) 74 | ) 75 | 76 | #expect( 77 | !OSCAddressSpace.Node.validate(name: "a bc", strict: true) 78 | ) 79 | 80 | #expect( 81 | !OSCAddressSpace.Node.validate(name: "abc{d,e}f", strict: true) 82 | ) 83 | 84 | #expect( 85 | !OSCAddressSpace.Node.validate(name: "abc{d,e}f", strict: true) 86 | ) 87 | 88 | #expect( 89 | !OSCAddressSpace.Node.validate(name: "/abcDEF1234", strict: true) 90 | ) 91 | 92 | #expect( 93 | !OSCAddressSpace.Node.validate(name: "abcDEF1234/", strict: true) 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCBundle/OSCBundle rawData Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCBundle rawData Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | @testable import OSCKitCore 8 | import Testing 9 | 10 | @Suite struct OSCBundle_rawData_Tests { 11 | // swiftformat:options --wrapcollections preserve 12 | 13 | @Test 14 | func empty() async throws { 15 | // tests an empty OSC bundle 16 | 17 | // manually build a raw OSC bundle 18 | 19 | var knownGoodOSCRawBytes: [UInt8] = [] 20 | 21 | // #bundle header 22 | knownGoodOSCRawBytes += [0x23, 0x62, 0x75, 0x6E, // "#bun" 23 | 0x64, 0x6C, 0x65, 0x00] // "dle" null 24 | // timetag 25 | knownGoodOSCRawBytes += [0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x01] // 1, int64 big-endian 27 | 28 | // decode 29 | 30 | let bundle = try OSCBundle(from: knownGoodOSCRawBytes.data) 31 | 32 | #expect(bundle.timeTag.rawValue == 1) 33 | #expect(bundle.elements.isEmpty) 34 | 35 | // re-encode 36 | 37 | #expect(try bundle.rawData() == knownGoodOSCRawBytes.data) 38 | } 39 | 40 | @Test 41 | func singleOSCMessage() async throws { 42 | // tests an OSC bundle, with one message containing an int32 value 43 | 44 | // manually build a raw OSC bundle 45 | 46 | var knownGoodOSCRawBytes: [UInt8] = [] 47 | 48 | // #bundle header 49 | knownGoodOSCRawBytes += [0x23, 0x62, 0x75, 0x6E, // "#bun" 50 | 0x64, 0x6C, 0x65, 0x00] // "dle" null 51 | // timetag 52 | knownGoodOSCRawBytes += [0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0x00, 0x01] // 1, int64 big-endian 54 | // size of first bundle element 55 | knownGoodOSCRawBytes += [0x00, 0x00, 0x00, 0x18] // 24, int32 big-endian 56 | 57 | // first bundle element: OSC Message 58 | 59 | // address 60 | knownGoodOSCRawBytes += [0x2F, 0x74, 0x65, 0x73, 61 | 0x74, 0x61, 0x64, 0x64, 62 | 0x72, 0x65, 0x73, 0x73, // "/testaddress" 63 | 0x00, 0x00, 0x00, 0x00] // null null null null 64 | // value type(s) 65 | knownGoodOSCRawBytes += [0x2C, 0x69, 0x00, 0x00] // ",i" null null 66 | // int32 67 | knownGoodOSCRawBytes += [0x00, 0x00, 0x00, 0xFF] // 255, big-endian 68 | 69 | // decode 70 | 71 | let bundle = try OSCBundle(from: knownGoodOSCRawBytes.data) 72 | 73 | #expect(bundle.timeTag.rawValue == 1) 74 | #expect(bundle.elements.count == 1) 75 | 76 | let msg = try #require(bundle.elements.first as? OSCMessage) 77 | #expect(msg.addressPattern.stringValue == "/testaddress") 78 | #expect(msg.values.count == 1) 79 | let val = try #require(msg.values.first as? Int32) 80 | #expect(val == 255) 81 | 82 | // re-encode 83 | 84 | #expect(try bundle.rawData() == knownGoodOSCRawBytes.data) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCMessage/OSCMessage Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCMessage Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKitCore 8 | import SwiftASCII 9 | import Testing 10 | 11 | @Suite struct OSCMessage_Tests { 12 | // MARK: - Equatable 13 | 14 | @Test 15 | func equatable() { 16 | let msg1 = OSCMessage("/msg1") 17 | let msg2 = OSCMessage("/msg2") 18 | let msg3 = OSCMessage("/msg1", values: [Int32(123)]) 19 | 20 | #expect(msg1 == msg1) 21 | #expect(msg2 == msg2) 22 | #expect(msg3 == msg3) 23 | 24 | #expect(msg1 != msg2) 25 | #expect(msg1 != msg3) 26 | 27 | #expect(msg2 != msg3) 28 | } 29 | 30 | // MARK: - Hashable 31 | 32 | @Test 33 | func hashable() { 34 | let msg1 = OSCMessage("/msg1") 35 | let msg2 = OSCMessage("/msg2") 36 | let msg3 = OSCMessage("/msg1", values: [Int32(123)]) 37 | 38 | let set: Set = [msg1, msg1, msg2, msg2, msg3, msg3] 39 | 40 | #expect(set == [msg1, msg2, msg3]) 41 | } 42 | 43 | // MARK: - CustomStringConvertible 44 | 45 | @Test 46 | func customStringConvertible1() { 47 | let msg = OSCMessage("/") 48 | 49 | #expect(msg.description == #"OSCMessage("/")"#) 50 | } 51 | 52 | @Test 53 | func customStringConvertible2() { 54 | let msg = OSCMessage( 55 | "/address", 56 | values: [Int32(123), String("A string")] 57 | ) 58 | 59 | #expect( 60 | msg.description == 61 | #"OSCMessage("/address", values: [123, "A string"])"# 62 | ) 63 | } 64 | 65 | @Test 66 | func descriptionPretty() { 67 | let msg = OSCMessage( 68 | "/address", 69 | values: [Int32(123), String("A string")] 70 | ) 71 | 72 | #expect( 73 | msg.descriptionPretty == 74 | #""" 75 | OSCMessage("/address") with values: 76 | Int32: 123 77 | String: A string 78 | """# 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCObject/OSCObject Static Constructors Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCObject Static Constructors Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKitCore 8 | import SwiftASCII 9 | import Testing 10 | 11 | @Suite struct OSCObject_StaticConstructors_Tests { 12 | // MARK: - OSCMessage 13 | 14 | @Test 15 | func oscMessage_AddressPatternString() throws { 16 | let addr = String("/msg1") 17 | let obj: any OSCObject = .message( 18 | addr, 19 | values: [Int32(123)] 20 | ) 21 | 22 | let msg: OSCMessage = try #require(obj as? OSCMessage) 23 | 24 | #expect(msg.addressPattern.stringValue == "/msg1") 25 | #expect(msg.values[0] as? Int32 == Int32(123)) 26 | } 27 | 28 | @Test 29 | func oscMessage_AddressPattern() throws { 30 | let obj: any OSCObject = .message( 31 | OSCAddressPattern("/msg1"), 32 | values: [Int32(123)] 33 | ) 34 | 35 | let msg: OSCMessage = try #require(obj as? OSCMessage) 36 | 37 | #expect(msg.addressPattern.stringValue == "/msg1") 38 | #expect(msg.values[0] as? Int32 == Int32(123)) 39 | } 40 | 41 | // MARK: - OSCBundle 42 | 43 | @Test 44 | func oscBundle() throws { 45 | let obj: any OSCObject = .bundle([ 46 | .message("/", values: [Int32(123)]) 47 | ]) 48 | 49 | let bundle: OSCBundle = try #require(obj as? OSCBundle) 50 | 51 | #expect(bundle.elements.count == 1) 52 | 53 | let msg = try #require(bundle.elements[0] as? OSCMessage) 54 | #expect(msg.values[0] as? Int32 == Int32(123)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCObject/OSCObject Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCObject Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import OSCKitCore 9 | import Testing 10 | 11 | @Suite struct OSCObject_Tests { 12 | @Test 13 | func appearsToBeOSC() throws { 14 | let bundle = try OSCBundle([]).rawData() 15 | let msg = try OSCMessage("/").rawData() 16 | 17 | // OSC bundle 18 | #expect(bundle.oscObjectType == .bundle) 19 | #expect(bundle.oscObjectType != .message) 20 | 21 | // OSC message 22 | #expect(msg.oscObjectType == .message) 23 | #expect(msg.oscObjectType != .bundle) 24 | 25 | // empty bytes 26 | #expect(Data().oscObjectType == nil) 27 | 28 | // garbage bytes 29 | #expect(Data([0x98, 0x42, 0x01, 0x7E]).oscObjectType == nil) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCTimeTag/OSCTimeTag Static Constructors Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCTimeTag Static Constructors Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import Numerics 9 | import OSCKitCore 10 | import Testing 11 | 12 | @Suite struct OSCTimeTag_StaticConstructors_Tests { 13 | #if os(macOS) || os(iOS) 14 | let tolerance: TimeInterval = 0.005 15 | #elseif os(tvOS) || os(watchOS) 16 | // allow more time variance for CI pipeline to de-flake 17 | let tolerance: TimeInterval = 0.01 18 | #endif 19 | 20 | // MARK: - Static Constructors 21 | 22 | @Test 23 | func timeTagImmediate() { 24 | let val: OSCTimeTag = .immediate() 25 | #expect(val == OSCTimeTag(1)) 26 | } 27 | 28 | // MARK: - `any OSCValue` Constructors 29 | 30 | @Test 31 | func oscValue_timeTag() { 32 | let val: any OSCValue = .timeTag(123, era: 1) 33 | #expect(val as? OSCTimeTag == OSCTimeTag(123, era: 1)) 34 | } 35 | 36 | @Test 37 | func oscValue_timeTagImmediate() { 38 | let val: any OSCValue = .timeTagImmediate() 39 | #expect(val as? OSCTimeTag == OSCTimeTag.immediate()) 40 | } 41 | 42 | @Test(.enabled(if: isSystemTimingStable())) 43 | func oscValue_timeTagNow() throws { 44 | let val: any OSCValue = .timeTagNow() 45 | let now = OSCTimeTag.now() 46 | 47 | let valTI = try #require((val as? OSCTimeTag)?.timeInterval(since: primeEpoch)) 48 | let nowTI = now.timeInterval(since: primeEpoch) 49 | #expect(valTI.isApproximatelyEqual(to: nowTI, absoluteTolerance: tolerance)) 50 | } 51 | 52 | @Test(.enabled(if: isSystemTimingStable())) 53 | func oscValue_timeTagTimeIntervalSinceNow() throws { 54 | let val: any OSCValue = .timeTag(timeIntervalSinceNow: 5.0) 55 | let now = OSCTimeTag(timeIntervalSinceNow: 5.0) 56 | 57 | let valTI = try #require((val as? OSCTimeTag)?.timeInterval(since: primeEpoch)) 58 | let nowTI = now.timeInterval(since: primeEpoch) 59 | 60 | #expect(valTI.isApproximatelyEqual(to: nowTI, absoluteTolerance: tolerance)) 61 | } 62 | 63 | @Test 64 | func oscValue_timeTagTimeIntervalSince1900() { 65 | let val: any OSCValue = .timeTag(timeIntervalSince1900: 9_467_107_200.0) 66 | #expect(val as? OSCTimeTag == OSCTimeTag(timeIntervalSince1900: 9_467_107_200.0)) 67 | } 68 | 69 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6, *) 70 | @Test 71 | func oscValue_timeTagFuture() { 72 | let futureDate = Date().advanced(by: 200.0) 73 | let val: any OSCValue = .timeTag(future: futureDate) 74 | #expect(val as? OSCTimeTag == OSCTimeTag(future: futureDate)) 75 | } 76 | } 77 | 78 | // MARK: - Helpers 79 | 80 | /// NTP prime epoch, a.k.a. era 0. 81 | private let primeEpoch: Date = DateComponents( 82 | calendar: .current, 83 | timeZone: .current, 84 | year: 1900, 85 | month: 1, 86 | day: 1, 87 | hour: 0, 88 | minute: 0, 89 | second: 0 90 | ).date! 91 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCValue/Extended Types/OSCImpulseValue Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCImpulseValue Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKitCore 8 | import Testing 9 | 10 | @Suite struct OSCImpulseValue_Tests { 11 | // MARK: - `any OSCValue` Constructors 12 | 13 | @Test 14 | func oscValue_impulse() { 15 | let val: any OSCValue = .impulse 16 | #expect(val as? OSCImpulseValue == OSCImpulseValue()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCValue/Extended Types/OSCMIDIValue Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCMIDIValue Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKitCore 8 | import Testing 9 | 10 | @Suite struct OSCMIDIValue_Tests { 11 | // MARK: - `any OSCValue` Constructors 12 | 13 | @Test 14 | func oscValue_midi() { 15 | let val: any OSCValue = .midi(portID: 0x01, status: 0x90, data1: 0x02, data2: 0x03) 16 | #expect( 17 | val as? OSCMIDIValue == 18 | OSCMIDIValue(portID: 0x01, status: 0x90, data1: 0x02, data2: 0x03) 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCValue/Extended Types/OSCNullValue Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCNullValue Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKitCore 8 | import Testing 9 | 10 | @Suite struct OSCNullValue_Tests { 11 | // MARK: - `any OSCValue` Constructors 12 | 13 | @Test 14 | func oscValue_null() { 15 | let val: any OSCValue = .null 16 | #expect(val as? OSCNullValue == OSCNullValue()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/OSCValue/Extended Types/OSCStringAltValue Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSCStringAltValue Tests.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import OSCKitCore 8 | import Testing 9 | 10 | @Suite struct OSCStringAltValue_Tests { 11 | // MARK: - `any OSCValue` Constructors 12 | 13 | @Test 14 | func anyOSCValue_stringAlt() { 15 | let val: any OSCValue = .stringAlt("A string") 16 | #expect(val as? OSCStringAltValue == OSCStringAltValue("A string")) 17 | } 18 | 19 | // MARK: - Equatable Operators 20 | 21 | @Test 22 | func equatable() { 23 | let stringAlt1 = OSCStringAltValue("A string") 24 | let stringAlt2 = OSCStringAltValue("A string") 25 | 26 | #expect(stringAlt1 == stringAlt2) 27 | #expect(stringAlt2 == stringAlt1) 28 | 29 | #expect(!(stringAlt1 != stringAlt2)) 30 | #expect(!(stringAlt2 != stringAlt1)) 31 | } 32 | 33 | @Test 34 | func equatableWithString() { 35 | let string = "A string" 36 | let stringAlt = OSCStringAltValue(string) 37 | 38 | #expect(string == stringAlt) 39 | #expect(stringAlt == string) 40 | 41 | #expect(!(string != stringAlt)) 42 | #expect(!(stringAlt != string)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/OSCKitCoreTests/Test Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test Utilities.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import Testing 9 | 10 | /// Use as a condition for individual tests that rely on stable/precise system timing. 11 | func isSystemTimingStable( 12 | duration: TimeInterval = 0.1, 13 | tolerance: TimeInterval = 0.01 14 | ) -> Bool { 15 | let durationMS = UInt32(duration * TimeInterval(USEC_PER_SEC)) 16 | 17 | let start = Date() 18 | usleep(durationMS) 19 | let end = Date() 20 | let diff = end.timeIntervalSince(start) 21 | 22 | let range = (duration - tolerance) ... (duration + tolerance) 23 | return range.contains(diff) 24 | } 25 | -------------------------------------------------------------------------------- /Tests/OSCKitTests/Test Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test Utilities.swift 3 | // OSCKit • https://github.com/orchetect/OSCKit 4 | // © 2020-2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import Testing 9 | 10 | #if !os(watchOS) 11 | 12 | func wait( 13 | expect condition: @Sendable () async throws -> Bool, 14 | timeout: TimeInterval, 15 | pollingInterval: TimeInterval = 0.1, 16 | _ comment: Testing.Comment? = nil, 17 | sourceLocation: Testing.SourceLocation = #_sourceLocation 18 | ) async rethrows { 19 | let startTime = Date() 20 | 21 | while Date().timeIntervalSince(startTime) < timeout { 22 | if try await condition() { return } 23 | try? await Task.sleep(seconds: pollingInterval) 24 | } 25 | 26 | #expect(try await condition(), comment, sourceLocation: sourceLocation) 27 | } 28 | 29 | func wait( 30 | require condition: @Sendable () async throws -> Bool, 31 | timeout: TimeInterval, 32 | pollingInterval: TimeInterval = 0.1, 33 | _ comment: Testing.Comment? = nil, 34 | sourceLocation: Testing.SourceLocation = #_sourceLocation 35 | ) async throws { 36 | let startTime = Date() 37 | 38 | while Date().timeIntervalSince(startTime) < timeout { 39 | if try await condition() { return } 40 | try await Task.sleep(seconds: pollingInterval) 41 | } 42 | 43 | try #require(await condition(), comment, sourceLocation: sourceLocation) 44 | } 45 | 46 | #endif 47 | 48 | /// Use as a condition for individual tests that rely on stable/precise system timing. 49 | func isSystemTimingStable( 50 | duration: TimeInterval = 0.1, 51 | tolerance: TimeInterval = 0.01 52 | ) -> Bool { 53 | let durationMS = UInt32(duration * TimeInterval(USEC_PER_SEC)) 54 | 55 | let start = Date() 56 | usleep(durationMS) 57 | let end = Date() 58 | let diff = end.timeIntervalSince(start) 59 | 60 | let range = (duration - tolerance) ... (duration + tolerance) 61 | return range.contains(diff) 62 | } 63 | --------------------------------------------------------------------------------