git clone https://github.com/Nakildias/elgato-wave3-linux && cd elgato-wave3-linux && sudo bash ./install.shThree separate problems with the Elgato Wave:3 on Linux, and the fix for each:
| Symptom | Cause | Fix |
|---|---|---|
usb_set_interface failed (-110), snd-usb-audio stuck in D-state, PipeWire frozen, device dead in both directions until USB power is cycled |
UAC1 has no clock source unit, so the driver never arbitrates sample rate between the two endpoints and lets one be reprogrammed under a live stream | kernel patch (DKMS) |
| Microphone records digital silence whenever anything is playing to the device | device firmware: its ADC only initialises if the capture stream started while playback was not running | WirePlumber rule |
| Playback stutters in PipeWire's Pro Audio profile | the sink runs as a follower with a buffer too small for the graph quantum, and starves | PipeWire quantum floor |
Only the first is a kernel bug. The other two are device and configuration behaviour that no kernel patch can fix.
Everything here was verified against the real sound/usb tree and against the
device on the wire with usbmon not from memory.
Supported: Arch Linux, Fedora, Debian/Ubuntu.
Not supported: atomic / image-based distros (Silverblue, Kinoite, Bazzite,
SteamOS, openSUSE MicroOS). The installer detects those and refuses rather
than half-working DKMS there needs rpm-ostree layering or a systemd
sysext. The two userspace config files still work on those systems if you copy
them in by hand.
git clone https://github.com/Nakildias/elgato-wave3-linux && cd elgato-wave3-linux && sudo bash ./install.shRun it from your normal user account with sudo not as root directly, since
it installs config into your home directory.
The installer pulls its own build dependencies:
| Distro | Packages |
|---|---|
| Arch | dkms, <kernelbase>-headers, base-devel, curl, python, tar, xz, usbutils |
| Fedora | dkms, kernel-devel-$(uname -r), gcc, make, curl, python3, xz, tar, usbutils, elfutils-libelf-devel |
| Debian/Ubuntu | dkms, linux-headers-$(uname -r), build-essential, curl, python3, xz-utils, usbutils |
On Arch the headers package is derived from /usr/lib/modules/$(uname -r)/pkgbase,
so linux-lts, linux-zen and friends get the right one. pacman is called
without -y on purpose: pacman -Sy <pkg> is a partial upgrade and can
break an Arch system. If a package is not found, the script tells you to run
sudo pacman -Syu yourself and re-run.
Kernel headers only match the kernel you are actually running. If your distro just installed headers for a newer kernel than the one booted, the module cannot be built yet. The installer detects this, tells you, and still applies the microphone and stutter fixes those need no kernel build. Reboot and re-run to get the lockup fix too.
| Part | Fixes | Where |
|---|---|---|
| kernel patch (DKMS) | usb_set_interface -110 lockup |
/usr/src/snd-usb-audio-wave3-1.0 |
51-wave3.conf |
mic silent whenever playback is active | ~/.config/wireplumber/wireplumber.conf.d/ |
51-wave3-quantum.conf |
Pro Audio playback stutter | ~/.config/pipewire/pipewire.conf.d/ |
The three parts are independent. If the kernel build fails, the other two are still installed, because they are what fix the audible day-to-day problems.
The installer verifies at the end: patched module loaded, capture stream held open, and a real 8-second recording with a non-zero peak.
sudo ./uninstall.shRemoves the DKMS module and both config files, then reloads the audio stack and prints a health check.
It will not leave you without a USB audio driver: it checks that a stock
snd-usb-audio actually exists before unloading the patched one, and if none
is found it removes only the config and says so. Build dependencies
(dkms, headers, gcc) are deliberately left installed other modules on
the system may need them.
grep -E "Rates:|Channel map:" /proc/asound/card2/stream0 # both rates, maps present
arecord -D hw:Wave3 -f S24_3LE -c1 -r48000 -d 3 /tmp/t.wav
# arbitration: with playback live, capture must offer only the running rate
aplay -D hw:Wave3 -f S24_3LE -c2 -r48000 -d 8 /dev/zero &
arecord -D hw:Wave3 --dump-hw-params -d 1 /dev/null 2>&1 | grep ^RATE
dmesg | grep -iE "cannot set freq|-110|usb_set_interface" # must be emptyscripts/control-test.sh swaps in the stock module DKMS saved aside and
records with both, for A/B comparison. It refuses to run if snd_usb_audio
has a non-zero refcount, because a silently-failed module swap invalidates the
whole test.
Everything below was verified against the real sound/usb tree and
against the device on the wire with usbmon not from memory.
The Wave:3 is UAC1 (bcdADC 1.00), full speed, and runs both converters
from one clock domain:
iface 0 alt 0 AudioControl, feature units 5 (headphone) and 6 (mic)
iface 1 alt 1 playback 2ch S24_3LE EP 0x01 OUT async (+ EP 0x81 IN feedback)
iface 2 alt 1 capture 1ch S24_3LE EP 0x82 IN async
Both AS endpoints carry their own UAC1 sampling-frequency control and both
advertise {48000, 96000}. But UAC1 has no Clock Source unit, so the driver
cannot know the two are linked and its entire cross-endpoint rate
arbitration is gated on exactly that:
-
clock.csnd_usb_clock_find_source()returns-EINVALforUAC_VERSION_1. -
stream.cint clock = 0;is only ever assigned frombCSourceID, a UAC2/3 terminal field. Every UAC1 format ends up withfp->clock == 0. -
endpoint.cthe decisive line:if (fp->protocol != UAC_VERSION_1) { ep->clock_ref = clock_ref_find(chip, fp->clock);
For UAC1,
ep->clock_refis never created.
With clock_ref NULL, both guards are dead code:
update_clock_ref_rate()returns atif (!clock || ...), so the"Mismatched sample rate"path can never fire.hw_rule_rate()misses all three constraints endpoint numbers differ (0x01 vs 0x82),implicit_fbis false (EP 0x81 is explicit feedback,bmAttributes 0x11), andsnd_usb_endpoint_get_clock_rate()opens withif (!clock) return 0;. It falls through tofp->rate_tableand advertises both rates.
So userspace opens capture at 96 kHz while playback streams at 48 kHz.
set_sample_rate_v1() issues a SET_CUR on EP 0x82 asking the firmware to
re-lock the clock under a live endpoint; it stops answering control transfers,
the next usb_set_interface() burns USB_CTRL_SET_TIMEOUT and returns -110
with chip->mutex held, and the whole card wedges.
Add QUIRK_FLAG_UAC1_SHARED_CLOCK, which tags a device's UAC1 audioformats
with a synthetic non-zero clock ID so they share one snd_usb_clock_ref and
join the same arbitration as UAC2/3. Four small changes:
| File | Change |
|---|---|
usbaudio.h |
QUIRK_TYPE_UAC1_SHARED_CLOCK = 30, the flag macro, UAC1_SHARED_CLOCK_ID 0xff, docs |
stream.c |
in the UAC1 parse branch, clock = UAC1_SHARED_CLOCK_ID when the flag is set |
endpoint.c |
if (fp->protocol != UAC_VERSION_1 || fp->clock) let those formats get a clock_ref |
quirks.c |
name-table entry (index must match the enum) + the DEVICE_FLG |
Parsed UAC1 formats always leave fp->clock == 0, so this is inert for
every other device in the tree.
The result: the second stream to open is constrained to the rate already
running, and init_sample_rate() skips the now-redundant SET_CUR via
clock->need_setup. Crucially the descriptor parser is untouched, so chmap,
fmt_bits, maxpacksize and both sample rates are all preserved.
/proc/asound/card2/stream0 is byte-identical to stock Rates: 48000, 96000,
Bits: 24, Channel map: FL FR / MONO.
Arbitration, observed live:
playback running at 48k -> capture offers RATE: 48000
nothing running -> capture offers RATE: [48000 96000]
A 96 kHz capture request against live 48 kHz playback is clamped by the hw_params rule instead of being sent to the hardware.
30 mixed-rate full-duplex open/close cycles, liveness-checked every 5:
fixed-endpoint approach (see attic/) |
this patch | |
|---|---|---|
cannot set freq |
5 / 20 | 0 / 30 |
-110 / timeouts |
0 | 0 |
| D-state | 0 | 0 |
| Wedges | 0 | 0 |
| 96 kHz available | no | yes |
End to end: raw ALSA capture, playback and PipeWire capture (1.9 MB) all work,
with profile switching clean in the kernel log. Pro Audio input needs
userspace/51-wave3.conf as well see below; that is a device ordering
quirk, not a driver issue.
The one Mismatched sample rate 48000 vs 96000 for EP 0x82 seen under stress
is update_clock_ref_rate() working clamping a racing request instead of
reprogramming live hardware. That guard was unreachable before this patch.
If the Wave:3 has already been wedged (by a pre-patch -110, or by either
dead end in attic/), nothing will work until you unplug it. Neither a
reboot nor USBDEVFS_RESET helps both leave VBUS on. Pull the cable for
~30 s.
Wedged state is unambiguous on the wire: the device still enumerates and ACKs
SET_INTERFACE and SET_CUR, but returns zero-length packets on every audio
IN endpoint, so capture and the 3.5 mm jack go silent together. Check the
feedback endpoint with usbmon:
EP 0x81 descriptor 0:0:0 -> wedged 0:0:3 -> alive
The Wave:3's ADC only initialises correctly if its capture interface is enabled while the playback interface is not already streaming. Verified on raw ALSA with PipeWire stopped, and identical on the stock in-tree driver, so this is device firmware behaviour that no kernel quirk can fix:
| Sequence | Result |
|---|---|
| capture alone | audio |
| capture already running, playback starts | audio survives indefinitely |
| playback already streaming, capture opened | digital silence |
| playback stops, capture reopened | audio again |
When it fails, 144000 frames/3 s still arrive on time and the kernel log is
clean the device sends full-size packets of zeros. The mic feature unit
reports [on] at +40.00dB throughout, re-asserting it changes nothing, and
playback content is irrelevant (silence kills it exactly like a tone). This
is distinct from the wedged state above, where packets are zero length.
This is what made PipeWire's Pro Audio profile look broken: Pro Audio keeps source and sink open together, so playback was always already streaming when capture opened. The other profiles only worked by luck, because PipeWire suspends idle nodes and capture usually ended up running alone.
Keep the capture node permanently processing, so it is never the second stream:
mkdir -p ~/.config/wireplumber/wireplumber.conf.d
cp userspace/51-wave3.conf ~/.config/wireplumber/wireplumber.conf.d/
systemctl --user restart wireplumbernode.always-process = true is the load-bearing property.
session.suspend-timeout-seconds = 0 alone is not sufficient it only
prevents suspension after activation and never brings the node up. With
always-process the ALSA capture PCM sits at state: RUNNING from startup.
Two things that do not work, tested and discarded:
QUIRK_FLAG_IFACE_SKIP_CLOSE(kernel side). The interface does stay at Altset 1, and capture is still silent so the altsetting reset is not the trigger. Stopping and restarting the isochronous URB stream is.- Adding rules for the output node (
api.alsa.period-size,api.alsa.headroom) or lowering the capture node'spriority.driver. Both perturb which node PipeWire opens first, and the microphone loses its head startpriority.driver = 1000broke the mic even with no playback at all. Keep this file to the capture node only.
Confirmed working by the user in the Pro Audio profile: microphone works and playback is smooth. Also verified here across repeated profile switches, with the Wave:3 as default sink, after a full PipeWire/WirePlumber restart, and after USB re-enumeration.
install.sh / uninstall.sh one-shot install and revert (start here)
quirk/0001-...patch the kernel patch
userspace/51-wave3.conf WirePlumber rule -- mic in full duplex
userspace/51-wave3-quantum.conf PipeWire quantum -- Pro Audio stutter
scripts/apply-wave3.py anchored applier, checks flag/name indices agree
scripts/prepare-src.sh fetch + stage + patch sound/usb
scripts/control-test.sh stock vs patched A/B, with a refcount guard
scripts/clean.sh remove generated src/ and the tarball cache
dkms/dkms.conf DKMS build recipe
attic/ superseded attempts + dead ends worth not repeating
GPL-2.0. See LICENSE.
The kernel files the patch modifies carry SPDX-License-Identifier: GPL-2.0-or-later upstream; the patch does not change those headers, so those
files keep their own terms. Everything original in this repository the
installer, the scripts and the two config files is GPL-2.0,
Copyright (C) 2026 Nakildias nakildiaspro@gmail.com.