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