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:
| Service | Role | Priority |
|---|---|---|
hifi-player.service | Audio engine: stream → decoder → jitter buffer → ALSA hw: | Real-time (SCHED_FIFO 70, locked memory) |
hifi-kiosk.service | Native touch UI (hifi-client --mode kiosk): draws directly on the display (DRM/KMS), touch through evdev, talks to the server itself | normal, the OOM killer takes it before the engine |
hifi-display.service | Web kiosk only: serves the browser UI on 127.0.0.1:3000 | normal (Nice=5) |
hifi-player-kiosk.service | Web 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.
1. Hardware and operating system
Section titled “1. Hardware and operating system”- 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_GOVERNORinplayer.env;hifi-player.servicesets 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’sondemand. - Wi-Fi power saving off (
/etc/NetworkManager/conf.d/bitwerk-wifi.conf,wifi.powersave = 2, and at once withiw), so WebSocket pings and discovery broadcasts get through over Wi-Fi. - USB autosuspend off (
usbcore.autosuspend=-1in/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.
2. Install
Section titled “2. Install”Ready-made image
Section titled “Ready-made image”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:
| Image | For | Tuned for the board |
|---|---|---|
bitwerk-player-pi3-arm64.img.xz | Pi 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.xz | Pi 4B, 400, CM4; also Pi 5, 500, CM5 | arm_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-btinconfig.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 setsRuntimeWatchdogSec=1m); - keep the kernel log of a crash across the reset (
dtoverlay=ramoops;systemd-pstoresaves 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, noe2scrub_reap, no ModemManager, no APT catch-up run at boot, and cloud-init only on the first boot.
-
Download
https://github.com/Arteiii/bitwerk/releases/latest/download/bitwerk-player-pi4-arm64.img.xz(or…-pi3-arm64.img.xz). -
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.
-
Optional, before ejecting: edit
bitwerk.txton the boot partition (any computer can open it):Key Meaning ROOMRoom name (default: the hostname) DAC_OVERLAYOverlay of a DAC HAT without EEPROM, e.g. hifiberry-dacplus; the Pi reboots onceALSA_DEVICEA specific device instead of the detected DAC TOUCH_DISPLAYauto(default: if a DSI/HDMI screen is detected),yesornoPLAYERnofor a display-only PiPAIRING_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) -
Boot.
bitwerk-firstboot.servicerunshifi-setup --yeswith 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). WithDAC_OVERLAYthe 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, thensudo hifi-setup). -
Pair, unless
PAIRING_CODEwas set: click “Pair” at the Pi under New devices in the web UI (Settings → Clients), or enter a code on the touch display or withsudo 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.
Installer
Section titled “Installer”On a freshly installed Raspberry Pi OS Lite (64-bit), via SSH or at the console:
curl -fsSL https://github.com/Arteiii/bitwerk/releases/latest/download/get.sh | sudo shThe installer (deploy/rpi/get.sh):
- 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 downloadshifi-client_arm64.debfrom the latest GitHub release. Before installing it checks the release’s signedSHA256SUMSwithgpgvand stops if the package does not match (specification). - Detection: takes the first external DAC from
aplay -land reads its capabilities fromaplay --dump-hw-params:HIFI_RATES(all bitwerk rates within the DAC’s range),HIFI_MAX_BITS,HIFI_DSD(nativeonly forDSD_U32_BE, because the player opens no other DSD format) and the volume control (hardwareif the DAC has a mixerDigital,PCM,Master,SpeakerorHeadphone). The internal outputs (HDMI, headphone jack) are never taken by themselves: without an external DAC,ALSA_DEVICEstays 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. - 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 asconfig.txt.bitwerk.bak) and asks for a reboot. - 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.
nasks for the room name, the output (all outputs fromaplay -l, the internal ones marked) and the display. With--advancedit also asks for rates, bit depth, DSD and volume; invalid answers are checked like the options and asked again. - Display: with a display it sets up the native kiosk (part of
hifi-client, nothing to download) and the boot splash;--kiosk webinstalls the Chromium kiosk instead (web kiosk). The choice is saved asKIOSK_UIinplayer.env. - 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_reapat boot, swap to compressed RAM only, and cloud-init off after its first run. - Services: writes
/etc/hifi/player.env, enableshifi-playerand, if needed, the kiosk (hifi-kiosk, orhifi-display+hifi-player-kioskfor the web kiosk) and starts them. It ends with how this Pi gets updates. - 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:
| Command | Effect |
|---|---|
sudo hifi-setup | Set up DAC, room and display again (one question; --advanced also asks for rates, bit depth, DSD and volume) |
sudo hifi-setup --display | Set up the touch display (native kiosk) and the boot splash |
sudo hifi-setup --display --kiosk web | Switch to the web kiosk (installs cage, Chromium and X11, about 200 MB); --kiosk native switches back |
sudo hifi-setup --pair | Pair, asks for the pairing code |
sudo hifi-setup --update | Install the latest package, keep the setup |
sudo hifi-setup --upgrades auto | Automatic 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:
curl -fsSL https://github.com/Arteiii/bitwerk/releases/latest/download/get.sh | sudo sh -s -- --yes --room Kitchen --overlay hifiberry-dacplus --no-display| Option | Meaning |
|---|---|
--room NAME | Room name (CLIENT_ID) |
--device DEV | ALSA device, e.g. hw:CARD=D90,DEV=0 (default: detected DAC) |
--advanced | Also ask for rates, bit depth, DSD and volume (otherwise taken from the detection) |
--rates, --max-bits, --dsd, --volume, --mixer | Values instead of the suggestions from --dump-hw-params |
--overlay NAME | Add a DAC HAT overlay, e.g. hifiberry-dacplus, allo-boss-dac-pcm512x-audio |
--display / --no-display | Set up the touch display or not |
--kiosk native|web | Which kiosk: native (default, the built-in touch UI) or web (Chromium); implies --display |
--no-player | Display only (remote control), no audio output |
--pairing-code CODE | Pair right after setup |
--upgrades auto|manual | Automatic updates on or off |
--version X.Y.Z, --deb FILE, --no-apt | Package source: specific version, local package, no APT repository |
--apt-repo | Only 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.
Updates
Section titled “Updates”The player: with the APT repository, update only the player package:
sudo apt update && sudo apt install --only-upgrade hifi-clientThis 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.
Automatic updates
Section titled “Automatic updates”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-clientfrom 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.
APT repository
Section titled “APT repository”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.listwithsigned-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.
Debian package by hand
Section titled “Debian package by hand”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-debof the workflow run. - For a version tag (
v1.0.0) it is attached to the GitHub release.
sudo apt install ./hifi-client_*_arm64.debThe 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.
3. Set up the DAC
Section titled “3. Set up the DAC”3.1 Determine the capabilities
Section titled “3.1 Determine the capabilities”hifi-client --mode dac-report # every card: formats, rates, mixers, list entry, suggestionaplay -l # cards and names (CARD=…)aplay -D hw:CARD=sndrpihifiberry,DEV=0 --dump-hw-params /dev/zero # FORMAT / RATEsudo -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:
RATEgivesHIFI_RATES; take only the rates the DAC clocks natively.FORMATdeterminesHIFI_MAX_BITSandHIFI_DSD:S32_LEmeans a 32-bit container. 24-bit material is padded with zeros in it, bit-perfectly.DSD_U32_BEmeans 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.
3.2 HiFiBerry (I²S)
Section titled “3.2 HiFiBerry (I²S)”In /boot/firmware/config.txt (with dtparam=audio=off):
| Board | Overlay | ALSA_DEVICE | HIFI_RATES | HIFI_MAX_BITS | HIFI_DSD |
|---|---|---|---|---|---|
| DAC+ Standard/Pro, DAC2 Pro, Amp2/Amp4 | dtoverlay=hifiberry-dacplus (Amp4: hifiberry-dacplus-std) | hw:CARD=sndrpihifiberry,DEV=0 | 44100,48000,88200,96000,176400,192000 | 24 | none |
| Amp4 Pro | dtoverlay=hifiberry-amp4pro | hw:CARD=sndrpihifiberry,DEV=0 | 44100,48000,88200,96000,176400,192000 | 24 | none |
| DAC+ HD / DAC2 HD | dtoverlay=hifiberry-dacplushd | hw:CARD=sndrpihifiberry,DEV=0 | 44100,48000,88200,96000,176400,192000 | 24 | none |
| DAC+ Light / DAC+ Zero / MiniAmp | dtoverlay=hifiberry-dac | hw:CARD=sndrpihifiberry,DEV=0 | 44100,48000,88200,96000,176400,192000 | 24 | none |
| Digi+ / Digi2 Pro (S/PDIF) | dtoverlay=hifiberry-digi or hifiberry-digi-pro | hw:CARD=sndrpihifiberry,DEV=0 | 44100,48000,88200,96000,176400,192000 | 24 | dop¹ |
¹ 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.
3.3 Allo (I²S)
Section titled “3.3 Allo (I²S)”| Board | Overlay | HIFI_RATES | HIFI_MAX_BITS | HIFI_DSD |
|---|---|---|---|---|
| Boss DAC / Boss2 | dtoverlay=allo-boss-dac-pcm512x-audio (Boss2: allo-boss2-dac-audio) | up to 192000 | 24 | none |
| Katana | dtoverlay=allo-katana-dac-audio | up to 384000 | 32 | dop² |
| DigiOne / DigiOne Signature (S/PDIF) | dtoverlay=allo-digione | up to 192000 | 24 | dop¹ |
| Piano 2.1 | dtoverlay=allo-piano-dac-plus-pcm512x-audio | up to 192000 | 24 | none |
² 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.
Audiophonics (I²S)
Section titled “Audiophonics (I²S)”| Board | Overlay | HIFI_RATES | HIFI_MAX_BITS | HIFI_DSD |
|---|---|---|---|---|
| I-Sabre ES9038Q2M / ES9028Q2M (also Kali Edition) | dtoverlay=i-sabre-q2m | up to 384000 | 32 | dop |
| I2S DACs without a control interface (I-Sabre ES9023, TDA1387) | dtoverlay=hifiberry-dac | up to 192000 | 24 | none |
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.
Which board the player shows
Section titled “Which board the player shows”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.
3.4 IQaudIO / Raspberry Pi DAC (I²S)
Section titled “3.4 IQaudIO / Raspberry Pi DAC (I²S)”| Board | Overlay | HIFI_RATES | HIFI_MAX_BITS |
|---|---|---|---|
| Raspberry Pi DAC+ / IQaudIO DAC+, DAC Pro | dtoverlay=rpi-dacplus (older images: iqaudio-dacplus) | up to 192000 | 24 |
| Raspberry Pi DigiAMP+ / IQaudIO DigiAMP+ | dtoverlay=rpi-digiampplus,unmute_amp (old: iqaudio-dacplus,unmute_amp) | up to 192000 | 24 |
| Raspberry Pi DAC Pro | dtoverlay=rpi-dacpro | up to 192000 | 24 |
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.
3.5 USB DACs (UAC2)
Section titled “3.5 USB DACs (UAC2)”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-audiooffersDSD_U32_BE. If it appears in--dump-hw-params, useHIFI_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 inHIFI_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. WithHIFI_VOLUME=hardwarethe engine findsPCMautomatically; other names are set withHIFI_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: 12in--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.
3.6 Overview: what is output how?
Section titled “3.6 Overview: what is output how?”The media-service negotiates the format per track and player (GET /api/tracks/{id}/playback-info?player_id=):
| Source | Profile | Output | Bit-perfect |
|---|---|---|---|
| FLAC 24/96 | rates include 96000, max. 24 bit | 96 kHz, S24_3LE or S32_LE | ✅ |
| FLAC 24/192 | max. 96 kHz | Resampling 192 → 96 kHz (rubato sinc, 64 bit), TPDF dither to 24 bit | ❌ |
| FLAC 16/44.1 | 48 kHz family only | Resampling 44.1 → 48 kHz, TPDF dither to 16 bit | ❌ |
| WAV 32-bit float | max. 24 bit | Requantization with dither | ❌ |
| DSF DSD64 | dop | DoP at 176.4 kHz, 24 bit | ✅ |
| DSF DSD64 | native | DSD_U32_BE at 88.2 kHz word rate | ✅ |
| DSF DSD64 | none | DSD → PCM 88.2 kHz (FIR decimation, 0 dB SACD = −6 dBFS), 24 bit with dither | ❌ |
| DSF DSD128 | dop, rates without 352800 | DoP 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:
| Conversion | x86 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.
4. Configuration (/etc/hifi/player.env)
Section titled “4. Configuration (/etc/hifi/player.env)”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).
| Variable | Default | Meaning |
|---|---|---|
HIFI_SERVER, HIFI_PLAYER_ID | – | Discovery is skipped only if both are set |
HIFI_MEDIA | server 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.token | File 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_ID | client-<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_MAC | MAC of the interface with the route to the server | MAC the player identifies with (discovery, pairing); only for special cases, see discovery.md |
ALSA_DEVICE | empty | Output 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_RATES | 44.1k–192k (or the verified DAC profile’s) | Native rates of the DAC |
HIFI_MAX_BITS | 24 (or the verified DAC profile’s) | Highest integer bit depth: 16, 24 or 32 (another value warns and uses the default) |
HIFI_DSD | none (or the verified DAC profile’s) | none, dop, native |
HIFI_VOLUME | software | software (digital, 64-bit + TPDF dither), hardware (DAC mixer, bit-perfect), fixed |
HIFI_MIXER | the verified DAC profile’s, else the first of Digital, PCM, Master, Speaker, Headphone | Mixer element for HIFI_VOLUME=hardware; if it is missing, the engine controls volume in software |
HIFI_BUFFER_MS / HIFI_PREFILL_MS | 2000 / 500 | Jitter buffer and prefill |
KIOSK_UI | native | Which kiosk hifi-setup sets up: native or web (written by hifi-setup --kiosk) |
CPU_GOVERNOR | performance (written by hifi-setup) | CPU frequency governor, set by hifi-player.service at each start; empty = the system’s own (ondemand) |
KIOSK_ROTATE | panel orientation (web kiosk: 0) | 0, 90, 180, 270; the native kiosk turns picture and touch together, the web kiosk only the picture |
KIOSK_OUTPUT | first connected of DSI, HDMI-A-1, HDMI-A-2 | Display connector for the native kiosk |
KIOSK_DIM_AFTER | 300 | Seconds without a touch, while nothing plays, until the native kiosk switches the screen off; 0 = never |
KIOSK_AMBIENT | 30 | Seconds 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_MOTION | 0 | 1 turns the native kiosk’s animations off: screens change at once, the identify frame stands still, the loading bar jumps |
KIOSK_BACKEND | auto | Native 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/splash | Where the kiosk finds the splash’s start time (written by kiosk-handoff.sh) |
KIOSK_BIND | 127.0.0.1:3000 | Web kiosk only: address of the browser UI (hifi-display) |
KIOSK_URL / KIOSK_SESSION / KIOSK_BROWSER / KIOSK_FLAGS | http://localhost:3000 / auto / Chromium / – | Web kiosk only: Chromium |
HIFI_SPOTIFY | false | Announce the player as a Spotify Connect device (see Spotify Connect) |
HIFI_SPOTIFY_NAME | CLIENT_ID | Name 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/spotify | Reusable 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.
Spotify Connect
Section titled “Spotify Connect”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
spotifyfeature (the.debfrom 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.
5. Start, pairing, verification
Section titled “5. Start, pairing, verification”sudo systemctl enable --now hifi-player.servicejournalctl -u hifi-player -f-
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). -
With Extended security: pair it, see below. The engine starts as soon as the token exists.
-
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”.
-
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/defaultas 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.
Pairing
Section titled “Pairing”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.
- 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”.
- Click “Pair”, enter the room name (prefilled with the name the Pi reports) and confirm.
- Within a few seconds the Pi picks up its token, stores it in
/var/lib/hifi/player.tokenand 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:
- 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. - 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 --pairasks for the code. You can also use the client directly; without--pairing-codeit 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 --pairreads/etc/hifi/player.envfor this, so it also reports the room name correctly. -
Via the configuration: Enter
HIFI_PAIRING_CODE=7K3M-Q9TDin/etc/hifi/player.envand runsudo systemctl restart hifi-player hifi-kiosk(web kiosk:hifi-display).
-
- The token ends up in
/var/lib/hifi/player.token(mode 0600, ownerhifi). Running services check the file every 10 s (the kiosk every 3 s) and start by themselves; a restart does no harm. An enteredHIFI_PAIRING_CODEis 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.
Identify and health readings
Section titled “Identify and health readings”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:
| Reading | Source on the Pi |
|---|---|
| SoC temperature | /sys/class/thermal/thermal_zone0/temp (millidegrees) |
| Under-voltage, each now and occurred since the previous reading | The 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_volt | The 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 |
| Uptime | first 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-playerlogsidentify requestedand blinks the activity LED (/sys/class/leds/ACT,led0on older kernels) about three times a second: it saves the LED’s trigger (normallymmc0, the SD card activity), setsnone, switchesbrightnesson 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/, frominstall.sh/etc/udev/rules.d/) gives the LED’striggerandbrightnessto the groupaudioand makes them group-writable. udev sets modes only for device nodes, not for sysfs files, so the rule runschgrpandchmod. The package applies it at once (udevadm trigger), otherwise it applies at the next boot. hifi-player.servicehasReadWritePaths=-/sys/class/leds/ACT -/sys/class/leds/led0: withProtectKernelTunables=truethe rest of/sysstays read-only for the player. systemd follows the/sys/class/ledslinks to the LED’s device directory, and the-skips the name a board does not have. A changed unit takes effect withsudo 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.
6. Real time and latency
Section titled “6. Real time and latency”hifi-player.service sets:
CPUSchedulingPolicy=fifowith priority 70,LimitRTPRIO=95,LimitMEMLOCK=infinity,IOSchedulingClass=realtimeandNice=-15.
The engine works in its own OS threads, not on the Tokio runtime:
hifi-sessiondecodes and fills the jitter buffer.hifi-dacdrains it period by period (50 ms) into the 200 ms ALSA buffer.
More tuning options if jitter buffer underrun appears in the log:
| Symptom | Remedy |
|---|---|
| Underruns during network load spikes | HIFI_BUFFER_MS=4000, HIFI_PREFILL_MS=1000 |
| ALSA xruns (clicks) under system load | PREEMPT_RT kernel, rtirq with higher priority for the I²S/USB IRQ, isolcpus=3 + CPUAffinity=3 |
| Dropouts over Wi-Fi | Ethernet; Wi-Fi power saving is off after hifi-setup (check: iw dev wlan0 get power_save) |
| USB DAC briefly disappears | a 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) |
7. Touch display
Section titled “7. Touch display”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 ofhifi-client, - saves
KIOSK_UI=nativeinplayer.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_OUTPUTpicks 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|270inplayer.envrotates 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): use90or270for landscape. Do not set the touch overlay parameters (swapxy,invx,invy) orvideo=…,rotate=incmdline.txtfor the native kiosk: the overlay parameters would turn the touch a second time, androtate=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
XKBLAYOUTin/etc/default/keyboard(sudo dpkg-reconfigure keyboard-configuration; German: Y and Z swapped). - Dimming: after
KIOSK_DIM_AFTERseconds (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 ruledeploy/rpi/99-bitwerk-backlight.rules(installed with the package) lets the kiosk’s groupvideoswitch the backlight. - Ambient view: after
KIOSK_AMBIENTseconds (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=offstarts 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=truein/var/lib/hifi/kiosk-prefs);sudo hifi-setup --displaydeletes 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_TOKENor its token file); a separate player entry per display is simpler.
Web kiosk (fallback)
Section titled “Web kiosk (fallback)”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
hifitovideo,render,input,ttyand_seatd(if present), -
enables
hifi-display.serviceandhifi-player-kiosk.serviceand disableshifi-kiosk.service. -
hifi-displayserves the kiosk UI immediately athttp://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_BINDon 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-kioskoccupiestty1. The login prompt there is gone; SSH stays available. -
Rotating:
KIOSK_ROTATE=90|180|270inplayer.envturns the picture (cage:wlr-randr, X11:xrandr), not the touch. On Touch Display 2 also set the touch parameters on its overlay line inconfig.txtby hand (swapxy, plusinvxorinvydepending on the direction);hifi-setupdoes not set them. -
Force X11 or Wayland:
KIOSK_SESSION=x11orwaylandinplayer.env(no automatic switch). -
Chromium flags:
kiosk.shadds 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_FLAGSinplayer.envcomes last, e.g.KIOSK_FLAGS=--disable-gpu-rasterizationif 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.
8. Troubleshooting
Section titled “8. Troubleshooting”| Message / symptom | Cause | Solution |
|---|---|---|
hw:0,0 output is not available in this build | built without --features alsa | rebuild with --features alsa |
cannot open hw:…: Device or resource busy | another process (PulseAudio/PipeWire) holds the DAC | disable PipeWire: systemctl --user mask pipewire pipewire-pulse wireplumber |
… does not support format S24_3LE | format not in the driver | falls 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 → resampling | check HIFI_RATES (/proc/asound/card0/stream0 or the DAC’s hw_params) |
| DSF plays as PCM 88.2 kHz instead of DSD | HIFI_DSD=none or DoP carrier rate not in HIFI_RATES | dop/native if the DAC supports it; for DSD128 over DoP, 352800 must be in HIFI_RATES |
| DSD → PCM quieter than PCM tracks | intended: 0 dB SACD = −6 dBFS (headroom against clipping) | compensate at the amplifier |
| Underruns only with resampling/DSD → PCM | CPU too slow (Pi 3, DSD256/512) | increase HIFI_BUFFER_MS, use DoP/native |
Log no hardware mixer element found; falling back to software volume | HIFI_MIXER does not match the card, or the card has no mixer | amixer -c <card> scontrols and adjust HIFI_MIXER; Digi/S/PDIF cards have no volume mixer |
| Volume control has no effect with DSD | DAC does not apply its mixer to DoP | control volume at the amplifier or use HIFI_DSD=native |
| Clicking between tracks of different rates | DAC re-clocks on format change | intended; same formats play gapless |
| Player goes “offline” after some time although it is running | WebSocket 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 approved | Web UI → Settings → Clients → New devices → Pair (or Approve & pair with a code) |
Log not paired: … | no (valid) player token | Pairing |
Log player token rejected by the server (revoked?) | client revoked, removed or re-paired | generate and redeem a new pairing code |
pairing failed: invalid or expired pairing code | code expired (10 min), already used or server restarted | generate a new code in the web UI |
pairing failed: this pairing code belongs to another device | code generated for a different client | generate the code on the right client |
cannot write token file /var/lib/hifi/player.token | directory 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 … s | rate limit after several wrong codes | wait, check the code |
server did not report a player ID | a very old server version that does not report player IDs yet, or HIFI_SERVER without HIFI_PLAYER_ID | update the server or set both variables |
Log cannot blink the activity LED for identify …: Permission denied or Read-only file system | udev 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 screen | the native kiosk does not start or keeps restarting | systemctl status hifi-kiosk, journalctl -u hifi-kiosk; a self-built hifi-client needs the native-ui feature |
| Kiosk: the screen stays off | dimmed after KIOSK_DIM_AFTER seconds without a touch | touch the screen once (it only wakes it); KIOSK_DIM_AFTER=0 keeps it on |
| Kiosk: only the cover and large text, no buttons | the ambient view after KIOSK_AMBIENT seconds without a touch while music plays | touch 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 way | panel orientation not what you want | KIOSK_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 update | apt never switches the kiosk, and the first hifi-setup --update from an older release still ran the old hifi-setup | sudo 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 screen | Chromium or cage did not start | journalctl -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 use | journalctl -u hifi-display |
Web kiosk log cage failed after …, switching to X11 | cage cannot open the display or seat on this device | nothing to do, X11 runs; to try Wayland again: sudo hifi-setup --display --kiosk web |
Web kiosk log no logind runtime directory | logind opened no session for hifi (old unit) | harmless, /run/hifi-kiosk is used; reinstall the units (apt upgrade) |
| Image: no player after the first boot | bitwerk-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 OS | reflash Raspberry Pi OS Lite (64-bit) |
| Installer: “No external DAC found” | HAT overlay missing or USB DAC not connected | select 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 version | the DAC played during every run (it waits at most four hours), or the repository is not reachable | journalctl -u hifi-update; systemctl list-timers hifi-update.timer; update now with sudo hifi-setup --update |