├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
2 |
3 | # OSCKit
4 |
5 | [](https://swiftpackageindex.com/orchetect/OSCKit) [](https://swiftpackageindex.com/orchetect/OSCKit) [](https://developer.apple.com/swift) [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------