Bug 295933 - uaudio(4): idle capture stream clobbers active playback sample rate on devices with a shared UAC2 Clock Source - device unusable
Summary: uaudio(4): idle capture stream clobbers active playback sample rate on devic...
Status: Open
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: 15.0-RELEASE
Hardware: Any Any
: --- Affects Only Me
Assignee: freebsd-multimedia (Nobody)
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2026-06-08 12:04 UTC by delleceste
Modified: 2026-07-15 11:42 UTC (History)
3 users (show)

See Also:


Attachments
proposed patch, to audit and review by the maintainers (4.59 KB, patch)
2026-07-07 09:58 UTC, delleceste
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description delleceste 2026-06-08 12:04:11 UTC
## 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.

---
Comment 1 delleceste 2026-06-08 12:05:16 UTC
## Appendix — raw evidence

### FreeBSD `dmesg` (with `hw.usb.uaudio.debug=15`), 44.1 kHz playback start
```
uaudio_configure_msg_sub: fps=8000 sample_rem=4100          # 44100 % 8000 = 4100  (playback)
uaudio20_set_speed: ifaceno=0 clockid=41 speed=44100
uaudio20_set_speed: ifaceno=0 clockid=41 speed=48000
uaudio_configure_msg_sub: fps=8000 sample_rem=0             # 48000 % 8000 = 0      (capture default)
uaudio_chan_play_sync_callback: Value = 0x0006000a
uaudio_chan_play_sync_callback: Comparing 48001 Hz :: 44100 Hz
```

### FreeBSD `sndstat` (`hw.snd.verbose=2`), 44.1 kHz playback
```
[dsp0.play.0]:   spd 44100, fmt 0x00201000, flags 0x2000014c
                 interrupts 3863, underruns 0, feed 3862, ready 119784
                 channel flags=0x2000014c<RUNNING,TRIGGERED,NBIO,BUSY,BITPERFECT>
                 {userland} -> feeder_root(0x00201000) -> {hardware}
[dsp0.record.0]: spd 48000, fmt 0x00200010/0x00201000, flags 0x00000000
```

### Linux `/proc/asound/card0/stream0` (same device, 44.1 family plays fine)
```
Playback:
  Status: Running
    Interface = 1, Altset = 1
    Format: S32_LE, Channels: 2
    Endpoint: 0x01 (1 OUT) (ASYNC)
    Rates: 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000
    Sync Endpoint: 0x81 (1 IN)          # explicit Q16.16 feedback
Capture:
  Status: Stop
    Interface 2 ...                     # capture interface present but idle
```

### FreeBSD enumeration (single programmable clock feeds both directions)
```
uaudio0: Play[0]:   384000 / ... / 48000 / 44100 Hz, 2 ch, 32-bit S-LE PCM
uaudio0: Record[0]: 384000 / ... / 48000 / 44100 Hz, 2 ch, 32-bit S-LE PCM
```
Comment 2 delleceste 2026-06-22 09:04:00 UTC
When the digital room correction chain is (re)started (/dev/dsp0 opened at a different sample frequency), the very same DAC (OKTO DAC8) frequently opens the stream but routes no audio on the first open. The front panel shows "play" and the host sees a healthy, running stream (USB async feedback present, no underruns), yet nothing comes out of the speakers. Opening the device a second time at the same rate fixes it.


Test environment (live, this box)

-

DAC: 	OKTO RESEARCH DAC8 STEREO (pcm0: <OKTO RESEARCH DAC8STEREO> (play) default)
Host OS: 	FreeBSD 15.1 (GENERIC, amd64), uaudio(4)
Device node: 	/dev/dsp0, single-open: dev.pcm.0.bitperfect: 1, dev.pcm.0.play.vchans: 0
Async feedback (idle): 	dev.pcm.0.feedback_rate: 48001
hw.usb.uaudio.buffer_ms 	8 (already the maximum)

Player: 	MPD, OSS output to /dev/dsp0 (direct) or /dev/dsp.play (DRC)
DRC (Digital room correction) chain: 	MPD → /dev/dsp.play → virtual_oss → /dev/dsp.loop → brutefir → /dev/dsp0
Comment 3 Christos Margiolis freebsd_committer freebsd_triage 2026-06-27 14:12:58 UTC
As a first step, does the issue persist if you set the rate to 44.1 kHz for both directions?

# sndctl play.rate=44100 rec.rate=44100
Comment 4 Christos Margiolis freebsd_committer freebsd_triage 2026-06-27 14:14:38 UTC
(In reply to Christos Margiolis from comment #3)
Since you are using bitperfect, you can also you can set the following sysctl:

# sysctl hw.usb.uaudio.default_rate=44100
Comment 5 delleceste 2026-06-27 19:58:38 UTC
 Thanks Christos — I tested both suggestions. Short version: both make the
  device lock at 44.1 kHz with no flicker.

  (1) sndctl play.rate=44100 rec.rate=44100  (comment #3)
      This is only settable outside bitperfect: with bitperfect on, vchans are 0
      so play.rate/rec.rate write dev.pcm.0.{play,rec}.vchanrate, which don't
      exist / aren't writable. So I enabled vchans and disabled bitperfect, then
      as root:
          # sysctl dev.pcm.0.play.vchans=1
          # sysctl dev.pcm.0.rec.vchans=1
          # sndctl play.rate=44100 rec.rate=44100
      (vchanrate is root-only, and rec needs its own vchan or the rec half EPERMs.)
      Result: locks at 44.1 kHz, stable, no flicker.

  (2) bitperfect + hw.usb.uaudio.default_rate=44100  (comment #4)
          # sysctl hw.usb.uaudio.default_rate=44100
      then power-cycled the DAC so uaudio re-reads it at attach (setting it on an
      already-attached device had no effect on its own).
      Result: locks at 44.1 kHz, stable, no flicker; play.rate and rec.rate both
      read 44100.

  Interpretation:
  The DAC8 STEREO (0x152a:0x88c5) exposes a single UAC2 Clock Source shared
  between the playback (AS interface 1) and the capture (AS interface 2)
  interfaces. The 44.1 kHz-family flicker comes from the idle capture side
  defaulting to 48 kHz and reprogramming that shared clock out from under the
  active 44.1 kHz playback stream. Both workarounds remove the conflict simply by
  making the capture side use the same 44.1 kHz rate, so nothing reprograms the
  clock. This is consistent with our earlier finding that dropping the (vestigial
  — the DAC8 has no analog inputs) capture interface also fixes it.

  Related observation (format rather than rate):
  In bitperfect at 44.1 kHz a 16-bit source is shown by the DAC as 24-bit.
  uaudio exposes only s32le for playback (play.formats: s32le), although AS
  interface 1 declares native 16- and 24-bit alt-settings (FORMAT_TYPE_I):
      Alt1: subslot 4 / 24-bit
      Alt2: subslot 2 / 16-bit
      Alt3,4: subslot 4 / 32-bit
  Linux snd-usb-audio switches alt-setting per stream and shows 16-bit for 16-bit
  content. (hw.usb.uaudio.default_bits forces a depth, but it's a global,
  attach-time choice.) I mention it because it looks like the same underlying
  behaviour: uaudio fixes one (rate, bits) at attach instead of following the
  active stream / the shared clock.

  Happy to test patches or provide the full descriptor dump and debug traces.
Comment 6 delleceste 2026-07-06 11:13:29 UTC
hi Christos.
any news?
let me know when something is ready for testing or if you need more information
Comment 7 Mark Linimon freebsd_committer freebsd_triage 2026-07-06 21:36:07 UTC
^Triage: assign.

Also, please, no longer Cc: hselasky@:

https://forums.freebsd.org/threads/in-memoriam-hans-petter-william-sirevag-selasky.89697/
Comment 8 delleceste 2026-07-07 09:58:06 UTC
Created attachment 272568 [details]
proposed patch, to audit and review by the maintainers
Comment 9 Alex S 2026-07-07 11:52:36 UTC
Can we stick to the basics? A human-written description of the issue, reproduction steps and so on.
Comment 10 delleceste 2026-07-07 12:26:14 UTC
 I am a software engineer but not a kernel developer and I am trying to do the best I can. I could as well have ignored the idsue and go on using what I have been so far, but I really want to contribute the best I can. 
what I've done so far has already taken quite a lot of time, because this is not the only issue in the stack.
I'll soon write a summary of, and a report after further testing.
if the patch works, regession testing and definitive integration will be on your side. 

respectfully
Comment 11 Christos Margiolis freebsd_committer freebsd_triage 2026-07-07 17:14:50 UTC
(In reply to delleceste from comment #8)
Thank you for this. I can also test it on my USB card, although I'm not sure if it executes those code paths. In any case, to make reviewing easier, please submit this patch on https://reviews.freebsd.org/, or FreeBSD's GitHub.
Comment 12 delleceste 2026-07-15 09:57:49 UTC
(In reply to Christos Margiolis from comment #11)
Thanks Christos. 
I registered to phabricator but it is not clear to me how to contribute there. There is an (apparently empty) audio group under Projects.

As to FreeBSD's GitHub, do you mean opening a pull request?

In the meantime, I have been successfully using the device for some days with the provided patch.
Comment 13 delleceste 2026-07-15 09:59:39 UTC
(In reply to Mark Linimon from comment #7)
very sorry for that. I hope my new comments are not CC-ing hselasky anymore
Comment 14 delleceste 2026-07-15 11:42:04 UTC
(In reply to Christos Margiolis from comment #11)

I've prepared the change as a pull request against freebsd/freebsd-src.
It's the same change as attachment 272568 [details], on a branch off main, committed with a PR: 295933 trailer so it links back here.

https://github.com/freebsd/freebsd-src/pull/2323

thank you