Re: [PATCH] ALSA: usb-audio: Check sticky mixers precisely

Rong Zhang <[email protected]>
Newsgroups org.kernel.vger.linux-sound,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Hi Alexander,

On Sun, 2026-08-16 at 16:09 +0200, Alexander Niemeyer wrote:
> Hi Rong,
> 
> Sure. The libusb tests were direct USB Audio Class 1 control transfers 
> to the headset using libusb/PyUSB, not ALSA mixer operations.
> 
> I accessed Feature Unit 2 on AudioControl interface 0, master channel 0, 
> with the UAC1 Volume control selector:
> 
> wValue = 0x0200 /* Volume control, master channel */
> wIndex = 0x0200 /* Feature Unit 2, interface 0 */
> 
> I used the standard class-specific requests directly, including GET_CUR, 
> GET_MIN, GET_MAX, GET_RES and SET_CUR, with signed 16-bit little-endian 
> volume values in 1/256 dB units.
> 
> The device reported:
> 
> GET_CUR: 0 ( 0 dB in that test)
> GET_MIN: -16384 (-64 dB)
> GET_MAX: 0 ( 0 dB)
> GET_RES: 256 ( 1 dB)
> 
> For the timing tests I issued SET_CUR for a target value and then 
> repeatedly queried GET_CUR until the value changed or the timeout expired.
> 
> Valid values became visible after roughly:
> 
> -1 dB ~81 ms
> -2 dB ~52 ms
> -4 dB ~47 ms
> -8 dB ~47 ms
> -16 dB ~52 ms
> -32 dB ~47 ms
> 
> The advertised -64 dB minimum behaved differently: SET_CUR returned 
> successfully, but GET_CUR did not change even after 1000 ms.
> 
> To access the AudioControl interface with libusb, I unbound the 
> AudioControl interface from snd-usb-audio for the duration of the test.
> 
> I did not intentionally open a playback stream during those libusb 
> tests. Because the AudioControl interface had been unbound from 
> snd-usb-audio, I also do not believe there was an active ALSA playback 
> stream at that point.

Thanks for the information.

Unfortunately, I still don't exactly see why the device behaved
differently when GET_CUR/SET_CUR requests were sent from snd-usb-audio
compared to your libusb tests.

snd-usb-audio also tries SET_RES to test the sanity of GET_RES. Could you
test if it breaks your device's GET_CUR? 

Maybe comparing them with usbmon can show some clues. You can use
Wireshark to sniff /dev/usbmon*. 

Hint: a Thunderbolt port usually corresponds to a dedicated USB root hub.
If you have one, plug the device to it to get pure usbmon trace results
with no noisy URBs from other devices.

Thanks,
Rong

> 
> If the open-stream state is important, I can repeat the experiment 
> specifically controlling for playback-stream-open versus 
> playback-stream-closed.
> 
> Thanks,
> Alexander
> 
> Am 16.08.2026 um 15:50 schrieb Rong Zhang:
> > Hi Alexander,
> > 
> > On Sun, 2026-08-16 at 07:14 +0200, Alexander Niemeyer wrote:
> > > Hi Rong,
> > > 
> > > I tested the sticky-check part of your patch on the Logitech PRO X
> > > Wireless (046d:0aba) on Fedora 44, kernel 7.1.8-200.fc44.x86_64.
> > > 
> > > Since your patch is based on a newer tree, I used a minimal backport of
> > > the new ~16-value / 10 ms sticky-check logic to the 7.1.8 code. The
> > > GET_CUR-broken handling from the newer tree was not included; GET_CUR
> > > itself succeeds on this device.
> > > 
> > > Unfortunately, the playback control is still classified as sticky:
> > > 
> > > 2:0: sticky mixer values (-16384/0/256 => -3840), disabling
> > > 
> > > I then instrumented the check and tried an additional diagnostic: after
> > > every successful SET_CUR, wait 100 ms and perform another GET_CUR before
> > > issuing the next SET_CUR.
> > > 
> > > For the playback volume, the saved value was -3840 and GET_CUR remained
> > > at -3840 for every tested value, even after 100 ms, for example:
> > > 
> > > test=-15104 immediate=-3840 after100ms=-3840
> > > test=-13824 immediate=-3840 after100ms=-3840
> > > test=-3584 immediate=-3840 after100ms=-3840
> > > test=-2304 immediate=-3840 after100ms=-3840
> > > test=-1024 immediate=-3840 after100ms=-3840
> > > test=0 immediate=-3840 after100ms=-3840
> > > 
> > > So in this case the issue does not appear to be simply that the
> > > accumulated 10 ms sleeps are too short. During the probe-time sticky
> > > check, SET_CUR succeeds but GET_CUR for the playback control remains
> > > unchanged even when each SET_CUR is given 100 ms before the next one.
> > > 
> > > This differs from my previous direct libusb tests with the AudioControl
> > > interface unbound, where valid SET_CUR values became visible through
> > > GET_CUR after roughly 47–81 ms.
> > Really interesting. Maybe the mixer changes its value only when there is
> > an opened playback stream.
> > 
> > Could you clarify your "libusb tests"?
> > 
> > Thanks,
> > Rong
> > 
> > > The first debug line I saw with |saved=0| was from the Mic Capture
> > > Volume control; that control changed immediately and returned as
> > > non-sticky. The sequence above with |saved=-3840| is the problematic PCM
> > > Playback Volume control.
> > > 
> > > I'd be happy to test another version or run additional diagnostics if
> > > useful.
> > > 
> > > Best regards,
> > > Alexander
> > > 
> > > 
> > > Am 15.08.2026 um 23:47 schrieb Rong Zhang:
> > > > Some mixers are asynchronous, and some have broken min/max. They are
> > > > mistakenly considered sticky due to how the check is implemented.
> > > > 
> > > > Check sticky mixers more precisely by checking approximately 16 values
> > > > and adding a msleep(10) between each check, so that asynchronous mixers
> > > > have enough time to change the value and mixers with broken min/max are
> > > > checked properly. Additionally, mark GET_CUR as broken when
> > > > get_cur_mix_raw() fails, instead of returning successfully.
> > > > 
> > > > Reported-by: Alexander Niemeyer<[email protected]>
> > > > Closes:https://lore.kernel.org/r/[email protected]
> > > > Signed-off-by: Rong Zhang<[email protected]>
> > > > ---
> > > >    sound/usb/mixer.c | 51 ++++++++++++++++++++++++++++++++++++++++++++-------
> > > >    1 file changed, 44 insertions(+), 7 deletions(-)
> > > > 
> > > > diff --git a/sound/usb/mixer.c b/sound/usb/mixer.c
> > > > index 703c118f9d4e..3d0f97730a06 100644
> > > > --- a/sound/usb/mixer.c
> > > > +++ b/sound/usb/mixer.c
> > > > @@ -1256,22 +1256,59 @@ static void init_cur_mix_raw(struct usb_mixer_elem_info *cval, int ch, int idx)
> > > >    static int check_sticky_volume_control(struct usb_mixer_elem_info *cval,
> > > >    				       int channel, int saved)
> > > >    {
> > > > -	int sticky_test_values[] = { cval->min, cval->max };
> > > > -	int test, check, i;
> > > > +	int test, check, res;
> > > > +
> > > > +	/*
> > > > +	 * Check approximately 16 values (15 intervals).
> > > > +	 * If the resolution is not fine enough, check fewer values.
> > > > +	 */
> > > > +	res = DIV_ROUND_UP(cval->max - cval->min, 15);
> > > > +	res = res ? roundup(res, cval->res) : cval->res;
> > > > +
> > > > +	/*
> > > > +	 * If (cval->max - cval->min) is not a multiple of cval->res, we still
> > > > +	 * want to test cval->max anyway.
> > > > +	 */
> > > > +	for (test = cval->min; test < cval->max + res; test += res) {
> > > > +		if (test > cval->max)
> > > > +			test = cval->max;
> > > >    
> > > > -	for (i = 0; i < ARRAY_SIZE(sticky_test_values); i++) {
> > > > -		test = sticky_test_values[i];
> > > >    		if (test == saved)
> > > >    			continue;
> > > >    
> > > >    		/* Assume non-sticky on failure. */
> > > > -		if (snd_usb_set_cur_mix_value(cval, channel, 0, test) ||
> > > > -		    get_cur_mix_raw(cval, channel, &check) ||
> > > > -		    check != saved) /* SET_CUR effective, non-sticky. */
> > > > +		if (snd_usb_set_cur_mix_value(cval, channel, 0, test))
> > > > +			return 0;
> > > > +
> > > > +		if (get_cur_mix_raw(cval, channel, &check))
> > > > +			goto get_cur_broken;
> > > > +		if (check != saved) /* SET_CUR effective, non-sticky. */
> > > >    			return 0;
> > > > +
> > > > +		/*
> > > > +		 * Leave some time for asynchronous mixers to change the value.
> > > > +		 *
> > > > +		 * Note that there is no need to wait between SET_CUR and
> > > > +		 * GET_CUR, as we don't care whether the GET_CUR value matches
> > > > +		 * the SET_CUR one. IOW, what we expect is just a GET_CUR value
> > > > +		 * differing from the saved one.
> > > > +		 *
> > > > +		 * Mixers of most devices are synchronous. The should have
> > > > +		 * returned early without extra sleep. Asynchronous mixers will
> > > > +		 * return once the accumulated time is enough for them to change
> > > > +		 * the value.
> > > > +		 */
> > > > +		msleep(10);
> > > >    	}
> > > >    
> > > > +	/* Check again after the last msleep(). */
> > > > +	if (get_cur_mix_raw(cval, channel, &check))
> > > > +		goto get_cur_broken;
> > > > +	if (check != saved)
> > > > +		return 0;
> > > > +
> > > >    	if (cval->head.mixer->chip->quirk_flags & QUIRK_FLAG_MIXER_GET_CUR_BROKEN) {
> > > > +get_cur_broken:
> > > >    		usb_audio_info(cval->head.mixer->chip,
> > > >    			       "%d:%d: broken mixer GET_CUR (%d/%d/%d => %d)\n",
> > > >    			       cval->head.id, mixer_ctrl_intf(cval->head.mixer),
> > > > 
> > > > ---
> > > > base-commit: 3eb40771c00a8488fa6ed2cc1fe203477908bf38
> > > > change-id: 74676fce-uac-precise-sticky-check-94474a22b57d
> > > > 
> > > > Thanks,
> > > > Rong
> > > >
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.