Commits
Server advertises `_zerod._tcp.local.` via a new pure-Rust `zerod-discovery`
crate wrapping mdns-sd. Clients omit `--host` and the binary browses the
LAN for ~1.5s; multiple responders error out with a listing, `--name`
(or `ZEROD_NAME`) filters. `zerod discover` lists every responder.
IPv4 picker skips loopback and Docker's 172.17.0.0/16 default bridge so
the wrong interface doesn't leak into the URL.
`[mdns] enabled / name` in zerod.toml; instance name defaults to
gethostname().
Adds RGB-color Styles to the clap derive entry point so the help
menu picks up Charm's signature purple-violet-blue (#7D56F4) on
headers/usage, brighter violet on commands/flags, and soft lavender
on placeholders. Drops in a preview screenshot at the top of the
README.
Tagging a `v*` release now fans out across five build jobs and uploads
prebuilt tarballs (+ sha256) to the GitHub release:
- macOS: aarch64-apple-darwin, x86_64-apple-darwin (native cargo build
via dtolnay/rust-toolchain + brew protoc)
- Linux: x86_64 / aarch64 / armhf (cross + docker buildx; the per-target
Dockerfile handles protoc 25.1 + multiarch dev libs)
taiki-e/install-action installs cross from the prebuilt binary so we
skip the cold cargo install. Swatinem/rust-cache keyed per target.
softprops/action-gh-release@v2 attaches the artifacts with
fail_on_unmatched_files so a missing build fails the release.
README gains an "Install → From a release tarball" section with a
curl one-liner pointing at github.com/tsirysndr/zerod (the GitHub
mirror used for CI/releases; tangled.org/tsiry-sandratraina.com/zerod
remains the canonical source host).
The previous template left ZEROD_BEARER_TOKEN unset, which meant zerod
would generate a new random token on every restart and log it once —
unworkable for any client that wants a stable token, and easy to miss
in the journal.
Now `zerod service install`:
- generates a 32-byte hex token via getrandom (or accepts --token <v>),
- substitutes it into Environment=ZEROD_BEARER_TOKEN= in the unit,
- writes the file mode 0600 so other users can't read the secret,
- prints the token to stdout with the client-side ZEROD_BEARER_TOKEN
export to copy-paste.
Re-running with --force rotates the token (unless --token is supplied);
without --force the install errors out so an in-flight client doesn't
silently break.
Also fixes Documentation= URL to point at the actual repo
(tangled.org/tsiry-sandratraina.com/zerod).
Adds three subcommands so the Pi/SBC user flow is a copy-paste from
README rather than hand-writing a service file:
zerod service install # writes ~/.config/systemd/user/zerod.service
zerod service uninstall # removes it
zerod service path # prints where it would land
The unit template lives at assets/zerod.service.in and is embedded via
include_str!. `service install` substitutes {{exec_start}} with
current_exe() (canonicalised), writes atomically via tempfile + rename,
and prints the systemctl --user follow-up commands plus a loginctl
enable-linger hint so it survives logout.
Linux-only — non-Linux builds expose the subcommand but every action
returns "zerod service: linux only" rather than producing a unit file
no one can use.
cpal's ALSA backend uses Access::MMapInterleaved, which crashes inside
libasound's pulse plugin on Raspberry Pi OS (build_output_stream →
snd_pcm_mmap_begin SIGSEGV). aplay / speaker-test work because they
use snd_pcm_writei. Match that path.
- New crates/stream/src/alsa_sink.rs: alsa::pcm::PCM with
Access::RWInterleaved, dedicated writer thread draining a bounded
mpsc channel (cap 64 ≈ 3s buffer), EPIPE/xrun recovery via
pcm.try_recover. Linux only.
- player::build_sink: route AudioOutput::Cpal to AlsaSink on Linux,
CpalSink elsewhere. Proto variant stays Cpal for cross-platform
API parity.
- crates/stream/Cargo.toml: cpal moves to non-Linux target deps,
alsa = "0.9" added for Linux. Smaller dep tree on the Pi.
- CLI: drop --cpal-device. The named-device path walked cpal's
output_devices() enumeration, which triggers libasound plugin
probes that segfault on the JACK plugin destructor. ALSA routing
belongs in ~/.asoundrc, not a CLI knob.
- cpal_sink.rs: trace logs around each cpal call kept for the
non-Linux diagnostic path.
127.0.0.1 default left the daemon unreachable from the LAN, which is
the common deployment shape (Pi audio appliance + remote CLI). Flip the
default to all-interfaces and lean on the bearer token (random fallback)
for the no-zerod.toml case. Document the security trade-off in the
Security section and tell users to set bearer_token explicitly when
exposing on a network they don't fully trust.
Two cooperating layers, Linux-only for the system part:
- crates/volume: alsa-lib wrapper for mixer ops (list / get / set /
mute / unmute) gated on cfg(target_os = "linux"). Defaults to the
`default` card and `Master` selem, falling back to `PCM` when
`Master` isn't exposed. Cross-platform stubs return Err on macOS.
- VolumeService in zerod.v1alpha1 wired into the tonic server, plus a
`zerod volume` client subcommand tree.
- Per-stream software gain applied in the player loop before the
sink, so it works uniformly across cpal / stdout / pipe with one
atomic. Exposed via SetStreamVolume / GetStreamVolume on
StreamService and `zerod stream volume {get,set}`.
- Cross.toml + per-target Dockerfiles for arm-unknown-linux-gnueabihf,
aarch64-unknown-linux-gnu, and x86_64-unknown-linux-gnu. Each installs
upstream protoc 25.1, multiarch ALSA/D-Bus/systemd dev libs, and sets
CARGO_TARGET_<TRIPLE>_RUSTFLAGS with -rpath-link so ld can find
libdbus-1's indirect libsystemd.so.0 dep.
- crates/proto/build.rs: pass --experimental_allow_proto3_optional so
older protoc accepts proto3 optional (no-op on 3.15+).
- LICENSE (MIT) and README with cross-build instructions per target.
Server advertises `_zerod._tcp.local.` via a new pure-Rust `zerod-discovery`
crate wrapping mdns-sd. Clients omit `--host` and the binary browses the
LAN for ~1.5s; multiple responders error out with a listing, `--name`
(or `ZEROD_NAME`) filters. `zerod discover` lists every responder.
IPv4 picker skips loopback and Docker's 172.17.0.0/16 default bridge so
the wrong interface doesn't leak into the URL.
`[mdns] enabled / name` in zerod.toml; instance name defaults to
gethostname().
Tagging a `v*` release now fans out across five build jobs and uploads
prebuilt tarballs (+ sha256) to the GitHub release:
- macOS: aarch64-apple-darwin, x86_64-apple-darwin (native cargo build
via dtolnay/rust-toolchain + brew protoc)
- Linux: x86_64 / aarch64 / armhf (cross + docker buildx; the per-target
Dockerfile handles protoc 25.1 + multiarch dev libs)
taiki-e/install-action installs cross from the prebuilt binary so we
skip the cold cargo install. Swatinem/rust-cache keyed per target.
softprops/action-gh-release@v2 attaches the artifacts with
fail_on_unmatched_files so a missing build fails the release.
README gains an "Install → From a release tarball" section with a
curl one-liner pointing at github.com/tsirysndr/zerod (the GitHub
mirror used for CI/releases; tangled.org/tsiry-sandratraina.com/zerod
remains the canonical source host).
The previous template left ZEROD_BEARER_TOKEN unset, which meant zerod
would generate a new random token on every restart and log it once —
unworkable for any client that wants a stable token, and easy to miss
in the journal.
Now `zerod service install`:
- generates a 32-byte hex token via getrandom (or accepts --token <v>),
- substitutes it into Environment=ZEROD_BEARER_TOKEN= in the unit,
- writes the file mode 0600 so other users can't read the secret,
- prints the token to stdout with the client-side ZEROD_BEARER_TOKEN
export to copy-paste.
Re-running with --force rotates the token (unless --token is supplied);
without --force the install errors out so an in-flight client doesn't
silently break.
Also fixes Documentation= URL to point at the actual repo
(tangled.org/tsiry-sandratraina.com/zerod).
Adds three subcommands so the Pi/SBC user flow is a copy-paste from
README rather than hand-writing a service file:
zerod service install # writes ~/.config/systemd/user/zerod.service
zerod service uninstall # removes it
zerod service path # prints where it would land
The unit template lives at assets/zerod.service.in and is embedded via
include_str!. `service install` substitutes {{exec_start}} with
current_exe() (canonicalised), writes atomically via tempfile + rename,
and prints the systemctl --user follow-up commands plus a loginctl
enable-linger hint so it survives logout.
Linux-only — non-Linux builds expose the subcommand but every action
returns "zerod service: linux only" rather than producing a unit file
no one can use.
cpal's ALSA backend uses Access::MMapInterleaved, which crashes inside
libasound's pulse plugin on Raspberry Pi OS (build_output_stream →
snd_pcm_mmap_begin SIGSEGV). aplay / speaker-test work because they
use snd_pcm_writei. Match that path.
- New crates/stream/src/alsa_sink.rs: alsa::pcm::PCM with
Access::RWInterleaved, dedicated writer thread draining a bounded
mpsc channel (cap 64 ≈ 3s buffer), EPIPE/xrun recovery via
pcm.try_recover. Linux only.
- player::build_sink: route AudioOutput::Cpal to AlsaSink on Linux,
CpalSink elsewhere. Proto variant stays Cpal for cross-platform
API parity.
- crates/stream/Cargo.toml: cpal moves to non-Linux target deps,
alsa = "0.9" added for Linux. Smaller dep tree on the Pi.
- CLI: drop --cpal-device. The named-device path walked cpal's
output_devices() enumeration, which triggers libasound plugin
probes that segfault on the JACK plugin destructor. ALSA routing
belongs in ~/.asoundrc, not a CLI knob.
- cpal_sink.rs: trace logs around each cpal call kept for the
non-Linux diagnostic path.
127.0.0.1 default left the daemon unreachable from the LAN, which is
the common deployment shape (Pi audio appliance + remote CLI). Flip the
default to all-interfaces and lean on the bearer token (random fallback)
for the no-zerod.toml case. Document the security trade-off in the
Security section and tell users to set bearer_token explicitly when
exposing on a network they don't fully trust.
Two cooperating layers, Linux-only for the system part:
- crates/volume: alsa-lib wrapper for mixer ops (list / get / set /
mute / unmute) gated on cfg(target_os = "linux"). Defaults to the
`default` card and `Master` selem, falling back to `PCM` when
`Master` isn't exposed. Cross-platform stubs return Err on macOS.
- VolumeService in zerod.v1alpha1 wired into the tonic server, plus a
`zerod volume` client subcommand tree.
- Per-stream software gain applied in the player loop before the
sink, so it works uniformly across cpal / stdout / pipe with one
atomic. Exposed via SetStreamVolume / GetStreamVolume on
StreamService and `zerod stream volume {get,set}`.
- Cross.toml + per-target Dockerfiles for arm-unknown-linux-gnueabihf,
aarch64-unknown-linux-gnu, and x86_64-unknown-linux-gnu. Each installs
upstream protoc 25.1, multiarch ALSA/D-Bus/systemd dev libs, and sets
CARGO_TARGET_<TRIPLE>_RUSTFLAGS with -rpath-link so ld can find
libdbus-1's indirect libsystemd.so.0 dep.
- crates/proto/build.rs: pass --experimental_allow_proto3_optional so
older protoc accepts proto3 optional (no-op on 3.15+).
- LICENSE (MIT) and README with cross-build instructions per target.