The Linux audio stack consists of multiple layers working together:
┌─────────────────────────────────────────────────────────────┐
│ HOST SYSTEM │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ User Applications │ │
│ │ (Browser, Media Player, Voice Assistant, etc.) │ │
│ └────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌────────────────▼────────────────────────────────────┐ │
│ │ PipeWire / PulseAudio │ │
│ │ (Sound Server) │ │
│ │ - Audio mixing & routing │ │
│ │ - Device abstraction │ │
│ │ - Network audio support │ │
│ │ - Per-application volume control │ │
│ └────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌────────────────▼────────────────────────────────────┐ │
│ │ ALSA │ │
│ │ (Advanced Linux Sound Architecture) │ │
│ │ - Kernel-level audio driver framework │ │
│ │ - Direct hardware access │ │
│ │ - Controls: /dev/snd/ │ │
│ └────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌────────────────▼────────────────────────────────────┐ │
│ │ Hardware (Sound Card) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────────────────┐ │ │
│ │ │Microphon│ │ Speaker │ │ DSP/Codec │ │ │
│ │ └─────────┘ └─────────┘ └───────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ LVA_XDG_RUNTIME_DIR=/run/user/1000 │
│ └── Socket: /run/user/1000/pipewire-0 │
│ Socket: /run/user/1000/pulse/native │
└─────────────────────────────────────────────────────────────┘
│
│ Device Passthrough
▼
┌─────────────────────────────────────────────────────────────┐
│ DOCKER CONTAINER │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Container Process │ │
│ │ (Linux Voice Assistant) │ │
│ │ │ │
│ │ Accesses audio via: │ │
│ │ - PipeWire/PulseAudio client library │ │
│ │ - Uses same LVA_XDG_RUNTIME_DIR socket path │ │
│ │ │ │
│ │ Environment Variables: │ │
│ │ - LVA_XDG_RUNTIME_DIR=/run/user/1000 (host path) │ │
│ │ - AUDIO_INPUT_DEVICE="default" │ │
│ │ - AUDIO_OUTPUT_DEVICE="default" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Device Access: │
│ - Socket: /run/user/1000/pipewire-0 (volume mount) │
│ - Socket: /run/user/1000/pulse/native (volume mount) │
└─────────────────────────────────────────────────────────────┘
-
Host Layer:
- ALSA provides kernel drivers for audio hardware
- PipeWire/PulseAudio sits on top of ALSA as a sound server
- Applications connect to PipeWire/PulseAudio, not directly to ALSA, otherwhise the container would need access to the ALSA devices. If you use ALSA only only one application can connect to the device at a time.
-
Docker Integration:
- The LVA_XDG_RUNTIME_DIR socket must be mounted from host to container
- The container user must have matching UID/GID with host user or the audio group is added.
-
Communication Flow:
Container App → PipeWire Client Library → Host PipeWire/PulseAudio ↓ ↓ (via mounted socket) (via ALSA drivers) ↓ ↓ ┌─────────────────────────────────────────────────────┐ │ Audio Data flows to Hardware │ └─────────────────────────────────────────────────────┘ -
Key Configuration Points:
LVA_XDG_RUNTIME_DIRmust point to host's runtime directory- Container user UID/GID should match host user
services:
voice-assistant:
image: ghcr.io/ohf-voice/linux-voice-assistant:latest
environment:
- LVA_XDG_RUNTIME_DIR=/run/user/1000
- AUDIO_INPUT_DEVICE=default
- AUDIO_OUTPUT_DEVICE=default
volumes:
- /run/user/1000:/run/user/1000:rw
devices:
- /dev/snd:/dev/snd
group_add:
- audioTo list available audio devices, run:
# List audio devices
docker run --rm -it -e LIST_DEVICES="1" ghcr.io/florian-asche/linux-voice-assistant:develop-final-docker-versionUpdate the .env file with your device names:
AUDIO_INPUT_DEVICE="default"
AUDIO_OUTPUT_DEVICE="default"If you work with the root user, you need to:
export LVA_XDG_RUNTIME_DIR=/run/user/${USERLVA_USER_ID💡 Note: Replace `$USERLVA_USER_IDth your actual user id that you want to run the voice assistant.
If the container cannot access audio devices, ensure:
- The USERLVA_USER_ID
.envmatches your actual users ID (runid -u $USERto check) - The LVA_XDG_RUNTIME_DIR path exists on the host
tbd
If you experience audio quality issues:
- Check the audio device settings with
alsamixer - Try setting
AUDIO_INPUT_DEVICEandAUDIO_OUTPUT_DEVICEexplicitly - Adjust the microphone gain in your system settings
Wake word arbitration settles which satellite answers when several of them hear
the same wake word: the device that heard it loudest responds, the rest stand
down. When two devices both answer the same utterance, the usual cause is a
wake word name mismatch — claims match on the configured wake word string,
not on the audio, so HAL on one device and Hal V2 on another arbitrate as
two different words and both answer.
To diagnose, enable the Wake Arbitration switch on each device's HA device
page, say the wake word once, and read each device's log (script/run --debug,
or journalctl --user -u linux-voice-assistant -f):
# Won: this device answers. An empty "heard" list means no same-name claim arrived.
INFO:linux_voice_assistant.wake_arbitration:Wake arbitration: answering at 13.8 dB
# Lost: this device stayed silent, the named peer answered. Arbitration is working.
INFO:linux_voice_assistant.wake_arbitration:Wake arbitration: 192.168.1.32 answers instead (34.4 dB), this device heard 13.8 dB; heard 192.168.1.32 34.4 dB at -55 ms
If every device logs answering at with an empty heard list while more than
one device woke, the devices are claiming different names. Fix the name on
either side so all devices claim the same string (matching is case-insensitive
with collapsed whitespace, so HAL and hal are the same; hal and
hal v2 are not):
- Model manifest
.jsonfiles — LVA reads each model'swake_wordentry from its.jsonmanifest, and Kiosk Satellite reads the same field for MicroWakeWord models. Edit thewake_wordvalue so it matches the other devices. .tflitefile names — Kiosk Satellite OpenWakeWord models ship no manifest, so the kiosk derives the phrase from the file name:hal_v2.tflitebecomes "Hal V2" and claimshal v2. Rename the file so its stem matches the other devices' phrase (a file namedhal.tfliteclaimshal), upload it to the kiosk, and re-select the model.- Repeat the wake test: exactly one device should log
answering atand every other device should loganswers instead(or stay silent if it never detected the wake).
See docs/wake-word-arbitration.md for the wire protocol and the window/offset entities.
See PiCompose for more debugging information.