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
- Library-first architecture - Primary use case is embedding in Go applications
- Layered APIs - High-level convenience for most users, low-level building blocks for power users
- Aggressive migration - Complete restructure, not gradual wrapper approach
- Backward compatibility - CLI tools use library but maintain same functionality
- 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 implementationexamples/basic-server/- Simple server implementationexamples/custom-source/- Custom AudioSource implementationexamples/multi-room/- Multiple synchronized playersexamples/audio-pipeline/- Using low-level audio components
These examples serve dual purpose:
- Documentation for library users
- Integration tests for the library
Migration Strategy
Step-by-Step Plan
- Create new package structure - Set up
pkg/directories with package stubs - Move audio primitives first -
pkg/audio/,pkg/audio/decode/, etc. (foundation layer) - Move protocol layer -
pkg/protocol/,pkg/sync/,pkg/discovery/ - Build high-level APIs -
pkg/resonate/wrapping the components - Migrate CLI tools - Rewrite to use
pkg/resonate/ - Add examples - Create
examples/directory with working code - 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.0when complete - Signals stable library API and commitment to compatibility
- Previous v0.0.x were "pre-library" releases for CLI tools only
Rollout Plan
- Work in a feature branch/worktree (
library-refactor) - Validate each layer works before moving to next
- Merge to main when:
- All packages implemented
- CLI tools work with library
- Examples run successfully
- Tests pass
- Tag
v1.0.0release
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
- ✅ All code moved from
internal/topkg/where appropriate - ✅ High-level API works for simple player/server use cases
- ✅ Low-level APIs allow building custom pipelines
- ✅ CLI tools work using public library APIs
- ✅ All examples run successfully
- ✅ All tests pass
- ✅ Documentation complete (godoc + README + examples)
- ✅ 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
- Set up git worktree for isolated development
- Create detailed implementation plan with tasks
- Begin migration starting with foundation layer