Architecture
February 18, 2026 · View on GitHub
Intended audience: OSS developers contributing to or extending CamillaEQ.
This document does not cover: End-user setup, deployment, or system administration.
High-Level Responsibilities
CamillaEQ Server (Node.js/Fastify)
Does:
- Serves the web UI (static files in production)
- Persists EQ presets to disk (
/api/configs/*) - Provides write-through recovery cache for last-applied DSP state (
/api/state/latest) - Exposes version/health endpoints
Does NOT:
- Proxy CamillaDSP WebSocket connections
- Process audio
- Participate in the realtime spectrum path
Key files:
server/src/index.ts- Application entry pointserver/src/app.ts- Fastify app setup, middleware, error handlersserver/src/routes/*.ts- REST endpoint handlersserver/src/services/configStore.ts- Atomic config persistenceserver/src/services/configsLibrary.ts- Preset library management (user + AutoEQ)server/data/configs/autoeq/- Pre-imported AutoEQ library (read-only)server/data/configs/autoeq/index.json- AutoEQ manifest (performance optimization)
CamillaEQ Client (Svelte/TypeScript)
Does:
- Connects directly to CamillaDSP via two WebSocket connections
- Renders EQ controls, spectrum overlay, pipeline editor
- Manages UI state (bands, mixer, pipeline)
- Uploads config changes to CamillaDSP with convergence model (upload → re-download → sync)
- Persists presets to backend server (best-effort)
Does NOT:
- Process audio
- Run DSP algorithms (except UI-only smoothing/averaging of spectrum data)
- Maintain persistent connection to backend (REST only)
Key modules:
client/src/state/- Global stores (DSP session, EQ bands, pipeline editor)client/src/lib/camillaDSP.ts- CamillaDSP WebSocket clientclient/src/pages/- Top-level pages (Connect, EQ, Presets, Pipeline)client/src/dsp/- DSP math (filter response, spectrum parsing, temporal averaging)client/src/ui/rendering/- Canvas/SVG renderers
Path Separation
Realtime Path (High-Frequency)
Frequency: ~10 Hz (spectrum polling)
Components:
- Browser → CamillaDSP spectrum WebSocket:
GetPlaybackSignalPeakcommand client/src/dsp/spectrumParser.ts- Parse response (dBFS array)client/src/dsp/spectrumAnalyzer.ts- Temporal averaging (STA/LTA/Peak)client/src/dsp/fractionalOctaveSmoothing.ts- Spatial smoothingclient/src/ui/rendering/SpectrumCanvasRenderer.ts- Canvas redraw
Constraints:
- No DOM mutations
- No allocations in hot loop (reuse arrays where possible)
- Canvas-only rendering
Interactive Path (User-Triggered)
Frequency: Human interaction (~1-10 Hz burst)
Components:
- User drags token / adjusts knob
client/src/state/eqStore.ts- Update band state- Debounced upload (200ms) →
camillaDSP.uploadConfig() SetConfigJsoncommand sent to control WebSocket- CamillaDSP confirms →
GetConfigJsonre-download → UI convergence
Constraints:
- Optimistic UI updates OK (with convergence)
- Validation before upload (fail fast)
- Best-effort persistence to backend
Architectural Boundaries
Frontend ↔ CamillaDSP
Protocol: WebSocket (direct, no proxy)
Control socket:
- Commands:
GetConfigJson,SetConfigJson,GetVolume,SetVolume,GetVersion,GetState - Used for: Config management, volume control, metadata queries
Spectrum socket:
- Commands:
GetPlaybackSignalPeak - Used for: Real-time spectrum data polling
Rationale for two sockets:
- Separation of concerns: config operations don't block spectrum polling
- Allows degraded mode (control works, spectrum unavailable)
Frontend ↔ Backend
Protocol: HTTP REST
Endpoints:
GET /api/configs- List presetsGET /api/configs/:id- Load presetPUT /api/configs/:id- Save presetGET /api/state/latest- Get last-applied DSP statePUT /api/state/latest- Save last-applied DSP stateGET /api/settings- Get server-provided connection defaults (CamillaDSP WebSocket URLs)GET /api/version- Get server versionGET /health- Health check
Rationale:
- Backend is not in the realtime path
- REST is sufficient for non-realtime operations
- Presets are human-triggered, infrequent
Connection defaults:
- Server can optionally provide CamillaDSP WebSocket URLs via environment variables (
CAMILLA_CONTROL_WS_URL,CAMILLA_SPECTRUM_WS_URL) - Client fetches these via
GET /api/settingswhen localStorage is empty (first-time connection) - Enables production deployments to preconfigure connection parameters
Read-only mode:
- Server can be configured with
SERVER_READ_ONLY=trueto block write operations to/api/* - Used for safer public exposure (prevents persistence changes)
- CamillaDSP control via WebSocket remains fully functional (not affected by read-only mode)
Module Responsibilities
State Management (client/src/state/)
dspStore.ts
- Owns singleton
CamillaDSPinstance - Connection lifecycle (connect, disconnect, auto-reconnect)
- Global DSP config (
dspConfigstore) - Volume control (debounced
SetVolume) - Failure tracking (last 50 failures for diagnostics)
eqStore.ts
- Owns EQ band UI state (frequency, gain, Q, type, enabled)
- Single source of truth for EQ editor
- Debounced upload (200ms) with convergence:
- Upload →
SetConfigJson→GetConfigJson→ extract bands → sync UI
- Upload →
- Best-effort persistence to
/api/state/latest
pipelineEditor.ts
- Pipeline upload helper (validation + debounced upload)
- Status callback for UI feedback
- Re-initializes
eqStoreafter pipeline changes (sync band order)
DSP Client (client/src/lib/camillaDSP.ts)
Responsibilities:
- WebSocket lifecycle management (connect, disconnect)
- Per-socket request queues (
SocketRequestQueue) - Timeout protection (5s control, 2s spectrum)
- Config upload/download with validation
- Lifecycle event callbacks (
onSocketLifecycleEvent,onDspSuccess,onDspFailure)
Key methods:
connect(server, controlPort, spectrumPort)- Establish both socketsuploadConfig()-SetConfigJson+GetConfigJsonre-downloadgetSpectrumData()-GetPlaybackSignalPeakon spectrum socketvalidateConfig()- Check pipeline references exist
DSP Math (client/src/dsp/)
filterResponse.ts
- RBJ biquad filter response calculation (7 filter types)
- Magnitude response at N frequency points
spectrumParser.ts
- Parse
GetPlaybackSignalPeakresponse (array of dBFS values) - Validate: must be ≥3 numeric values (rejects stereo-only)
spectrumAnalyzer.ts
- Temporal averaging in dB domain:
- STA (short-term average, τ=0.8s)
- LTA (long-term average, τ=8s)
- Peak hold (hold=2s, decay=12 dB/s)
fractionalOctaveSmoothing.ts
- Spatial smoothing (1/12, 1/6, 1/3 octave)
- Per-bin weighted average across neighboring bins
Rendering (client/src/ui/rendering/)
EqSvgRenderer.ts
- Generates SVG path data for EQ curves
- Sum curve + per-band curves
- Focus mode visualization (band fill, bandwidth markers)
SpectrumCanvasRenderer.ts
- Canvas-based spectrum rendering (~10 Hz)
- Pluggable layer architecture (
CanvasVisualizationLayer) - DPR-aware scaling
- Stale data detection (fade to 30% if no data >500ms)
canvasLayers/SpectrumAnalyzerLayer.ts
- Renders STA/LTA/Peak series as lines
- Color-coded for pre/post modes
canvasLayers/SpectrumHeatmapLayer.ts
- Renders spectrum as vertical orange lines (heatmap)
- Amplitude via opacity and brightness
- Supports masking (top/bottom/full) relative to histogram curve
- Opacity and line width per series
Backend Services (server/src/services/)
configStore.ts
- Atomic file writes (write temp → rename)
- Read/write single config file (
data/config.jsonordata/latest_dsp_state.json) - Size limits (1MB max)
- JSON validation
configsLibrary.ts
- Preset library management (
data/configs/*.json) - List/get/save operations
- Auto-generates kebab-case IDs from filenames
- Reads
configNamefrom JSON for display
mockCamillaDSP.ts
- Fake CamillaDSP instance for testing
- Returns realistic spectrum data (256 bins)
- Implements control + spectrum WebSocket protocol
Data Models
CamillaDSP Config (Canonical Format)
Schema: client/src/lib/camillaSchema.ts
Shape:
{
devices: { samplerate, chunksize, capture, playback },
filters: Record<string, FilterDefinition>,
mixers: Record<string, MixerDefinition>,
processors: Record<string, ProcessorDefinition>,
pipeline: PipelineStep[]
}
Pipeline steps:
{ type: 'Filter', channels: number[], names: string[] }{ type: 'Mixer', name: string }{ type: 'Processor', name: string }(or custom type)
Pipeline Config (On-Disk Preset Format)
Schema: client/src/lib/pipelineConfigMapping.ts
Legacy format (EQ-only):
{
configName: string,
filterArray: Array<{ FilterName: { freq, gain, q, type } }>,
accessKey?: string
}
Extended format (full pipeline):
{
configName: string,
filterArray: [], // can be empty
filters: Record<string, any>,
mixers: Record<string, any>,
processors: Record<string, any>,
pipeline: PipelineStep[],
title?: string,
description?: string
}
Note: Devices are never stored in presets. They always come from the current DSP config or a template.
Extension Points
For detailed extension guidance, see extension-points.md.
Safe extensions:
- New visualization layers (
CanvasVisualizationLayerinterface) - New filter types (extend
knownTypes.ts+ parameter editors) - New pipeline block operations (follow
pipelineBlockEdit.tspatterns)
Unsafe extensions:
- Backend proxying of spectrum WebSocket (breaks degraded mode)
- DOM mutations in spectrum rendering loop (causes jank)
- Skipping convergence step after config uploads (causes UI/DSP drift)
Next Steps
- Runtime Topology - Process diagram and socket inventory
- Data Flow - Control and data flow diagrams
- Frontend - Detailed client architecture
- Backend - Detailed server architecture
- State and Persistence - State ownership model
- Extension Points - Safe extension patterns