Serve a raw PCM audio stream to any Squeezelite / Squeezebox client over the SlimProto protocol.
squeezed takes a raw PCM S16LE audio stream from stdin, a FIFO, a unix socket, or a TCP socket, and turns it into a Squeezebox server. Point any Squeezelite instance at it — or let them auto-discover it — and the audio comes out the other end, in sync across as many players as you like.
┌────────────┐ PCM ┌───────────────────────────────────┐ SlimProto ┌──────────────┐
│ ffmpeg / │ ───────► │ squeezed │ ◄──────────► │ squeezelite │
│ any PCM │ stdin / │ • SlimProto control (tcp :3483) │ (tcp) │ (player 1) │
│ producer │ fifo / │ • HTTP audio stream (tcp :9000) │ HTTP audio ├──────────────┤
│ │ unix / │ • UDP discovery (udp :3483) │ ───────────► │ squeezelite │
└────────────┘ tcp └───────────────────────────────────┘ │ (player 2) │
└──────────────┘
- Features
- Install
- Build from source
- Quick start
- Input sources
- Configuration
- Audio format
- Discovery
- Multiroom sync
- More recipes
- Troubleshooting
- How it works
- License
- Any PCM source — read from
stdin, a named pipe, a unix socket, or a TCP listener. - macOS virtual output device —
sudo squeezed driver installadds a "Squeezed" device to System Settings → Sound; anything your Mac plays to it streams to your Squeezelite players, volume keys included. See Virtual audio device (macOS). - Configurable format — sample rate, channel count, and bit depth (8/16/24/32).
- Zero-config playback — answers SlimProto UDP discovery so clients find the server automatically; or point them at it with
-s host. - True multiroom sync — measures each player's clock and playback position and continuously nudges them so every room renders the same sample at the same instant. See Multiroom sync.
- Configurable everything — CLI flags and/or a TOML file, with clear precedence (defaults ← file ← flags).
- Tiny & dependency-light — a single static-ish binary; no LMS, no Perl, no database.
Prebuilt binaries are published for Linux and macOS (x86_64 and arm64) plus FreeBSD/NetBSD (amd64) on every tagged release.
You'll also want a PCM producer (ffmpeg) and a player (squeezelite) — both are in most package managers (brew install ffmpeg squeezelite, apt install ffmpeg squeezelite, …).
Packages are hosted on Gemfury:
echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" \
| sudo tee /etc/apt/sources.list.d/squeezed.list
sudo apt-get update
sudo apt-get install squeezedsudo tee /etc/yum.repos.d/squeezed.repo >/dev/null <<'EOF'
[fury]
name=Gemfury Private Repo
baseurl=https://yum.fury.io/tsiry/
enabled=1
gpgcheck=0
EOF
sudo dnf install squeezed# From the flake (installs into your profile)
nix profile install github:tsirysndr/squeezed
# Or run without installing
nix run github:tsirysndr/squeezed -- --helpbrew install tsirysndr/tap/squeezedThe @tsiry/squeezed package
downloads the prebuilt binary for your platform from GitHub Releases on install:
npm install -g @tsiry/squeezedGrab a tarball for your platform from the releases page:
# Example: Linux x86_64 — pick the asset matching your OS/arch
curl -fsSL -o squeezed.tar.gz \
https://github.com/tsirysndr/squeezed/releases/latest/download/squeezed-<version>-linux-amd64.tar.gz
tar -xzf squeezed.tar.gz
sudo install -m 0755 squeezed /usr/local/bin/squeezedNeeds a recent Rust toolchain:
git clone https://github.com/tsirysndr/squeezed
cd squeezed
cargo build --release
# binary at ./target/release/squeezedOptionally drop it on your PATH:
install -m755 target/release/squeezed /usr/local/bin/squeezedPipe from ffmpeg, play with squeezelite.
Terminal 1 — start the server and feed it audio from ffmpeg:
ffmpeg -re -i song.flac -f s16le -ar 44100 -ac 2 - | squeezed-restreams at real-time rate (so playback isn't a fast-forward blur).-f s16le -ar 44100 -ac 2 -emits raw signed 16-bit little-endian stereo PCM to stdout, whichsqueezedreads from stdin.
Terminal 2 — start a player. It auto-discovers squeezed:
squeezelite -n Living-Room…or skip discovery and point it straight at the server:
squeezelite -n Living-Room -s 127.0.0.1That's it — song.flac now plays through squeezelite. Start more squeezelite instances (on this or other machines) and they'll all play in sync.
Tip: to verify the plumbing without an audio device, have squeezelite decode to stdout:
squeezelite -s 127.0.0.1 -o - -a 16 > out.pcm
Select the source with --source (or [input] source in the TOML file).
Best for pipelines. EOF on stdin cleanly shuts the server down.
ffmpeg -re -i input.mp3 -f s16le -ar 44100 -ac 2 - | squeezed --source stdinThe server stays up across producers — when one writer closes, squeezed waits for the next.
mkfifo /tmp/squeezed.fifo
squeezed --source fifo --path /tmp/squeezed.fifo &
# feed it whenever you like; the server keeps running between tracks
ffmpeg -re -i track1.flac -f s16le -ar 44100 -ac 2 - > /tmp/squeezed.fifo
ffmpeg -re -i track2.flac -f s16le -ar 44100 -ac 2 - > /tmp/squeezed.fifoLike a FIFO, but connection-oriented. squeezed creates (and cleans up) the socket.
squeezed --source unix --path /tmp/squeezed.sock &
ffmpeg -re -i input.wav -f s16le -ar 44100 -ac 2 - | socat - UNIX-CONNECT:/tmp/squeezed.sockFeed audio over the network. squeezed accepts one writer at a time and waits for the next.
squeezed --source tcp --tcp-bind 0.0.0.0:4711 &
# from anywhere on the network:
ffmpeg -re -i input.opus -f s16le -ar 44100 -ac 2 - | nc server-host 4711Turn your whole Mac into the PCM source: install a Core Audio driver that adds a "Squeezed" output device to System Settings → Sound → Output. Everything the Mac plays to it — Music, Spotify, Safari, anything — is captured by squeezed and streamed to your players, in sync. The device has a working volume slider, and the volume/mute keys apply to what the players hear.
# one-time: install the driver bundle to /Library/Audio/Plug-Ins/HAL
# (restarts coreaudiod — audio apps may hiccup for a second)
sudo squeezed driver install
# check it took
squeezed driver status
# run the server against the virtual device
squeezed --source virtual
# now pick "Squeezed" in System Settings → Sound → Output and press playThe driver is compiled into the squeezed binary itself — there is nothing else to download. Remove it any time with sudo squeezed driver uninstall.
Notes:
- macOS only; on other platforms
--source virtualand thedriversubcommand are unavailable. - The device runs at the configured
--sample-rate(supported: 44100, 48000, 88200, 96000 Hz — the default 44100 is fine for almost everything). - While nothing is routed to the device, the players receive silence; there is no disconnect between tracks.
- End-to-end latency ≈ the
--latency-kbpre-buffer (default 30 KB ≈ 170 ms at 44.1k/16/2). Lower it for lip-sync with video; raise it (up to 255) if audio stutters over WiFi.
To keep the virtual source running in the background, install the example LaunchAgent from contrib/launchd/com.tsirysndr.squeezed.plist (edit the binary path inside to match your install — /opt/homebrew/bin for Homebrew on Apple Silicon):
cp contrib/launchd/com.tsirysndr.squeezed.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.tsirysndr.squeezed.plistIt starts at login, restarts on crash, and logs to /tmp/squeezed.log. Stop it with:
launchctl bootout gui/$(id -u)/com.tsirysndr.squeezedIt's a LaunchAgent (per-user), not a system LaunchDaemon, because capturing from the audio device has to happen inside your login session.
Options can come from CLI flags, a TOML file (--config), or both. Precedence, lowest to highest:
built-in defaults < --config file < command-line flags
Copy squeezed.example.toml and edit. Every key is optional; a partial file only overrides what it sets.
[input]
source = "fifo" # stdin | fifo | unix | tcp | virtual (macOS)
path = "/tmp/squeezed.fifo" # for fifo/unix
# bind = "0.0.0.0:4711" # for tcp
[audio]
sample_rate = 44100 # 8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, ...
channels = 2 # 1 or 2
bits = 16 # 8, 16, 24 or 32
[server]
bind_ip = "0.0.0.0"
slim_port = 3483 # SlimProto control + UDP discovery
http_port = 9000 # HTTP audio stream
discovery = true # answer UDP discovery
sync = true # multiroom sync: keep all players sample-aligned
name = "squeezed" # device/server name advertised to clients
# buffer_bytes = 4194304 # rolling PCM retention window (~23s @ 44.1k/16/2)Run it:
squeezed --config squeezed.example.toml
# override individual values on top of the file:
squeezed --config squeezed.example.toml --http-port 9001 --name Kitchen| Flag | TOML key | Default | Description |
|---|---|---|---|
-c, --config <FILE> |
— | — | Load a TOML config file (flags still win). |
-s, --source <SRC> |
input.source |
stdin |
stdin | fifo | unix | tcp | virtual (macOS). |
--path <PATH> |
input.path |
— | Path for fifo / unix. |
--tcp-bind <ADDR> |
input.bind |
0.0.0.0:4711 |
Bind address for tcp. |
--sample-rate <HZ> |
audio.sample_rate |
44100 |
PCM sample rate. |
--channels <N> |
audio.channels |
2 |
1 (mono) or 2 (stereo). |
--bits <BITS> |
audio.bits |
16 |
8, 16, 24 or 32. |
--bind-ip <IP> |
server.bind_ip |
0.0.0.0 |
Interface to listen on. |
--slim-port <PORT> |
server.slim_port |
3483 |
SlimProto control + UDP discovery port. |
--http-port <PORT> |
server.http_port |
9000 |
HTTP audio port. |
--discovery <BOOL> |
server.discovery |
true |
Answer UDP discovery. |
--sync <BOOL> |
server.sync |
true |
Continuously align connected players (multiroom sync). |
--name <NAME> |
server.name |
squeezed |
Device/server name. |
--buffer-bytes <N> |
server.buffer_bytes |
4194304 |
Rolling PCM retention window. |
--latency-kb <KB> |
server.latency_kb |
30 |
Player pre-buffer before playback ≈ end-to-end latency (1..=255). Lower for video sync, raise for flaky WiFi. |
Logging verbosity is controlled by the SQUEEZED_LOG environment variable (error, warn, info, debug, trace; default info):
SQUEEZED_LOG=debug squeezed --source stdin < /dev/nullThe stream must be raw, signed, little-endian PCM matching the configured sample_rate / channels / bits. That's exactly what ffmpeg's -f s16le (or s24le/s32le), -ar <rate>, -ac <n> produce. Mismatched format = wrong-speed or noisy playback, so keep the ffmpeg flags and the [audio] settings in agreement.
| bits | ffmpeg format |
|---|---|
| 16 | -f s16le |
| 24 | -f s24le |
| 32 | -f s32le |
Supported sample rates: 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000 Hz.
Example — 48 kHz / 24-bit:
ffmpeg -re -i hi-res.flac -f s24le -ar 48000 -ac 2 - | squeezed --sample-rate 48000 --bits 24With discovery = true (default), squeezed answers SlimProto UDP discovery on the SlimProto port, so squeezelite finds it with no -s. Because Squeezelite hard-codes discovery to port 3483, auto-discovery only works while slim_port is left at the default; with a custom port, point clients at the server explicitly (-s host:port).
Start players in as many rooms as you like and squeezed keeps them sample-aligned — the same audio comes out of every room at the same instant, and stays that way even though each device runs on its own crystal clock.
squeezelite -n Kitchen -s 192.168.1.10 &
squeezelite -n Living-Room -s 192.168.1.10 &
squeezelite -n Bedroom -s 192.168.1.10 &For each connected player, squeezed:
- Measures its clock — sends a SlimProto
strm 't'timing probe once per second; the player replies with its clock and echoes the server timestamp. That round trip (kept from the lowest-latency sample) yields the offset between the player's clock and the server's, NTP-style. - Locates its playhead — the player reports how much audio it has played; combined with the byte position at which its stream started, that gives its absolute position in the stream, expressed on the server clock. This is the player's anchor.
- Corrects drift — the most-delayed player is the reference; any player running ahead of it is told to
strm 'p'(timed pause) for exactly the difference. Squeezelite plays the pause as inserted silence, in full, without discarding any buffered audio — so the paused player's buffer grows by the pause length, and its play counter (which counts only real frames) keeps the position model honest.
Aligning to the laggard (rather than skipping laggards forward to the fastest player) matters for a live stream: the most-advanced player is by definition the one with the least audio buffered, so pulling everyone up to it would leave every room with a near-empty buffer, one WiFi hiccup away from underrun. Pausing the leaders instead gives every player the laggard's safety margin — at the cost of a little end-to-end latency.
Two safeguards keep a misbehaving player from ruining the group. A player whose playback position is receding (its output stalled — suspended machine, hung audio device) is never used as the reference and is never corrected until it stabilizes. And the group only follows a laggard so far: a player parked more than ~1.5 s behind the most-advanced player is skipped forward to the group (strm 'a') instead of the group sinking down to it — its stream kept flowing while its output was stalled, so the catch-up skip simply drains the surplus audio sitting in its buffer.
The result: a player that stalls or drifts is re-aligned within one correction round (verified in test: a forced 500 ms stall on one player produced a single compensating pause on the other, returning the group to ≤1 ms spread). Real devices drift by only parts-per-million, so steady-state corrections are rare and small.
Turn it off with --sync false (or sync = false) for plain simultaneous playback without correction.
Notes
- Every connection is tracked by a per-session id, so even multiple
squeezeliteinstances sharing a MAC sync correctly. Giving each instance its own-m <mac>is still recommended — it makes the logs legible.- Sync tracks steady-state drift; it does not attempt sample-accurate cross-fade on join. A late joiner snaps into alignment within a few seconds of starting.
Re-broadcast an internet radio stream:
ffmpeg -re -i https://stream.example.com/radio.mp3 -f s16le -ar 44100 -ac 2 - | squeezedPlay a whole folder, gaplessly, over a FIFO:
mkfifo /tmp/squeezed.fifo
squeezed --source fifo --path /tmp/squeezed.fifo &
for f in ~/Music/*.flac; do
ffmpeg -re -i "$f" -f s16le -ar 44100 -ac 2 -
done > /tmp/squeezed.fifoStream your desktop/system audio (PipeWire/PulseAudio via ffmpeg):
ffmpeg -f pulse -i default -f s16le -ar 44100 -ac 2 - | squeezedStream microphone/line-in on macOS (avfoundation):
ffmpeg -f avfoundation -i ":0" -f s16le -ar 44100 -ac 2 - | squeezedGenerate a test tone:
ffmpeg -re -f lavfi -i "sine=frequency=440:sample_rate=44100" -ac 2 -f s16le - | squeezed- No players show up / auto-discovery fails — pass
-s <server-ip>to squeezelite. Discovery needsslim_portat its default3483and UDP broadcast reachable on your LAN. bind … failed: Address already in use— something already owns the port (often a running Logitech Media Server on3483). Pick another with--slim-port/--http-port(and then point players with-s host:port).- Playback is a fast-forward blur — you forgot
-reon ffmpeg; without it, ffmpeg pushes PCM as fast as it can.-repaces it to real time. - Noise / wrong pitch — the ffmpeg output format doesn't match
[audio]. Make-f s16le -ar <rate> -ac <n>agree withbits/sample_rate/channels. - "Squeezed" missing from Sound settings (macOS) — check
squeezed driver status. If the bundle is installed but the device isn't visible, restart coreaudiod (sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod) or log out and back in. --source virtualplays silence (macOS) — the virtual device only carries what is routed to it: pick "Squeezed" as the output in System Settings → Sound (or in the app you're playing from), and check it isn't muted / volume isn't at zero.- See what's happening — run with
SQUEEZED_LOG=debug.
- Input pump reads PCM from the configured source into a one-writer / N-reader rolling broadcast buffer.
- SlimProto server (TCP, default
:3483) accepts each Squeezelite connection, reads itsHELO, and replies with astrm"start" command describing the raw-PCM format and telling the client to fetch audio over HTTP (with its MAC in the URL, to correlate the two connections). - HTTP server (TCP, default
:9000) streams the shared PCM buffer to each connected player, recording the byte position where each stream began. - Sync engine probes each player's clock, computes its stream anchor, and issues timed-pause corrections to hold every room in alignment (see Multiroom sync).
- Discovery responder (UDP, default
:3483) answers SlimProto discovery so clients find the server automatically.
The SlimProto implementation is derived from the rockbox-slim crate.
MIT © Tsiry Sandratraina
