Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Elgato Wave:3 (0fd9:0070) Linux fixes

git clone https://github.com/Nakildias/elgato-wave3-linux && cd elgato-wave3-linux && sudo bash ./install.sh

Three 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.


Installation

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.

One line

git clone https://github.com/Nakildias/elgato-wave3-linux && cd elgato-wave3-linux && sudo bash ./install.sh

Run 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.

If it says REBOOT REQUIRED

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.

What gets installed

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.

Uninstall

sudo ./uninstall.sh

Removes 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.

Verify

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 empty

scripts/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.


How it works

Everything below was verified against the real sound/usb tree and against the device on the wire with usbmon not from memory.

The bug

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.c snd_usb_clock_find_source() returns -EINVAL for UAC_VERSION_1.

  • stream.c int clock = 0; is only ever assigned from bCSourceID, a UAC2/3 terminal field. Every UAC1 format ends up with fp->clock == 0.

  • endpoint.c the decisive line:

    if (fp->protocol != UAC_VERSION_1) {
            ep->clock_ref = clock_ref_find(chip, fp->clock);

    For UAC1, ep->clock_ref is never created.

With clock_ref NULL, both guards are dead code:

  • update_clock_ref_rate() returns at if (!clock || ...), so the "Mismatched sample rate" path can never fire.
  • hw_rule_rate() misses all three constraints endpoint numbers differ (0x01 vs 0x82), implicit_fb is false (EP 0x81 is explicit feedback, bmAttributes 0x11), and snd_usb_endpoint_get_clock_rate() opens with if (!clock) return 0;. It falls through to fp->rate_table and 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.

The fix

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.

Verified results

/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.

Important: the device wedges, and only a power cycle recovers it

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

Device quirk: capture must be opened before playback

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.

The fix: userspace/51-wave3.conf

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 wireplumber

node.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's priority.driver. Both perturb which node PipeWire opens first, and the microphone loses its head start priority.driver = 1000 broke 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.

Files

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

License

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.

About

A Quirk/Config that makes the Elgato Wave 3 Microphone work correctly on Linux.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages