https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=295933
Bug ID: 295933
Summary: uaudio(4): idle capture stream clobbers active
playback sample rate on devices with a shared UAC2
Clock Source - device unusable
Product: Base System
Version: 15.0-RELEASE
Hardware: Any
OS: Any
Status: New
Severity: Affects Only Me
Priority: ---
Component: kern
Assignee: [email protected]
Reporter: [email protected]
## TL;DR
On a UAC2 device that exposes **one Clock Source entity shared between its
playback and capture interfaces**, `uaudio(4)` programs that clock for *both*
directions. When playback uses a sample rate from the **44.1 kHz family**
(44100 / 88200 / 176400 / 352800 Hz) and the (idle, non‑streaming) capture
channel is configured for a **48 kHz‑family** default, the capture channel's
`SET_CUR(CUR_SAM_FREQ)` is issued **after** the playback one and **overwrites
it on the shared clock**. The device's converter then runs at ~48 kHz while the
host streams 44.1 kHz data → continuous input‑FIFO underrun inside the device →
the DAC repeatedly drops and re‑acquires USB streaming lock (audible dropouts;
front‑panel "play/idle" flicker several times per second).
* The **48 kHz family is unaffected** — both directions then request 48000, so
there is no conflict on the shared clock.
* The **same device works perfectly under Linux** (`snd-usb-audio`), which does
not let an idle capture stream reprogram the clock used by active playback.
* A device with **no capture interface** (Cambridge Audio DacMagic 100) never
shows the problem on the same FreeBSD host.
The host side is verifiably healthy throughout (`underruns 0`, channel stays
`RUNNING`); the fault is the **wrong clock rate programmed into the device**,
not
data starvation.
---
## Affected component and environment
| Item | Value |
|------|-------|
| Driver | `sys/dev/sound/usb/uaudio.c` (`uaudio(4)` / `snd_uaudio.ko`) |
| OS (reproduced on) | FreeBSD **15.1‑RC1**, amd64, `GENERIC`
(`releng/15.1-n283533`) |
| Expected to affect | All branches; the code paths below are long‑standing |
| Host controller | Intel Sunrise Point‑LP xHCI, USB High‑Speed (480 Mbps) |
| Reference player | MPD 0.24.12, OSS output to `/dev/dsp0`,
`dev.pcm.0.bitperfect=1` |
### Device used to reproduce
| Item | Value |
|------|-------|
| Device | OKTO RESEARCH **DAC8 STEREO** (D/A only — no analog inputs) |
| USB IDs | idVendor `0x152a`, idProduct `0x88c5`, bcdDevice `0x0160` |
| Firmware | Thesycon UAC2 (`bInterfaceProtocol 0x20`), `bcdUSB 0x0200` |
| Link | USB High‑Speed |
> Note: although the DAC8 has **no physical inputs**, its USB descriptor still
> advertises a UAC2 **capture interface** (Interface 2) — common boilerplate in
> Thesycon/XMOS reference firmware. It is this *vestigial, never‑streaming*
> capture interface that triggers the bug.
## Root cause — measured
### 1. Two `SET_CUR` calls to the shared clock, capture wins
With `sysctl hw.usb.uaudio.debug=15` (GENERIC builds `options USB_DEBUG`), at
the
start of 44.1 kHz playback:
```
uaudio_chan_set_param_speed: Selecting alt 6
uaudio_chan_set_param_speed: Selecting alt 7
uaudio20_set_speed: ifaceno=0 clockid=41 speed=44100 <-- playback sets
shared clock to 44100
uaudio20_set_speed: ifaceno=0 clockid=41 speed=48000 <-- capture default
48000 then overwrites it
```
Same `clockid=41`. The second call wins; the device clock ends at 48 kHz.
### 2. The capture channel is idle, yet it owns the clock
`cat /dev/sndstat` with `hw.snd.verbose=2`, during 44.1 kHz playback:
```
[dsp0.play.0]: spd 44100, fmt 0x00201000, flags
...<RUNNING,TRIGGERED,NBIO,BUSY,BITPERFECT>
interrupts <climbing>, underruns 0, feed <climbing>, ready
<near full>
[dsp0.record.0]: spd 48000, fmt 0x00200010/0x00201000, flags 0x00000000 <--
NOT running, but holds 48000
```
The record channel has flags `0x00000000` (not `RUNNING`) — it is not streaming
a single byte — yet its 48000 Hz configuration is what sits on the shared
clock.
### 3. The device confirms it is running at 48 kHz, not 44.1 kHz
The playback OUT endpoint is asynchronous with an explicit feedback IN endpoint
(`0x81`, Q16.16 at High‑Speed). While the host streams 44100:
```
uaudio_chan_play_sync_callback: Value = 0x0006000a
uaudio_chan_play_sync_callback: Comparing 48001 Hz :: 44100 Hz
```
`0x0006000a` in 16.16 = 6.0001 samples/microframe × 8000 = **~48001 Hz**. The
device's converter is clocked at 48 kHz while being fed 44.1 kHz frames → it
drains ~3900 samples/s faster than it is filled → underrun/mute/relock cycle.
### 4. It is a host/driver problem, not the device firmware
* The **same device, cable and host play the 44.1 kHz family perfectly under
Linux** (`snd-usb-audio`). Linux programs the shared clock from the active
playback stream and does not let the idle capture stream override it.
* A DAC with **no capture interface** (DacMagic 100) is stable at every rate on
the same FreeBSD host.
---
## Relevant code paths
Line numbers are approximate, from FreeBSD **15.1‑RC1**
`sys/dev/sound/usb/uaudio.c`.
1. **`uaudio_configure_msg()` (~line 1539)** — runs on the USB explore task and
unconditionally reconfigures *both* directions of every child:
```c
for (i = 0; i != UAUDIO_MAX_CHILD; i++) {
uaudio_configure_msg_sub(sc, &sc->sc_play_chan[i], PCMDIR_PLAY);
uaudio_configure_msg_sub(sc, &sc->sc_rec_chan[i], PCMDIR_REC);
}
```
2. **`uaudio_configure_msg_sub()` (~line 1352)** — on `CHAN_OP_START` it
selects
the alt setting and then, for UAC2, programs the sample rate by iterating
every clock id flagged for the channel's direction and calling
`uaudio20_set_speed()` (~line 1449):
```c
} else if (sc->sc_audio_rev >= UAUDIO_VERSION_20) {
for (x = 0; x != 256; x++) {
if (dir == PCMDIR_PLAY) {
if (!(sc->sc_mixer_clocks.bit_output[x/8] & (1u << (x%8))))
continue;
} else {
if (!(sc->sc_mixer_clocks.bit_input[x/8] & (1u << (x%8))))
continue;
}
if (uaudio20_set_speed(sc->sc_udev, sc->sc_mixer_iface_no, x,
chan_alt->sample_rate))
DPRINTF("setting of sample rate failed! (continuing anyway)\n");
}
}
```
Because clock id 41 is set in **both** `bit_output` and `bit_input`
(shared clock — see `uaudio20_mixer_find_clocks_sub()` ~line 4858), the REC
pass reprograms the very clock the PLAY pass just set.
3. **Asynchronous feedback handling — secondary issue.** In
`uaudio_chan_play_sync_callback()` (~line 2255) the explicit feedback
endpoint
value is only turned into a rate correction when there is **no** capture
channel (`if (ch->priv_sc->sc_rec_chan[i].num_alt == 0)`, ~line 2306), and
the
feedback transfer is not even submitted when a capture channel exists
(~line 2332). For a device whose capture interface never streams, this means
the explicit feedback endpoint is ignored, so there is no closed‑loop
correction to mask a mis‑programmed clock either.
---
## Proposed fix (direction)
**Primary — do not let an idle/secondary direction reprogram a clock that is
shared with the active streaming direction.** The active stream must own the
shared Clock Source. Concretely, in the UAC2 rate‑setting loop of
`uaudio_configure_msg_sub()`, before issuing `SET_CUR` to a clock id, skip it
if
that clock id is also referenced by the *other* direction's channel that is
currently `RUNNING` at a different rate. Equivalently: only program a clock for
a
channel that is the one actually transitioning to `RUNNING`, and never let a
non‑running channel push its default rate onto a clock another running channel
depends on.
UAC2 constraint to respect: a single Clock Source physically cannot serve two
different rates simultaneously, so when both directions stream concurrently
they
must already agree on one rate; that concurrent case needs its own coherent
handling (e.g. the second stream adopts the first's rate). The common failure
mode here, however, is purely an **idle** capture stream forcing its default —
which should never override active playback.
**Secondary (optional, matches Linux behaviour).** Consider honouring the
device's explicit asynchronous feedback endpoint even when a capture interface
is present, rather than only when `sc_rec_chan[i].num_alt == 0`. Not required
if
the clock is programmed correctly, but it brings `uaudio` in line with
`snd-usb-audio` for async devices.
### Diagnostic stopgap used to confirm the hypothesis (NOT proposed for
upstream)
To verify the diagnosis, the reporter dropped the capture channels for this one
device, right after `uaudio_chan_fill_info()` in `uaudio_attach()`:
```c
/* local diagnostic only — confirms the shared-clock hypothesis */
if (uaa->info.idVendor == 0x152a && uaa->info.idProduct == 0x88c5) {
for (i = 0; i != UAUDIO_MAX_CHILD; i++)
sc->sc_rec_chan[i].num_alt = 0;
}
```
Result: the OKTO then enumerates play‑only, the shared clock follows playback,
and **44.1 kHz locks cleanly and bit‑perfect** (`underruns 0`, gap‑free, stable
front panel). This is offered only as confirmation of the root cause; the real
fix should be the general clock‑ownership change above, which preserves capture
for devices that genuinely record.
---
## Reproduction
1. FreeBSD 15.1‑RC1, `uaudio(4)`, a UAC2 device whose playback and capture
interfaces share one Clock Source, on a High‑Speed port.
2. Play bit‑perfect audio at any **44.1 kHz‑family** rate to `/dev/dsp0`
(e.g. MPD OSS output, `dev.pcm.0.bitperfect=1`, no rate enforcement).
→ device drops/re‑acquires lock continuously; `sndstat` shows the play
channel `RUNNING` with `underruns 0`, and the record channel idle at a
48 kHz‑family rate.
3. Play any **48 kHz‑family** rate → stable.
4. Same machine/cable/port under Linux (`snd-usb-audio`) → 44.1 kHz plays
perfectly.
---
--
You are receiving this mail because:
You are the assignee for the bug.