# Snapcast
A pure-Elixir **Snapcast server**: speak [Snapcast](https://github.com/badaix/snapcast)'s
binary protocol directly to `snapclient`s, owning the audio clock and timestamping every
chunk — so there is **no external snapserver** and no ffmpeg/snapserver pacing to fight.
The server stamps each `WireChunk` with the server-clock time at which it should play, and
each client plays it `bufferMs` later on its sync-corrected clock. Because the server (not
arrival order) assigns timestamps, there is no producer/consumer drift: the only requirement
is to send a chunk before its play deadline, and the `bufferMs` lead absorbs all jitter.
`ffmpeg` is required on the `PATH` (or configured) — it is used purely as a decoder to turn
sources into raw PCM.
## Install
```elixir
def deps do
[{:snapcast, "~> 0.2.0"}]
end
```
## Configure
```elixir
config :snapcast,
enabled: true,
port: 1704,
bind_ip: {0, 0, 0, 0},
# default PCM output format: {sample_rate, bits_per_sample, channels}
format: {48_000, 16, 2},
# optional: receive lifecycle events
listener: MyApp.SnapcastListener
```
All settings are optional; see `Snapcast` for the full list and defaults
(`format`, `chunk_ms`, `buffer_ms`, mDNS advertising, a supervised local
`snapclient`, the `ffmpeg` path, …).
Snapcast decodes sources to the configured default PCM output format unless a
play call supplies a per-stream `:format`. To make the default 24-bit/96 kHz
stereo PCM, configure:
```elixir
config :snapcast,
format: {96_000, 24, 2}
```
Supported PCM bit depths are 16, 24, and 32 bits. The default remains
48 kHz/16-bit stereo for broad snapclient compatibility; use a higher default
or per-stream format only when the target clients and output chain support it.
> **24-bit framing.** Snapcast has no packed 3-byte PCM format — it carries
> 24-bit audio as a 4-byte sample (`sample_format.cpp` forces `sample_size_ = 4`
> for 24-bit): `S24_LE` in a 32-bit word. The client also scales samples as
> `int32_t` for software volume, so the 24-bit value is **sign-extended** into a
> valid little-endian int32 (low 3 bytes = sample, high byte = sign). ffmpeg
> decodes 24-bit to packed `s24le`, so each sample is widened before it goes on
> the wire. This is transparent — request `{_, 24, _}` and clients receive (and
> report) a genuine 24-bit stream.
Version `0.2.0` adds per-stream PCM formats. This lets an application play a
44.1 kHz file as `{44_100, 16, 2}` and a high-resolution file as its own
supported format instead of forcing every stream through the default format.
## Supervise
```elixir
children = [
# ... your other children ...
] ++ Snapcast.children()
Supervisor.start_link(children, strategy: :one_for_one)
```
`Snapcast.children/0` returns the server subtree when `enabled: true`, otherwise `[]`.
## Play
```elixir
# Stream a file/URL to one or more connected clients (by their snapclient host id)
Snapcast.play("/music/track.flac", ["kitchen", "office"],
position_ms: 0,
format: {44_100, 16, 2}
)
Snapcast.pause()
Snapcast.resume()
Snapcast.seek(30_000)
Snapcast.set_volume("kitchen", 40)
Snapcast.stop_playback()
Snapcast.clients()
#=> [%{pid: #PID<...>, client_id: "kitchen", name: "Kitchen"}, ...]
```
A `source` may be a binary path/URL or a **0-arity function** returning one — the
function is called when the stream (re)starts, which is handy for short-lived signed
URLs that must be fetched fresh on each play/seek.
## Lifecycle events
Implement `Snapcast.Listener` to be told when clients connect/disconnect and how
playback is progressing:
```elixir
defmodule MyApp.SnapcastListener do
@behaviour Snapcast.Listener
@impl true
def clients_changed, do: MyApp.broadcast_endpoints()
@impl true
def progress(endpoint, position_ms), do: MyApp.Playback.progress(endpoint, position_ms)
@impl true
def ended(endpoint), do: MyApp.Playback.ended(endpoint)
end
```
The `endpoint` term is whatever you passed as `:endpoint` to `Snapcast.play/3` — it is
opaque to the server and echoed back unchanged.
## License
GPL-3.0-or-later. See [LICENSE](LICENSE).