Skip to content

Raspberry Pi as a bitwerk Player: DAC setup

This guide turns a Raspberry Pi into a bit-perfect network player, with or without a touch display. The Pi runs hifi-client (crate client) in up to two roles, the audio engine and the touch display:

ServiceRolePriority
hifi-player.serviceAudio engine: stream → decoder → jitter buffer → ALSA hw:Real-time (SCHED_FIFO 70, locked memory)
hifi-kiosk.serviceNative touch UI (hifi-client --mode kiosk): draws directly on the display (DRM/KMS), touch through evdev, talks to the server itselfnormal, the OOM killer takes it before the engine
hifi-display.serviceWeb kiosk only: serves the browser UI on 127.0.0.1:3000normal (Nice=5)
hifi-player-kiosk.serviceWeb kiosk only: full-screen Chromium on tty1 (Wayland/cage, otherwise X11)normal

The native kiosk is the default. The web kiosk (Chromium) remains as an opt-in fallback (sudo hifi-setup --display --kiosk web); the two never run at the same time.

The files are in deploy/rpi/. A Pi can only output audio (headless), only be a display (remote control for another room), or both.

flowchart LR
    subgraph Pi["Raspberry Pi"]
        E["hifi-player<br/>engine"] -- "hw:CARD,DEV" --> DAC["DAC (I²S / USB)"]
        K["hifi-kiosk<br/>native touch UI"] --> S["touch display<br/>(DRM/KMS, evdev)"]
    end
    CS["control-server :8080"] -- "WS Protobuf (commands)" --> E
    E -- "OutputStatus, TrackEnded, DeviceTelemetry" --> CS
    E -- "playback-info (token) / signed stream (range)" --> MS["media-service :8082"]
    K -- "WS JSON, REST, covers (player token)" --> CS

Engine and kiosk authenticate with the player token, which the Pi receives once during pairing. It is stored in /var/lib/hifi/player.token and leaves the Pi only in the Authorization header. With the web kiosk, hifi-display proxies the browser’s requests to the server with the token, so the browser never sees it.

  • Raspberry Pi 4 or 5 (the Pi 3B+ works up to 192 kHz PCM; DSD256 and USB DACs rather not). The minimum is a Pi 3B+ for the installer get.sh; the ready-made image also runs on a Pi 3B. Use Ethernet instead of Wi-Fi for the network connection.
  • Raspberry Pi OS Lite (64-bit), or the ready-made bitwerk image based on it. A desktop is not needed, not even for the touch display.
  • Optional PREEMPT_RT kernel if underruns occur with small buffers. With the default buffers (2 s jitter buffer, 200 ms ALSA buffer) the normal kernel is enough.

The recommended system settings come with the setup: the installer, sudo hifi-setup, sudo hifi-setup --update and the image apply them, with nothing to do by hand:

  • CPU governor performance (CPU_GOVERNOR in player.env; hifi-player.service sets it each time it starts), so the clock does not change under the DAC thread or a DSD → PCM conversion. Empty or comment out the line to keep Raspberry Pi OS’s ondemand.
  • Wi-Fi power saving off (/etc/NetworkManager/conf.d/bitwerk-wifi.conf, wifi.powersave = 2, and at once with iw), so WebSocket pings and discovery broadcasts get through over Wi-Fi.
  • USB autosuspend off (usbcore.autosuspend=-1 in /boot/firmware/cmdline.txt, and at once for the devices already plugged in), so a USB DAC, or the hub in front of it, never sleeps between tracks. Only when a USB audio device is connected; the setup says when it changes the command line.

Settings you changed yourself stay: an emptied or commented-out CPU_GOVERNOR, or another usbcore.autosuspend= value on the kernel command line. The onboard audio may stay on: the setup addresses the DAC by name (hw:CARD=<name>), and with a DAC HAT overlay it switches the onboard audio off itself (dtparam=audio=off). Power supply: 5.1 V / 3 A (5 V / 5 A on the Pi 5), more so with Touch Display 2, which takes its power from the Pi’s GPIO header; undervoltage is the most common cause of a corrupted SD card.

The quickest way: every release carries two images, Raspberry Pi OS Lite (64-bit, Trixie) with hifi-client (including the native touch kiosk), the bitwerk boot splash and a first-boot setup already installed. No browser, compositor or X11 is on the image. Take the one for your Pi:

ImageForTuned for the board
bitwerk-player-pi3-arm64.img.xzPi 3B, 3B+ (1 GB), 3A+ (512 MB)no Pi 5 kernel (kernel updates build one initramfs instead of two), gpu_mem=64
bitwerk-player-pi4-arm64.img.xzPi 4B, 400, CM4; also Pi 5, 500, CM5arm_boost, keeps the Pi 5 kernel

Both are 64-bit (arm64): the hifi-client package is built only for arm64, and every Pi from the 3B on has a 64-bit CPU, so a 32-bit (armhf) Pi 3 image would need a second build of the player for no gain. The Pi 5 is covered by the Pi 4 image (same Raspberry Pi OS kernel set) but has no tuning of its own. Each image also boots on the other boards, only without their tuning. Both images:

  • switch Bluetooth off (dtoverlay=disable-bt in config.txt; delete the line to use it) and skip the camera probe (camera_auto_detect=0);
  • arm the hardware watchdog from the firmware on (kernel_watchdog_timeout=60): a hang in the initramfs or while mounting the root filesystem resets the player instead of leaving it stuck (systemd keeps the watchdog fed from then on, Raspberry Pi OS sets RuntimeWatchdogSec=1m);
  • keep the kernel log of a crash across the reset (dtoverlay=ramoops; systemd-pstore saves it to /var/lib/systemd/pstore, since the journal itself lives in RAM only);
  • carry no kernel headers (linux-headers-*, linux-kbuild-*): a player builds no kernel modules;
  • get the same boot tweaks as hifi-setup: swap to compressed RAM only (zram, never a swap file on the SD card), Wi-Fi power saving off, no man-page indexing, no e2scrub_reap, no ModemManager, no APT catch-up run at boot, and cloud-init only on the first boot.
  1. Download https://github.com/Arteiii/bitwerk/releases/latest/download/bitwerk-player-pi4-arm64.img.xz (or …-pi3-arm64.img.xz).

  2. Flash it with Raspberry Pi Imager (Choose OS → Use custom). In the OS customisation set the hostname (it becomes the room name), Wi-Fi and, if you want a shell, a user and SSH. Without customisation the image has no login at all and SSH is off; Ethernet works without any setting.

  3. Optional, before ejecting: edit bitwerk.txt on the boot partition (any computer can open it):

    KeyMeaning
    ROOMRoom name (default: the hostname)
    DAC_OVERLAYOverlay of a DAC HAT without EEPROM, e.g. hifiberry-dacplus; the Pi reboots once
    ALSA_DEVICEA specific device instead of the detected DAC
    TOUCH_DISPLAYauto (default: if a DSI/HDMI screen is detected), yes or no
    PLAYERno for a display-only Pi
    PAIRING_CODEPair right away (code from the web UI, valid for 10 minutes); used once, then removed
    UPGRADESauto: install new player versions by themselves (automatic updates); manual (default)
  4. Boot. bitwerk-firstboot.service runs hifi-setup --yes with these values: it picks the DAC and its capabilities, writes /etc/hifi/player.env, applies the recommended system settings, enables the services and, with a display, starts the kiosk. The boot splash shows a status line meanwhile (the kiosk waits for the setup). With DAC_OVERLAY the Pi reboots once and runs the setup again, so the DAC that appears only then is set up too. On success it disables itself, so later boots never wait for it. If it fails, it repeats on the next boot, at most three times in all; then it gives up with a message on the splash (journalctl -u bitwerk-firstboot, then sudo hifi-setup).

  5. Pair, unless PAIRING_CODE was set: click “Pair” at the Pi under New devices in the web UI (Settings → Clients), or enter a code on the touch display or with sudo hifi-setup --pair.

Later changes work as with the installer (sudo hifi-setup). Updates come from the APT repository: the image always carries it, and if the build could not reach it, the first boot sets it up (if it does not answer then either, the setup log says so; sudo hifi-setup --update and the automatic updates try again). The image holds no state of the build machine: /var/lib/hifi (device key, player token) is empty, so every card gets its own identity on its first boot and keeps it from then on.

How the images are built, and how to build one yourself: Raspberry Pi internals.

On a freshly installed Raspberry Pi OS Lite (64-bit), via SSH or at the console:

Terminal window
curl -fsSL https://github.com/Arteiii/bitwerk/releases/latest/download/get.sh | sudo sh

The installer (deploy/rpi/get.sh):

  1. Package: checks the model and system (64-bit, Pi 3B+ or newer) and installs hifi-client. If the bitwerk APT repository is reachable, it sets it up. If not, it says so with a warning and how this Pi gets updates then (sudo hifi-setup --update), and downloads hifi-client_arm64.deb from the latest GitHub release. Before installing it checks the release’s signed SHA256SUMS with gpgv and stops if the package does not match (specification).
  2. Detection: takes the first external DAC from aplay -l and reads its capabilities from aplay --dump-hw-params: HIFI_RATES (all bitwerk rates within the DAC’s range), HIFI_MAX_BITS, HIFI_DSD (native only for DSD_U32_BE, because the player opens no other DSD format) and the volume control (hardware if the DAC has a mixer Digital, PCM, Master, Speaker or Headphone). The internal outputs (HDMI, headphone jack) are never taken by themselves: without an external DAC, ALSA_DEVICE stays unset, so the player never points at a card that does not exist. The room name is the Pi’s hostname (or the one set before); a touch display counts as connected if a DSI/HDMI screen is detected.
  3. DAC HAT without a driver: If no external DAC is visible, it offers the HATs from section 3 (HiFiBerry, Allo, Raspberry Pi/IQaudIO), adds the overlay to /boot/firmware/config.txt (dtparam=audio=off, backup as config.txt.bitwerk.bak) and asks for a reboot.
  4. One question: it shows what it found (room, DAC with its rates, bit depth, DSD and volume, display) and asks Use these settings? [y]. Enter takes them. n asks for the room name, the output (all outputs from aplay -l, the internal ones marked) and the display. With --advanced it also asks for rates, bit depth, DSD and volume; invalid answers are checked like the options and asked again.
  5. Display: with a display it sets up the native kiosk (part of hifi-client, nothing to download) and the boot splash; --kiosk web installs the Chromium kiosk instead (web kiosk). The choice is saved as KIOSK_UI in player.env.
  6. Recommended system settings and boot tweaks: CPU governor, Wi-Fi power saving and USB autosuspend as in section 1, and a faster, sturdier boot: no APT catch-up run, man-page indexing or e2scrub_reap at boot, swap to compressed RAM only, and cloud-init off after its first run.
  7. Services: writes /etc/hifi/player.env, enables hifi-player and, if needed, the kiosk (hifi-kiosk, or hifi-display + hifi-player-kiosk for the web kiosk) and starts them. It ends with how this Pi gets updates.
  8. Pairing: waits a moment in case the server admits the Pi by itself, then shows how to pair it: click “Pair” at the Pi in the web UI and press Enter (the running player picks up its token by itself), or type a code from “Approve & pair” (see Pairing).

The package ships the installer as hifi-setup. With it you change the setup later without downloading anything again:

CommandEffect
sudo hifi-setupSet up DAC, room and display again (one question; --advanced also asks for rates, bit depth, DSD and volume)
sudo hifi-setup --displaySet up the touch display (native kiosk) and the boot splash
sudo hifi-setup --display --kiosk webSwitch to the web kiosk (installs cage, Chromium and X11, about 200 MB); --kiosk native switches back
sudo hifi-setup --pairPair, asks for the pairing code
sudo hifi-setup --updateInstall the latest package, keep the setup
sudo hifi-setup --upgrades autoAutomatic updates on (manual: off); changes nothing else

Without a terminal (or with --yes) the installer asks nothing and takes the detected values. Once player.env names a DAC (ALSA_DEVICE), a later run keeps its audio settings (device, rates, bit depth, DSD, volume, mixer) and only changes what you give it now: --device/--overlay, another output picked after answering n, the answers with --advanced, or single flags such as --volume. Everything can be preset with flags, for example for several identical Pis:

Terminal window
curl -fsSL https://github.com/Arteiii/bitwerk/releases/latest/download/get.sh | sudo sh -s -- --yes --room Kitchen --overlay hifiberry-dacplus --no-display
OptionMeaning
--room NAMERoom name (CLIENT_ID)
--device DEVALSA device, e.g. hw:CARD=D90,DEV=0 (default: detected DAC)
--advancedAlso ask for rates, bit depth, DSD and volume (otherwise taken from the detection)
--rates, --max-bits, --dsd, --volume, --mixerValues instead of the suggestions from --dump-hw-params
--overlay NAMEAdd a DAC HAT overlay, e.g. hifiberry-dacplus, allo-boss-dac-pcm512x-audio
--display / --no-displaySet up the touch display or not
--kiosk native|webWhich kiosk: native (default, the built-in touch UI) or web (Chromium); implies --display
--no-playerDisplay only (remote control), no audio output
--pairing-code CODEPair right after setup
--upgrades auto|manualAutomatic updates on or off
--version X.Y.Z, --deb FILE, --no-aptPackage source: specific version, local package, no APT repository
--apt-repoOnly set up the APT repository if it is missing (used by the image’s first boot and the automatic updates)

A reboot after a new overlay happens only after confirmation; without a terminal the sudo reboot hint is shown at the end.

The player: with the APT repository, update only the player package:

Terminal window
sudo apt update && sudo apt install --only-upgrade hifi-client

This leaves kernel, firmware and device trees alone, so a power cut during it cannot keep the Pi from booting. The one exception is a release that changes the boot splash: then the update rebuilds the initramfs once (about a minute on a Pi 3; the output says so), and the Pi should stay powered until it is done. Without the APT repository, sudo hifi-setup --update downloads the latest .deb from the release (it tries the repository again first, and it also applies the recommended system settings and boot tweaks a player set up with an older release lacks). In both cases /etc/hifi/player.env and the player token are kept, and the enabled services that run restart with the new binary. player.env is not a conffile, so an update never asks about it: when a release adds a setting, the package appends its line from the template (/usr/share/hifi/player.env) and leaves every line you or hifi-setup wrote as it is. At the end of every setup and update, hifi-setup says how this Pi gets updates: automatically, with apt, or, without a reachable repository, with sudo hifi-setup --update.

apt never switches the kiosk. sudo hifi-setup --update and sudo hifi-setup --display keep the kiosk saved in KIOSK_UI; a player set up before the native kiosk existed (no KIOSK_UI) moves to the native kiosk with either of them. On such a player the first sudo hifi-setup --update still runs the old hifi-setup (the package replaces it while it runs), which updates the package but keeps the web kiosk: run sudo hifi-setup --update once more, or sudo hifi-setup --display. Cage and Chromium stay installed until you remove them (sudo apt purge cage chromium, or chromium-browser where that is the installed package).

The operating system (kernel, firmware, libraries): sudo apt update && sudo apt full-upgrade, at a time when the player is not needed. It rewrites the kernel, initramfs and device trees on the boot partition, which takes minutes on a Pi 3: do not unplug the Pi until it is done, then reboot.

Opt-in, for players nobody logs in to: sudo hifi-setup --upgrades auto (or UPGRADES=auto in the image’s bitwerk.txt, or --upgrades auto at install) switches on hifi-update.timer. Once a day at about 4:00 (a random delay of up to an hour, so not every player of a site at once) and 45 minutes after boot it runs hifi-update.service, which

  • updates only hifi-client from the bitwerk APT repository (and what the new version needs), never the kernel, firmware or the rest of the system;
  • never interrupts playback: while an ALSA stream runs, drains or is paused, it waits (at most four hours, then it tries again at the next run) before it installs, and restarts the services with the new binary only once the DAC is idle;
  • sets up the APT repository first if it is missing (an image whose first boot could not reach it).

sudo hifi-setup --upgrades manual switches it off; --update and the setup keep the current choice. Log: journalctl -u hifi-update. Kernel and firmware stay with you, as above.

From the web UI: Update players in the web UI (an admin) asks the players to install the server’s version, also with --upgrades manual. The player writes the version into /var/lib/hifi/update-request, and the package’s hifi-update-now.path (enabled by the package) starts the same hifi-update.service at once: same rules as above, never during playback. A player that is off gets the request when it connects again.

The project can publish a signed APT repository for hifi-client with its website (https://bitwerk.systems/apt on Cloudflare Pages, suite stable, component main; a fork publishes at https://<owner>.github.io/<repo>/apt or sets its own address). The installer uses it automatically as soon as it is reachable:

  • key to /etc/apt/keyrings/bitwerk.gpg,
  • source /etc/apt/sources.list.d/bitwerk.list with signed-by=, so it applies only to this repository.

The installer fetches the key over HTTPS from the same site as the repository; this equals the trust in a direct download of the .deb. A different address is set with BITWERK_APT_URL.

Fork operators who publish their own repository: see Raspberry Pi internals.

CI builds the client for aarch64 with ALSA support and packages it as a .deb:

  • On every push it is available as the artifact hifi-client-arm64-deb of the workflow run.
  • For a version tag (v1.0.0) it is attached to the GitHub release.
Terminal window
sudo apt install ./hifi-client_*_arm64.deb

The package has the same file layout as install.sh. It creates the user hifi, installs the units to /lib/systemd/system/, the installer as /usr/sbin/hifi-setup and creates /etc/hifi/player.env from the template /usr/share/hifi/player.env (kept across updates, which only add new settings). The services are not enabled automatically: sudo hifi-setup sets up DAC, room and display and starts them, or adjust player.env by hand, run systemctl enable --now … as in section 5 and pair the Pi. The player token in /var/lib/hifi survives updates; only apt purge removes it.

To build the player yourself, on the Pi or cross-compiled: Raspberry Pi internals.

Terminal window
hifi-client --mode dac-report # every card: formats, rates, mixers, list entry, suggestion
aplay -l # cards and names (CARD=…)
aplay -D hw:CARD=sndrpihifiberry,DEV=0 --dump-hw-params /dev/zero # FORMAT / RATE
sudo -u hifi hifi-client --mode probe # server found? approved? paired?

DAC report (hifi-client --mode dac-report [--json]): lists each sound card with its ID, driver, USB VID:PID and bcdDevice, codec, every playback device’s formats (including DSD), rates, channel, period and buffer limits, its mixer controls with their dB range, the entry of the DAC compatibility list (Setup & support → DAC compatibility list on the website) that matches it and the settings hifi-setup would suggest; then board, kernel, overlays, HAT EEPROM, the DAC keys of player.env and what the auto profile does. It reads only local files and asks ALSA without playing; it holds no serial numbers, host names, IP or MAC addresses and sends nothing. While the player plays, its device is busy and the report shows the format in use instead of the capabilities; for the full list stop it first (sudo systemctl stop hifi-player, then start). --json prints hifi.dac.v1.DacReport for a support request.

--dump-hw-params shows the real capabilities of the hardware. These matter for player.env:

  • RATE gives HIFI_RATES; take only the rates the DAC clocks natively.
  • FORMAT determines HIFI_MAX_BITS and HIFI_DSD:
    • S32_LE means a 32-bit container. 24-bit material is padded with zeros in it, bit-perfectly.
    • DSD_U32_BE means native DSD (HIFI_DSD=native).

Always use hw:. plughw:, default and dmix may resample or mix. The engine then warns, and the server does not report the output as bit-perfect. Stable names (hw:CARD=<name>,DEV=0) are also kept when USB devices are detected in a different order.

In /boot/firmware/config.txt (with dtparam=audio=off):

BoardOverlayALSA_DEVICEHIFI_RATESHIFI_MAX_BITSHIFI_DSD
DAC+ Standard/Pro, DAC2 Pro, Amp2/Amp4dtoverlay=hifiberry-dacplus (Amp4: hifiberry-dacplus-std)hw:CARD=sndrpihifiberry,DEV=044100,48000,88200,96000,176400,19200024none
Amp4 Prodtoverlay=hifiberry-amp4prohw:CARD=sndrpihifiberry,DEV=044100,48000,88200,96000,176400,19200024none
DAC+ HD / DAC2 HDdtoverlay=hifiberry-dacplushdhw:CARD=sndrpihifiberry,DEV=044100,48000,88200,96000,176400,19200024none
DAC+ Light / DAC+ Zero / MiniAmpdtoverlay=hifiberry-dachw:CARD=sndrpihifiberry,DEV=044100,48000,88200,96000,176400,19200024none
Digi+ / Digi2 Pro (S/PDIF)dtoverlay=hifiberry-digi or hifiberry-digi-prohw:CARD=sndrpihifiberry,DEV=044100,48000,88200,96000,176400,19200024dop¹

¹ DoP over S/PDIF works only if the downstream DAC recognizes DoP. With a 176.4 kHz carrier rate, DSD64 is the maximum.

Every HiFiBerry card has the card ID sndrpihifiberry, so the player names the board after the codec its card reports (aplay -l: HiFiBerry DAC+ HD HiFi pcm179x-hifi-0 is a DAC+ HD, pcm512x a DAC+, tas5754m an Amp4, wm8804 a Digi). The hifiberry-dac overlay reports the codec pcm5102a for every board on it, also for other manufacturers’ boards; the player then shows I2S DAC (PCM5102A) until you name the board in the web UI.

The I²S drivers offer S16_LE, S24_LE and S32_LE. For 24-bit material the engine picks S32_LE with padded LSBs; this is bit-perfect. Volume: The DAC+ models have the PCM512x mixer Digital, so use HIFI_VOLUME=hardware with HIFI_MIXER=Digital (bit-perfect, see §3.4). Digi+ cards have no volume mixer, so software stays. If you control volume at the amplifier, use fixed; the path then stays fully bit-perfect.

BoardOverlayHIFI_RATESHIFI_MAX_BITSHIFI_DSD
Boss DAC / Boss2dtoverlay=allo-boss-dac-pcm512x-audio (Boss2: allo-boss2-dac-audio)up to 19200024none
Katanadtoverlay=allo-katana-dac-audioup to 38400032dop²
DigiOne / DigiOne Signature (S/PDIF)dtoverlay=allo-digioneup to 19200024dop¹
Piano 2.1dtoverlay=allo-piano-dac-plus-pcm512x-audioup to 19200024none

² Whether DoP or native DSD is possible depends on the driver version. --dump-hw-params tells you; if DSD_U32_BE appears, use native.

The card name comes from aplay -l, e.g. hw:CARD=BossDAC,DEV=0.

BoardOverlayHIFI_RATESHIFI_MAX_BITSHIFI_DSD
I-Sabre ES9038Q2M / ES9028Q2M (also Kali Edition)dtoverlay=i-sabre-q2mup to 38400032dop
I2S DACs without a control interface (I-Sabre ES9023, TDA1387)dtoverlay=hifiberry-dacup to 19200024none

The card shows up as card N: DAC [I-Sabre Q2M DAC], so the device is hw:CARD=DAC,DEV=0. Its card ID is generic: the player recognizes the board by the overlay in config.txt and names it in the web UI and on the touch display. Status “documented” (manufacturer specification, not yet tested with bitwerk). Audiophonics’ USB DACs (XMOS based, e.g. U-Sabre, EVO-Sabre’s USB input) work as plain USB Audio Class 2 devices; they get a list entry once someone reports their lsusb IDs.

The boards on hifiberry-dac report the same card (sndrpihifiberry, codec pcm5102a) as a HiFiBerry DAC+ Light, so the player shows them as I2S DAC (PCM5102A); name them in the web UI.

The web UI and the touch display name each output after the DAC compatibility list (common/data/dacs/). For a HAT the player decides by the codec its card reports (the end of the PCM name in aplay -l, e.g. pcm179x), together with the card ID; the dtoverlay= lines of config.txt only decide between boards with the same codec, or find a board whose card names no codec. When that does not leave exactly one board, it shows a neutral I2S DAC (<CODEC>) (or the ALSA card name) without a profile rather than a wrong name.

Name the board yourself in the web UI: Settings → Player → Output, choose the room, type the name under Name (e.g. Audiophonics ES9023) and Save name. The server keeps it for that player and device, the touch display shows it too, and the field’s hint quotes the name the player detected; an empty name brings that one back.

BoardOverlayHIFI_RATESHIFI_MAX_BITS
Raspberry Pi DAC+ / IQaudIO DAC+, DAC Prodtoverlay=rpi-dacplus (older images: iqaudio-dacplus)up to 19200024
Raspberry Pi DigiAMP+ / IQaudIO DigiAMP+dtoverlay=rpi-digiampplus,unmute_amp (old: iqaudio-dacplus,unmute_amp)up to 19200024
Raspberry Pi DAC Prodtoverlay=rpi-dacproup to 19200024

The PCM512x/PCM5122 chips (also HiFiBerry DAC+ and Allo Boss/Piano) have a hardware mixer called Digital (−103.5 dB to +24 dB in 0.5 dB steps). With HIFI_VOLUME=hardware and HIFI_MIXER=Digital the engine sets the volume control from the web UI, kiosk or OSC directly on the mixer:

  • The sample path stays bit-perfect, and the display keeps showing “Bit-Perfect”.
  • 100 % corresponds to 0 dB; the engine never uses gain above 0 dB.
  • The curve is the same as for software volume: 50 % ≈ −6 dB.
  • Mute toggles the playback switch of the element.

amixer -c sndrpihifiberry scontrols shows the available elements. If the element does not exist (typo, different overlay), the journal says no hardware mixer element found; falling back to software volume, and the player reports volume_mode = software to the server. Details: features.md §10.

USB DACs conforming to Audio Class 2 need no overlay. aplay -l gives the name, e.g. hw:CARD=D90,DEV=0. Notes:

  • Connect the DAC directly to a USB port of the Pi, not through a hub. On the Pi 4 use the blue USB 3 ports.
  • Native DSD: For known DACs (Topping, SMSL, iFi, RME, Denafrips among others) snd-usb-audio offers DSD_U32_BE. If it appears in --dump-hw-params, use HIFI_DSD=native. The engine then clocks at DSD rate/32, for example an 88.2 kHz word rate for DSD64.
  • DoP: DACs without native support get DoP with HIFI_DSD=dop. The markers are 0x05/0xFA, the carrier rate is DSD rate/16. So DSD128 needs 352.8 kHz in HIFI_RATES.
  • Typical values: HIFI_RATES=44100,48000,88200,96000,176400,192000,352800,384000, HIFI_MAX_BITS=32.
  • Volume at the DAC: Many USB DACs report a feature-unit mixer, usually PCM, more rarely <name> Playback. With HIFI_VOLUME=hardware the engine finds PCM automatically; other names are set with HIFI_MIXER (amixer -c D90 scontrols).
  • Multichannel soundcards: Some USB soundcards take only a fixed, larger channel count, for example the 12-channel card of the Allen & Heath Xone:96 DJ mixer (CHANNELS: 12 in --dump-hw-params). The engine then opens the card with that count and plays the track bit-perfect on channels 1/2; the other channels get silence (journal: DAC takes only 12 channels). Mono tracks and native DSD are not padded this way.

If your DAC does not run bit-perfect or is missing here, request it through the issue form “Request DAC support” in the repository and paste the output of hifi-client --mode dac-report --json (see 3.1). If it works, a “DAC report” with the same output helps build the compatibility list. The list itself lives in common/data/dacs/ (one TOML file per device); the website shows it as a table under Setup & support → DAC compatibility list.

The media-service negotiates the format per track and player (GET /api/tracks/{id}/playback-info?player_id=):

SourceProfileOutputBit-perfect
FLAC 24/96rates include 96000, max. 24 bit96 kHz, S24_3LE or S32_LE✅
FLAC 24/192max. 96 kHzResampling 192 → 96 kHz (rubato sinc, 64 bit), TPDF dither to 24 bit❌
FLAC 16/44.148 kHz family onlyResampling 44.1 → 48 kHz, TPDF dither to 16 bit❌
WAV 32-bit floatmax. 24 bitRequantization with dither❌
DSF DSD64dopDoP at 176.4 kHz, 24 bit✅
DSF DSD64nativeDSD_U32_BE at 88.2 kHz word rate✅
DSF DSD64noneDSD → PCM 88.2 kHz (FIR decimation, 0 dB SACD = −6 dBFS), 24 bit with dither❌
DSF DSD128dop, rates without 352800DoP discarded → DSD → PCM 88.2 kHz❌

Resampling and DSD → PCM run on the hifi-session thread, never on the DAC thread. On an x86 desktop core (release build, stereo) they take the following share of real time:

Conversionx86 core
44.1 → 48 kHz≈ 0.1 %
192 → 96 kHz≈ 0.3 %
DSD64 → PCM≈ 1.4 %
DSD256 → PCM≈ 6 %
DSD512 → PCM≈ 12 %

A Pi 4 is roughly 5 to 8 times slower. Resampling and DSD64/128 → PCM are uncritical there. DSD512 → PCM can largely saturate one core, especially on a Pi 3. Then take a look at top; prefer dop/native if the DAC supports it.

All variables are commented in deploy/rpi/player.env. An unknown value for HIFI_DSD or HIFI_VOLUME falls back to the default; the player logs a warning with the accepted values (journalctl -u hifi-player).

VariableDefaultMeaning
HIFI_SERVER, HIFI_PLAYER_ID–Discovery is skipped only if both are set
HIFI_MEDIAserver host, media_port from discovery (8082)media-service URL
HIFI_PAIRING_CODE–One-time pairing code from the web UI; redeemed as long as no valid token exists
HIFI_TOKEN_FILE/var/lib/hifi/player.tokenFile with the player token (mode 0600), shared by engine and display; the device key for one-click pairing (device.key) lives next to it
HIFI_PLAYER_TOKEN–The token itself (e.g. from provisioning); takes precedence over the file
CLIENT_IDclient-<MAC>Room name in the player selector; reported via beacon, pairing and capabilities, a name set in the web UI takes precedence. hifi-setup writes it (suggestion: the hostname)
HIFI_MACMAC of the interface with the route to the serverMAC the player identifies with (discovery, pairing); only for special cases, see discovery.md
ALSA_DEVICEemptyOutput device; empty: the first USB or HAT card the player finds, else the Pi’s own output (it says which in the log, the web UI and on the touch display). hifi-setup writes the DAC it detects; without an external DAC the line stays unset
HIFI_RATES44.1k–192k (or the verified DAC profile’s)Native rates of the DAC
HIFI_MAX_BITS24 (or the verified DAC profile’s)Highest integer bit depth: 16, 24 or 32 (another value warns and uses the default)
HIFI_DSDnone (or the verified DAC profile’s)none, dop, native
HIFI_VOLUMEsoftwaresoftware (digital, 64-bit + TPDF dither), hardware (DAC mixer, bit-perfect), fixed
HIFI_MIXERthe verified DAC profile’s, else the first of Digital, PCM, Master, Speaker, HeadphoneMixer element for HIFI_VOLUME=hardware; if it is missing, the engine controls volume in software
HIFI_BUFFER_MS / HIFI_PREFILL_MS2000 / 500Jitter buffer and prefill
KIOSK_UInativeWhich kiosk hifi-setup sets up: native or web (written by hifi-setup --kiosk)
CPU_GOVERNORperformance (written by hifi-setup)CPU frequency governor, set by hifi-player.service at each start; empty = the system’s own (ondemand)
KIOSK_ROTATEpanel orientation (web kiosk: 0)0, 90, 180, 270; the native kiosk turns picture and touch together, the web kiosk only the picture
KIOSK_OUTPUTfirst connected of DSI, HDMI-A-1, HDMI-A-2Display connector for the native kiosk
KIOSK_DIM_AFTER300Seconds without a touch, while nothing plays, until the native kiosk switches the screen off; 0 = never
KIOSK_AMBIENT30Seconds without a touch on Now playing, while music plays, until the native kiosk shows its ambient view (the style is chosen on the kiosk under Settings → Display and stored in /var/lib/hifi/kiosk-prefs); on = 30, off or 0 = the view starts off until a style is chosen there
KIOSK_REDUCED_MOTION01 turns the native kiosk’s animations off: screens change at once, the identify frame stands still, the loading bar jumps
KIOSK_BACKENDautoNative kiosk output for debugging: auto, drm, fbdev, headless or headless:<w>x<h> (with KIOSK_HEADLESS_PNG and KIOSK_HEADLESS_INPUT, see the README)
KIOSK_SPLASH_FILE/run/hifi-kiosk/splashWhere the kiosk finds the splash’s start time (written by kiosk-handoff.sh)
KIOSK_BIND127.0.0.1:3000Web kiosk only: address of the browser UI (hifi-display)
KIOSK_URL / KIOSK_SESSION / KIOSK_BROWSER / KIOSK_FLAGShttp://localhost:3000 / auto / Chromium / –Web kiosk only: Chromium
HIFI_SPOTIFYfalseAnnounce the player as a Spotify Connect device (see Spotify Connect)
HIFI_SPOTIFY_NAMECLIENT_IDName shown in the Spotify apps; the player reports it to the control-server, which finds the room’s device by it to play Spotify there from bitwerk
HIFI_SPOTIFY_CACHE/var/lib/hifi/spotifyReusable Spotify login (mode 0700); empty = log in again after every restart

Auto profile: when the card the player plays on matches a verified entry of the DAC compatibility list (by USB ID, or for a HAT by its card ID, codec and overlay), the player takes that entry’s rates, bit depth, DSD mode and mixer element at its start for every one of these four keys that player.env leaves unset (commented out or empty); a value set there always wins. Entries with another status are not adopted, because they were not tested with bitwerk. The log names what it took (journalctl -u hifi-player | grep "DAC profile"), and the DAC report shows it under “Auto profile”. hifi-setup writes HIFI_RATES, HIFI_MAX_BITS and HIFI_DSD, so a player set up with it keeps its values until they are commented out.

The engine reports the capabilities (HIFI_RATES, HIFI_MAX_BITS, HIFI_DSD, HIFI_VOLUME, ALSA_DEVICE) to the server on connect via PATCH /api/players/{id}/profile. For the volume mode this is the value actually in effect, so software if the mixer is missing. The preferences (ReplayGain mode, preamp, PEQ), in contrast, are maintained in the web UI under Settings → Player.

With HIFI_SPOTIFY=true the player shows up in the Spotify apps on the same network (zeroconf/mDNS) under HIFI_SPOTIFY_NAME. It is off by default.

  • Requirements: a Spotify Premium account and a build with the spotify feature (the .deb from the APT repository has it; when building yourself, add --features alsa,spotify).
  • First login: select the player once in a Spotify app on the same network. The app hands the login over; the player stores a reusable login in HIFI_SPOTIFY_CACHE (never the access token) and reconnects on its own after a restart. If the internet is not up yet, or drops later, it keeps the login and retries (5 s, doubling up to 5 min).
  • Sound: Spotify delivers Ogg Vorbis at 320 kbit/s (Spotify Lossless is only available to certified devices). The audio takes the same jitter buffer and DAC path as the library, at 44.1 kHz or the nearest rate the DAC supports. Volume from the Spotify app goes to the player’s volume control (HIFI_VOLUME); librespot never changes the samples. The output status shows “lossy source” instead of bit-perfect.
  • Library and Spotify: starting Spotify stops the library track (the web UI shows it paused); playing from the web UI pauses Spotify.
  • Only for private listening: Spotify’s terms do not cover public playback (bars, clubs, events). The receiver is based on librespot, an unofficial client.
  • Network: mDNS needs UDP 5353 and a random TCP port for the hand-over. If the player is on a different subnet than the phone, it does not appear.
Terminal window
sudo systemctl enable --now hifi-player.service
journalctl -u hifi-player -f
  1. The player sends discovery beacons and asks the server for its token. On the local network it gets one right away (automatic pairing, the default) and appears in the web UI under Settings → Clients as “Approved”. With Extended security on, it appears as “Pending” (under New devices), asks every few seconds whether it was paired, and writes a hint to the log every minute (not paired).

  2. With Extended security: pair it, see below. The engine starts as soon as the token exists.

  3. The room appears in the web UI’s player selector. The DAC display then shows e.g. “96 kHz / 24-bit” with the green badge “Bit-Perfect”.

  4. Cross-check on the Pi during playback:

    Terminal window
    cat /proc/asound/card0/pcm0p/sub0/hw_params # format: S32_LE, rate: 96000 (96000/1)

What cancels bit-perfection (the badge then switches to the amber “DSP”):

  • software volume below 100 %,
  • applied ReplayGain,
  • active monitor functions (mono, L−R, dim, trim, A/B level matching),
  • an active PEQ profile,
  • plughw/default as the device.

For DSD, DSP stages are bypassed in general; volume is then controlled by the amplifier or, with HIFI_VOLUME=hardware, by the DAC’s mixer. Whether the DAC applies its mixer to DoP is device-specific. If DSD drops out at reduced volume, set it to 100 % for DSD. On pause and track change the engine sends DSD silence (0x69) instead of PCM zeros, so the DAC does not switch between DSD and PCM and click.

Gapless: Consecutive tracks with the same DAC format play without a gap and without re-clocking the DAC. On a format change, e.g. from 44.1 kHz to 96 kHz, a short pause to switch is unavoidable.

MAC approval alone is no longer enough (MACs can be forged, see access control). Every Pi needs its own player token. The default way needs nothing at all:

Automatic pairing (default). Switch the Pi on. As long as it has no token, it asks the server for one. If it talks to the server directly from the local network (a private address, not through a reverse proxy), the server admits it at once: the Pi gets its token, keeps the name it reports as the room name, and the web UI shows it under Settings → Clients with the badge “Joined automatically” for a day. At most 10 new players join this way within 10 minutes; more wait for a “Pair”. Any device on your network can join like this; where guests share the network, turn on “Extended security: new players need approval” in Settings → Clients (pairing modes). “Block” takes a player out again and keeps it out.

Updates and lost tokens. Updating the server or the player keeps the pairing: the token is stored on both sides. If a Pi’s token is rejected anyway (token file deleted, server restored from an older backup), the Pi gets a new one by itself with its device key (/var/lib/hifi/device.key), in both modes. A re-installed Pi (new device key, same MAC address) keeps its room in automatic mode; with Extended security pair it again with a code, or “Block” it and then “Pair” it. A server reinstalled without a backup knows no players: in automatic mode they join again by themselves, otherwise pair each once.

One-click pairing (Extended security). As long as the Pi has no token, it announces itself to the server and asks every few seconds whether it was paired.

  1. In the web UI, open Settings → Clients. The Pi appears under New devices with its host name, model (e.g. “Raspberry Pi 4 Model B Rev 1.4”), MAC address and version; the new web UI also shows a short notice “New device found”.
  2. Click “Pair”, enter the room name (prefilled with the name the Pi reports) and confirm.
  3. Within a few seconds the Pi picks up its token, stores it in /var/lib/hifi/player.token and starts; the room is “online” in the player selection.

The token goes only to the Pi that is waiting: it proves itself with its own device key (/var/lib/hifi/device.key, created on first start, mode 0600), and a pairing that the Pi does not pick up within 5 minutes lapses (click “Pair” again). Pair only devices you expect, right after switching them on: the server trusts what devices on your network report about themselves (LAN trust assumption). If the Pi does not show up under New devices, check that it is on and reaches the server (sudo -u hifi hifi-client --mode probe), or pair it with a code.

Pairing with a code (alternative). A code proves that you have the Pi in front of you; use it for a Pi with a touch display, or when one-click pairing is not possible (an older player version, another network). It works in both modes:

  1. In the web UI, under Settings → Clients, click “Approve & pair” on the Pi. The dialog shows a code like 7K3M-Q9TD, valid for 10 minutes and usable only once.
  2. Redeem it on the Pi, depending on your setup:
    • On the touch display: Until paired, the kiosk shows a setup screen with the device name, status (“Looking for bitwerk server …”, “Server found”, “Approved”) and the instructions; the code goes in on an on-screen keyboard (the native kiosk opens it with “Enter pairing code”). It is laid out as an English QWERTY keyboard, and only the characters a code can contain are active (no I, L, O, U). Type the code, “Pair” – done. A USB keyboard works too.

    • In the terminal: sudo hifi-setup --pair asks for the code. You can also use the client directly; without --pairing-code it asks as well:

      Terminal window
      sudo -u hifi hifi-client --mode pair --pairing-code 7K3M-Q9TD

      (without discovery, additionally --server http://<server>:8080). hifi-setup --pair reads /etc/hifi/player.env for this, so it also reports the room name correctly.

    • Via the configuration: Enter HIFI_PAIRING_CODE=7K3M-Q9TD in /etc/hifi/player.env and run sudo systemctl restart hifi-player hifi-kiosk (web kiosk: hifi-display).

  3. The token ends up in /var/lib/hifi/player.token (mode 0600, owner hifi). Running services check the file every 10 s (the kiosk every 3 s) and start by themselves; a restart does no harm. An entered HIFI_PAIRING_CODE is ignored afterwards and can be removed.

The kiosk redeems the code with the same flow as --mode pair (POST /api/pairing/redeem with the Pi’s MAC) and writes the token to the same file; the engine (hifi-player) picks it up without further action. An already paired Pi cannot be re-paired on the display; that only works in the terminal.

A code is bound to the client it was generated for: If another Pi (different MAC) redeems it, the server rejects it without consuming the code. Pairing again (with a code or one click) replaces the old token. “Block” or “Remove” in the web UI revokes the token: The engine disconnects after 15 s at the latest, exits, is restarted by systemd and waits for a new pairing. “Block” keeps the Pi out until you approve it again; after “Remove” a Pi that is still on comes back as a new player (in automatic mode it joins again).

Servers with AUTH_DISABLED=true need no token; the player detects this itself.

The player reports how its device is doing and can show where it is; what the server and the web UI make of it is in features.md.

Health readings (DeviceTelemetry): right after the engine connects to the control-server and then every 30 s while it stays connected, hifi-player sends:

ReadingSource on the Pi
SoC temperature/sys/class/thermal/thermal_zone0/temp (millidegrees)
Under-voltage, each now and occurred since the previous readingThe hwmon device rpi_volt: /sys/class/hwmon/hwmon*/in0_lcrit_alarm. The kernel’s raspberrypi-hwmon driver asks the firmware every 2 s and has it clear its sticky flags each time, so the alarm means “under-voltage within the last ~2 s”
Throttling flags (ARM frequency capped, throttled, soft temperature limit), only on kernels without rpi_voltThe firmware’s get_throttled node with the whole mask, the same as vcgencmd get_throttled: /sys/devices/platform/soc/soc:firmware/get_throttled, else the first …firmware directory with that file up to three levels below /sys/devices/platform. Where rpi_volt exists these flags stay clear
Uptimefirst number of /proc/uptime
Board/proc/device-tree/model, e.g. Raspberry Pi 4 Model B Rev 1.5

The flags occurred since the previous reading (the player samples every second): the player reads the under-voltage alarm (or, on old kernels, the firmware node) every second while it is connected. A reading’s “now” flags are those of the latest sample; its “occurred” flags combine every sample since the previous reading and then start again. So a dip of the supply that lasts only a moment shows once, in the next reading; the first reading after a connect covers only its own sample. The player looks the source up once per start and never reads the firmware’s get_throttled node where rpi_volt exists: kernel 6.6 marks it deprecated (its first read puts a warning with a backtrace into dmesg) and 6.12, current Raspberry Pi OS, no longer has it.

The player only reads these files, so they need no setup and no vcgencmd. A source the device does not have (any machine that is not a Pi) stays empty; the player logs that at debug level only. To compare on the Pi: vcgencmd measure_temp, vcgencmd get_throttled (where rpi_volt exists, the kernel clears bits 16–19 every 2 s, so they mean “recently”, not “since boot”) and dmesg | grep -i voltage (the kernel’s own Undervoltage detected! messages).

Identify: Identify in the web UI makes the player show where it is, for 10 s unless the request says otherwise (1 to 60 s):

  • hifi-player logs identify requested and blinks the activity LED (/sys/class/leds/ACT, led0 on older kernels) about three times a second: it saves the LED’s trigger (normally mmc0, the SD card activity), sets none, switches brightness on and off every 150 ms and then puts the trigger back, also when the player stops while it blinks. A second request while it blinks only makes it blink longer. The audio engine is not involved; playback goes on.
  • A touch display that shows this player draws a pulsing frame in the brand colour around the screen for as long (a still frame with reduced motion). A new request starts the time again.

Writing to the LED needs two things; the package (also in the image) and deploy/rpi/install.sh install both:

  • The udev rule deploy/rpi/99-bitwerk-leds.rules (/lib/udev/rules.d/, from install.sh /etc/udev/rules.d/) gives the LED’s trigger and brightness to the group audio and makes them group-writable. udev sets modes only for device nodes, not for sysfs files, so the rule runs chgrp and chmod. The package applies it at once (udevadm trigger), otherwise it applies at the next boot.
  • hifi-player.service has ReadWritePaths=-/sys/class/leds/ACT -/sys/class/leds/led0: with ProtectKernelTunables=true the rest of /sys stays read-only for the player. systemd follows the /sys/class/leds links to the LED’s device directory, and the - skips the name a board does not have. A changed unit takes effect with sudo systemctl restart hifi-player.

Check: ls -l /sys/class/leds/ACT/ shows trigger and brightness as -rw-rw-r-- root audio. Without the permission, or on a board without such an LED, the player logs cannot blink the activity LED for identify once at info level and carries on; the frame on the touch display works either way.

hifi-player.service sets:

  • CPUSchedulingPolicy=fifo with priority 70, LimitRTPRIO=95,
  • LimitMEMLOCK=infinity,
  • IOSchedulingClass=realtime and Nice=-15.

The engine works in its own OS threads, not on the Tokio runtime:

  • hifi-session decodes and fills the jitter buffer.
  • hifi-dac drains it period by period (50 ms) into the 200 ms ALSA buffer.

More tuning options if jitter buffer underrun appears in the log:

SymptomRemedy
Underruns during network load spikesHIFI_BUFFER_MS=4000, HIFI_PREFILL_MS=1000
ALSA xruns (clicks) under system loadPREEMPT_RT kernel, rtirq with higher priority for the I²S/USB IRQ, isolcpus=3 + CPUAffinity=3
Dropouts over Wi-FiEthernet; Wi-Fi power saving is off after hifi-setup (check: iw dev wlan0 get power_save)
USB DAC briefly disappearsa dedicated power supply (5 V/3 A, or 5 V/5 A on the Pi 5); USB autosuspend is off after hifi-setup (check: cat /sys/module/usbcore/parameters/autosuspend shows -1)

The official Raspberry Pi Touch Display 1 and 2 (DSI) and HDMI touchscreens are supported.

sudo hifi-setup --display sets everything up (the image does it on first boot):

  • enables hifi-kiosk.service, the native kiosk (hifi-client --mode kiosk), and disables the web kiosk’s units; nothing is downloaded, the kiosk is part of hifi-client,
  • saves KIOSK_UI=native in player.env,
  • sets up the boot splash.

hifi-kiosk.service is a plain system service, without compositor, browser or X11: it takes over from the splash right after boot without waiting for the network, restarts by itself if it ever hangs or crashes, and gives way to the audio engine when memory runs short. It takes tty1, so there is no login prompt on the screen; SSH stays available. How it starts and what it may do: Raspberry Pi internals.

Until the Pi is paired, the UI shows the setup screen: the device name in large type (the name to look for in the web UI), the status (“Looking for bitwerk server …”, then “Server found. Pair this device in the bitwerk web UI: Settings → Clients.”) and the server and MAC address. Click “Pair” in the web UI (Pairing) and the kiosk moves on by itself; “Enter pairing code” on the screen takes a code instead. Then it shows Now Playing (cover, title, artist, album, the quality and bit-perfect badges, progress with tap-to-seek, transport with shuffle and repeat, and volume), the queue (a tap on an entry offers Play now, Play next and Remove), the albums and a search. After 2 minutes without a touch it returns to Now Playing. If the server is unreachable, it says so and freezes the position until the connection is back; if the player is revoked in the web UI, it returns to the setup screen.

Display and input:

  • Output: the first connected connector of a DSI panel, HDMI-A-1, HDMI-A-2; KIOSK_OUTPUT picks one. Without a display the kiosk waits instead of restarting and takes the screen as soon as one is plugged in.
  • Rotating: KIOSK_ROTATE=90|180|270 in player.env rotates picture and touch together, in the kiosk itself. Unset, the kiosk follows the panel’s orientation. Touch Display 2 is a portrait panel (720 × 1280): use 90 or 270 for landscape. Do not set the touch overlay parameters (swapxy, invx, invy) or video=…,rotate= in cmdline.txt for the native kiosk: the overlay parameters would turn the touch a second time, and rotate= only turns the text console.
  • Size: text and touch targets scale with the panel’s pixel density, so a button stays about 9 mm wide on Touch Display 1 and 2 alike.
  • Input: touchscreens, a mouse (HDMI setups) and a USB keyboard (pairing code, search). The on-screen keyboards are always English QWERTY. A USB keyboard’s keys follow XKBLAYOUT in /etc/default/keyboard (sudo dpkg-reconfigure keyboard-configuration; German: Y and Z swapped).
  • Dimming: after KIOSK_DIM_AFTER seconds (default 300, 0 = never) without a touch while nothing plays, the backlight goes off (HDMI: the display’s power saving). While music plays it stays on. The first touch only wakes the screen. The udev rule deploy/rpi/99-bitwerk-backlight.rules (installed with the package) lets the kiosk’s group video switch the backlight.
  • Ambient view: after KIOSK_AMBIENT seconds (default 30) without a touch on Now playing while music plays, the controls and the tab bar fade out and the screen shows what plays, in the style chosen under Settings → Display: Balanced (default: a large cover with title, artist and album at reading size, quality, Bit-Perfect or DSP and the progress with both times), Large (a cover with title and artist in large type, readable from about 1.5 m), Huge (title and artist alone, readable from about 3 m) or Off. The choice is kept in /var/lib/hifi/kiosk-prefs (far_view=balanced|large|huge|off); without one, KIOSK_AMBIENT=off starts with Off; Preview under the styles shows the chosen one at once. The first touch only brings the controls back; it never presses a button. The view leaves by itself when the music pauses or the server or the player goes away. The screen never goes dark while music plays; on the Touch Display the view lowers the backlight to 40 % of its level (HDMI screens stay as they are).
  • DJ sets: for a recorded set split by its cue sheet, Settings → Display chooses how Now playing shows songs that overlap: Mixing in (default: the song that carries the mix as the title and “Mixing in: …” under it) or Lanes (also a strip with one bar per song and the playhead). Kept in /var/lib/hifi/kiosk-prefs (dj_sets=mixing_in|lanes).
  • Display only: when an admin switches the room to Display only in the web UI (Settings › Player › Touch display), the kiosk shows what plays and takes no touch or key; a touch still wakes a dimmed screen. The kiosk keeps the mode across a restart and a server outage (display_only=true in /var/lib/hifi/kiosk-prefs); sudo hifi-setup --display deletes it, should the server be gone for good.
  • Song requests: in a room where guests ask for songs (web UI, Settings › Player › Guests), Now playing shows 3 requests › while some wait. A tap lists them; pick one for Play next, Add to queue or Decline. Undo on the toast takes a decision back within 8 seconds, before the kiosk sends it.
  • Settings → Info on the kiosk shows the player’s name, room, board, output, version and build, the server with its version, the IP and MAC address and the display’s resolution, handy when you report a problem.
  • Display only, no DAC: Do not enable hifi-player.service. The Pi is still paired and then shows its own player. A display for a different room needs that room’s token (HIFI_PLAYER_TOKEN or its token file); a separate player entry per display is simpler.

The Chromium kiosk from before the native kiosk remains as an opt-in fallback: sudo hifi-setup --display --kiosk web sets it up, sudo hifi-setup --display --kiosk native switches back. It needs about 200 MB of packages and more memory (Chromium); on a Pi 3A+ (512 MB) stay with the native kiosk. hifi-setup --display --kiosk web:

  • installs cage (Wayland), Chromium and the X11 fallback (xserver-xorg-core, xserver-xorg-legacy, xinit),

  • writes /etc/X11/Xwrapper.config (allowed_users=anybody, needs_root_rights=yes) for the fallback,

  • adds the user hifi to video, render, input, tty and _seatd (if present),

  • enables hifi-display.service and hifi-player-kiosk.service and disables hifi-kiosk.service.

  • hifi-display serves the kiosk UI immediately at http://localhost:3000, then looks for the server and authenticates with the player token (the same as the engine). As long as the Pi is not paired, the UI shows the setup screen with an on-screen keyboard (Pairing). The page talks only to this process: /ws, /cover/<track>, the album list (/library/albums) and “Play album” (/players/<id>/album) are forwarded to the control-server with the token. The display therefore sees and controls only its own player.

  • Leave KIOSK_BIND on loopback: Anyone who can reach the kiosk port can control this player. Any other address is acknowledged by the service with a warning in the log.

  • hifi-player-kiosk occupies tty1. The login prompt there is gone; SSH stays available.

  • Rotating: KIOSK_ROTATE=90|180|270 in player.env turns the picture (cage: wlr-randr, X11: xrandr), not the touch. On Touch Display 2 also set the touch parameters on its overlay line in config.txt by hand (swapxy, plus invx or invy depending on the direction); hifi-setup does not set them.

  • Force X11 or Wayland: KIOSK_SESSION=x11 or wayland in player.env (no automatic switch).

  • Chromium flags: kiosk.sh adds per board: Pi 3 --enable-low-end-device-mode --renderer-process-limit=1 --disk-cache-size=33554432 (1 GB, the profile lives in RAM); Pi 4/5 --enable-gpu-rasterization --ignore-gpu-blocklist. KIOSK_FLAGS in player.env comes last, e.g. KIOSK_FLAGS=--disable-gpu-rasterization if a display shows glitches. On every board it switches off what a kiosk on localhost never needs and what slows the start: extensions, sync, component updates, Cast discovery and other background requests.

How the kiosk session, the boot splash and the boot time work: Raspberry Pi internals.

Message / symptomCauseSolution
hw:0,0 output is not available in this buildbuilt without --features alsarebuild with --features alsa
cannot open hw:…: Device or resource busyanother process (PulseAudio/PipeWire) holds the DACdisable PipeWire: systemctl --user mask pipewire pipewire-pulse wireplumber
… does not support format S24_3LEformat not in the driverfalls back to S32_LE automatically; otherwise lower HIFI_MAX_BITS
Status without “Bit-Perfect”, log resampling (not bit-perfect)profile does not allow the source rate → resamplingcheck HIFI_RATES (/proc/asound/card0/stream0 or the DAC’s hw_params)
DSF plays as PCM 88.2 kHz instead of DSDHIFI_DSD=none or DoP carrier rate not in HIFI_RATESdop/native if the DAC supports it; for DSD128 over DoP, 352800 must be in HIFI_RATES
DSD → PCM quieter than PCM tracksintended: 0 dB SACD = −6 dBFS (headroom against clipping)compensate at the amplifier
Underruns only with resampling/DSD → PCMCPU too slow (Pi 3, DSD256/512)increase HIFI_BUFFER_MS, use DoP/native
Log no hardware mixer element found; falling back to software volumeHIFI_MIXER does not match the card, or the card has no mixeramixer -c <card> scontrols and adjust HIFI_MIXER; Digi/S/PDIF cards have no volume mixer
Volume control has no effect with DSDDAC does not apply its mixer to DoPcontrol volume at the amplifier or use HIFI_DSD=native
Clicking between tracks of different ratesDAC re-clocks on format changeintended; same formats play gapless
Player goes “offline” after some time although it is runningWebSocket pings do not get through (proxy, Wi-Fi power saving)sudo hifi-setup switches Wi-Fi power saving off (check: iw dev wlan0 get power_save); proxy timeouts > 12 s
Player stays “pending”not approvedWeb UI → Settings → Clients → New devices → Pair (or Approve & pair with a code)
Log not paired: …no (valid) player tokenPairing
Log player token rejected by the server (revoked?)client revoked, removed or re-pairedgenerate and redeem a new pairing code
pairing failed: invalid or expired pairing codecode expired (10 min), already used or server restartedgenerate a new code in the web UI
pairing failed: this pairing code belongs to another devicecode generated for a different clientgenerate the code on the right client
cannot write token file /var/lib/hifi/player.tokendirectory not writable (old unit without StateDirectory, wrong user)reinstall the units from deploy/rpi/; run --mode pair as sudo -u hifi
too many attempts, retry in … srate limit after several wrong codeswait, check the code
server did not report a player IDa very old server version that does not report player IDs yet, or HIFI_SERVER without HIFI_PLAYER_IDupdate the server or set both variables
Log cannot blink the activity LED for identify …: Permission denied or Read-only file systemudev rule not applied yet (Permission denied), or an old unit without ReadWritePaths= for the LED (Read-only file system; check with systemctl cat hifi-player | grep ReadWritePaths)the rule: sudo udevadm trigger --action=change --subsystem-match=leds, then sudo systemctl restart hifi-player (or reboot); the unit: update the package or re-run deploy/rpi/install.sh (it also replaces a copy in /etc/systemd/system/), then sudo systemctl daemon-reload && sudo systemctl restart hifi-player; see Identify
Kiosk: the splash stays, or a black screenthe native kiosk does not start or keeps restartingsystemctl status hifi-kiosk, journalctl -u hifi-kiosk; a self-built hifi-client needs the native-ui feature
Kiosk: the screen stays offdimmed after KIOSK_DIM_AFTER seconds without a touchtouch the screen once (it only wakes it); KIOSK_DIM_AFTER=0 keeps it on
Kiosk: only the cover and large text, no buttonsthe ambient view after KIOSK_AMBIENT seconds without a touch while music playstouch the screen once (it only brings the controls back); Settings → Display → Off on the kiosk turns it off
Kiosk: picture or touch turned the wrong waypanel orientation not what you wantKIOSK_ROTATE=0|90|180|270 in player.env, sudo systemctl restart hifi-kiosk; remove swapxy/invx/invy from the panel’s overlay line
Kiosk: still Chromium after an updateapt never switches the kiosk, and the first hifi-setup --update from an older release still ran the old hifi-setupsudo hifi-setup --display --kiosk native
hifi-setup: “This hifi-client has no native kiosk”an older hifi-client (for example from --version)install the latest one (sudo hifi-setup --update), or sudo hifi-setup --display --kiosk web
Web kiosk: black screenChromium or cage did not startjournalctl -u hifi-player-kiosk
Web kiosk stays on the logo, later “The bitwerk display service is not answering”hifi-display is not running / port in usejournalctl -u hifi-display
Web kiosk log cage failed after …, switching to X11cage cannot open the display or seat on this devicenothing to do, X11 runs; to try Wayland again: sudo hifi-setup --display --kiosk web
Web kiosk log no logind runtime directorylogind opened no session for hifi (old unit)harmless, /run/hifi-kiosk is used; reinstall the units (apt upgrade)
Image: no player after the first bootbitwerk-firstboot failed (it repeats on the next boots, three attempts in all)journalctl -u bitwerk-firstboot, then sudo hifi-setup
Kiosk stays at “Looking for bitwerk server …”broadcasts do not arrive (different VLAN, server without host network)put Pi and server in the same network; the control-server needs network_mode: host
Installer: “bitwerk needs a 64-bit system”32-bit Raspberry Pi OSreflash Raspberry Pi OS Lite (64-bit)
Installer: “No external DAC found”HAT overlay missing or USB DAC not connectedselect the overlay with sudo hifi-setup or connect the DAC, then run sudo hifi-setup; to play over HDMI or the headphone jack, answer n and pick it
Installer: “The bitwerk APT repository … is not reachable”no internet, a proxy, or the repository is not published (yet)the player still installs from the release; update with sudo hifi-setup --update, which tries the repository again
Automatic updates: no new versionthe DAC played during every run (it waits at most four hours), or the repository is not reachablejournalctl -u hifi-update; systemctl list-timers hifi-update.timer; update now with sudo hifi-setup --update