Linux control application for Elgato Wave audio devices — the Wave XLR microphone interface and the Wave:3 microphone. A reverse-engineered replacement for Elgato Wave Link, built with GTK4 + Adwaita.
| Device | USB ID | Controls |
|---|---|---|
| Wave XLR | 0fd9:007d |
Gain, mute, headphone volume, low impedance mode |
| Wave:3 | 0fd9:0070 |
Gain, mute, headphone volume, monitor mix |
- Microphone controls — Gain, mute (syncs with hardware button)
- Headphone controls — Volume (syncs with hardware knob), low impedance mode
- Hardware sync — 10 Hz polling keeps the app in sync with physical controls
- System integration — Mute and HP volume sync bidirectionally with PipeWire/ALSA
- Audio capture fix — Background daemon (systemd or runit) prevents the firmware race condition where mic goes silent
- System tray — Runs in background with tray icon, mute from tray menu
- First-run setup — Configures udev permissions and audio service automatically
Wave devices use USB Class control transfers on endpoint 0 for device configuration. On Linux, snd-usb-audio normally blocks these transfers because wIndex=0x3300 routes through interface 0 (owned by the audio driver). OpenWave uses wIndex=0x3303 instead — the firmware only checks the 0x33 prefix, while the kernel sees interface 3 (unclaimed) and lets the transfer through. No driver detach needed, audio is never interrupted.
Both devices speak the same vendor protocol (bRequest 0x85 read / 0x05 write) but with different config layouts: the Wave XLR uses a 34-byte block (gain uint16 @0, mute @4, HP volume int16 Q8.8 @9, knob mode @14, low-Z @33), the Wave:3 a 16-byte block (gain uint16 Q8.8 dB @0, mute @4, HP volume int16 Q8.8 @7, monitor mix uint16 Q8.8 percent @10, dial mode @12 — 1=gain, 2=headphones, 3=mix). Per-model constants live in wavexlr/profiles.py; python3 -m wavexlr.probe (dump / watch / poke) verifies a device against its profile and helps map new fields. The device services vendor transfers from only one process at a time, so quit OpenWave before probing.
One-liner — detects Arch, Debian/Ubuntu, Fedora, openSUSE, or Void; installs deps and OpenWave:
curl -fsSL https://raw.githubusercontent.com/rikkichy/openwave/main/install.sh | shOr from a checkout:
git clone https://github.com/rikkichy/openwave.git
cd openwave
./install.sh # default PREFIX=/usr/local
PREFIX=/usr ./install.sh # for packaging-style layoutUninstall:
sudo make -C /path/to/openwave uninstall PREFIX=/usr/local- Python 3.10+
- GTK4, libadwaita
- PipeWire (for audio capture fix)
- libusb 1.0
openwave # if installed via install.sh / PKGBUILD
python3 -m wavexlr # from a checkout, no install neededOn first launch, OpenWave will prompt to set up USB permissions (via polkit) and install the audio service.
OpenWave detects your init system at runtime:
-
systemd — the GUI installs a user unit at
~/.config/systemd/user/openwave.serviceand enables it. No root needed for install or status checks. -
runit (Artix, Void, Devuan-runit) — the GUI cannot install the system service itself (writing to
/etc/svrequires root). Create awavexlr-audioservice directory at/etc/sv/wavexlr-audio/whoserunscript execspython3 -m wavexlr.daemonas your user (typically viachpst -u), then enable it withln -s /etc/sv/wavexlr-audio /var/service/.Status detection from the non-root GUI uses
sv check; on stock Void the supervise FIFO is mode 0700, so OpenWave falls back to scanning/procfor the daemon process. -
other (macOS, Windows, no init detected) — the capture-fix section is disabled.
Start hidden in tray
python3 -m wavexlr --hidecp /usr/share/openwave/openwave-autostart.desktop ~/.config/autostart/Copy wavexlr.desktop to ~/.local/share/applications/ for app launcher integration.
wavexlr/
device.py — USB backend (raw libusb via ctypes, wIndex=0x3303 trick)
profiles.py — per-model protocol constants and capabilities
probe.py — vendor protocol verification CLI (dump / watch / poke)
app.py — GTK4/Adwaita UI with 10Hz polling
tray.py — StatusNotifierItem tray icon via D-Bus
audio.py — PipeWire capture keepalive (fixes firmware race condition)
daemon.py — Systemd service entry point
setup.py — First-run udev + systemd setup
USB protocol reverse-engineered from the macOS Wave Link application using Frida. Inspired by GoXLR-on-Linux/goxlr-utility.
MIT