{
  "$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"
}