Sendspin.SDK 7.3.0

dotnet add package Sendspin.SDK --version 7.3.0
                    
NuGet\Install-Package Sendspin.SDK -Version 7.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sendspin.SDK" Version="7.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sendspin.SDK" Version="7.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Sendspin.SDK" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sendspin.SDK --version 7.3.0
                    
#r "nuget: Sendspin.SDK, 7.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sendspin.SDK@7.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sendspin.SDK&version=7.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Sendspin.SDK&version=7.3.0
                    
Install as a Cake Tool

Sendspin SDK

A cross-platform .NET SDK for the Sendspin synchronized multi-room audio protocol. Build players that sync perfectly with Music Assistant and other Sendspin-compatible players.

NuGet GitHub

Features

  • Multi-room Audio Sync: Microsecond-precision clock synchronization using Kalman filtering
  • External Sync Correction (v5.0+): SDK reports sync error, your app applies correction
  • Platform Flexibility: Use playback rate, drop/insert, or hardware rate adjustment
  • Fast Startup: Audio plays within ~300ms of connection
  • Protocol Support: Full Sendspin WebSocket protocol implementation
  • Server Discovery: mDNS-based automatic server discovery
  • Audio Decoding: Built-in PCM, FLAC, and Opus codec support
  • Cross-Platform: Works on Windows, Linux, and macOS (.NET 8.0 / .NET 10.0)
  • NativeAOT & Trimming: Fully compatible with PublishAot and IL trimming for single-file native executables with no .NET runtime dependency
  • Audio Device Switching: Hot-switch audio output devices without interrupting playback

Installation

dotnet add package Sendspin.SDK

Quick Start

using Sendspin.SDK.Client;
using Sendspin.SDK.Connection;
using Sendspin.SDK.Synchronization;

// Create dependencies
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var connection = new SendspinConnection(loggerFactory.CreateLogger<SendspinConnection>());
var clockSync = new KalmanClockSynchronizer(loggerFactory.CreateLogger<KalmanClockSynchronizer>());

// Create client with device info
var capabilities = new ClientCapabilities
{
    ClientName = "My Player",
    ProductName = "My Awesome Player",
    Manufacturer = "My Company",
    SoftwareVersion = "1.0.0"
};

var client = new SendspinClientService(
    loggerFactory.CreateLogger<SendspinClientService>(),
    connection,
    clockSync,
    capabilities
);

// Connect to server
await client.ConnectAsync(new Uri("ws://192.168.1.100:8927/sendspin"));

// Handle events
client.GroupStateChanged += (sender, group) =>
{
    Console.WriteLine($"Now playing: {group.Metadata?.Title}");
};

// Send commands
await client.SendCommandAsync("play");
await client.SetVolumeAsync(75);

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     Your Application                            │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  SyncCorrectionCalculator  │  Your Resampler/Drop Logic │   │
│  │  (correction decisions)    │  (applies correction)      │   │
│  └─────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────┤
│  SendspinClientService    │  AudioPipeline    │  IAudioPlayer   │
│  (protocol handling)      │  (orchestration)  │  (your impl)    │
├─────────────────────────────────────────────────────────────────┤
│  SendspinConnection  │  KalmanClockSync  │  TimedAudioBuffer    │
│  (WebSocket)         │  (timing)         │  (reports error)     │
├─────────────────────────────────────────────────────────────────┤
│  OpusDecoder  │  FlacDecoder  │  PcmDecoder                     │
└─────────────────────────────────────────────────────────────────┘

Namespaces:

  • Sendspin.SDK.Client - Client services and capabilities
  • Sendspin.SDK.Connection - WebSocket connection management
  • Sendspin.SDK.Protocol - Message types and serialization
  • Sendspin.SDK.Synchronization - Clock sync (Kalman filter)
  • Sendspin.SDK.Audio - Pipeline, buffer, decoders, and sync correction
  • Sendspin.SDK.Discovery - mDNS server discovery
  • Sendspin.SDK.Models - Data models (GroupState, TrackMetadata)

Sync Correction System (v5.0+)

Starting with v5.0.0, sync correction is external - the SDK reports sync error and your application decides how to correct it. This enables platform-specific correction strategies:

  • Windows: WDL resampler, SoundTouch, or drop/insert
  • Browser: Native playbackRate (WSOLA time-stretching)
  • Linux: ALSA hardware rate adjustment, PipeWire rate
  • Embedded: Platform-specific DSP

How It Works

SDK (reports error only)              App (applies correction)
────────────────────────────────────────────────────────────────
TimedAudioBuffer                      SyncCorrectionCalculator
├─ ReadRaw() - no correction          ├─ UpdateFromSyncError()
├─ SyncErrorMicroseconds              ├─ DropEveryNFrames
├─ SmoothedSyncErrorMicroseconds      ├─ InsertEveryNFrames
└─ NotifyExternalCorrection()         └─ TargetPlaybackRate

Tiered Correction Strategy

The SyncCorrectionCalculator implements the same tiered strategy as the reference CLI:

Sync Error Correction Method Description
< 1ms None (deadband) Error too small to matter
1-15ms Playback rate adjustment Smooth resampling (imperceptible)
15-500ms Frame drop/insert Faster correction for larger drift
> 500ms Re-anchor Clear buffer and restart sync

Usage Example

using Sendspin.SDK.Audio;

// Create the correction calculator
var correctionProvider = new SyncCorrectionCalculator(
    SyncCorrectionOptions.Default,  // or SyncCorrectionOptions.CliDefaults
    sampleRate: 48000,
    channels: 2
);

// Subscribe to correction changes
correctionProvider.CorrectionChanged += provider =>
{
    // Update your resampler rate
    myResampler.Rate = provider.TargetPlaybackRate;

    // Or handle drop/insert
    if (provider.CurrentMode == SyncCorrectionMode.Dropping)
    {
        dropEveryN = provider.DropEveryNFrames;
    }
};

// In your audio callback:
public int Read(float[] buffer, int offset, int count)
{
    // Read raw samples (no internal correction)
    int read = timedAudioBuffer.ReadRaw(buffer, offset, count, currentTimeMicroseconds);

    // Update correction provider with current error
    correctionProvider.UpdateFromSyncError(
        timedAudioBuffer.SyncErrorMicroseconds,
        timedAudioBuffer.SmoothedSyncErrorMicroseconds
    );

    // Apply your correction strategy...
    // If dropping/inserting, notify the buffer:
    timedAudioBuffer.NotifyExternalCorrection(samplesDropped, samplesInserted);

    return outputCount;
}

Configuring Sync Behavior

// Use default settings (conservative: 2% max, 3s target)
var options = SyncCorrectionOptions.Default;

// Use CLI-compatible settings (aggressive: 4% max, 2s target)
var options = SyncCorrectionOptions.CliDefaults;

// Custom options
var options = new SyncCorrectionOptions
{
    MaxSpeedCorrection = 0.04,                    // 4% max rate adjustment
    CorrectionTargetSeconds = 2.0,                // Time to eliminate drift
    ResamplingThresholdMicroseconds = 15_000,     // Resampling vs drop/insert
    ReanchorThresholdMicroseconds = 500_000,      // Clear buffer threshold
    StartupGracePeriodMicroseconds = 500_000,     // No correction during startup
};

var calculator = new SyncCorrectionCalculator(options, sampleRate, channels);

Platform-Specific Audio

The SDK handles decoding, buffering, and sync error reporting. You implement IAudioPlayer for audio output:

public class MyAudioPlayer : IAudioPlayer
{
    public long OutputLatencyMicroseconds { get; private set; }

    public Task InitializeAsync(AudioFormat format, CancellationToken ct)
    {
        // Initialize your audio backend (WASAPI, PulseAudio, CoreAudio, etc.)
    }

    public int Read(float[] buffer, int offset, int count)
    {
        // Called by audio thread - read from TimedAudioBuffer.ReadRaw()
        // Apply sync correction externally
    }

    // ... other methods
}

Platform suggestions:

  • Windows: NAudio with WASAPI (WasapiOut)
  • Linux: OpenAL, PulseAudio, or PipeWire
  • macOS: AudioToolbox or AVAudioEngine
  • Cross-platform: SDL2

Server Discovery

Automatically discover Sendspin servers on your network:

var discovery = new MdnsServerDiscovery(logger);
discovery.ServerDiscovered += (sender, server) =>
{
    Console.WriteLine($"Found: {server.Name} at {server.Uri}");
};
await discovery.StartAsync();

Device Info

Identify your player to servers:

var capabilities = new ClientCapabilities
{
    ClientName = "Living Room",           // Display name
    ProductName = "MySpeaker Pro",        // Product identifier
    Manufacturer = "Acme Audio",          // Your company
    SoftwareVersion = "2.1.0"             // App version
};

All fields are optional and omitted from the protocol if null.

NativeAOT Support

Since v7.0.0, the SDK is fully compatible with NativeAOT deployment and IL trimming. This means you can publish your Sendspin player as a single native executable with no .NET runtime dependency — ideal for embedded devices, containers, or minimal Linux installations.

<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>
dotnet publish -c Release -r linux-x64
# Produces a single native binary (~15-25MB depending on dependencies)

How it works: The SDK uses source-generated System.Text.Json serialization (no runtime reflection) and built-in .NET WebSocket APIs. All public types are annotated with IsAotCompatible and IsTrimmable to ensure the .NET build analyzers catch any regressions.

Your code: If your IAudioPlayer implementation also avoids reflection, the entire stack will be AOT-safe. Most audio libraries (SDL2, OpenAL, PipeWire bindings) work fine with NativeAOT.

Migration Guide

Upgrading to v7.0.0

Breaking change: SendspinListener.ServerConnected event parameter type changed.

// Before (v6.x):
listener.ServerConnected += (sender, fleckConnection) => { /* Fleck.IWebSocketConnection */ };

// After (v7.0+):
listener.ServerConnected += (sender, wsConnection) => { /* WebSocketClientConnection */ };

No changes needed if you only use SendspinHostService or SendspinClientService (most consumers).

Upgrading to v5.0.0

Breaking change: Sync correction is now external. The SDK reports error; you apply correction.

Before (v4.x and earlier):

// SDK applied correction internally
var read = buffer.Read(samples, currentTime);
buffer.TargetPlaybackRateChanged += rate => resampler.Rate = rate;

After (v5.0+):

// Create correction provider
var correctionProvider = new SyncCorrectionCalculator(
    SyncCorrectionOptions.Default, sampleRate, channels);

// Read raw samples (no internal correction)
var read = buffer.ReadRaw(samples, offset, count, currentTime);

// Update and apply correction externally
correctionProvider.UpdateFromSyncError(
    buffer.SyncErrorMicroseconds,
    buffer.SmoothedSyncErrorMicroseconds);

// Subscribe to rate changes
correctionProvider.CorrectionChanged += p => resampler.Rate = p.TargetPlaybackRate;

// Notify buffer of any drops/inserts for accurate tracking
buffer.NotifyExternalCorrection(droppedCount, insertedCount);

Benefits:

  • Browser apps can use native playbackRate (WSOLA)
  • Windows apps can choose WDL resampler, SoundTouch, or drop/insert
  • Linux apps can use ALSA hardware rate adjustment
  • Testability: correction logic is isolated

Upgrading to v3.0.0

Breaking change: IClockSynchronizer requires HasMinimalSync property.

// Add to custom IClockSynchronizer implementations:
public bool HasMinimalSync => MeasurementCount >= 2;

Upgrading to v2.0.0

  1. HardwareLatencyMs removed - No action needed, latency handled automatically
  2. IAudioPipeline.SwitchDeviceAsync() required - Implement for device switching
  3. IAudioPlayer.SwitchDeviceAsync() required - Implement in your audio player

Example Projects

See the Windows client for a complete WPF implementation using NAudio/WASAPI with external sync correction.

License

MIT License - see LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.3.0 45 3/7/2026
7.2.1 94 3/4/2026
7.1.1 78 3/4/2026
7.1.0 77 3/4/2026
7.0.0 74 3/4/2026
6.3.6 76 3/4/2026
6.3.5 433 2/3/2026
6.3.4 104 2/3/2026
6.3.3 98 2/3/2026
6.3.2 99 2/3/2026
6.3.1 99 2/3/2026
6.3.0 114 2/3/2026
6.2.0-preview.2 53 2/3/2026
6.2.0-preview.1 65 2/3/2026
6.1.1 167 1/29/2026
6.1.0 93 1/29/2026
6.0.1 125 1/29/2026
6.0.0 124 1/26/2026
5.4.1 102 1/26/2026
Loading failed

v7.2.1 - Buffer Overrun Sync Fix:

Bug Fixes:
- Fixed multi-player sync catastrophe where buffer overruns during the server's initial
 audio burst destroyed the beginning of the stream. Compact codecs (OPUS) can burst 40+
 seconds of audio in seconds on LAN, causing 1700+ overruns that advanced the player
 20-35 seconds into the song while other players started from the beginning.
- Pre-playback overrun safety net: When the buffer fills before playback starts, incoming
 audio is now discarded instead of dropping the oldest. This preserves the stream's
 starting position so all players begin at the same point in the song.
- SkipStaleAudio: When a player's pipeline initializes late and the scheduled start time
 is already in the past, the buffer now skips forward through stale segments to the
 correct playback position. Previously, the player would start from the beginning of
 the buffer regardless of timestamps, causing permanent offset vs other players.
- Updated ClientCapabilities.BufferCapacity doc: Clarified that apps should derive this
 from actual PCM buffer duration and worst-case codec bitrate instead of using the
 hardcoded 32MB default.

v7.2.0 - Reconnect Resilience, Freeze/Thaw Kalman State, FLAC Fixes:

New Features:
- IClockSynchronizer.Freeze()/Thaw()/IsFrozen: Snapshot and restore Kalman filter state
 across reconnects. On disconnect, Freeze() saves converged offset/drift estimates.
 On reconnect, Thaw() restores them with 10x inflated covariance for fast adaptation.
 Converges in 1-2 measurements instead of 5+ from scratch. Matches Android client.
- Reconnect stabilization period: Suppresses sync corrections for 2 seconds after
 WebSocket reconnect while the Kalman filter re-converges. Prevents audible artifacts
 from unreliable sync error measurements. Matches Android client behavior.
- SyncCorrectionOptions.ReconnectStabilizationMicroseconds: Configurable duration
 (default: 2,000,000 = 2 seconds)
- Reanchor cooldown: 5-second minimum between re-anchors, matching Android/Python CLI.
 Prevents re-anchor loops on track change.
- SyncCorrectionOptions.ReanchorCooldownMicroseconds: Configurable cooldown duration
 (default: 5,000,000 = 5 seconds)
- ISyncCorrectionProvider.NotifyReconnect(): Default interface method for reconnect notification
- IAudioPlayer.NotifyReconnect(): Default interface method for player reconnect notification
- IAudioPipeline.NotifyReconnect(): Pipeline-level reconnect notification
- ITimedAudioBuffer.NotifyReconnect(): Buffer-level reconnect notification

Bug Fixes:
- Fixed FLAC 32-bit silence: Use actual STREAMINFO bit depth instead of stream/start header.
 The stream/start message may report incorrect bit depth for FLAC streams; the decoder now
 reads the true bit depth from the FLAC STREAMINFO metadata block.
- Fixed AudioFormat.BitDepth not updating from decoded FLAC STREAMINFO.
- Fixed artwork-only stream/start causing unnecessary pipeline start. stream/start messages
 with no "player" key are now correctly skipped.
- Increased default buffer capacity from 8s to 30s to prevent overruns during server's
 initial audio burst on track start.

BREAKING (minor):
- IClockSynchronizer: Added Freeze(), Thaw(), IsFrozen members.
 If you implement IClockSynchronizer directly (not using KalmanClockSynchronizer), add these.
- ITimedAudioBuffer: Added NotifyReconnect() method (no default implementation).
 If you implement ITimedAudioBuffer directly (not using TimedAudioBuffer), add this method.
- Most consumers are unaffected as the built-in implementations are the only known ones.

v7.1.1 - Documentation:
- Added NativeAOT support section to README with PublishAot usage and guidance
- Added v7.0.0 migration guide for SendspinListener.ServerConnected breaking change
- Listed NativeAOT & trimming compatibility in Features section

v7.1.0 - Protocol Compliance (aiosendspin audit):

New Features:
- PlayerStatePayload.StaticDelayMs: Reports configured static delay to server
 for GroupSync calibration. Server uses this to compensate for device output
 latency when calculating group-wide sync offsets.
- ISendspinClient.ArtworkCleared event: Raised when server sends empty artwork
 binary payload (type 8-11, 0-byte data) to signal "no artwork available".
 Previously, empty payloads were passed through as empty byte arrays.
- SendPlayerStateAsync now accepts optional staticDelayMs parameter (default 0.0)
- SendspinHostService.ArtworkCleared event: Forwarded from child clients

Bug Fixes:
- Empty artwork binary messages no longer treated as valid image data

v7.0.0 - NativeAOT Support:

BREAKING CHANGES:
- SendspinListener.ServerConnected event: parameter type changed from
 Fleck.IWebSocketConnection to Sendspin.SDK.Connection.WebSocketClientConnection
- Fleck NuGet dependency removed (replaced with built-in .NET WebSocket APIs)

New Features:
- Full NativeAOT compatibility: SDK can be used in NativeAOT-published applications
- JSON source generation: System.Text.Json uses compile-time source generation
 instead of runtime reflection (also improves startup performance)
- Built-in WebSocket server: TcpListener + WebSocket.CreateFromStream() replaces
 Fleck for server-initiated connections (no admin privileges, fully AOT-safe)
- IsAotCompatible and IsTrimmable properties enabled

Migration:
- If you subscribe to SendspinListener.ServerConnected directly, update the event
 handler parameter type from IWebSocketConnection to WebSocketClientConnection
- If you reference Fleck types through the SDK, switch to WebSocketClientConnection
- No changes needed if you only use SendspinHostService or SendspinClient

v6.3.4 - AudioBufferStats.TimingSourceName Fix:
- Fixed: AudioBufferStats.TimingSourceName property now included in GetStats() output
- Enables "Stats for Nerds" UI to display active timing source (audio-clock, monotonic, wall-clock)
- Note: This property was documented in 6.3.3 release notes but accidentally omitted from that build

v6.3.3 - Categorized Diagnostic Logging:
- All log messages now include category prefixes for easy filtering:
 - [ClockSync] - Clock synchronization, drift, static delay
 - [Timing] - Timing source selection and transitions
 - [Playback] - Pipeline start/stop, startup latency
 - [SyncError] - Periodic sync status monitoring
 - [Correction] - Frame drop/insert correction events
 - [Buffer] - Buffer overrun/underrun events
- Added timing source transition logging: logs when audio clock becomes available/unavailable
- Enhanced startup latency visibility: logs at Info level when non-zero
- Added AudioBufferStats.TimingSourceName property for UI "Stats for Nerds" display
- Example categorized output:
 "[ClockSync] Converged after 5 measurements"
 "[Timing] Source changed: monotonic → audio-clock"
 "[Playback] Starting playback: buffer=500ms, sync offset=+12.3ms"
 "[Correction] Started: DROPPING (syncError=+45ms, timing=audio-clock)"

v6.3.2 - Timing Source Visibility:
- Added ITimedAudioBuffer.TimingSourceName property to track active timing source
- Sync correction logs now include timing source (audio-clock, monotonic, wall-clock)
- AudioPipeline sets timing source name on buffer for consistent logging across components
- Improved logging clarity: MonotonicTimer stats only shown when it's the active source
- Skip MonotonicTimer reset on Clear() when audio clock is active (unnecessary operation)
- Example log output:
 "Sync correction started: DROPPING (..., timing=audio-clock)"
 "Sync correction ended: DROPPING complete (..., timing=monotonic)"

v6.3.1 - Sync Correction Diagnostic Logging:
- Added logging for sync correction mode transitions (None↔Dropping, None↔Inserting)
- Logs include: session counts, cumulative totals, duration, sync error values at transition
- Helps diagnose what triggers large frame drop/insert corrections in VM environments
- Example log output:
 "Sync correction started: DROPPING (syncError=+45.32ms, smoothed=+42.18ms, dropEveryN=480, elapsed=5000ms)"
 "Sync correction ended: DROPPING complete (dropped=11934 session, 11934 total, duration=850ms)"

v6.3.0 - Hybrid Audio Clock Support:
New Features:
- Added optional audio hardware clock support for VM-immune sync timing
 - IAudioPlayer.GetAudioClockMicroseconds(): Optional method returning hardware clock time
 - Default implementation returns null (backward compatible - no code changes needed)
 - AudioPipeline automatically prefers audio clock over MonotonicTimer when available
- Platform implementers can now override with their audio backend's hardware clock:
 - PulseAudio: pa_stream_get_time()
 - ALSA: snd_pcm_htimestamp()
 - PortAudio: outputBufferDacTime
 - CoreAudio: AudioTimeStamp.mHostTime
- Audio hardware clocks are immune to VM wall clock issues (hypervisor scheduling, time sync)
- Includes MonotonicTimer diagnostics from v6.2.0-preview.2

Backward Compatibility:
- Existing apps require ZERO code changes - MonotonicTimer fallback is automatic
- Only apps needing VM immunity need to implement GetAudioClockMicroseconds()

v6.2.0-preview.2 - MonotonicTimer Diagnostics:
- Added telemetry counters to MonotonicTimer for diagnosing timer behavior in VMs
 - TotalCalls, BackwardJumpCount, ForwardJumpCount
 - TotalBackwardJumpMicroseconds, TotalForwardJumpClampedMicroseconds
 - MaxBackwardJumpMicroseconds, MaxForwardJumpMicroseconds
 - GetStatsSummary() for formatted stats display
- MonotonicTimer now automatically reset on AudioPipeline.Clear() to avoid stale state
- Enhanced sync drift logging includes timer stats when error exceeds 50ms
- Reset() now accepts optional resetTelemetry parameter

v6.2.0-preview.1 - VM-Resilient Timing:
- Added MonotonicTimer wrapper for VM environments where wall clock can jump erratically
- Enforces monotonicity (never returns decreasing values) and clamps forward jumps to 50ms max
- Enabled by default in AudioPipeline - all SDK consumers automatically benefit
- No code changes required for existing consumers

v6.1.1 - Logging Improvements:
- Changed clock sync telemetry from Information to Debug log level
- Reduces log noise in production environments (offset/drift measurements are internal details)

v6.1.0 - Track End State Fix:

Bug Fixes:
- Fixed track end not clearing progress display (issue: UI showed stuck at final position with play button)
 - Server sends progress=null to signal track ended, but SDK was keeping the old progress value
 - Root cause: null-coalescing merge (meta.Progress ?? existing.Progress) couldn't distinguish
   "field absent" (partial update - keep existing) from "field is null" (track ended - clear progress)

New Features:
- Optional<T> type: Distinguishes JSON field absent from explicit null
 - Optional.IsPresent: Field was in the JSON (value may be null)
 - Optional.IsAbsent: Field was not in the JSON
 - Used for ServerMetadata.Progress to properly handle track end signal
- OptionalJsonConverterFactory: System.Text.Json converter for Optional<T>

BREAKING (compile-time only, minor):
- ServerMetadata.Progress type changed from PlaybackProgress? to Optional<PlaybackProgress?>
 - Most consumers use GroupState.Metadata.Progress (unchanged, still PlaybackProgress?)
 - Only affects code that directly accesses ServerMetadata from ServerStateMessage

Migration:
- If you directly access ServerMetadata.Progress:
 OLD: if (meta.Progress != null) { ... meta.Progress.TrackProgress ... }
 NEW: if (meta.Progress.IsPresent && meta.Progress.Value != null) { ... meta.Progress.Value.TrackProgress ... }
- GroupState.Metadata.Progress usage is unchanged (SDK handles the Optional internally)

v6.0.0 - Spec Compliance Overhaul:

This release brings the SDK into alignment with the official Sendspin protocol specification
(https://www.sendspin-audio.com/spec/). Non-spec extensions have been removed, missing fields
added, and naming mismatches corrected.

BREAKING CHANGES:

GroupUpdatePayload (group/update message):
- REMOVED: Volume, Muted, Metadata, Position, Shuffle, Repeat
- Per spec, group/update only contains: group_id, group_name, playback_state
- Volume/mute comes via server/state controller object
- Metadata comes via server/state metadata object

ServerHelloPayload (server/hello message):
- REMOVED: GroupId (not in spec)
- REMOVED: ProtocolVersion (string) - replaced by Version (int)
- REMOVED: Support dictionary (not in spec)
- ADDED: Version property (int, must be 1)

StreamStartPayload (stream/start message):
- REMOVED: StreamId (not in spec)
- REMOVED: TargetTimestamp (not in spec)

TrackMetadata (server/state metadata):
- REMOVED: ArtworkUri (not in spec - only ArtworkUrl exists)
- REMOVED: Uri (not in spec)
- REMOVED: MediaType (not in spec)
- CHANGED: Duration and Position are now READ-ONLY computed properties
 - Duration = Progress?.TrackDuration / 1000.0 (seconds from ms)
 - Position = Progress?.TrackProgress / 1000.0 (seconds from ms)
- ADDED: Timestamp (server timestamp in microseconds)
- ADDED: AlbumArtist (may differ from Artist on compilations)
- ADDED: Year (release year)
- ADDED: Track (track number on album)
- ADDED: Progress (PlaybackProgress object with TrackProgress, TrackDuration, PlaybackSpeed)
- ADDED: Repeat (string: "off", "one", "all")
- ADDED: Shuffle (bool)

ClientHelloPayload (client/hello message):
- FIXED: Property name changed from "player_support" to "player@v1_support" per spec

RETAINED EXTENSIONS (documented):
These SDK extensions are kept intentionally and documented as non-spec:
- client/sync_offset, client/sync_offset_ack: GroupSync acoustic calibration support
- PlayerStatePayload.BufferLevel: Diagnostic buffer level reporting
- PlayerStatePayload.Error: Error message reporting

DOCUMENTATION:
- GroupState: Updated XML docs clarifying field sources:
 - GroupId, Name, PlaybackState from group/update
 - Volume, Muted from server/state controller
 - Metadata, Shuffle, Repeat from server/state metadata

Migration Guide:
- If you accessed GroupUpdatePayload.Volume/Muted/Metadata, use GroupState which aggregates
 data from both group/update and server/state messages
- If you set TrackMetadata.Duration/Position directly, set Progress object instead
- If you used TrackMetadata.Uri/ArtworkUri, these were non-spec and have been removed
- If you accessed ServerHelloPayload.ProtocolVersion, use Version (int) instead

v5.4.1 - Group Handling Fixes:
Bug Fixes:
- Fixed GroupId not updating when player switches groups
 - HandleGroupUpdate() used null-coalescing assignment (??=) which only set GroupId when creating
   a new GroupState object, never updating it on subsequent group/update messages
 - Now always updates GroupId when non-empty, allowing proper group switching behavior
- Added group_name field to GroupUpdatePayload (per Sendspin spec)
 - GroupState.Name is now populated from server's group_name field
 - Enables proper display of group names in UI (e.g., Switch Group button tooltip)

Impact:
- Fixes group switching functionality for players moving between groups
- GroupState.Name now contains the friendly group name when provided by server

v5.4.0 - Player Volume Separation & Server Command ACK:
BREAKING CHANGES:
- GroupState.Volume and GroupState.Muted now represent the GROUP AVERAGE (display only)
 - Previously used for both group display and player audio control
 - Player volume/mute is now tracked separately via PlayerState

New Features:
- PlayerState model: Represents this player's own volume (0-100) and mute state
- PlayerStateChanged event: Fires when server changes player volume/mute via server/command
- CurrentPlayerState property: Access current player volume/mute state
- Automatic ACK: SDK sends client/state acknowledgement after applying server/command
 - Fixes spec compliance where server didn't know player applied volume changes
 - Enables correct group volume calculations for multi-player scenarios
- ClientCapabilities.InitialVolume/InitialMuted: Set player's initial state on connection

Migration:
- Subscribe to PlayerStateChanged (not GroupStateChanged) for volume/mute from server
- GroupStateChanged still used for playback state, metadata, track info
- See MIGRATION-5.4.0.md for detailed migration guide and code examples

Impact:
- Fixes group volume issues where players at different levels (15%, 45%) incorrectly
 applied group average (30%) to their own audio
- Now correctly ignores group average and only responds to server/command targeting
 this specific player

v5.3.0 - Artwork Support Fix & Discovery Improvements:
Bug Fixes:
- Fixed artwork@v1_support format to match Sendspin spec
 - Property name: artwork_support → artwork@v1_support
 - Structure: ArtworkSupport.Channels is now List<ArtworkChannelSpec>
 - Previous format caused handshake failures with spec-compliant servers

New Features:
- Added ArtworkChannelSpec class for artwork channel configuration
 - Source: "album" | "artist" | "none"
 - Format: "jpeg" | "png" | "bmp"
 - MediaWidth/MediaHeight: max dimensions in pixels
- Added MdnsServerDiscovery.IsDiscovering property

BREAKING (compile-time only):
- ArtworkSupport.Channels type changed from int to List<ArtworkChannelSpec>
- ArtworkSupport.SupportedFormats and MaxSize removed (use ArtworkChannelSpec)

v5.2.2 - Enhanced 3-Point Interpolation:
Improvements:
- Upgraded sync correction from 2-point to 3-point weighted interpolation
 - DROP: 0.25*lastOutput + 0.5*frameA + 0.25*frameB (was: (A+B)*0.5)
 - INSERT: 0.25*lastOutput + 0.5*nextFrame + 0.25*futureFrame (was: (last+next)*0.5)
- Added PeekSamplesFromBufferAtOffset() for looking ahead multiple frames
- INSERT now peeks 2 frames ahead for better curve fitting

Impact:
- Smoother transitions during sync corrections, especially for low-frequency content
- Better phase continuity through edit points
- Falls back to 2-point if insufficient frames available

v5.2.1 - Sync Correction Audio Quality:
Bug Fixes:
- Fixed audible clicks/pops during frame drop/insert sync corrections
 - Drop operations now blend adjacent frames: (frameA + frameB) * 0.5
 - Insert operations now interpolate: (lastOutput + nextInput) * 0.5
 - Previously used raw frame repetition causing waveform discontinuities
- Added PeekSamplesFromBuffer() helper for non-consuming buffer reads

Impact:
- Sync corrections (when error exceeds 15ms) are now inaudible
- Smoother audio during clock drift recovery
- No API changes - drop-in replacement for 5.2.0

v5.2.0 - Client Port Change (Spec Alignment):
BREAKING CHANGE:
- Default client listening port changed from 8927 to 8928
 - Aligns with Sendspin spec update (Sendspin/spec#62)
 - Servers: 8927 (_sendspin-server._tcp)
 - Clients: 8928 (_sendspin._tcp)
 - This allows servers and clients to coexist on the same machine without port conflicts

Affected Classes:
- ListenerOptions.Port: Default changed from 8927 to 8928
- AdvertiserOptions.Port: Default changed from 8927 to 8928

Migration:
- If your server expects clients on port 8927, update to discover/connect on 8928
- Or explicitly configure ListenerOptions/AdvertiserOptions to use 8927 for backward compatibility

v5.1.1 - Volume Handling Fix:
Bug Fixes:
- Fixed incorrect application of group volume to audio pipeline
 - HandleGroupUpdate and HandleServerState were applying GROUP volume (average of all players)
   to the local audio output, overriding player-specific volume settings
 - Per Sendspin spec: only server/command contains PLAYER volume to apply locally
 - group/update and server/state contain GROUP volume for UI display only
- This prevents volume sync issues in multi-player groups where each player has different
 volume levels (e.g., 60%, 80%, 100%) - previously all players would jump to group average

Impact:
- Multi-player groups now correctly maintain individual player volumes
- server/command remains the only message that applies volume to audio pipeline
- UI state (_currentGroup.Volume) still updated for display purposes

v5.1.0 - Faster Playback Startup:
Performance:
- Reduced audio startup delay from ~1000ms to ~200-300ms
 - Sync burst no longer blocks pipeline initialization
 - Smart burst skip: skips re-sync if clock already converged
 - Zero chunk loss during decoder/buffer initialization

New Features:
- IAudioPipeline.IsReady: Check if pipeline can accept audio chunks
- Pre-buffer queue: Captures early chunks during initialization, drains after startup

Implementation Details:
- SendTimeSyncBurstAsync now fire-and-forget (doesn't block StartAsync)
- Early chunks queued in ConcurrentQueue (max 100 chunks, ~2 seconds)
- Queue drained after pipeline.StartAsync() completes
- Queue cleared on stream/start and stream/end

Scenarios improved:
- First play after connection: No longer waits 450ms for sync burst
- Pause/resume: Near-instant resume when clock already synced
- Track changes: Faster transitions between tracks

For older release notes (v5.0.0 and earlier), see https://github.com/chrisuthe/windowsSpin/releases