tsirysndr/squeezed

Serve a raw PCM audio stream to any Squeezelite / Squeezebox client over the SlimProto protocol.

3

stars

14

commits

Rust

primary language

Sep 5, 2026

updated

audio
multi-room-audio
music
rust
slim-protocol
squeezebox
Browse cluster: Music Audio and Synthesis Tools

README

Preview

squeezed

Nix FlakeHub

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)  │
                                                                           └──────────────┘

Table of Contents


Features

  • Any PCM source — read from stdin, a named pipe, a unix socket, or a TCP listener.
  • macOS virtual output devicesudo squeezed driver install adds 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.

Install

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, …).

Debian / Ubuntu (apt)

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 squeezed

Fedora / RHEL (dnf)

sudo 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

Nix

# From the flake (installs into your profile)
nix profile install github:tsirysndr/squeezed

# Or run without installing
nix run github:tsirysndr/squeezed -- --help

Homebrew

brew install tsirysndr/tap/squeezed

npm

The @tsiry/squeezed package downloads the prebuilt binary for your platform from GitHub Releases on install:

npm install -g @tsiry/squeezed

Download from GitHub Releases

Grab 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/squeezed

Build from source

Needs a recent Rust toolchain:

git clone https://github.com/tsirysndr/squeezed
cd squeezed
cargo build --release
# binary at ./target/release/squeezed

Optionally drop it on your PATH:

install -m755 target/release/squeezed /usr/local/bin/squeezed

Quick start

Pipe 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
  • -re streams 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, which squeezed reads 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.1

That'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


Input sources

Select the source with --source (or [input] source in the TOML file).

stdin (default)

Best for pipelines. EOF on stdin cleanly shuts the server down.

ffmpeg -re -i input.mp3 -f s16le -ar 44100 -ac 2 - | squeezed --source stdin

FIFO (named pipe)

The 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.fifo

Unix domain socket

Like 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.sock

TCP socket

Feed 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 4711

Virtual audio device (macOS)

Turn 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 play

The 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 virtual and the driver subcommand 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-kb pre-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.

Run at login (launchd)

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.plist

It starts at login, restarts on crash, and logs to /tmp/squeezed.log. Stop it with:

launchctl bootout gui/$(id -u)/com.tsirysndr.squeezed

It's a LaunchAgent (per-user), not a system LaunchDaemon, because capturing from the audio device has to happen inside your login session.


Configuration

Options can come from CLI flags, a TOML file (--config), or both. Precedence, lowest to highest:

built-in defaults  <  --config file  <  command-line flags

TOML file

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

CLI reference

FlagTOML keyDefaultDescription
-c, --config <FILE>Load a TOML config file (flags still win).
-s, --source <SRC>input.sourcestdinstdin | fifo | unix | tcp | virtual (macOS).
--path <PATH>input.pathPath for fifo / unix.
--tcp-bind <ADDR>input.bind0.0.0.0:4711Bind address for tcp.
--sample-rate <HZ>audio.sample_rate44100PCM sample rate.
--channels <N>audio.channels21 (mono) or 2 (stereo).
--bits <BITS>audio.bits168, 16, 24 or 32.
--bind-ip <IP>server.bind_ip0.0.0.0Interface to listen on.
--slim-port <PORT>server.slim_port3483SlimProto control + UDP discovery port.
--http-port <PORT>server.http_port9000HTTP audio port.
--discovery <BOOL>server.discoverytrueAnswer UDP discovery.
--sync <BOOL>server.synctrueContinuously align connected players (multiroom sync).
--name <NAME>server.namesqueezedDevice/server name.
--buffer-bytes <N>server.buffer_bytes4194304Rolling PCM retention window.
--latency-kb <KB>server.latency_kb30Player 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/null

Audio format

The 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.

bitsffmpeg 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 24

Discovery

With 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).


Multiroom sync

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 &

How the sync works

For each connected player, squeezed:

  1. 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.
  2. 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.
  3. Corrects drift — the most-advanced player is the reference; any player lagging behind it is told to strm 'a' skip-ahead by exactly the lag. Skip-ahead is the only nudge used, because squeezelite advances its play counter by the skipped amount — so the position model never drifts from reality.

The result: a player that starts to fall behind is continuously pulled back into alignment. In a stress test with a player deliberately running 4 % slow, its lag was held bounded (re-corrected every few seconds) instead of growing without limit; real devices drift by only parts-per-million, so the residual error is well under a millisecond.

Turn it off with --sync false (or sync = false) for plain simultaneous playback without correction.

Notes

  • Each player needs a unique MAC (real devices have one; multiple squeezelite instances on a single host must be started with -m <mac>). squeezed uses the MAC to correlate a player's control and audio connections.
  • 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.

More recipes

Re-broadcast an internet radio stream:

ffmpeg -re -i https://stream.example.com/radio.mp3 -f s16le -ar 44100 -ac 2 - | squeezed

Play 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.fifo

Stream your desktop/system audio (PipeWire/PulseAudio via ffmpeg):

ffmpeg -f pulse -i default -f s16le -ar 44100 -ac 2 - | squeezed

Stream microphone/line-in on macOS (avfoundation):

ffmpeg -f avfoundation -i ":0" -f s16le -ar 44100 -ac 2 - | squeezed

Generate a test tone:

ffmpeg -re -f lavfi -i "sine=frequency=440:sample_rate=44100" -ac 2 -f s16le - | squeezed

Troubleshooting

  • No players show up / auto-discovery fails — pass -s <server-ip> to squeezelite. Discovery needs slim_port at its default 3483 and UDP broadcast reachable on your LAN.
  • bind … failed: Address already in use — something already owns the port (often a running Logitech Media Server on 3483). Pick another with --slim-port / --http-port (and then point players with -s host:port).
  • Playback is a fast-forward blur — you forgot -re on ffmpeg; without it, ffmpeg pushes PCM as fast as it can. -re paces it to real time.
  • Noise / wrong pitch — the ffmpeg output format doesn't match [audio]. Make -f s16le -ar <rate> -ac <n> agree with bits / 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 virtual plays 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.

How it works

  1. Input pump reads PCM from the configured source into a one-writer / N-reader rolling broadcast buffer.
  2. SlimProto server (TCP, default :3483) accepts each Squeezelite connection, reads its HELO, and replies with a strm "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).
  3. HTTP server (TCP, default :9000) streams the shared PCM buffer to each connected player, recording the byte position where each stream began.
  4. Sync engine probes each player's clock, computes its stream anchor, and issues skip-ahead corrections to hold every room in alignment (see Multiroom sync).
  5. Discovery responder (UDP, default :3483) answers SlimProto discovery so clients find the server automatically.

The SlimProto implementation is derived from the rockbox-slim crate.


License

MIT © Tsiry Sandratraina

Contributors

tsirysndr

14 commits

tsirysndr/squeezed

Serve a raw PCM audio stream to any Squeezelite / Squeezebox client over the SlimProto protocol.

3

stars

14

commits

Rust

primary language

Sep 5, 2026

updated

audio
multi-room-audio
music
rust
slim-protocol
squeezebox
Browse cluster: Music Audio and Synthesis Tools

README

Preview

squeezed

Nix FlakeHub

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)  │
                                                                           └──────────────┘

Table of Contents


Features

  • Any PCM source — read from stdin, a named pipe, a unix socket, or a TCP listener.
  • macOS virtual output devicesudo squeezed driver install adds 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.

Install

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, …).

Debian / Ubuntu (apt)

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 squeezed

Fedora / RHEL (dnf)

sudo 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

Nix

# From the flake (installs into your profile)
nix profile install github:tsirysndr/squeezed

# Or run without installing
nix run github:tsirysndr/squeezed -- --help

Homebrew

brew install tsirysndr/tap/squeezed

npm

The @tsiry/squeezed package downloads the prebuilt binary for your platform from GitHub Releases on install:

npm install -g @tsiry/squeezed

Download from GitHub Releases

Grab 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/squeezed

Build from source

Needs a recent Rust toolchain:

git clone https://github.com/tsirysndr/squeezed
cd squeezed
cargo build --release
# binary at ./target/release/squeezed

Optionally drop it on your PATH:

install -m755 target/release/squeezed /usr/local/bin/squeezed

Quick start

Pipe 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
  • -re streams 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, which squeezed reads 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.1

That'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


Input sources

Select the source with --source (or [input] source in the TOML file).

stdin (default)

Best for pipelines. EOF on stdin cleanly shuts the server down.

ffmpeg -re -i input.mp3 -f s16le -ar 44100 -ac 2 - | squeezed --source stdin

FIFO (named pipe)

The 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.fifo

Unix domain socket

Like 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.sock

TCP socket

Feed 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 4711

Virtual audio device (macOS)

Turn 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 play

The 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 virtual and the driver subcommand 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-kb pre-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.

Run at login (launchd)

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.plist

It starts at login, restarts on crash, and logs to /tmp/squeezed.log. Stop it with:

launchctl bootout gui/$(id -u)/com.tsirysndr.squeezed

It's a LaunchAgent (per-user), not a system LaunchDaemon, because capturing from the audio device has to happen inside your login session.


Configuration

Options can come from CLI flags, a TOML file (--config), or both. Precedence, lowest to highest:

built-in defaults  <  --config file  <  command-line flags

TOML file

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

CLI reference

FlagTOML keyDefaultDescription
-c, --config <FILE>Load a TOML config file (flags still win).
-s, --source <SRC>input.sourcestdinstdin | fifo | unix | tcp | virtual (macOS).
--path <PATH>input.pathPath for fifo / unix.
--tcp-bind <ADDR>input.bind0.0.0.0:4711Bind address for tcp.
--sample-rate <HZ>audio.sample_rate44100PCM sample rate.
--channels <N>audio.channels21 (mono) or 2 (stereo).
--bits <BITS>audio.bits168, 16, 24 or 32.
--bind-ip <IP>server.bind_ip0.0.0.0Interface to listen on.
--slim-port <PORT>server.slim_port3483SlimProto control + UDP discovery port.
--http-port <PORT>server.http_port9000HTTP audio port.
--discovery <BOOL>server.discoverytrueAnswer UDP discovery.
--sync <BOOL>server.synctrueContinuously align connected players (multiroom sync).
--name <NAME>server.namesqueezedDevice/server name.
--buffer-bytes <N>server.buffer_bytes4194304Rolling PCM retention window.
--latency-kb <KB>server.latency_kb30Player 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/null

Audio format

The 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.

bitsffmpeg 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 24

Discovery

With 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).


Multiroom sync

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 &

How the sync works

For each connected player, squeezed:

  1. 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.
  2. 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.
  3. Corrects drift — the most-advanced player is the reference; any player lagging behind it is told to strm 'a' skip-ahead by exactly the lag. Skip-ahead is the only nudge used, because squeezelite advances its play counter by the skipped amount — so the position model never drifts from reality.

The result: a player that starts to fall behind is continuously pulled back into alignment. In a stress test with a player deliberately running 4 % slow, its lag was held bounded (re-corrected every few seconds) instead of growing without limit; real devices drift by only parts-per-million, so the residual error is well under a millisecond.

Turn it off with --sync false (or sync = false) for plain simultaneous playback without correction.

Notes

  • Each player needs a unique MAC (real devices have one; multiple squeezelite instances on a single host must be started with -m <mac>). squeezed uses the MAC to correlate a player's control and audio connections.
  • 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.

More recipes

Re-broadcast an internet radio stream:

ffmpeg -re -i https://stream.example.com/radio.mp3 -f s16le -ar 44100 -ac 2 - | squeezed

Play 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.fifo

Stream your desktop/system audio (PipeWire/PulseAudio via ffmpeg):

ffmpeg -f pulse -i default -f s16le -ar 44100 -ac 2 - | squeezed

Stream microphone/line-in on macOS (avfoundation):

ffmpeg -f avfoundation -i ":0" -f s16le -ar 44100 -ac 2 - | squeezed

Generate a test tone:

ffmpeg -re -f lavfi -i "sine=frequency=440:sample_rate=44100" -ac 2 -f s16le - | squeezed

Troubleshooting

  • No players show up / auto-discovery fails — pass -s <server-ip> to squeezelite. Discovery needs slim_port at its default 3483 and UDP broadcast reachable on your LAN.
  • bind … failed: Address already in use — something already owns the port (often a running Logitech Media Server on 3483). Pick another with --slim-port / --http-port (and then point players with -s host:port).
  • Playback is a fast-forward blur — you forgot -re on ffmpeg; without it, ffmpeg pushes PCM as fast as it can. -re paces it to real time.
  • Noise / wrong pitch — the ffmpeg output format doesn't match [audio]. Make -f s16le -ar <rate> -ac <n> agree with bits / 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 virtual plays 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.

How it works

  1. Input pump reads PCM from the configured source into a one-writer / N-reader rolling broadcast buffer.
  2. SlimProto server (TCP, default :3483) accepts each Squeezelite connection, reads its HELO, and replies with a strm "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).
  3. HTTP server (TCP, default :9000) streams the shared PCM buffer to each connected player, recording the byte position where each stream began.
  4. Sync engine probes each player's clock, computes its stream anchor, and issues skip-ahead corrections to hold every room in alignment (see Multiroom sync).
  5. Discovery responder (UDP, default :3483) answers SlimProto discovery so clients find the server automatically.

The SlimProto implementation is derived from the rockbox-slim crate.


License

MIT © Tsiry Sandratraina

Contributors

tsirysndr

14 commits

Languages

Rust

93.5%

JavaScript

3.3%

Nix

3.2%