The audiod party hub seen live in coppwr's patch-bay view (HP OMEN 16, Slackware64-current). Two independent sources — a HONOR 400 Pro phone over Bluetooth A2DP (bluez_input.CC_62_00_5C_54_9F) and a local Firefox — both feed the virtual hub_combined sink. PipeWire sums them once, and hub_combined mirrors the mix out to every output at the same time: the built-in Speaker, all three HDMI outputs, and a BT Speaker (bluez_output.41_42_E4_0C_EA_BB). Each mirror leg is its own output.hub_combined_ node. The capture devices — the HP Wide Vision HD Camera and the two microphones (Stereo and Digital) — are present in the graph but are deliberately not wired into hub_combined: combine mirrors playback only, so sending music everywhere never routes your microphone anywhere. The profiler on the right shows the whole graph running with essentially zero xruns — clean, glitch-free multi-source, multi-output audio.
audiod is the small piece that non-systemd Slackware has been missing for
years: a per-user audio session manager. It gives the PipeWire stack the
ordered startup, readiness gating, per-user lifecycle, and clean teardown that
systemd --user provides on systemd distributions — built entirely from
elogind + libslack daemon(1) + shell. No systemd. No dinit. No new
service manager. elogind is used completely unmodified.
It exists to fix a real, long-standing Slackware bug: after login, PipeWire
sometimes takes seconds-to-minutes to produce sound, or comes up muted / as a
"Dummy Output". That happens because the stock start scripts fire
wireplumber and pipewire-pulse before the PipeWire core is ready andrely on retry/respawn to eventually converge — a race that some machines lose.
(Fixed for slackware-current 8/7/26)
On top of the core session manager, audiod has an optional hub mode that
turns the machine into a party audio hub: Bluetooth phones play through the box,
sound mirrors to every output at once, and you drive it all from the terminal.
Hub mode is off by default and changes nothing until you turn it on — see
Party hub mode and the companion GAMER-GUIDE.md.
- What it does
- What it does NOT do
- How it works
- Requirements
- Files installed
- Build & install
- Enabling it
- Configuration
- Using audioctl
- Going back to PulseAudio
- Uninstalling / rollback
- Troubleshooting
- Design notes & rationale
- Scope, limitations & known behaviour
- Party hub mode (optional)
-
Reacts to login/logout, not to shells. It watches elogind's
org.freedesktop.login1D-Bus interface forUserNew/UserRemovedandSessionNew/SessionRemoved. elogind reference-counts a user's sessions, soUserNewfires only on the user's first login andUserRemovedonly on their last logout. One PipeWire stack per user, surviving VT switches and multiple concurrent sessions of the same user.The session signals are not redundant. With
loginctl enable-lingerthe user object is created at boot, before anyone logs in — so the later graphical login raisesSessionNewand noUserNewat all. Keying onUserNewalone meant nothing ever started on a lingering system. Both pairs are handled, and the handlers are idempotent, so whichever arrives first wins and the other is a no-op. -
Starts the stack in dependency order, with a readiness gate. On first login it starts
pipewire, waits for its socket to actually appear, then startswireplumber, thenpipewire-pulse. This is the core fix: no race, no retry storm, no "wait a few minutes for sound". -
Supervises the stack. Each service runs under libslack
daemon -rB(respawn + stop-when-session-ends) — the exact mechanism Slackware's stock scripts already use, so crash-restart still works. -
Tears down cleanly on last logout. On
UserRemovedit stops the user's stack, targeted by name. No orphaned PipeWire processes left behind (a real gap in the stock setup, especially on multi-user machines). -
Handles the D-Bus session bus sensibly. It detects an existing session bus wherever it lives — including the
/tmp/dbus-XXXXbus that desktops create viadbus-run-session— by inspecting the session environment (elogind does not track the bus). It reuses that bus and only optionally spawns one for bus-less console/bare-WM logins. -
Respects the system's audio-server choice. If the machine is set to PulseAudio,
audiodstands down and does nothing. -
Provides a user tool,
audioctl. Any user can check/start/stop/restart their own audio stack without root and without logging out. -
Works regardless of desktop or login path. SDDM+Plasma, lightdm+XFCE, bare WM,
startxfrom runlevel 3, pure console — all covered, because it keys off the elogind session, not the desktop.
-
It does not replace elogind, logind, seatd, or a service manager. It is only the "launch and supervise the user audio stack" piece. Session/seat tracking,
XDG_RUNTIME_DIR, sleep, and ACLs remain elogind's job. -
It does not modify elogind. No patches, no rebuild. It only reads from elogind's public D-Bus interface.
-
It does not touch device/channel configuration (in core mode). It starts and stops daemons; it never remaps channels, sets defaults, changes volumes, or edits WirePlumber/PipeWire config. (Hub mode, when explicitly enabled, does create a combine sink and manage Bluetooth on request — but only then.)
-
It does not fix kernel-level device enumeration. If the ALSA driver itself takes minutes to expose the card,
audiodcannot make the hardware appear sooner — but it makes WirePlumber wait correctly for it instead of timing out, which fixes the common (race-based) form of the delay. -
It does not make two simultaneous desktops share one sound card. With a single card on one seat, only the active session gets the hardware; the other falls back to Dummy Output. This is a fundamental limitation of shared-seat audio and behaves identically under systemd — it is out of scope. (For cross-user sharing over the network see the separate
audioshareproject.) -
It does not manage system services (bluetoothd, the system D-Bus bus, etc.). Those start at boot, before any login, and
audiodrelies on them being present. -
It is not strictly required for basic playback — it changes when, how, in what order, and for whom the stack starts and stops.
elogind (login1 D-Bus) audiod (root daemon)
---------------------- --------------------
user first login --> UserNew(uid) ---> gate: seat + Class=user + Remote=no ?
...or, with linger --> SessionNew(id) --/ (id resolved to its uid)
|
v
ensure session bus (detect/reuse, opt. spawn)
|
v (as the user, via setpriv)
start pipewire --> wait for socket
--> start wireplumber
--> start pipewire-pulse
(each under: daemon -rB, respawn)
user last logout --> UserRemoved(uid) ---> stop the stack, targeted by name
...or, with linger --> SessionRemoved --/ (only once no seated session is left)
- Reactor (
/usr/sbin/audiod): a foreground root daemon. It attaches to the login1 signal stream viagdbus monitor, then reconciles users already logged in — in that order, and on every reconnect. Attaching first means anything that happens during reconcile is buffered rather than lost; reconciling after every reconnect means a login that occurred while the stream was down is still picked up. Reconciling once at startup, before attaching, left a window in which a login fell through both mechanisms. - Privilege drop: services run as the target user via
setpriv --reuid --regid --init-groupswith a cleanenv -ienvironment (no PAM → it never opens a second elogind session that would corrupt the refcount). - Shared library (
/usr/libexec/audiod/audiod-lib.sh): the gate, audio-server detection, bus detection, and stack operations — sourced by bothaudiodandaudioctlso policy lives in one place. - Hub library (
/usr/libexec/audiod/hub.sh): all optional hub logic, sourced only, inert unlessHUB_MODE=yes. - Single source of truth (
/etc/audiod/stack.conf): the ordered service list, read by both tools.
- elogind (any reasonably recent version that exposes the login1 D-Bus
interface with
UserNew/UserRemoved— verified on 257.16). - psmisc (provides
fuser, used for the session-bus liveness check) — part of the Slackware base. - libslack
daemon(1)— already used by Slackware's stock PipeWire scripts. - PipeWire (
pipewire,wireplumber,pipewire-pulse) — the payload. pam_elogindmust be in the login PAM stacks (Slackware default), so that local logins create elogind sessions andUserNewfires.- For hub mode only:
bluez(BlueZ 5.x) withbluetoothdrunning and a powered adapter.
/usr/sbin/audiod root:root 0755 the reactor (system daemon)
/usr/bin/audioctl root:root 0755 user control tool
/usr/libexec/audiod/audiod-lib.sh root:root 0644 shared logic (sourced)
/usr/libexec/audiod/hub.sh root:root 0644 hub logic (sourced, opt-in)
/usr/libexec/audiod/hub-btwatch.sh root:root 0755 hub BT VT-switch watcher
/usr/libexec/audiod/hub-pulsewatch.sh root:root 0755 restores hub extras after
pipewire-pulse is replaced
/usr/sbin/audiod-takeover.sh root:root 0755 disable stock autostart
/usr/sbin/audiod-restore.sh root:root 0755 re-enable stock autostart
/usr/sbin/audiod-doctor.sh root:root 0755 repair what upgrades break
/etc/rc.d/rc.audiod root:root 0644 init script (0644 = OFF)
/etc/audiod/audiod.conf root:root 0644 configuration
/etc/audiod/stack.conf root:root 0644 ordered service list
/usr/doc/audiod-<version>/ root:root 0644 docs
Runtime (not part of the package): /run/audiod.pid (root); and, owned by the
user, ~/.run/{pipewire,wireplumber,pipewire-pulse,dbus}.pid plus
/run/user/$uid/bus if audiod spawns a bus. Hub mode also uses
/run/audiod-hub-owner (auto-owner state, cleared on reboot) and
/run/user/$uid/audiod-hub-combine (records whether you switched the combine
sink off by hand, so the watcher does not put it back).
cd audiod
bash audiod.SlackBuild # produces /tmp/audiod-<ver>-noarch-<b>_rtz.txz
sudo installpkg /tmp/audiod-*.txzTo ship the ready-to-go gamer/party preset as the default config, build with
GAME=ON:
GAME=ON bash audiod.SlackBuild # installs the gamer preset (hub + combine on)The package is noarch (pure shell) and builds from the bundled src/ tree
— nothing is downloaded or compiled.
audiod replaces how PipeWire is started, so the stock start mechanisms
must be disabled first (this does not switch you to PulseAudio):
sudo /usr/sbin/audiod-takeover.sh # chmod -x profile.d/pipewire.{sh,csh}
# + Hidden=true on the XDG autostart entriesNote: upgrading the
pipewirepackage can reinstall an executableprofile.d/pipewire.sh. If you useinstall-new, your disabled copy is kept and the new one arrives as.new(no conflict). If in doubt after a pipewire upgrade, re-runaudiod-takeover.sh.
Try it in the foreground first (safe: stopping audiod does not stop audio):
sudo sed -i 's/^DEBUG=no/DEBUG=yes/' /etc/audiod/audiod.conf
sudo /usr/sbin/audiod # watch the log; Ctrl-C to stopOnce happy, enable it at boot — it must start after elogind:
sudo chmod +x /etc/rc.d/rc.audiod
# add to /etc/rc.d/rc.local, after elogind is up:
# [ -x /etc/rc.d/rc.audiod ] && /etc/rc.d/rc.audiod startControl it like any Slackware service:
/etc/rc.d/rc.audiod {start|stop|restart|status}/etc/audiod/audiod.conf — core keys:
| Key | Default | Meaning |
|---|---|---|
AUDIO_SERVER |
auto |
auto detects PipeWire vs Pulse; force with pipewire/pulse. In pulse mode audiod is idle. |
MANAGE_DBUS |
no |
no: never spawn a session bus (desktops bring their own; console basic audio needs none). yes: spawn one at $XDG_RUNTIME_DIR/bus if, after a short wait, none appears. |
BUS_WAIT |
3 |
Seconds to wait for a desktop bus before spawning one (when MANAGE_DBUS=yes). |
SEAT_WAIT |
5 |
Seconds to wait at login for a valid seat session before deciding the login is not seated. |
PIPEWIRE_WAIT |
10 |
Seconds to wait for the PipeWire socket before starting dependent services. |
DEBUG |
no |
Verbose logging to syslog (tag audiod) and stderr in foreground. |
Hub keys (all inert unless HUB_MODE=yes) are documented in
Party hub mode below.
/etc/audiod/stack.conf — the ordered stack (name binary ready), read by
both audiod and audioctl. ready = socket:<name> waits for
$XDG_RUNTIME_DIR/<name> before the next service; - means no wait.
Run as your own user (no root, no logout). It respects the same audio-server gate, so in PulseAudio mode it declines.
audioctl status # show your stack (dbus + the three services)
audioctl start # start the stack, ordered
audioctl stop # stop the stack, targeted
audioctl restart # restart the whole stack, ordered
audioctl restart wireplumber # restart a single componentThis is the easy, discoverable way to do "my sound is stuck, fix it" without a
logout — it wraps the same daemon(1) supervisors the stock setup already uses.
The audioctl hub ... subcommands are covered in
Party hub mode.
audiod never blocks this. Use Slackware's own switch
(pipewire-disable.sh), which sets PulseAudio autospawn on; audiod then
detects Pulse mode and stays idle automatically. Or set AUDIO_SERVER=pulse
in audiod.conf. To fully restore the stock PipeWire start mechanism, run
audiod-restore.sh.
sudo /etc/rc.d/rc.audiod stop # (or Ctrl-C if running in foreground)
sudo /usr/sbin/audiod-restore.sh # re-enable the stock autostart
sudo removepkg audiod # removes every packaged file (tracked)
# then remove the rc.local line you added, if anyNothing runtime survives a reboot (it lives on tmpfs). Edited *.conf files
may remain as harmless leftovers.
-
Nothing starts on login. Confirm elogind is up and
pam_elogindis in the login PAM stack (loginctl list-sessionsshould show your session). Runaudiodin the foreground withDEBUG=yesand log in on another VT to watch the events. -
audiodsays "PulseAudio mode". The system is set to Pulse. Switch withpipewire-enable.shor setAUDIO_SERVER=pipewire. -
A second, unused session bus appears. Ensure you are on build 4+ (adds a liveness check so stale/zombie bus sockets from earlier runs are ignored). For desktop systems keep
MANAGE_DBUS=no. -
Sound is "Dummy Output" with two desktops open at once. Expected — see Limitations. Only the active session gets the card.
-
audioctl restartseems to do nothing. Check you are in PipeWire mode and that~/.runholds the pidfiles (ls ~/.run). -
Audio works, but the hub is missing. Almost always means
pactlis talking to a different server than WirePlumber. A real PulseAudio daemon andpipewire-pulseboth want/run/user/$uid/pulse/native, and whoever binds it first owns everypactlcall — sohub_combinedgets created inside PulseAudio whilewpctlandcoppwrnever see it:pactl info | grep -i 'server name' # want: PulseAudio (on PipeWire x.y.z) pgrep -a pulseaudio # want: nothing
Two things cause it, and
audiod-doctor.shfixes both:/etc/xdg/autostart/pulseaudio.desktopreinstalled by a package upgrade.- A per-user
~/.config/pulse/client.conf. libpulse uses the per-user file instead of/etc/pulse/client.conf— it does not merge them — so a system-wideautospawn = nosilently stops applying. A file containing nothing but a commented-out line is enough to do this.
-
Nothing starts at boot, but
rc.audiod restartfixes it. Checkloginctl show-user <uid> -p Linger. With linger enabled the user object exists from boot, so logging in raisesSessionNewand neverUserNew. Handled since this version; if you see it, an olderaudiodis installed (grep -c SessionNew /usr/sbin/audiodshould be non-zero). -
After a pipewire/pulseaudio upgrade. Run
sudo audiod-doctor.sh. Use--checkfirst if you want to see what it would change.
- Why elogind's
UserNew/UserRemovedand not pollingloginctl? They are edge-triggered and carry the uid directly; elogind already does the per-user session reference counting, so we get correct first-login / last-logout semantics for free. Verified empirically: a second login on another VT does not re-fireUserNew. - Why not use the new elogind userdb/varlink features? They are identity only (users/groups) and expose no session-bus or service-control surface; the login1 D-Bus interface (core logind, present in every elogind) is what we need. Verified: elogind does not track the session bus anywhere.
- Why
setprivand notsu/runuserlogin mode?setprivchanges identity without PAM, so it does not open a second elogind session that would corrupt the refcount audiod relies on. - Why inspect
/proc/$pid/environfor the bus? Desktops started viadbus-run-sessionput the bus in/tmpand advertise it only throughDBUS_SESSION_BUS_ADDRESSin the session environment — elogind has no idea where it is, so the environment is the only source of truth. - Why
SessionNewas well asUserNew? Because withenable-lingerthe user object predates every login, soUserNewnever fires for the login you care about. Sessions are the event that actually correlates with "someone is now sitting at this machine". - Why reconcile on every stream reconnect?
gdbus monitorcan drop, and a login during the gap would otherwise be invisible forever. Reconcile is cheap and idempotent, so running it on each reconnect is strictly better than trusting a single pass at startup. - Why a watcher for
pipewire-pulserather than a config drop-in? Apactl-loaded module lives in the server process. Putting the combine sink in a config file instead would fix the lifetime but lose the runtime control (audioctl hub combine [off]) and the ability to enumerate sinks at the moment of creation. The watcher keeps both. - Why a liveness check on the standard-path bus? A stale socket left at
/run/user/$uid/busby a dead daemon would otherwise be mistaken for a live bus;fuserconfirms something is actually serving it.
- Target case (works, verified): one user per seat — the overwhelming
majority of real usage, and exactly the forum bug this addresses. Ordered
start, readiness gating, per-user lifecycle, clean teardown, bus reuse: all
confirmed working, including on a bare console (
Type=tty) session. - Two simultaneous active desktops on one seat/one card: the inactive session's PipeWire falls back to Dummy Output because the active session holds the hardware. This is inherent to shared-seat audio and matches systemd behaviour; it is not something audiod can or should change.
startxsessions stayType=ttyin elogind (the X server runs inside the console session). audiod starts the stack at console-login time, so audio is ready before the desktop; whether the desktop's later session bus is picked up depends on how it is launched.- Kernel/hardware-level enumeration delays are outside audiod's control; it makes WirePlumber wait correctly rather than time out.
audiod has an optional hub extension that turns the machine into a party
audio hub. It is off by default (HUB_MODE=no) and, when off, changes
nothing — audiod behaves exactly as described above. Turn it on only when you
want the box to accept Bluetooth phones, take audio from trusted LAN machines,
or mirror playback to several outputs. Gamers: see GAMER-GUIDE.md for a
task-oriented walkthrough.
- Bluetooth speaker. Phones pair (with the normal PIN) and play through the box's speakers, mixed by PipeWire. Paired phones are trusted so BlueZ reconnects them on its own.
- Terminal scan + connect.
audioctl hub scandiscovers nearby devices (speakers and phones), shows a numbered list with each device's state (connected / paired / new), and lets you connect — or disconnect — by number, so you don't need the desktop's Bluetooth menu. - Media control from the box.
audioctl hub play|pause|next|prevdrives the connected phone over AVRCP, so you can control music from the keyboard without touching someone's phone. - Combine output (sound everywhere). Mirror playback to several outputs at
once — built-in speakers, every HDMI output, and any Bluetooth speaker — via a
hub_combinedsink that becomes the default. HDMI is included on purpose (gamers/AV setups want it). - Network audio (optional). Trusted LAN machines can stream to the box.
- VT-switch watcher. Stops Bluetooth cleanly on VT switch so the speakers never "drone"; opt-in auto-resume when you return.
- pipewire-pulse watcher.
hub_combinedand the network-audio module are loaded intopipewire-pulseand die with it. Its supervisor then restarts the service, so the stack looks perfectly healthy afterwards while the hub is silently gone — and no login1 signal is raised, so nothing would rebuild it.hub-pulsewatch.shnotices the server was replaced and re-applies the owner's extras. It respectsaudioctl hub combine off.
Only members of HUB_GROUP (default audiohub) get hub behaviour; every other
user is ignored entirely, so hub mode never touches non-members' sessions.
groupadd audiohub # once (Slackware: by hand)
gpasswd -a <user> audiohub # add each member, then re-login
# in /etc/audiod/audiod.conf: HUB_MODE=yesIf the group doesn't exist, nobody is allowed (safe default). audiohub is just
a permission label for the hub — it is not the system audio group.
The card and the single system-wide Bluetooth adapter can only be held by one
user at a time. The first hub member to log in automatically becomes the
owner (recorded in /run/audiod-hub-owner, cleared on reboot); later members
are kept from grabbing the shared adapter. You normally set nothing.
HUB_OWNER=<user> is an optional override to pin ownership regardless of login
order.
Pairing is on-demand and bounded — audioctl hub pair makes the box
discoverable for a short window (default 120s) that you trigger, and pairing
still requires the normal BlueZ PIN/confirmation. There is no blind
auto-accept, and the box is not left discoverable permanently.
Switching to another VT makes the owner's session inactive, and the card/BT
handoff would otherwise cut the phone's A2DP stream mid-buffer — the speakers
then repeat the last buffer as a loud drone. The per-owner watcher
(hub-btwatch.sh) listens to elogind for active-session changes and, when the
owner leaves the active VT, pauses the phone and suspends the BT stream
cleanly so there is no drone. Plain speaker/HDMI audio is unaffected.
Resuming when you switch back is opt-in, because auto-resume can make the box start music on its own:
HUB_BT_AUTORESUME=no— reconnect the phone on return (defaultno).HUB_BT_AUTOPLAY=no— also send AVRCP play on reconnect (defaultno).
With both no, you resume manually with audioctl hub reconnect or by pressing
play. (Some phones block remote resume regardless.)
Deny-by-default and per-user. NET_TCP=yes alone does nothing; you must also
list explicit addresses in NET_ACL (e.g. 192.168.1.0/24). Anyone allowed in
can also see that user's microphone and monitors, so only allow machines you
trust.
HUB_MODE=no # master switch; yes = enable hub
HUB_GROUP=audiohub # only members of this group get the hub
HUB_OWNER= # empty = first login wins; or pin a username
BT_PAIR_SECONDS=120 # length of the 'audioctl hub pair' window
HUB_BT_AUTORESUME=no # reconnect BT when you switch back to your VT
HUB_BT_AUTOPLAY=no # also send AVRCP play on reconnect (needs AUTORESUME)
NET_TCP=no # network audio in (per-user)
NET_ACL= # REQUIRED allow-list, e.g. 192.168.1.0/24 (empty=off)
NET_PORT=4713
COMBINE=no # mirror playback to several outputs, made default sink
COMBINE_SLAVES= # empty = ALL sinks (Speaker+HDMI+BT); or a,b to restrict
COMBINE_WAIT=15 # seconds to wait for a real sink before creating combine
HUB_PULSEWATCH_ENABLE=yes # rebuild hub extras when pipewire-pulse restarts
HUB_PULSEWATCH_INTERVAL=5 # how often the watcher checks (seconds)
man audiod, man audiod
audioctl hub status # HUB_MODE, your role (owner/guest), BT/net/combine
audioctl hub scan [seconds] # scan + connect/disconnect a device by number
audioctl hub pair [seconds] # open a bounded Bluetooth pairing window (PIN)
audioctl hub play|pause|next|prev # control the connected phone (AVRCP)
audioctl hub reconnect # reconnect BT to you (manual recovery)
audioctl hub combine [off] # create (or remove) the combine sink
audioctl hub net # (re)apply network audio from the config
- It does not leave Bluetooth discoverable permanently, and never auto-accepts unknown devices (PIN/confirm always required).
- It does not open network audio without an explicit allow-list.
- It does not give non-members any hub access.
- It does not manage bluetoothd or the system bus.
- It does not make two different local users share one card at once (kernel/ALSA
limit; see the separate
audioshareproject for host+guests).
- On VT switch, plain speaker/HDMI audio recovers on its own, but Bluetooth resume is best-effort and can depend on the phone (some block remote play).
audioctl hub scanonly finds devices that are actively advertising; a smartwatch already bonded to a phone, for example, won't appear unless you put it into pairing mode.- The combine sink is static. It mirrors to the sinks that existed when it
was created. Connect a Bluetooth speaker afterwards and it will not join
the mix on its own — re-create it with
audioctl hub combine offfollowed byaudioctl hub combine. (Making it follow new sinks dynamically needs the nativemodule-combine-streamwithstream.rulesrather than the PulseAudio compatibility module; not done yet.) - A microphone near loud speakers will howl. Combine plays the same audio
from the built-in speakers and any Bluetooth speaker, and a laptop's
built-in mic array sits centimetres from those speakers. Recording while the
hub is loud gives acoustic feedback — that is physics, not a bug. Use
headphones for monitoring, or load
module-echo-cancel.
Built for Slackware-current. elogind unmodified; no systemd.
