All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.