Linux USB
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Ismaïl Bahloul" <i.bahloul01@gmail.com>, linux-sound@vger.kernel.org
Cc: linux-usb@vger.kernel.org
Subject: Re: [PATCH v7 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls
Date: Tue, 27 Oct 2026 11:58:18 +0000	[thread overview]
Message-ID: <sashiko-outbox-186491@kernel.org> (raw)
In-Reply-To: <20261027113140.134257-7-i.bahloul01@gmail.com>

Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: MIX wheel stuck due to raw fader quantization
- [High] ALSA: usb: babyfacepro: unlocked panel_select races in bf_panel_tick()
- [High] ALSA: usb: babyfacepro: unchecked snd_ctl_new1() NULL dereference
- [Medium] ALSA: usb: babyfacepro: broken SELECT handling in bf_panel_mix_wheel()
- [Medium] ALSA: usb: babyfacepro: panel_saw_fader reset clobbered in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: snd_card_register() called before panel init
- [Medium] ALSA: usb: babyfacepro: panel_start reset on resume breaks select put
- [Medium] ALSA: usb: babyfacepro: tick-based SELECT hold in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: OUT wheel and resync overwrite active Phones DIM state
- [Medium] ALSA: usb: babyfacepro: stale panel_master_last after master updates
- [Low] ALSA: usb: babyfacepro: undocumented panel_poll_ms and urb_split params
--

commit c4b87dcdef0e3e95b0b69cf856f88b85d3f4ed85
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>

ALSA: usb: babyfacepro: add the front-panel poll and controls

This commit adds periodic polling of the Babyface Pro front-panel state
via vendor request 0x17 and exposes the decoded buttons, rotary wheel,
and channel selection state as mixer controls. It also implements host-driven
handling for output volume, balance, preamp gain, phantom power, and
monitoring fader levels in response to front-panel input.

> diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
> index 39584ec719b23..cf41093dcbcaf 100644
> --- a/sound/usb/babyfacepro/babyfacepro-ctl.c
> +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
> @@ -611,6 +612,7 @@ int babyface_create_xpoints(struct snd_usb_babyface *chip)
>  			err = snd_ctl_add(chip->card, kctl);
>  			if (err < 0)
>  				return err;
> +			chip->xpoint_kctl[out][src] = kctl;
>  		}
>  	}

[Severity: High]
What happens in babyface_create_xpoints() if snd_ctl_new1() returns NULL
on allocation failure?

Before calling snd_ctl_add(chip->card, kctl) (which checks for a NULL
pointer), babyface_create_xpoints() writes directly into kctl->id.name:

    kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
        ...
    }, chip);
    strscpy(kctl->id.name, bf_sources[src].name,
            sizeof(kctl->id.name));
    strlcat(kctl->id.name, " Playback Volume",
            sizeof(kctl->id.name));
    err = snd_ctl_add(chip->card, kctl);

The same pattern also appears in babyface_create_masters() and
babyface_create_trim(), which were introduced earlier in this series and
call strscpy() or strlcat() on kctl->id.name before snd_ctl_add().

Should these functions check for !kctl and return -ENOMEM before modifying
kctl->id.name?

> @@ -834,6 +836,83 @@ static int bf_split_put(struct snd_kcontrol *kctl,
>  	return ret;
>  }
>  
> +/* MIX-mode monitoring level (fader curve).
> + * Calibrated crosspoint-fader curve (AN1->AN1/2, cap_calib.pcap
> + * 2026-08-22; the same table as tuxmix-core/src/usb.rs FADER_CURVE).
> + * dB stored x2 (half-dB grid): the MIX wheel steps +/-0.5 dB per click
> + * on this curve (cap_mix.pcap).  0x0000 = -inf (digital mute),
> + * 0x0003 = -62 dB, ... 0x2D41 = +6 dB.  Raw values interpolate linearly
> + * between the 1-dB points.
> + */
> +#define BF_FADER_DB2_INF	(-130)	/* -65 dB = the wheel's -inf floor */
> +
> +static const struct bf_fader_pt {
> +	s16 db2;	/* dB x 2 */
> +	u16 raw;
> +} bf_fader_curve[] = {
> +	{ -124, 0x0003 }, { -122, 0x0004 }, { -120, 0x0005 },
> +	{ -118, 0x0006 }, { -116, 0x0007 }, { -114, 0x0008 },
> +	{ -112, 0x0009 }, { -110, 0x000a }, { -108, 0x000b },
> +	{ -106, 0x000d }, { -104, 0x000e }, { -102, 0x0010 },
> +	{ -100, 0x0012 }, {  -98, 0x0014 }, {  -96, 0x0017 },
> +	{  -94, 0x0019 }, {  -92, 0x001d }, {  -90, 0x0020 },
> +	{  -88, 0x0024 }, {  -86, 0x0029 }, {  -84, 0x002e },
> +	{  -82, 0x0033 }, {  -80, 0x003a }, {  -78, 0x0041 },
> +	{  -76, 0x0049 }, {  -74, 0x0051 }, {  -72, 0x005b },
> +	{  -70, 0x0067 }, {  -68, 0x0073 }, {  -66, 0x0081 },
> +	{  -64, 0x0091 }, {  -62, 0x00a3 }, {  -60, 0x00b7 },
> +	{  -58, 0x00cd }, {  -56, 0x00e6 }, {  -54, 0x0102 },
> +	{  -52, 0x0122 }, {  -50, 0x0145 }, {  -48, 0x016d },
> +	{  -46, 0x019a }, {  -44, 0x01cc }, {  -42, 0x0204 },
> +	{  -40, 0x0243 }, {  -38, 0x028a }, {  -36, 0x02d9 },
> +	{  -34, 0x0332 }, {  -32, 0x0396 }, {  -30, 0x0406 },
> +	{  -28, 0x0483 }, {  -26, 0x0510 }, {  -24, 0x05af },
> +	{  -22, 0x0660 }, {  -20, 0x0727 }, {  -18, 0x0807 },
> +	{  -16, 0x0902 }, {  -14, 0x0a1b }, {  -12, 0x0b57 },
> +	{  -10, 0x0cb9 }, {   -8, 0x0e47 }, {   -6, 0x1004 },
> +	{   -4, 0x11f9 }, {   -2, 0x142a }, {    0, 0x16a0 },
> +	{    2, 0x1963 }, {    4, 0x1c7c }, {    6, 0x1ff6 },
> +	{    8, 0x23dc }, {   10, 0x283d }, {   12, 0x2d41 },
> +};
> +
> +/* Fader raw -> dBx2 (linear interpolation; raw 0 = -inf). */
> +static int bf_fader_raw_to_db2(u16 raw)
> +{
> +	int i;
> +
> +	if (raw == 0 || raw < bf_fader_curve[0].raw)
> +		return BF_FADER_DB2_INF;
> +	for (i = 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) {
> +		if (raw <= bf_fader_curve[i + 1].raw) {
> +			u32 num = (u32)(raw - bf_fader_curve[i].raw) *
> +				  (u32)(bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2);
> +			u32 den = bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw;
> +
> +			return bf_fader_curve[i].db2 + (int)((num + den / 2) / den);
> +		}
> +	}
> +	return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].db2;
> +}
> +
> +/* dBx2 -> fader raw (linear interpolation; below -62 dB = mute 0). */
> +static u16 bf_fader_db2_to_raw(int db2)
> +{
> +	int i;
> +
> +	if (db2 <= bf_fader_curve[0].db2)
> +		return db2 < bf_fader_curve[0].db2 ? 0 : bf_fader_curve[0].raw;
> +	for (i = 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) {
> +		if (db2 <= bf_fader_curve[i + 1].db2) {
> +			u32 num = (u32)(db2 - bf_fader_curve[i].db2) *
> +				  (u32)(bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw);
> +			u32 den = bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2;
> +
> +			return bf_fader_curve[i].raw + (u16)((num + den / 2) / den);
> +		}
> +	}
> +	return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].raw;
> +}

[Severity: High]
Can the monitoring fader wheel in bf_panel_mix_wheel() get stuck at mute
(raw == 0) when turning up, and stuck at -52 dB (0x000e) or -54 dB (0x000b)
when turning down?

In bf_panel_mix_wheel(), each poll tick converts chip->panel_mix_raw to db2,
adds delta, converts back to raw, and stores only raw in chip->panel_mix_raw:

    db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
    db2 = clamp(db2 + delta, BF_FADER_DB2_INF, 12);
    raw = bf_fader_db2_to_raw(db2);
    chip->panel_mix_raw = raw;

In babyface_write_default_mixer(), all hardware input crosspoints start at
raw = 0. When panel_mix_raw is 0, bf_fader_raw_to_db2(0) returns
BF_FADER_DB2_INF (-130), whereas bf_fader_curve[0].db2 is -124 and
bf_fader_db2_to_raw(db2) returns 0 for all db2 < -124. If a user turns the
wheel clockwise by 1 to 5 clicks within a single 20 ms poll window,
db2 = -130 + delta is in [-129, -125], so bf_fader_db2_to_raw(db2) returns 0
and chip->panel_mix_raw stays 0. On the next poll tick, bf_fader_raw_to_db2(0)
starts from -130 again, discarding the clicks unless at least 6 clicks occur
in a single 20 ms interval.

Similarly, when turning the wheel down by a single click (delta = -1) at any
segment in bf_fader_curve[] where bf_fader_curve[i + 1].raw -
bf_fader_curve[i].raw == 1 across a db2 span of 2 (such as {-106, 0x000d} to
{-104, 0x000e}, and {-124, 0x0003} through {-108, 0x000b}),
db2 = bf_fader_curve[i + 1].db2 - 1 evaluates in bf_fader_db2_to_raw() to:

    bf_fader_curve[i].raw + ((1 + 1) / 2) = bf_fader_curve[i + 1].raw

which rounds back up to the starting raw value and prevents single-click
decrements from moving past those points.

Would tracking the monitoring fader state in half-dB (db2) rather than
re-deriving it from the quantized raw value on every tick avoid losing
half-dB steps?

[ ... ]

> +static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta)
> +{
> +	int out = bf_panel_out_pair(chip);
> +	u8 targets[2];
> +	int n = 0;
> +	int db2;
> +	u16 raw, flag;
> +	int i;
> +
> +	/* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT
> +	 * steps left/right/both; none = nothing selected = no-op wheel).
> +	 * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4.
> +	 */
> +	if (READ_ONCE(chip->panel_in) == 3) {
> +		targets[0] = 4;		/* Opt: the AS1/2 pair */
> +		n = 1;
> +	} else if (READ_ONCE(chip->panel_select) != 3) {
> +		int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
> +
> +		targets[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
> +		n = 1;
> +		if (READ_ONCE(chip->panel_select) == 2)
> +			targets[n++] = base + 1;
> +	} else {
> +		bf_panel_sel_hint(chip);
> +	}
> +
> +	mutex_lock(&chip->mutex);
> +	db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
> +	db2 = clamp(db2 + delta, BF_FADER_DB2_INF, 12);
> +	raw = bf_fader_db2_to_raw(db2);
> +	chip->panel_mix_raw = raw;

[Severity: Medium]
How is channel selection handled in bf_panel_mix_wheel() and bf_panel_tick()
when panel_select is unknown/none or changes while mix mode is active?

There appear to be four related issues here:

1. When panel_in == 3 (Opt), bf_panel_mix_wheel() checks panel_in == 3 before
   checking panel_select != 3, so it modifies targets[0] = 4 even when
   panel_sel[2] ("Front Panel Selection" index 2) is unknown or none (3),
   without calling bf_panel_sel_hint().

2. When panel_in != 3 and panel_select == 3, the else branch calls
   bf_panel_sel_hint(chip) without returning, so bf_panel_mix_wheel() still
   acquires chip->mutex and modifies chip->panel_mix_raw when n == 0.

3. In bf_panel_tick(), chip->panel_mix_raw and BF_REG_PANEL_GAIN are only
   seeded on the mix_flash entry edge:

    out = bf_panel_out_pair(chip);
    ref = READ_ONCE(chip->panel_in) == 3 ? 4 :
          (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) +
          (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
    chip->panel_mix_raw = chip->xpoint[out][ref][0];

   If the user presses the select button (or writes "Front Panel Selection"
   via bf_panel_select_put()) or switches the input or output pair while
   panel_mix is already true, chip->panel_mix_raw is not re-seeded from the
   newly selected channel's crosspoint, causing the next wheel click to jump
   the new channel to the previous channel's level.

4. On that mix_flash entry edge in bf_panel_tick(), when panel_select == 3
   (none/unknown), ref evaluates to base + 0 and writes BF_REG_PANEL_GAIN + ref
   for an unselected channel, whereas when panel_select == 2 (both), only
   base + 0 is written to BF_REG_PANEL_GAIN and base + 1 is omitted.

Should bf_panel_mix_wheel() return early when panel_select == 3 (including
for panel_in == 3), and should the fader reference level and display registers
stay synchronized with the active selection?

[ ... ]

> +/* The wheel's view of one side: its own tracked level while the master
> + * is still what the wheel wrote, else the master's level.
> + */
> +static int bf_out_wheel_level(struct snd_usb_babyface *chip, int out, int ch)
> +{
> +	u16 step = chip->master[out][ch];
> +
> +	if (step == chip->panel_master_last[out][ch])
> +		return chip->panel_out_hdb[out][ch];
> +	if (!step)
> +		return BF_OUT_WHEEL_MUTED;
> +	return step - BF_MASTER_STEP_0DB;
> +}

[Severity: Medium]
Can bf_out_wheel_level() use a stale sub-floor offset from
chip->panel_out_hdb[out][ch] after the master volume is updated by
bf_master_put() or bf_panel_balance_wheel()?

When the output wheel turns an output with a stereo balance offset down so
that the quieter side drops below BF_OUT_WHEEL_FLOOR (-180),
bf_panel_out_wheel() records:

    chip->panel_master_last[out][ch] = 0;
    chip->panel_out_hdb[out][ch] = db[ch]; /* e.g. -220 */

If the user then updates the master volume via bf_master_put() (for example,
setting both channels to 0) or adjusts balance via bf_panel_balance_wheel() ->
bf_panel_write_master(), neither path invalidates chip->panel_master_last[out]
to 0xffff (or clears chip->panel_out_resync). When the output wheel is later
turned up, bf_out_wheel_level() sees step == chip->panel_master_last[out][ch]
(0 == 0) and returns the old sub-floor panel_out_hdb[out][ch] instead of
BF_OUT_WHEEL_MUTED (-181), reintroducing the stale balance offset.

Should bf_master_put() and bf_panel_write_master() reset
chip->panel_master_last[out] to 0xffff?

[ ... ]

> +static void bf_panel_out_resync(struct snd_usb_babyface *chip)
> +{
> +	int out, ch;
> +	int db[2];
> +	u16 flag;
> +
> +	if (!chip->panel_out_resync ||
> +	    ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) <
> +	    BF_OUT_WHEEL_RESYNC_MS)
> +		return;
> +
> +	mutex_lock(&chip->mutex);
> +	chip->panel_out_resync = false;
> +	out = chip->panel_out_wheel_out;
> +	for (ch = 0; ch < 2; ch++) {
> +		if (chip->master[out][ch] != chip->panel_master_last[out][ch])
> +			goto unlock;
> +		db[ch] = chip->panel_out_hdb[out][ch];
> +		if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
> +			goto unlock;
> +	}
> +	if (!chip->muted[out]) {
> +		flag = bf_flag_cycle[chip->flag_cnt];
> +		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
> +		bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[0],
> +				BF_REG_MASTER_8 + 2 * out);
> +		bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[1],
> +				BF_REG_MASTER_8 + 2 * out + 1);
> +		bf_vendor_write(chip, BF_REQ_CROSSPOINT,
> +				bf_master_reg16(chip->master[out][0]),
> +				(BF_REG_MASTER_16 + 2 * out) | flag);
> +		bf_vendor_write(chip, BF_REQ_CROSSPOINT,
> +				bf_master_reg16(chip->master[out][1]),
> +				(BF_REG_MASTER_16 + 2 * out + 1) | flag);
> +	}
> +unlock:
> +	mutex_unlock(&chip->mutex);
> +}

[Severity: Medium]
Does turning the output or balance wheel on Phones (out == 1) while
chip->dim is engaged, or engaging "Dim Switch" within the 150 ms resync
window, overwrite the hardware's -20 dB dim attenuation with the full
un-dimmed volume?

When "Dim Switch" is engaged via bf_dim_put() -> bf_dim_apply(chip, true),
the driver writes the fixed -20 dB values (BF_MASTER_MINUS20_8 and
BF_MASTER_MINUS20_16) to the Phones master registers while leaving
chip->master[1] holding the un-dimmed level.

If a user turns the output wheel on Phones (out == 1) and "Dim Switch" is
then engaged within the 150 ms BF_OUT_WHEEL_RESYNC_MS window,
bf_dim_apply(chip, true) does not change chip->master[1]. When
bf_panel_out_resync() runs 150 ms after the click,
chip->master[1][ch] == chip->panel_master_last[1][ch] and !chip->muted[1]
are still true, so bf_panel_out_resync() writes the un-dimmed 8-bit and
16-bit levels to the hardware, wiping out the -20 dB dim attenuation.

Similarly, in bf_panel_write_master() and bf_panel_out_wheel_write():

    chip->master[out][0] = l;
    chip->master[out][1] = r;
    /* A Phones change while DIM is engaged re-bases the restore. */
    if (chip->dim && out == 1) {
        chip->dim_saved[0] = l;
        chip->dim_saved[1] = r;
    }

both functions update chip->dim_saved when chip->dim && out == 1, but still
write the un-dimmed l and r values to BF_REG_MASTER_8 and BF_REG_MASTER_16
while chip->dim remains true.

Should bf_panel_write_master(), bf_panel_out_wheel_write(), and
bf_panel_out_resync() avoid writing un-dimmed master levels to out == 1 while
chip->dim is active (or clear chip->dim if the wheel is meant to cancel it)?

[ ... ]

> +static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
> +{
> +	u16 bits = 0;
> +	u16 old;
> +	int m;
> +
> +	if (READ_ONCE(chip->panel_mix) || READ_ONCE(chip->panel_in) != 1)
> +		return;
> +	if (READ_ONCE(chip->panel_select) == 3) {
> +		bf_panel_sel_hint(chip);
> +		return;
> +	}
> +	if (READ_ONCE(chip->panel_select) != 1)
> +		bits |= BF_PREAMP_48V_MIC1;
> +	if (READ_ONCE(chip->panel_select) != 0)
> +		bits |= BF_PREAMP_48V_MIC2;
> +
> +	mutex_lock(&chip->mutex);
> +	old = chip->preamp;
> +	/* One channel selected: toggle it.  Both selected: ALIGN both to
> +	 * the same state, so repeated SET presses cycle all-on <-> all-off
> +	 * (a mixed phantom state cannot persist with both selected).
> +	 */
> +	if (READ_ONCE(chip->panel_select) == 2) {
> +		if ((chip->preamp & bits) == bits)
> +			chip->preamp &= ~bits;
> +		else
> +			chip->preamp |= bits;
> +	} else {
> +		chip->preamp ^= bits;
> +	}

[Severity: High]
Can a concurrent write in bf_panel_select_put() race with
bf_panel_set_phantom(), bf_panel_gain_wheel(), and bf_panel_mix_wheel() and
cause +48V phantom power or gain to be applied to the wrong channels?

bf_panel_select_put() updates chip->panel_select_armed,
chip->panel_sel[pair], chip->panel_select_known, and chip->panel_select
(via bf_panel_sel_load()) without taking chip->mutex, while
bf_panel_set_phantom() re-evaluates READ_ONCE(chip->panel_select) four
separate times instead of snapshotting it once under a lock.

If chip->panel_select is 0 or 1 when bf_panel_set_phantom() checks:

    if (READ_ONCE(chip->panel_select) == 3)

and bf_panel_select_put() concurrently sets chip->panel_select to 3
(none/unknown) before the next two reads:

    if (READ_ONCE(chip->panel_select) != 1)
        bits |= BF_PREAMP_48V_MIC1;
    if (READ_ONCE(chip->panel_select) != 0)
        bits |= BF_PREAMP_48V_MIC2;

then both conditions evaluate to true (setting both BF_PREAMP_48V_MIC1 and
BF_PREAMP_48V_MIC2), and READ_ONCE(chip->panel_select) == 2 evaluates to
false, toggling +48V phantom power on both microphone inputs when no channel
is selected.

Similarly, bf_panel_gain_wheel() and bf_panel_mix_wheel() re-read
READ_ONCE(chip->panel_select) multiple times across the != 3, == 1, and == 2
checks, which can select channel 0 if panel_select changes to 3 or push
duplicate channel 1 entries if panel_select changes from 1 to 2.

Should panel_select state updates in bf_panel_select_put() and reads in the
panel worker be serialized (or at least read once into a local variable)?

[ ... ]

> +		if (READ_ONCE(chip->panel_mix))
> +			bf_panel_mix_wheel(chip, delta);
> +		else if (chip->panel_sel_hold >= 10 && cls == 1)
> +			bf_panel_balance_wheel(chip, delta);
> +		else if (cls == 1)
> +			bf_panel_out_wheel(chip, delta);
> +		else if (cls == 2)
> +			bf_panel_gain_wheel(chip, delta);
> +	}
> +	bf_panel_out_resync(chip);
> +
> +	/* Selections - keep the previous when the field is not in range
> +	 * (the fader-mode readback drops the IN position bits).
> +	 */
> +	in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);
> +	if (in && in != READ_ONCE(chip->panel_in)) {
> +		WRITE_ONCE(chip->panel_in, in);
> +		/* The unit keeps one SELECT selection per IN pair, and an
> +		 * IN switch does not clear it (hardware-verified
> +		 * 2026-10-02: back on Ch 1/2 after a trip through Ch 3/4,
> +		 * the first SELECT press showed the L it had left, while
> +		 * Ch 3/4 showed nothing).  Switch to the new pair's.
> +		 */
> +		bf_panel_sel_load(chip);
> +		/* The LEDs go dark on an IN switch: the next SELECT press
> +		 * only shows the selection again (no step), the one after
> +		 * that steps it (user-verified 2026-08-28).
> +		 */
> +		chip->panel_select_armed = false;
> +		bf_panel_notify(chip, BF_PANEL_KCTL_IN);
> +	}
> +	out = bf_panel_out_decode(st[1] & 0x07);
> +	if (out && out != READ_ONCE(chip->panel_out)) {
> +		WRITE_ONCE(chip->panel_out, out);
> +		bf_panel_notify(chip, BF_PANEL_KCTL_OUT);
> +	}
> +
> +	/* SELECT press cycles the channel selection L -> R -> both -> none
> +	 * -> L (manual sec. 5.1).  The state is NOT in the readback
> +	 * (panelprobe 2026-08-24), so it is tracked host-side.
> +	 */
> +	if (st[3] == BF_PANEL_FLASH_SELECT &&
> +	    chip->panel_prev[3] != BF_PANEL_FLASH_SELECT &&
> +	    chip->panel_select_known) {
> +		if (!chip->panel_select_armed) {
> +			/* Disarmed (IN switch since the last step): the press
> +			 * only shows the selection - the device steps it on
> +			 * the NEXT press (user-verified 2026-08-28).
> +			 */
> +			chip->panel_select_armed = true;
> +		} else {
> +			int sel = (READ_ONCE(chip->panel_select) + 1) & 3;
> +			struct snd_kcontrol *kctl =
> +				chip->panel_select_kctl[READ_ONCE(chip->panel_in) - 1];
> +
> +			WRITE_ONCE(chip->panel_select, sel);
> +			chip->panel_sel[READ_ONCE(chip->panel_in) - 1] = sel;
> +			if (kctl)
> +				snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
> +					       &kctl->id);
> +		}
> +	}
> +	/* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output
> +	 * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz
> +	 * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and
> +	 * byte0 does NOT gain the 0x80 engaged bit - so the duration is
> +	 * the only discriminator: >= 10 ticks (200 ms at 50 Hz) = held.
> +	 */
> +	if (st[3] == BF_PANEL_FLASH_SELECT)
> +		chip->panel_sel_hold++;
> +	else
> +		chip->panel_sel_hold = 0;

[Severity: Medium]
Does counting 10 poll ticks in chip->panel_sel_hold accurately measure a
200 ms hold when fast polling is active or when panel_poll_ms is changed?

bf_panel_tick() checks chip->panel_sel_hold >= 10 assuming a fixed 20 ms
poll interval. However, during the 200 ms fast-poll window after an output
wheel click (chip->panel_fast_until), babyface_panel_work() polls every
BF_PANEL_FAST_POLL_MS (5 ms), so 10 ticks elapse in 50 ms (shorter than the
100-150 ms flash of a normal tap). Conversely, if the user sets the
panel_poll_ms module parameter up to 1000 ms, 10 ticks require holding the
button for up to 10 seconds.

In addition, because the rising-edge check:

    if (st[3] == BF_PANEL_FLASH_SELECT &&
        chip->panel_prev[3] != BF_PANEL_FLASH_SELECT &&
        chip->panel_select_known)

runs unconditionally on the first tick of BF_PANEL_FLASH_SELECT, holding
select in output mode to adjust balance also steps chip->panel_select on the
initial press edge.

Would measuring the hold duration with jiffies or ktime_t (and avoiding
stepping the input channel selection on an output-mode balance hold) work
more reliably?

[ ... ]

> +	mix_flash = st[3] == BF_PANEL_FLASH_MIX &&
> +		    chip->panel_prev[3] != BF_PANEL_FLASH_MIX;
> +	fader_now = (st[2] >> 4) == 0x0;
> +
> +	if (mix_flash) {
> +		if (READ_ONCE(chip->panel_mix)) {
> +			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
> +			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
> +			WRITE_ONCE(chip->panel_mix, false);
> +			WRITE_ONCE(chip->panel_saw_fader, false);
> +		} else {
> +			int ref, out;
> +			int m;
> +
> +			bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
> +			WRITE_ONCE(chip->panel_mix, true);

[ ... ]

> +		}
> +		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
> +	}
> +	if (fader_now) {
> +		WRITE_ONCE(chip->panel_saw_fader, true);
> +	} else if (READ_ONCE(chip->panel_mix) && READ_ONCE(chip->panel_saw_fader) &&
> +		   st[3] != BF_PANEL_FLASH_MIX) {
> +		/* device left fader mode by itself (IN/OUT/SET press) */
> +		bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
> +		bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
> +		WRITE_ONCE(chip->panel_mix, false);
> +		WRITE_ONCE(chip->panel_saw_fader, false);
> +		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
> +	}

[Severity: Medium]
When exiting mix mode via a second press of the mix button, doesn't the
subsequent if (fader_now) check immediately overwrite chip->panel_saw_fader
back to true?

While mix mode is engaged, the device is in fader mode, so fader_now
((st[2] >> 4) == 0x0) is true in the readback that reports the second
BF_PANEL_FLASH_MIX press. The mix_flash exit branch sets:

    WRITE_ONCE(chip->panel_mix, false);
    WRITE_ONCE(chip->panel_saw_fader, false);

but immediately after the if (mix_flash) block, if (fader_now) executes
unconditionally and sets chip->panel_saw_fader back to true. Because the
mix_flash entry branch does not clear panel_saw_fader to false either, the
next press of the mix button starts with panel_saw_fader == true before the
device has switched st[2] >> 4 to 0x0, which can trigger the device-driven
exit branch prematurely if the 0x44 flash clears before fader_now becomes
true.

Should if (fader_now) be gated on READ_ONCE(chip->panel_mix) (or cleared on
mix entry)?

[ ... ]

> +void babyface_panel_start(struct snd_usb_babyface *chip)
> +{
> +	chip->panel_seen = false;
> +	/* The SELECT channel selection is not in the readback, so the
> +	 * driver follows it from the presses.  The unit keeps one per IN
> +	 * pair across a driver reload and even a power cycle, with the LEDs
> +	 * dark until the next SELECT press, which shows it again instead of
> +	 * stepping (hardware-verified 2026-09-29: after an unplug, the
> +	 * first press lit both channels).  What panel_sel[] holds comes
> +	 * from before a suspend, from alsactl after a boot, or is not
> +	 * known, and an unknown selection targets nothing: SET, the IN
> +	 * wheel and the MIX wheel then do nothing rather than act on the
> +	 * wrong channel, until "Front Panel Select" is set to what the LEDs
> +	 * show.
> +	 */
> +	bf_panel_sel_load(chip);
> +	chip->panel_select_armed = false;
> +	chip->panel_start = jiffies;

[ ... ]

> +static int bf_panel_select_put(struct snd_kcontrol *kctl,
> +			       struct snd_ctl_elem_value *ucontrol)
> +{
> +	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
> +	unsigned int v = ucontrol->value.enumerated.item[0];
> +	int pair = kctl->private_value;
> +	/* alsactl restores the values of its last store shortly after
> +	 * every probe.
> +	 */
> +	bool restore = time_is_after_jiffies(chip->panel_start + 3 * HZ);
> +	s8 sel = v < 4 ? v : -1;
> +
> +	if (v > 4)
> +		return -EINVAL;
> +	/* After a boot they are what there is.  After a re-probe the unit
> +	 * may have taken SELECT presses since that store, and a wrong
> +	 * selection makes SET and the wheels act on a channel that is not
> +	 * the lit one: leave the pair unknown, as for a never-seen pair.
> +	 */
> +	if (restore && chip->reprobe)
> +		return 0;
> +	if (pair == READ_ONCE(chip->panel_in) - 1 && !restore && sel >= 0)
> +		/* Set while the LEDs show the selection: the next press steps
> +		 * it.
> +		 */
> +		chip->panel_select_armed = true;
> +	if (sel == chip->panel_sel[pair])
> +		return 0;
> +	chip->panel_sel[pair] = sel;
> +	if (pair == READ_ONCE(chip->panel_in) - 1)
> +		bf_panel_sel_load(chip);
> +	return 1;
> +}

[Severity: Medium]
Does resetting chip->panel_start = jiffies in babyface_panel_start() on
system resume cause bf_panel_select_put() to misclassify writes for 3 seconds
after every resume?

babyface_resume() calls babyface_panel_start(chip), which resets
chip->panel_start = jiffies. For 3 seconds after every system resume,
restore = time_is_after_jiffies(chip->panel_start + 3 * HZ) in
bf_panel_select_put() is true, so user or application writes to
"Front Panel Selection" are silently ignored if chip->reprobe is true, or
fail to set chip->panel_select_armed = true if chip->reprobe is false.

Conversely, during boot or probe, if alsactl restore runs more than 3 seconds
after probe, restore evaluates to false, bypassing the chip->reprobe check
and setting chip->panel_select_armed = true while the unit's selection
indicators are still dark, which desynchronizes the host-tracked selection on
the next physical button press.

Should chip->panel_start only be set at probe time, or is there a more
direct way to handle initial state restoration without a 3-second timer
window?

> diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
> index e0eb0ce9b92f3..a1aafebb2037c 100644
> --- a/sound/usb/babyfacepro/babyfacepro.c
> +++ b/sound/usb/babyfacepro/babyfacepro.c
> @@ -1479,8 +1527,12 @@ module_param(frames_per_urb, int, 0444);
>  MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
>  module_param(urb_frames_min, int, 0444);
>  MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
> +module_param(urb_split, int, 0444);
> +MODULE_PARM_DESC(urb_split, "Split the period into two URBs, queueing 1.5 periods instead of 2: 0 = auto (default), 1 = never, 2 = always.");
>  module_param(nurbs, int, 0444);
>  MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");
> +module_param(panel_poll_ms, int, 0444);
> +MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz).");

[Severity: Low]
Should the new panel_poll_ms and urb_split module parameters be documented
in Documentation/admin-guide/kernel-parameters.txt?

Also, urb_split belongs to the pcm period-splitting logic from the earlier
streaming patch rather than the front-panel changes described in this commit.

> @@ -1731,12 +1787,24 @@ static int babyface_probe(struct usb_interface *intf,
>  		goto error;
>  	}
>  
> +	err = babyface_create_panel(chip);
> +	if (err < 0) {
> +		dev_err(&intf->dev, "front-panel control creation failed: %d\n", err);
> +		goto error;
> +	}
> +
>  	err = snd_card_register(chip->card);
>  	if (err < 0) {
>  		dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
>  		goto error;
>  	}
>  
> +	/* The panel poll mirrors the physical buttons/wheel into the
> +	 * Front Panel controls; it runs for the whole card lifetime.
> +	 */
> +	chip->reprobe = bf_seen_before(dev);
> +	babyface_panel_start(chip);

[Severity: Medium]
Can userspace access "Front Panel Selection" via bf_panel_select_put()
between snd_card_register() and babyface_panel_start()?

snd_card_register(chip->card) exposes the control device to userspace and
triggers udev's alsactl restore before chip->reprobe = bf_seen_before(dev)
and babyface_panel_start(chip) have run. If bf_panel_select_put() executes
during that window on a re-probed device, chip->reprobe is still false and
chip->panel_start is still 0, so the if (restore && chip->reprobe) check in
bf_panel_select_put() is bypassed and babyface_panel_start() subsequently
overwrites chip->panel_select_armed.

Should chip->reprobe and the initial panel state in babyface_panel_start()
be initialized before calling snd_card_register()?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261027113140.134257-1-i.bahloul01@gmail.com?part=6

  reply	other threads:[~2026-10-27 11:58 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 11:31 [PATCH v7 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-10-27 12:02   ` sashiko-bot
2026-10-27 11:31 ` [PATCH v7 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
2026-10-27 11:48   ` sashiko-bot
2026-10-27 11:31 ` [PATCH v7 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
2026-10-27 11:48   ` sashiko-bot
2026-10-27 11:31 ` [PATCH v7 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
2026-10-27 11:47   ` sashiko-bot
2026-10-27 11:31 ` [PATCH v7 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
2026-10-27 11:53   ` sashiko-bot
2026-10-27 11:31 ` [PATCH v7 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
2026-10-27 11:58   ` sashiko-bot [this message]
2026-10-27 11:31 ` [PATCH v7 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-10-27 11:46   ` sashiko-bot
2026-10-27 11:31 ` [PATCH v7 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
2026-10-27 11:37   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-186491@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=i.bahloul01@gmail.com \
    --cc=linux-sound@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox