Packages

A pure-Elixir Snapcast server: speak Snapcast's binary protocol directly to snapclients, owning the audio clock and timestamping every chunk (PCM or FLAC transport) — no external snapserver.

Current section

Files

Jump to
snapcast lib snapcast clock.ex
Raw

lib/snapcast/clock.ex

defmodule Snapcast.Clock do
@moduledoc """
The server's monotonic audio clock, shared by all sessions and streams.
Snapcast timestamps are `{sec, usec}` on an arbitrary steady clock — only
*differences* matter (the client derives its offset from Time sync), so we use a
single process-wide epoch and express everything relative to it. Keeping values
small also keeps `sec` within int32.
"""
@key {__MODULE__, :epoch_us}
@doc "Fix the epoch (call once at startup). Safe to call repeatedly."
def init, do: epoch()
@doc "Microseconds since the server epoch (monotonic, non-negative)."
def now_us, do: System.monotonic_time(:microsecond) - epoch()
@doc "Convert an absolute `System.monotonic_time(:microsecond)` value to this clock."
def from_monotonic_us(monotonic_us) when is_integer(monotonic_us), do: monotonic_us - epoch()
@doc "Current time as a snapcast `{sec, usec}` tv."
def now_tv, do: us_to_tv(now_us())
@doc "`{sec, usec}` → microseconds."
def tv_to_us({sec, usec}), do: sec * 1_000_000 + usec
@doc """
Microseconds → `{sec, usec}`. Uses truncating div/rem so `sec*1e6 + usec == us`
exactly, including for negatives (Time-sync deltas can be negative).
"""
def us_to_tv(us), do: {div(us, 1_000_000), rem(us, 1_000_000)}
defp epoch do
case :persistent_term.get(@key, nil) do
nil ->
e = System.monotonic_time(:microsecond)
:persistent_term.put(@key, e)
e
e ->
e
end
end
end