Files
-rpi-sendspin/third_party/sendspin-go/docs/plans/2025-10-25-library-refactor-design.md

14 KiB

Library Refactor Design

Date: 2025-10-25 Status: Approved Approach: Ground-Up Redesign (Approach C)

Overview

Convert resonate-go from a CLI-focused project to a library-first architecture with layered APIs. The existing CLI tools will become thin wrappers that use the public library APIs, serving as reference implementations.

Goals

  1. Library-first architecture - Primary use case is embedding in Go applications
  2. Layered APIs - High-level convenience for most users, low-level building blocks for power users
  3. Aggressive migration - Complete restructure, not gradual wrapper approach
  4. Backward compatibility - CLI tools use library but maintain same functionality
  5. Clean package design - Intuitive organization, clear boundaries, good documentation

Use Cases

Primary Use Cases

  • Embed player in Go applications (desktop music apps, smart home controllers)
  • Embed server in Go applications (music streaming services, home media servers)
  • Build custom audio pipelines (decoders, resamplers, encoders for custom processing)
  • Enable Music Assistant and similar systems to integrate Resonate directly

Example Use Cases

  • Desktop music player with custom UI
  • Multi-room audio controller
  • Home media server with Resonate output
  • Audio processing pipeline with hi-res support
  • Music Assistant native Resonate provider

Architecture

Three-Layer Design

Layer 1: High-Level Convenience (pkg/resonate/)

  • Simple constructors: NewPlayer(), NewServer()
  • Sensible defaults for common use cases
  • Hides complexity: connection management, format negotiation, error recovery
  • Target: Users who want "just play audio" or "just serve audio"

Layer 2: Component APIs (pkg/audio/, pkg/protocol/, pkg/sync/, pkg/discovery/)

  • Building blocks for custom implementations
  • Each package focused on one concern
  • Composable: mix and match components
  • Target: Users building custom audio pipelines or integrations

Layer 3: Internal Implementation (internal/)

  • CLI app logic (internal/app/)
  • TUI implementation (internal/ui/)
  • Implementation details not exposed as public API

Package Structure

resonate-go/
├── pkg/
│   ├── resonate/           # High-level convenience API
│   │   ├── player.go       # Player API
│   │   ├── server.go       # Server API
│   │   └── source.go       # AudioSource interface + built-ins
│   ├── audio/              # Audio fundamentals
│   │   ├── format.go       # Format, Buffer types
│   │   ├── types.go        # Constants, conversion helpers
│   │   ├── decode/         # Decoders
│   │   │   ├── decoder.go  # Interface
│   │   │   ├── pcm.go      # PCM decoder
│   │   │   ├── opus.go     # Opus decoder
│   │   │   ├── flac.go     # FLAC decoder
│   │   │   └── mp3.go      # MP3 decoder
│   │   ├── encode/         # Encoders
│   │   │   ├── encoder.go  # Interface
│   │   │   ├── pcm.go      # PCM encoder
│   │   │   └── opus.go     # Opus encoder
│   │   ├── resample/       # Resampling
│   │   │   └── resampler.go
│   │   └── output/         # Audio output
│   │       ├── output.go   # Interface
│   │       └── portaudio.go
│   ├── protocol/           # Resonate wire protocol
│   │   ├── messages.go     # Protocol message types
│   │   └── client.go       # WebSocket client
│   ├── sync/               # Clock synchronization
│   │   └── clock.go
│   └── discovery/          # mDNS discovery
│       └── mdns.go
├── cmd/
│   ├── resonate-player/    # Thin CLI wrapper
│   └── resonate-server/    # Thin CLI wrapper
├── internal/
│   ├── app/                # CLI app logic
│   └── ui/                 # TUI implementation
├── examples/               # Example code
│   ├── basic-player/
│   ├── basic-server/
│   ├── custom-source/
│   ├── multi-room/
│   └── audio-pipeline/
└── docs/
    └── plans/

API Design

High-Level API (pkg/resonate/)

Player API

package resonate

// PlayerConfig for creating a player
type PlayerConfig struct {
    ServerAddr string  // "localhost:8927" or discovered via mDNS
    PlayerName string  // Display name
    Volume     int     // 0-100
    DebugMode  bool    // Enable debug logging
}

// Player represents a Resonate audio player
type Player struct {
    // unexported fields
}

// NewPlayer creates a new player instance
func NewPlayer(cfg PlayerConfig) (*Player, error)

// Connect to the configured server
func (p *Player) Connect() error

// Play starts playback
func (p *Player) Play() error

// Pause pauses playback
func (p *Player) Pause() error

// Stop stops playback and disconnects
func (p *Player) Stop() error

// SetVolume adjusts volume (0-100)
func (p *Player) SetVolume(vol int) error

// Mute toggles mute
func (p *Player) Mute(muted bool) error

// Status returns current playback status
func (p *Player) Status() PlayerStatus

// Close releases resources
func (p *Player) Close() error

Server API

package resonate

// ServerConfig for creating a server
type ServerConfig struct {
    Address    string      // "0.0.0.0"
    Port       int         // 8927
    Source     AudioSource // Where audio comes from
    EnablemDNS bool        // Advertise via mDNS
    DebugMode  bool
}

// Server serves audio to Resonate clients
type Server struct {
    // unexported fields
}

// NewServer creates a new server instance
func NewServer(cfg ServerConfig) (*Server, error)

// Start begins serving (blocks)
func (s *Server) Start() error

// Stop gracefully shuts down
func (s *Server) Stop() error

// Clients returns connected client info
func (s *Server) Clients() []ClientInfo

AudioSource Interface

package resonate

// AudioSource provides audio samples for the server
type AudioSource interface {
    Read(samples []int32) (int, error)
    SampleRate() int
    Channels() int
    BitDepth() int
    Metadata() (title, artist, album string)
    Close() error
}

// Built-in source constructors
func FileSource(path string) (AudioSource, error)
func TestToneSource(frequency float64) AudioSource

Component-Level APIs

pkg/audio/ - Audio Fundamentals

package audio

// Core types
type Format struct {
    Codec      string
    SampleRate int
    Channels   int
    BitDepth   int
    CodecHeader []byte
}

type Buffer struct {
    Timestamp int64
    PlayAt    time.Time
    Samples   []int32
    Format    Format
}

// Constants
const (
    Max24Bit = 8388607  // 2^23 - 1
    Min24Bit = -8388608 // -2^23
)

// Conversion helpers
func SampleToInt16(sample int32) int16
func SampleFromInt16(sample int16) int32
func SampleTo24Bit(sample int32) [3]byte
func SampleFrom24Bit(b [3]byte) int32

pkg/audio/decode/ - Decoders

package decode

// Decoder interface
type Decoder interface {
    Decode(data []byte) ([]int32, error)
    Close() error
}

// Constructors for each codec
func NewPCM(format audio.Format) (Decoder, error)
func NewOpus(format audio.Format) (Decoder, error)
func NewFLAC(format audio.Format) (Decoder, error)
func NewMP3(format audio.Format) (Decoder, error)

pkg/audio/encode/ - Encoders

package encode

type Encoder interface {
    Encode(samples []int32) ([]byte, error)
    Close() error
}

func NewPCM(format audio.Format) (Encoder, error)
func NewOpus(format audio.Format) (Encoder, error)

pkg/audio/resample/ - Resampling

package resample

type Resampler struct {
    // unexported
}

func New(inputRate, outputRate, channels int) *Resampler
func (r *Resampler) Resample(input, output []int32) int

pkg/audio/output/ - Audio Output

package output

type Output interface {
    Open(sampleRate, channels int) error
    Write(samples []int32) error
    Close() error
}

func NewPortAudio() Output

pkg/protocol/ - Resonate Wire Protocol

package protocol

// Message types
type HelloMessage struct {
    PlayerName       string
    SupportFormats   []Format
    SupportCodecs    []string
    SupportSampleRates []int
    SupportBitDepth  []int
}

type StartMessage struct {
    Format       Format
    ServerTime   int64
    StreamOffset int64
}

type ChunkMessage struct {
    Timestamp int64
    Data      []byte
}

type ControlMessage struct {
    Command string // "play", "pause", "stop"
}

// Client for low-level protocol control
type Client struct {
    // unexported
}

func NewClient(serverAddr string) (*Client, error)
func (c *Client) SendHello(hello HelloMessage) error
func (c *Client) ReadMessage() (interface{}, error)
func (c *Client) Close() error

pkg/sync/ - Clock Synchronization

package sync

type Clock struct {
    // unexported
}

func NewClock() *Clock
func (c *Clock) Sync(serverAddr string) error
func (c *Clock) ServerTime() int64
func (c *Clock) LocalTime() time.Time
func (c *Clock) Offset() int64

pkg/discovery/ - mDNS Server Discovery

package discovery

type Service struct {
    Name    string
    Address string
    Port    int
}

// Discover servers on the network
func Discover(timeout time.Duration) ([]Service, error)

// Advertise this server
func Advertise(name string, port int) error
func StopAdvertising() error

CLI Migration

The existing CLI tools (cmd/resonate-player/main.go, cmd/resonate-server/main.go) will be rewritten to use the high-level pkg/resonate/ API. They will handle only:

  • Flag parsing
  • TUI setup (using internal/ui/)
  • Signal handling
  • Calling library functions

Example - New Player CLI

package main

import (
    "github.com/harperreed/resonate-go/pkg/resonate"
    "github.com/harperreed/resonate-go/internal/ui"
)

func main() {
    // Parse flags
    cfg := resonate.PlayerConfig{
        ServerAddr: *serverFlag,
        PlayerName: *nameFlag,
        Volume:     *volumeFlag,
        DebugMode:  *debugFlag,
    }

    // Create player using library
    player, err := resonate.NewPlayer(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer player.Close()

    // Connect and play
    if err := player.Connect(); err != nil {
        log.Fatal(err)
    }

    // Run TUI (internal implementation)
    ui.RunPlayerUI(player)
}

Examples

Create examples/ directory with real-world usage:

  • examples/basic-player/ - Simple player implementation
  • examples/basic-server/ - Simple server implementation
  • examples/custom-source/ - Custom AudioSource implementation
  • examples/multi-room/ - Multiple synchronized players
  • examples/audio-pipeline/ - Using low-level audio components

These examples serve dual purpose:

  1. Documentation for library users
  2. Integration tests for the library

Migration Strategy

Step-by-Step Plan

  1. Create new package structure - Set up pkg/ directories with package stubs
  2. Move audio primitives first - pkg/audio/, pkg/audio/decode/, etc. (foundation layer)
  3. Move protocol layer - pkg/protocol/, pkg/sync/, pkg/discovery/
  4. Build high-level APIs - pkg/resonate/ wrapping the components
  5. Migrate CLI tools - Rewrite to use pkg/resonate/
  6. Add examples - Create examples/ directory with working code
  7. Documentation - README updates, godoc comments on all exported types/functions

Testing Strategy

  • Move existing tests alongside the code as packages are migrated
  • Add integration tests in examples/ (they serve dual purpose as docs)
  • Ensure CLI tools work identically to current behavior
  • Test both high-level and low-level APIs work independently

Version Strategy

  • This is a major refactor → tag as v1.0.0 when complete
  • Signals stable library API and commitment to compatibility
  • Previous v0.0.x were "pre-library" releases for CLI tools only

Rollout Plan

  1. Work in a feature branch/worktree (library-refactor)
  2. Validate each layer works before moving to next
  3. Merge to main when:
    • All packages implemented
    • CLI tools work with library
    • Examples run successfully
    • Tests pass
  4. Tag v1.0.0 release

Documentation Requirements

Package Documentation

Every exported package, type, function must have godoc comments:

  • Package-level doc explaining purpose and usage
  • Type-level doc with example
  • Function-level doc with parameters and return values

README Updates

  • Add "Using as a Library" section
  • Show both high-level and low-level API examples
  • Link to examples directory
  • Keep CLI usage docs

Examples

Each example should be a complete, runnable program showing real-world usage.

Success Criteria

  1. All code moved from internal/ to pkg/ where appropriate
  2. High-level API works for simple player/server use cases
  3. Low-level APIs allow building custom pipelines
  4. CLI tools work using public library APIs
  5. All examples run successfully
  6. All tests pass
  7. Documentation complete (godoc + README + examples)
  8. Tagged as v1.0.0

Timeline Estimate

  • 5-7 days total for complete migration
  • Foundation (audio/protocol): 2 days
  • High-level API: 1-2 days
  • CLI migration: 1 day
  • Examples + documentation: 1-2 days
  • Testing + polish: 1 day

Trade-offs

Advantages

  • Best API design - clean separation, intuitive naming
  • Maximum flexibility - every component usable independently
  • Good documentation story - clear package boundaries
  • Future-proof - stable v1.0.0 API

Disadvantages

  • Most work - complete restructure vs. simple wrapper
  • Higher risk - more things to potentially break
  • Longer timeline - 5-7 days vs. 1-2 days for simple approach

Mitigation

  • Work in isolated worktree/branch
  • Validate each layer before proceeding
  • Keep existing code as reference
  • Comprehensive testing at each stage

Next Steps

  1. Set up git worktree for isolated development
  2. Create detailed implementation plan with tasks
  3. Begin migration starting with foundation layer