Thank you for your interest in contributing to the Sendspin Windows Client! This document provides guidelines and information to help you get started.
- Code of Conduct
- Getting Started
- Development Setup
- Project Structure
- Code Style Guidelines
- Testing
- Pull Request Process
- Code Review Expectations
- Commit Messages
- Documentation
This project adheres to a code of conduct that we expect all contributors to follow:
- Be respectful and inclusive
- Welcome newcomers and help them learn
- Focus on constructive feedback
- Assume good intentions
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/your-username/windowsSpin.git cd windowsSpin - Add the upstream repository:
git remote add upstream https://github.com/chrisuthe/windowsSpin.git
- Create a branch for your changes:
git checkout -b feature/your-feature-name
- Visual Studio 2022 (17.8 or later) with:
- .NET desktop development workload
- Windows 10 SDK (10.0.17763.0)
- OR JetBrains Rider (2023.3 or later)
- .NET 10.0 SDK
- Git for version control
- ReSharper or Visual Studio IntelliCode
- CodeMaid - Code cleanup and organization
- Markdown Editor - For documentation editing
-
Restore NuGet packages:
dotnet restore
-
Build the solution:
dotnet build
-
Verify the build succeeds with no errors
-
Run the application:
dotnet run --project src/SendspinClient/SendspinClient.csproj
For development and testing, you need a Music Assistant server with Sendspin support:
- Install Music Assistant following the official documentation
- Enable Sendspin in Music Assistant settings
- Note the server IP/hostname for testing
windowsSpin/
├── src/
│ ├── Sendspin.SDK/ # Cross-platform protocol SDK (NuGet package)
│ │ ├── Audio/ # Audio pipeline, buffer, decoders
│ │ ├── Client/ # Client services and capabilities
│ │ ├── Connection/ # WebSocket connection management
│ │ ├── Discovery/ # mDNS discovery and advertisement
│ │ ├── Protocol/ # Message serialization and parsing
│ │ │ └── Messages/ # Protocol message types
│ │ ├── Models/ # Data models
│ │ └── Synchronization/ # Clock synchronization
│ ├── SendspinClient.Services/ # Windows-specific services (NAudio, notifications)
│ └── SendspinClient/ # WPF desktop application
│ ├── ViewModels/ # MVVM view models
│ ├── Views/ # XAML views
│ ├── Resources/ # UI resources and converters
│ └── MainWindow.xaml # Main UI
├── docs/ # Documentation
├── .editorconfig # Editor configuration
├── stylecop.json # StyleCop settings
├── CodeAnalysis.ruleset # Analyzer configuration
├── Directory.Build.props # Shared MSBuild properties
└── SendspinClient.sln # Solution file
This project enforces consistent code style through automated analyzers and .editorconfig settings.
The project uses three code analyzers configured in Directory.Build.props:
- Roslynator - Comprehensive code analysis and refactoring
- StyleCop - Code style enforcement
- SonarAnalyzer - Code quality checks
- Indentation: 4 spaces (no tabs)
- Line endings: CRLF (Windows)
- Encoding: UTF-8 with BOM
- End of file: Blank line required
- Private fields: Camel case with underscore prefix (
_fieldName) - Public members: Pascal case (
PropertyName,MethodName()) - Constants: Pascal case (
MaxBufferSize) - Interfaces: Pascal case with
Iprefix (IConnection) - Type parameters: Pascal case with
Tprefix (TMessage)
- Using directives: Outside namespace, system directives first
- File-scoped namespaces: Use file-scoped namespace declarations
- One type per file: Each public type in its own file
- File naming: Match the primary type name
// Avoid var for built-in types
int count = 10;
string name = "test";
// Use var when type is apparent
var client = new SendspinClientService();
var servers = GetDiscoveredServers();// Use for simple properties
public string Name => _name;
// Use for single-line methods
public int GetCount() => _items.Count;
// Avoid for constructors
public MyClass(string name)
{
_name = name;
}// Prefer pattern matching
if (obj is string text)
{
Process(text);
}
// Use switch expressions
var result = value switch
{
0 => "zero",
> 0 => "positive",
< 0 => "negative"
};// Use nullable reference types
public void Process(string? input)
{
if (input is null) return;
// Process non-null input
}
// Use null-coalescing
var value = input ?? defaultValue;
// Use null-conditional
var length = text?.Length ?? 0;All public APIs must have XML documentation comments:
/// <summary>
/// Connects to a Sendspin server asynchronously.
/// </summary>
/// <param name="serverUri">The WebSocket URI of the server.</param>
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
/// <returns>A task representing the asynchronous connection operation.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="serverUri"/> is null.</exception>
/// <exception cref="TimeoutException">Thrown when the handshake times out.</exception>
public async Task ConnectAsync(Uri serverUri, CancellationToken cancellationToken = default)
{
// Implementation
}Required Tags:
<summary>: Brief description of the member<param>: Description for each parameter<returns>: Description of return value (for non-void methods)<exception>: Document exceptions that can be thrown
// Always suffix async methods with Async
public async Task ConnectAsync()
// Always accept CancellationToken
public async Task ProcessAsync(CancellationToken cancellationToken = default)
// Use ConfigureAwait(false) in libraries
await SomeOperationAsync().ConfigureAwait(false);
// Avoid async void (except event handlers)
private async void OnButtonClick(object sender, EventArgs e)// Use specific exceptions
throw new ArgumentNullException(nameof(parameter));
throw new InvalidOperationException("Connection not established");
// Log exceptions before re-throwing
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process message");
throw;
}
// Use when clause for specific handling
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
_logger.LogError("Operation timed out");
}// Constructor injection for required dependencies
public class MyService
{
private readonly ILogger<MyService> _logger;
private readonly IConnection _connection;
public MyService(
ILogger<MyService> logger,
IConnection connection)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
}
}
// Optional parameters for optional dependencies
public MyService(
ILogger<MyService> logger,
IConnection connection,
IClockSynchronizer? clockSync = null)
{
_clockSync = clockSync ?? new DefaultClockSynchronizer();
}Tests will be organized into:
- Unit Tests: Test individual components in isolation
- Integration Tests: Test component interactions
- End-to-End Tests: Test complete workflows
[Fact]
public async Task ConnectAsync_WithValidUri_ShouldConnect()
{
// Arrange
var client = CreateClient();
var serverUri = new Uri("ws://localhost:8080/sendspin");
// Act
await client.ConnectAsync(serverUri);
// Assert
Assert.Equal(ConnectionState.Connected, client.ConnectionState);
}# Run all tests
dotnet test
# Run tests with coverage
dotnet test /p:CollectCoverage=true-
Ensure code builds without errors or warnings:
dotnet build --configuration Release
-
Fix all analyzer warnings:
- StyleCop warnings
- Roslynator suggestions
- SonarAnalyzer issues
-
Add/update documentation:
- XML documentation for public APIs
- README.md for new features
- Code comments for complex logic
-
Test your changes:
- Manual testing with real Music Assistant server
- Automated tests (when available)
-
Push your branch to your fork:
git push origin feature/your-feature-name
-
Create a pull request on GitHub with:
- Clear title: Concise description of the change
- Description: What, why, and how
- Related issues: Link to any related issues
- Screenshots: For UI changes
- Testing notes: How to test the changes
-
PR Template (example):
## Description Implements mDNS server discovery using Zeroconf library. ## Changes - Added MdnsServerDiscovery class - Implemented IServerDiscovery interface - Added discovery events (ServerFound, ServerLost) ## Related Issues Closes #123 ## Testing - Tested with Music Assistant 2.0 - Verified discovery on local network - Tested server connection after discovery ## Screenshots (if applicable)
- Be responsive to feedback and questions
- Explain your approach if it's non-obvious
- Be open to suggestions and alternative approaches
- Update the PR based on feedback
Reviews should focus on:
- Correctness: Does it work as intended?
- Code Quality: Is it clean, readable, and maintainable?
- Design: Does it fit the architecture?
- Performance: Are there any performance concerns?
- Security: Are there any security issues?
- Testing: Is it adequately tested?
- Documentation: Is it properly documented?
Review Checklist:
- Code follows style guidelines
- Public APIs have XML documentation
- No analyzer warnings
- Appropriate error handling
- Logging added where appropriate
- No hardcoded values (use configuration)
- Thread-safe where necessary
- Disposable resources properly disposed
<type>(<scope>): <subject>
<body>
<footer>
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- style: Code style changes (formatting, no logic change)
- refactor: Code refactoring
- perf: Performance improvements
- test: Adding or updating tests
- chore: Build process or tooling changes
feat(discovery): add mDNS server discovery
Implements automatic discovery of Sendspin servers on the local network
using Zeroconf library. Supports continuous monitoring and one-time scans.
Closes #123
fix(connection): handle WebSocket disconnect gracefully
Previously, unexpected disconnects would cause unhandled exceptions.
Now properly catches and logs disconnection events.
Fixes #456
- Use imperative mood ("add" not "added" or "adds")
- Keep subject line under 72 characters
- Separate subject from body with blank line
- Wrap body at 72 characters
- Reference issues in footer
- XML comments for all public APIs (required)
- Inline comments for complex algorithms or non-obvious code
- README updates for new features or breaking changes
- Architecture decisions documented in code or separate docs
- Be Clear: Use simple, precise language
- Be Accurate: Keep documentation in sync with code
- Be Helpful: Explain why, not just what
- Provide Examples: Show usage where appropriate
- Link to Related Docs: Reference protocol specs, related classes, etc.
/// <summary>
/// Synchronizes the client clock with the server using NTP-style measurements.
/// Uses a Kalman filter to estimate clock offset and drift, providing sub-millisecond
/// accuracy for multi-room audio synchronization.
/// </summary>
/// <remarks>
/// The synchronization process follows these steps:
/// 1. Client sends timestamp T1 (client transmitted)
/// 2. Server records T2 (server received) and T3 (server transmitted)
/// 3. Client records T4 (client received)
/// 4. Kalman filter processes the four timestamps to estimate offset and drift
///
/// See: https://github.com/music-assistant/sendspin for protocol details
/// </remarks>
/// <param name="t1">Client transmission timestamp (microseconds)</param>
/// <param name="t2">Server reception timestamp (microseconds)</param>
/// <param name="t3">Server transmission timestamp (microseconds)</param>
/// <param name="t4">Client reception timestamp (microseconds)</param>
public void ProcessMeasurement(long t1, long t2, long t3, long t4)
{
// Implementation
}If you need help or have questions:
-
Check existing documentation:
- README.md
- XML documentation in code
- Protocol specification
-
Search existing issues on GitHub
-
Ask in discussions or create a new issue
-
Join the community:
- Music Assistant Discord
- GitHub Discussions
Contributors will be:
- Listed in project contributors
- Credited in release notes for significant contributions
- Acknowledged in documentation for major features
Thank you for contributing to Sendspin Windows Client!