{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreig6yflevbvgeudds4msmrixsenpjrcp2g4pauii6nwocpchlkfl4u",
"commit": {
"cid": "bafyreihcswj5fq6dsihaq2vzg7lo2knwntebsv5gvf2abe4xes4e2m3xsq",
"rev": "3mnz5dgitso2p"
},
"uri": "at://did:plc:7vdlgi2bflelz7mmuxoqjfcr/app.bsky.feed.post/3mnz5dgh5ws2x",
"validationStatus": "valid"
},
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I've had a Raspberry Pi Zero W sitting under my soundbar for about a year. The job description was simple: be the one device on the soundbar's Bluetooth allowlist so I never have to repair anything, and play whatever audio my MacBook shoves at it. That's it. No screen, no DAC, no hat. Just a tiny board acting as a permanent Bluetooth client."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 133,
"byteStart": 128
}
}
],
"plaintext": "Getting there involved more yak-shaving than I'd like to admit, so I eventually wrote a small Rust daemon to do it. It's called zerod. This post is about why it exists, what it actually does, and how the music ends up in my living room."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.image",
"aspectRatio": {
"height": 4032,
"width": 2268
},
"image": {
"$type": "blob",
"ref": {
"$link": "bafkreiaxwjvot3ca5qu4fnus2eq7ju6vkyq52auvbnno54iwampb5e47rq"
},
"mimeType": "image/webp",
"size": 1291942
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.image",
"aspectRatio": {
"height": 848,
"width": 2414
},
"image": {
"$type": "blob",
"ref": {
"$link": "bafkreihmrehugot4mdpccetdetjaxsb4uffamfrsb6y5n52i5zv7x6cpfi"
},
"mimeType": "image/webp",
"size": 160404
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The setup, end to end"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "plaintext",
"plaintext": "\nMacBook (rockbox-zig) ──► HLS (m3u8 + .m4s segments) ──► Pi Zero W (zerod) ──► ALSA ──► bluez-alsa ──► Bluetooth A2DP ──► soundbar\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#link",
"uri": "https://github.com/tsirysndr/rockbox-zig"
}
],
"index": {
"byteEnd": 28,
"byteStart": 17
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 196,
"byteStart": 190
}
}
],
"plaintext": "The MacBook runs rockbox-zig, a fork of Rockbox I've been hacking on, with a built-in HLS server bolted to its output stage. So the player itself is the source — no BlackHole loopback, no ffmpeg capture, no second process. It just exposes whatever it's currently decoding as an HLS playlist on the LAN."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The Pi pulls that playlist, decodes the AAC segments, and writes PCM into ALSA. ALSA hands it to BlueZ, which streams A2DP to the soundbar."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The Pi never disconnects from the soundbar. The MacBook never sees the soundbar at all. The Pi is the only paired device — that's the whole reason the soundbar works reliably. Anyone who's lived with multi-device Bluetooth knows what I'm talking about."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Why not just use what already exists"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Honest answer: I tried."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 8,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#link",
"uri": "https://github.com/tsirysndr/rockbox-zig"
}
],
"index": {
"byteEnd": 198,
"byteStart": 187
}
}
],
"plaintext": "Snapcast is fantastic but it wants its own server-client world, not \"ingest an HLS URL.\" I'd be running a snapserver on the Mac and a snapclient on the Pi just to move PCM around — and rockbox-zig already speaks HLS."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 14,
"byteStart": 0
}
}
],
"plaintext": "shairport-sync is great if you want AirPlay, but I wanted the music player itself to be the source of truth, not a shim that re-wraps system audio."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 42,
"byteStart": 0
}
}
],
"plaintext": "bluealsa + a shell script + a systemd unit is what I ran for months. It worked. It was also impossible to debug from the couch, and every Bluetooth hiccup meant either SSH'ing in or walking over to the Pi."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 118,
"byteStart": 106
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 251,
"byteStart": 236
}
}
],
"plaintext": "What I actually wanted was one binary on the Pi that could (a) play an HLS stream to ALSA, (b) let me run bluetoothctl-equivalent commands from my laptop without SSH, (c) restart whatever systemd unit I'd inevitably wedge, and (d) edit snapserver.conf and friends without me opening another shell."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "So I wrote it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "What zerod is"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 173,
"byteStart": 168
}
}
],
"plaintext": "One Rust binary. When you run it with no arguments, it's a daemon exposing a gRPC API on port 50151. When you run it with subcommands, it's a CLI that talks to another zerod over the same API. Same binary on both sides."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The API surface is intentionally small:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 24,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 85,
"byteStart": 76
}
}
],
"plaintext": "HLS / MPEG-DASH playback — fetch a manifest, follow segments, decode with symphonia, push PCM to a sink."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 13,
"byteStart": 0
}
}
],
"plaintext": "BlueZ control — scan, pair, connect, disconnect. Just the verbs I actually use."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 15,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 80,
"byteStart": 70
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 142,
"byteStart": 133
}
}
],
"plaintext": "systemd control — start/stop/restart, restricted to an allowlist in zerod.toml so the daemon can't be turned into a generic remote systemctl."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 11,
"byteStart": 0
}
}
],
"plaintext": "ALSA volume — get/set any selem on any card."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 18,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 81,
"byteStart": 66
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 102,
"byteStart": 83
}
}
],
"plaintext": "Remote config edit — atomic read/write of a fixed set of files (snapserver.conf, shairport-sync.conf, etc.), with an optional reload-or-restart of the bound unit after every write."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 58,
"byteStart": 48
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 78,
"byteStart": 60
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 193,
"byteStart": 180
}
}
],
"plaintext": "Auth is a bearer token, three sources in order: zerod.toml, ZEROD_BEARER_TOKEN, or a random 32-byte one generated and logged once at startup. No TLS in v1 — the bind defaults to 0.0.0.0:50151 because I drive it from my laptop, and the bearer is the only line of defence. If the LAN ever stops being trusted, that's what WireGuard is for."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "How it actually plays a stream"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The player loop is the only part that's interesting. Everything else is a thin wrapper over a system library."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "plaintext",
"plaintext": "\nmanifest fetch ──► segment prefetch ──► decode ──► gain ──► sink.write()\n ▲ │\n └────────── live-refresh task ───────┘\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "A few details that matter on the Pi Zero:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 25,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 63,
"byteStart": 52
}
}
],
"plaintext": "Symphonia, not gstreamer. Pure-Rust decode means no apt install dance, no plugin discovery, no surprise dynamic linking. The binary on the Pi is one file. For HLS-with-AAC-in-m4s that's the common case anyway, and Symphonia handles it cleanly."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 18,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 25,
"byteStart": 18
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 36,
"byteStart": 25
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 90,
"byteStart": 86
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 380,
"byteStart": 377
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 457,
"byteStart": 443
}
}
],
"plaintext": "ALSA directly via alsa-rs, not cpal. This was the war story. On macOS the player uses cpal like any well-behaved cross-platform tool. On Linux, the same code segfaulted inside libasound's PulseAudio plugin on Raspberry Pi OS — cpal's ALSA backend uses mmap mode, and something in the pulse-plugin path doesn't survive contact with it on the Pi. After a couple of evenings of gdb, I gave up trying to fix it in cpal and just went straight to snd_pcm_writei:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "toml",
"plaintext": "\n[target.'cfg(target_os = \"linux\")'.dependencies]\nalsa = \"0.9\"\n\n[target.'cfg(not(target_os = \"linux\"))'.dependencies]\ncpal = \"0.15\"\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "That's the entire portability story. The sink trait is identical on both sides; only the implementation differs."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 17,
"byteStart": 0
}
}
],
"plaintext": "Live-edge starts. When the manifest is live, the player jumps to roughly the third-from-last segment instead of starting from the beginning. Otherwise you spend the first 30 seconds catching up to real-time and the laptop audio is hilariously behind the speaker:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "rust",
"plaintext": "\nif snap.is_live {\n let n = snap.segments.len();\n if n > 3 {\n next_play_seq = snap.segments[n - 3].seq;\n }\n}\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 28,
"byteStart": 0
}
}
],
"plaintext": "Per-stream gain in the loop. Independent from the ALSA mixer — I apply a 0..=100 scale to the i16 samples before they hit the sink. The system volume stays where the soundbar likes it; I attenuate per-stream:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "rust",
"plaintext": "\nfn apply_gain(samples: &mut [i16], volume_percent: u32) {\n if volume_percent >= 100 { return; }\n let num = volume_percent as i32;\n for s in samples {\n *s = ((*s as i32).saturating_mul(num) / 100) as i16;\n }\n}\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 16,
"byteStart": 2
}
}
],
"plaintext": "A saturating_mul keeps me away from i32 overflow on samples near the rails. Divide by 100 stays inside i16. Cheap, predictable, runs fine on a 1GHz ARMv6."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Cross-compiling for the Zero"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 54,
"byteStart": 27
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 74,
"byteStart": 69
}
}
],
"plaintext": "The Pi Zero W is ARMv6 — arm-unknown-linux-gnueabihf, not aarch64. cross handles most of it, but three things bit me hard enough that they're permanently committed to the per-target Dockerfile:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 6,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 43,
"byteStart": 6
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 63,
"byteStart": 55
}
}
],
"plaintext": "protoc from the cross base image is too old for proto3 optional. I pin 25.1 from upstream."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 17,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 100,
"byteStart": 96
}
}
],
"plaintext": "libsystemd0:armhf has to be installed explicitly so the multiarch linker can find it during the zbus build."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 3,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 13,
"byteStart": 3
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 22,
"byteStart": 13
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 40,
"byteStart": 37
}
}
],
"plaintext": "An rpath-link rustflag so transitive .so dependencies resolve at link time without polluting the final binary's RPATH."
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you ever cross-compile a Rust daemon for the original Pi Zero and run into the same wall, those three are what I'd check first."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "How I actually use it day to day"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 36,
"byteStart": 24
}
}
],
"plaintext": "The Pi is on the LAN as pizero.local. On the MacBook:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "\nexport ZEROD_HOST=pizero.local\nexport ZEROD_BEARER_TOKEN=\"$(cat ~/.zerod-token)\"\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Then a typical session looks like:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "\n# pair the soundbar (one-time)\nzerod bluetooth scan --timeout-secs 5\nzerod bluetooth connect AA:BB:CC:DD:EE:FF\n\n\n# start the laptop's HLS server \nrockboxd\n\n# tell the Pi to play it\nzerod stream play http://macbook.local:7882/hls/audio.m3u8\nzerod stream volume set 80\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "When the soundbar wakes up grumpy after a power cycle:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "\nzerod bluetooth disconnect AA:BB:CC:DD:EE:FF\nzerod bluetooth connect AA:BB:CC:DD:EE:FF\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "When BlueZ itself gets stuck (it happens, on every Linux distro, forever):"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "\nzerod systemd restart bluetooth.service\n\n",
"syntaxHighlightingTheme": "synthwave-84"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "It's a fresh project — the binary has only been on the Pi for a few days — but so far I haven't had to SSH in once. That was the whole point."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "What I'd do differently"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "A few things I'm already eyeing:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 15,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 39,
"byteStart": 27
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 65,
"byteStart": 60
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 106,
"byteStart": 100
}
}
],
"plaintext": "mDNS discovery. Hardcoding pizero.local works, but multiple zerod boxes on one LAN means I'm typing --host again."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 19,
"byteStart": 0
}
}
],
"plaintext": "Opus over the wire. HLS-with-AAC is convenient because every encoder produces it, but Opus at 96kbps would be plenty for a soundbar and would shrink the latency budget."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 39,
"byteStart": 0
}
}
],
"plaintext": "Some form of \"now playing\" passthrough. Right now I lose track metadata at the BlackHole capture point. ICY-style metadata on the segment fetcher would be enough — I don't need MPRIS."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "None of those are blocking. It does the one job I gave it, which is all I asked."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Source"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#link",
"uri": "https://github.com/tsirysndr/zerod"
}
],
"index": {
"byteEnd": 26,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 174,
"byteStart": 142
}
}
],
"plaintext": "github.com/tsirysndr/zerod — MIT, prebuilt tarballs for the ARMv6 / ARM64 / x86_64 / Apple Silicon / Intel Mac matrix on every release tag, brew install tsirysndr/tap/zerod if that's your thing."
}
}
],
"id": "019b4051-9c40-7ee0-8785-e782e6aa6a3a"
}
]
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiaxwjvot3ca5qu4fnus2eq7ju6vkyq52auvbnno54iwampb5e47rq"
},
"mimeType": "image/webp",
"size": 1291942
},
"description": "",
"path": "/3mnz5dda43k2z",
"publishedAt": "2026-06-11T11:40:07.672Z",
"site": "at://did:plc:7vdlgi2bflelz7mmuxoqjfcr/site.standard.publication/3maidt7jfqc2m",
"tags": [
"linux",
"raspberry pi",
"rust",
"hls",
"alsa",
"cpal"
],
"title": "I gave a Raspberry Pi Zero W a Bluetooth soundbar to drive"
}