What
Add per-client latency offsets using Snapcast's Client.SetLatency RPC. Nodes with different DAC hardware have different inherent output latencies — without per-client compensation, multi-room sync is off by a hardware-dependent constant. Snapcast already supports this; Audera just needs to expose it.
Three touch-points: the Player model, SnapserverClient, and the streamer UI Players tab.
Why
Different DAC hardware (USB, HDMI, I2S) introduces different amounts of output buffering and analog filter delay. Snapcast's latency offset is the standard mechanism for correcting this. ADR 003 documents the broader audio latency context.
Architecture Decisions
Snapcast owns latency state — Audera does not persist it.
Snapcast stores client configuration (including latency) on the server. get_clients() reads it back on every poll. Persisting latency_ms in ~/.audera/players/ would create two sources of truth and a synchronization problem. Instead, latency_ms is a runtime field on Player: parsed from the Snapcast response, excluded from to_dict() (like name), but included in __eq__ so the UI detects latency changes during polling.
No new helper method for the UI control.
The latency input follows the exact same callback pattern as the existing volume slider. Introduce an inline callback in _build_players_tab(), not a new _build_latency_control() method. One new abstraction is one more thing to maintain.
No ADR. This is a thin wrapper over an existing Snapcast RPC with a clear, obvious ownership model. ADR 003 already covers multi-room latency context.
Implementation Tasks
All code snippets are reference only. Read the existing implementation before writing anything.
What
Add per-client latency offsets using Snapcast's
Client.SetLatencyRPC. Nodes with different DAC hardware have different inherent output latencies — without per-client compensation, multi-room sync is off by a hardware-dependent constant. Snapcast already supports this; Audera just needs to expose it.Three touch-points: the
Playermodel,SnapserverClient, and the streamer UI Players tab.Why
Different DAC hardware (USB, HDMI, I2S) introduces different amounts of output buffering and analog filter delay. Snapcast's latency offset is the standard mechanism for correcting this. ADR 003 documents the broader audio latency context.
Architecture Decisions
Snapcast owns latency state — Audera does not persist it.
Snapcast stores client configuration (including latency) on the server.
get_clients()reads it back on every poll. Persistinglatency_msin~/.audera/players/would create two sources of truth and a synchronization problem. Instead,latency_msis a runtime field onPlayer: parsed from the Snapcast response, excluded fromto_dict()(likename), but included in__eq__so the UI detects latency changes during polling.No new helper method for the UI control.
The latency input follows the exact same callback pattern as the existing volume slider. Introduce an inline callback in
_build_players_tab(), not a new_build_latency_control()method. One new abstraction is one more thing to maintain.No ADR. This is a thin wrapper over an existing Snapcast RPC with a clear, obvious ownership model. ADR 003 already covers multi-room latency context.
Implementation Tasks
audera/models/player.py— Addlatency_ms: int(default0) toPlayer. Exclude fromto_dict()(alongsidename). Keep in__eq__so polling detects state changes. Updatefrom_dict()/from_config()to parse it from the Snapcast response (not from persisted JSON).Reference path in Snapcast response:
client['config']['latency']audera/clients/snapserver.py— Addset_client_latency(client_id: str, latency_ms: int) -> dictfollowing the pattern ofset_client_volume(). Updateget_clients()to populatelatency_mswhen constructing eachPlayer.Reference RPC call:
audera/ui/streamer/pages.py— Add a latency number input (min=-500,max=500,step=1) to each player card in_build_players_tab(), alongside the existing volume/mute controls. Wire itson_changecallback inline toset_client_latency(), matching the volume callback pattern.tests/clients/test_snapserver.py— Add a test forset_client_latency()following the existingtest_set_client_volumepattern. Requires Docker (snapserver_containerfixture).tests/ui/streamer/— Add a UI integration test asserting the latency input renders in the Players tab with the correct initial value, following the existing volume/mute control test pattern.