A Swift client library for the Sendspin Protocol — enabling synchronized multi-room audio playback on Apple platforms.
- Player Role — Synchronized audio playback with microsecond-precision clock sync
- Controller Role — Play, pause, skip, volume, shuffle, repeat across device groups
- Metadata Role — Track info, artwork URLs, and playback progress
- Artwork Role — Album art delivery with format and resolution negotiation
- Color Role — Synchronized album and audio-derived color themes
- Auto-discovery — mDNS/Bonjour server discovery with continuous or one-shot modes
- Multi-codec — PCM, Opus, and FLAC support with seamless mid-stream format switching
- Clock Sync — Kalman filter time synchronization with drift tracking and adaptive forgetting
- Hardware & Software Volume — Perceptual gain curve with per-device or per-queue control
- iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+
- Swift 6.2+
dependencies: [
.package(url: "https://github.com/Sendspin/SendspinKit.git", from: "1.0.0")
]import SendspinKit
// Create a player client
let client = try SendspinClient(
clientId: "my-device",
name: "Living Room Speaker",
roles: [.playerV1],
playerConfig: try PlayerConfiguration(
bufferCapacity: 1_048_576,
supportedFormats: [
try AudioFormatSpec(codec: .pcm, channels: 2, sampleRate: 48_000, bitDepth: 16),
try AudioFormatSpec(codec: .flac, channels: 2, sampleRate: 48_000, bitDepth: 16),
]
)
)
// Discover and connect to the first server found
let servers = try await SendspinClient.discoverServers(timeout: .seconds(5))
if let server = servers.first {
try await client.connect(to: server.url)
}
// React to events
for await event in client.events() {
switch event {
case .streamStarted(let format):
print("Playing \(format.codec) at \(format.sampleRate)Hz")
case .metadataReceived(let metadata):
print("Now playing: \(metadata.title ?? "Unknown")")
case .disconnected(let reason):
print("Disconnected: \(reason)")
default:
break
}
}let controller = try SendspinClient(
clientId: "my-remote",
name: "Kitchen Display",
roles: [.controllerV1, .metadataV1]
)
try await controller.connect(to: serverURL)
// Control playback
try await controller.play()
try await controller.next()
try await controller.setGroupVolume(75)
try await controller.setShuffle(true)import SendspinKit
import SwiftUI
extension Color {
init(_ rgb: RGBColor) {
self.init(
.sRGB,
red: Double(rgb.red) / 255,
green: Double(rgb.green) / 255,
blue: Double(rgb.blue) / 255,
opacity: 1
)
}
}
let colorDisplay = try SendspinClient(
clientId: "my-display",
name: "Kitchen Display",
roles: [.colorV1]
)
struct NowPlayingView: View {
let client: SendspinClient
var body: some View {
PlayerControls()
.background(client.currentColorState?.backgroundDark.map(Color.init) ?? .black)
.foregroundStyle(client.currentColorState?.onDark.map(Color.init) ?? .white)
}
}currentColorState is observable and contains the latest accumulated theme. Each state includes
serverTimestamp and, once clock synchronization is ready, localDisplayTime for consumers that
schedule color changes alongside audio, artwork, or visualizer updates.
let discovery = try await SendspinClient.discoverServers()
for await servers in discovery.servers {
print("Found \(servers.count) server(s):")
for server in servers {
print(" \(server.name) at \(server.url)")
}
}- PCM — Uncompressed audio up to 192kHz/32-bit (zero-copy passthrough)
- Opus — Low-latency lossy compression (8-48kHz, optimized for real-time)
- FLAC — Lossless compression with hi-res support (up to 192kHz/24-bit)
All codecs output normalized int32 PCM for consistent pipeline processing.
SendspinKit uses a Kalman filter for clock synchronization and timestamp-based audio scheduling:
- Clock Sync — Full 2D covariance Kalman filter with adaptive forgetting, drift SNR gating, and RTT floor
- AudioScheduler — Priority queue of audio chunks sorted by playback time
- Playback Window — Configurable tolerance for network jitter (default +/-50ms)
- Sync Correction — Frame-level drop/insert to maintain alignment without audible glitches
API documentation is available via DocC. Build it locally with:
swift package generate-documentationApache 2.0