[RFC PATCH v5 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls

Ismaïl Bahloul <[email protected]>
Newsgroups org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-sound,org.kernel.vger.linux-usb
Message-ID <[email protected]>
Adds the 0x17 readback poll (buttons, wheel, IN/OUT/SELECT, MIX, DIM)
as read-only ALSA controls, plus the host-side emulation of the
physical wheel/button behaviour (TotalMix's own role): the OUT/IN/MIX
wheels, SET's phantom toggle and DIM.

Suspend/resume now also stop/start the poll around a system sleep.

On an original Babyface Pro, front-panel OUT selector values 0/1/2
correspond to Ch 1/2, Phones and Opt; correct the mapping, which left
Ch 1/2 at the previous selection and assigned the other two values to
the wrong outputs. Verified against the physical LEDs and knob
targeting on both a non-FS unit and the Pro FS.

Turning the OUT wheel also made heavy zipper noise on the main and
headphone outputs. A loopback measurement (a tone from the headphone
output cabled into IN3/IN4) showed why: the firmware moves the analog
output level itself on each click, smoothed over about 12 ms, but the
driver also wrote the 8-bit (analog) master on every ~20 ms poll from
its own flat 1 dB/click count, overriding the firmware's own move each
time and saw-toothing the level by 1-3 dB while the wheel turned. The
firmware's actual step also depends on the level and on how fast the
wheel turns (0.5 to 3 dB per click, doubled below a ~62 ms gap between
clicks).

Write only the 16-bit (digital) master during the gesture, track the
wheel's own level per side in half-dB so the ALSA controls stay
accurate, poll the panel every 5 ms for 200 ms after a click to tell
fast clicks apart, and reconcile the real 8-bit master once the wheel
has rested for 150 ms, the way TotalMix does at the end of a gesture.
A muted output now stays muted during the gesture instead of being
unmuted by the 8-bit write, and an existing balance (hold-SELECT) is
preserved since both sides move by the louder side's step.

Co-developed-by: David Fredman <[email protected]>
Signed-off-by: David Fredman <[email protected]>
Signed-off-by: Ismaïl Bahloul <[email protected]>
---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 1081 +++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     |   20 +
 sound/usb/babyfacepro/babyfacepro.h     |  100 +++
 3 files changed, 1201 insertions(+)

diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 2b4dd471d..eac97c895 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -2117,3 +2117,1084 @@ int babyface_create_flags(struct snd_usb_babyface *chip)
 	return 0;
 }
 
+/* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */
+enum {
+	BF_PANEL_KCTL_BUTTON,
+	BF_PANEL_KCTL_WHEEL,
+	BF_PANEL_KCTL_IN,
+	BF_PANEL_KCTL_OUT,
+	BF_PANEL_KCTL_MIX,
+	BF_PANEL_KCTL_DIM,
+	BF_PANEL_KCTL_SELECT,
+	BF_PANEL_KCTL_NUM,
+};
+
+static const char *const bf_panel_in_texts[] = {
+	"Unknown", "Ch 1/2", "Ch 3/4", "Opt", NULL
+};
+
+static const char *const bf_panel_out_texts[] = {
+	"Unknown", "Ch 1/2", "Phones", "Opt", NULL
+};
+
+static const char *const bf_panel_select_texts[] = {
+	"Left", "Right", "Both", "None", NULL
+};
+
+/* byte3 button flash -> event code (0 = none).  The idle byte3 is 0x40;
+ * a press flashes the value below the base for one or two poll frames.
+ */
+static int bf_panel_button_decode(u8 flash)
+{
+	switch (flash) {
+	case BF_PANEL_FLASH_IN:		return BF_PANEL_BTN_IN;
+	case BF_PANEL_FLASH_SET:	return BF_PANEL_BTN_SET;
+	case BF_PANEL_FLASH_MIX:	return BF_PANEL_BTN_MIX;
+	case BF_PANEL_FLASH_OUT:	return BF_PANEL_BTN_OUT;
+	case BF_PANEL_FLASH_SELECT:	return BF_PANEL_BTN_SELECT;
+	case BF_PANEL_FLASH_DIM:	return BF_PANEL_BTN_DIM;
+	default:			return BF_PANEL_BTN_NONE;
+	}
+}
+
+/* (byte2 >> 4) & 7 = IN position 4/5/6 -> enum index (0 = not in range). */
+static int bf_panel_in_decode(u8 nib)
+{
+	switch (nib) {
+	case BF_PANEL_IN_CH12:		return 1;
+	case BF_PANEL_IN_CH34:		return 2;
+	case BF_PANEL_IN_OPT:		return 3;
+	default:			return 0;
+	}
+}
+
+/* byte1 & 7 = OUT position.  0/1/2 for physical Ch 1/2 / Phones / Opt,
+ * LED-correlated capture, hardware-verified on both an original
+ * Babyface Pro and the Pro FS.
+ */
+static int bf_panel_out_decode(u8 v)
+{
+	switch (v) {
+	case 0x00:
+	case BF_PANEL_OUT_CH12:		return 1;
+	case 0x01:
+	case BF_PANEL_OUT_PHONES:	return 2;
+	case 0x02:
+	case BF_PANEL_OUT_OPT:		return 3;
+	default:			return 0;
+	}
+}
+
+/* MIX-mode VU display law - monitoring dBx2 -> the 0x1A 0x000A display
+ * value.  Piecewise-linear through the captured (dB, display) points
+ * (cap_mix.pcap 2026-08-23: (-62,0) (-54,1) (-48,2) (-42.5,3)
+ * (-35,4) (-28.4,5); cap_panel.pcap: (-7.4,10) (-6.7,11)
+ * (-4.6,12)) - a log-ish VU scale (coarse at the bottom, ~1.4 dB/step
+ * near 0).  The -28..-8 dB middle is interpolated; the exact law is
+ * pending the cap_mixdisp.pcap full-range sweep (TODO 0g).
+ */
+static int bf_mix_display(int db2)
+{
+	static const struct {
+		s16 db2;
+		u8 disp;
+	} pts[] = {
+		{ -124, 0 }, { -108, 1 }, {  -96, 2 }, {  -85, 3 },
+		{  -70, 4 }, {  -57, 5 }, {  -15, 10 }, {  -13, 11 },
+		{   -9, 12 },
+	};
+	int i;
+
+	if (db2 <= pts[0].db2)
+		return 0;
+	for (i = 0; i < ARRAY_SIZE(pts) - 1; i++) {
+		if (db2 <= pts[i + 1].db2) {
+			u32 num = (u32)(db2 - pts[i].db2) *
+				  (u32)(pts[i + 1].disp - pts[i].disp);
+			u32 den = pts[i + 1].db2 - pts[i].db2;
+
+			return pts[i].disp + (int)((num + den / 2) / den);
+		}
+	}
+	/* Above -4.6 dB: keep the last slope (2 dB/step) up to +6 dB. */
+	return pts[ARRAY_SIZE(pts) - 1].disp +
+	       clamp((db2 - pts[ARRAY_SIZE(pts) - 1].db2) / 4, 0, 12);
+}
+
+/* The kernel driver plays the TotalMix role for the MIX button (the
+ * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs
+ * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT-
+ * chosen channel(s) of the IN-selected pair, into the OUT-selected
+ * output's crosspoint block - the STANDARD map only (cap_mix.pcap /
+ * cap_select2.pcap, no low-map mirror).  Mirrors the change into the
+ * xpoint cache so the ALSA controls follow the wheel.  Takes the mutex
+ * (the 0x12 writes cycle the transaction flag like the mixer puts).
+ */
+static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	/* Canonical output of the OUT selection (enum 1 = Ch1/2,
+	 * 2 = Phones, 3 = Opt): AN1/2, PH3/4, ADAT7/8 (the optical
+	 * output) respectively.
+	 */
+	int out = chip->panel_out == 3 ? 5 :
+		  chip->panel_out == 2 ? 1 : 0;
+	unsigned int blk = bf_xpoint_block[out];
+	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 (chip->panel_in == 3) {
+		targets[0] = 4;		/* Opt: the AS1/2 pair */
+		n = 1;
+	} else if (chip->panel_select != 3) {
+		int base = chip->panel_in == 2 ? 2 : 0;
+
+		targets[0] = base + (chip->panel_select == 1 ? 1 : 0);
+		n = 1;
+		if (chip->panel_select == 2)
+			targets[n++] = base + 1;
+	}
+
+	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;
+	for (i = 0; i < n; i++) {
+		const struct bf_source *s = &bf_sources[targets[i]];
+
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+				(BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+				 s->idx_l) | flag);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+				(BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+				 s->idx_r) | flag);
+		chip->xpoint[out][targets[i]][0] = raw;
+		chip->xpoint[out][targets[i]][1] = raw;
+		/* MIX-mode VU display shadow (0x1A 0x000A+mic): TotalMix
+		 * mirrors the monitoring level into the panel display family
+		 * (cap_mix/cap_panel.pcap) - the input VU segments follow it.
+		 * Written only on change (the captures show TotalMix updating
+		 * it on segment crossings).  Law = bf_mix_display (TODO 0g
+		 * pending the exact full-range capture).
+		 */
+		if (targets[i] < 4) {
+			int disp = bf_mix_display(db2);
+
+			if (disp != chip->panel_mix_disp[targets[i]]) {
+				bf_vendor_write(chip, BF_REQ_GAIN,
+						(u16)disp,
+						BF_REG_PANEL_GAIN + targets[i]);
+				chip->panel_mix_disp[targets[i]] = disp;
+			}
+		}
+	}
+	mutex_unlock(&chip->mutex);
+}
+
+/* Write an output's L/R masters (8-bit companions + 16-bit with the
+ * transaction flag) and mirror into the cache - shared by the OUT
+ * volume wheel and the balance wheel.  Caller holds the mutex.
+ */
+static void bf_panel_write_master(struct snd_usb_babyface *chip, int out,
+				  u16 l, u16 r)
+{
+	u16 flag;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+	bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l),
+			BF_REG_MASTER_8 + 2 * out);
+	bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r),
+			BF_REG_MASTER_8 + 2 * out + 1);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			(BF_REG_MASTER_16 + 2 * out) | flag);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	chip->master[out][0] = l;
+	chip->master[out][1] = r;
+	chip->muted[out] = false;
+	/* 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;
+	}
+}
+
+/* Front-panel OUT wheel, measured on hardware 2026-09-17 with a tone
+ * looped from the headphone output into IN3/IN4:
+ *
+ *  - The firmware moves an analog output's level by itself when the
+ *    wheel turns, with its own ~12 ms smoothing, even when the host
+ *    writes nothing.  See bf_out_wheel_step() for the step size.
+ *  - A host write to the 8-bit master (the analog gain) overrides that.
+ *    Writing it on every poll from the host's own count made the level
+ *    saw-tooth by 1-3 dB while turning, heard as heavy zipper noise.
+ *  - The 16-bit master does not affect the analog level, but it is the
+ *    digital outputs' level.  TotalMix writes only the 16-bit during a
+ *    wheel gesture, and the 8-bit once the wheel is at rest.
+ *
+ * So while the wheel turns, only the 16-bit is written, and the cache
+ * follows the firmware's own count so the ALSA controls read the real
+ * level.  A muted output stays muted: only the cache moves.  Both
+ * sides move by the louder side's step, so a balance (hold-SELECT) is
+ * kept.  Same output mapping as the MIX wheel (Phones = canon 1,
+ * Opt = ADAT7/8 = canon 5, else AN1/2).
+ */
+static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out,
+				     u16 l, u16 r)
+{
+	u16 flag;
+
+	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_CROSSPOINT, l,
+				(BF_REG_MASTER_16 + 2 * out) | flag);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+				(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	}
+	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;
+	}
+}
+
+/* A click that follows the previous one within this gap moves twice as
+ * far.  In the recordings the firmware doubled clicks up to 58 ms apart
+ * and not from 67 ms.  A normal poll every ~21 ms can't tell those
+ * apart, so after an OUT wheel click the panel is polled every
+ * BF_PANEL_FAST_POLL_MS for BF_PANEL_FAST_HOLD_MS, and each click is
+ * dated at the middle of the interval it was seen in.  A wrong guess is
+ * fixed by the resync at rest below.
+ */
+#define BF_OUT_WHEEL_ACCEL_MS	62
+#define BF_PANEL_FAST_POLL_MS	5
+#define BF_PANEL_FAST_HOLD_MS	200
+
+/* Quiet time after the last click before the resync write. */
+#define BF_OUT_WHEEL_RESYNC_MS	150
+
+/* The device's own OUT wheel range: below -90 dB a side is muted.
+ * BF_OUT_WHEEL_MUTED is where a muted louder side is kept.
+ */
+#define BF_OUT_WHEEL_FLOOR	(-180)		/* half-dB */
+#define BF_OUT_WHEEL_MUTED	(BF_OUT_WHEEL_FLOOR - 1)
+/* Lowest level the 8-bit master is known to take (BF_MASTER_8_MIN). */
+#define BF_OUT_WHEEL_8BIT_MIN	(BF_MASTER_8_MIN - BF_MASTER_8_0DB)
+
+/**
+ * bf_out_wheel_step - the firmware's OUT wheel step, in half-dB
+ * @half_db: the louder side's level before the click, in half-dB
+ *
+ * Measured on the headphone output (a tone looped into IN3/IN4, the
+ * driver writing nothing):
+ *
+ *	-9.5 dB and up		0.5 dB per click
+ *	-10 to -25.5 dB		1 dB
+ *	-26 to -41.5 dB		1.5 dB
+ *	-42 to -57.5 dB		2 dB
+ *	-58 dB and down		3 dB
+ *
+ * A fast click moves twice as far.  Both sides move by the step of the
+ * louder one, so a balance is kept.
+ */
+static int bf_out_wheel_step(int half_db)
+{
+	if (half_db >= -19)
+		return 1;
+	if (half_db >= -51)
+		return 2;
+	if (half_db >= -83)
+		return 3;
+	if (half_db >= -115)
+		return 4;
+	return 6;
+}
+
+/* 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 raw = chip->master[out][ch];
+
+	if (raw == chip->panel_master_last[out][ch])
+		return chip->panel_out_hdb[out][ch];
+	if (!raw)
+		return BF_OUT_WHEEL_MUTED;
+	return bf_master_half_db(raw);
+}
+
+/* One click on both sides.  They move together, so a balance is kept.
+ * When the louder side would go below the floor, both stop there and the
+ * louder side mutes; from mute, turning up continues from the floor
+ * (-90 dB up one step is -87).  A quieter side below the floor is muted
+ * but keeps its offset, so the balance comes back as the level rises.
+ */
+static void bf_out_wheel_click(int db[2], int step)
+{
+	int louder = max(db[0], db[1]);
+	int shift = step;
+
+	if (louder < BF_OUT_WHEEL_FLOOR) {
+		if (step < 0)
+			return;
+		shift = BF_OUT_WHEEL_FLOOR - louder + step;
+	} else if (louder + step < BF_OUT_WHEEL_FLOOR) {
+		shift = BF_OUT_WHEEL_MUTED - louder;
+	} else if (louder + step > 12) {
+		shift = 12 - louder;
+	}
+	db[0] += shift;
+	db[1] += shift;
+}
+
+static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = chip->panel_out == 3 ? 5 :
+		  chip->panel_out == 2 ? 1 : 0;
+	int dir = delta > 0 ? 1 : -1;
+	int clicks = abs(delta);
+	ktime_t now = ktime_get();
+	bool fast;
+	int ch, i, step, louder;
+	int db[2];
+	u16 next[2];
+
+	chip->panel_fast_until = jiffies +
+				 msecs_to_jiffies(BF_PANEL_FAST_HOLD_MS);
+	/* The click came some time since the previous poll: take the
+	 * middle.  Clicks after the first in this poll came within one
+	 * interval, so only the first can be slow.  A reversal starts a
+	 * new turn.
+	 */
+	if (chip->panel_poll_t)
+		now = ktime_sub(now,
+				ktime_divns(ktime_sub(now, chip->panel_poll_t), 2));
+
+	mutex_lock(&chip->mutex);
+	fast = chip->panel_out_wheel_t &&
+	       dir == chip->panel_out_wheel_dir &&
+	       out == chip->panel_out_wheel_out &&
+	       ktime_ms_delta(now, chip->panel_out_wheel_t) <
+	       BF_OUT_WHEEL_ACCEL_MS;
+	for (ch = 0; ch < 2; ch++)
+		db[ch] = bf_out_wheel_level(chip, out, ch);
+	for (i = 0; i < clicks; i++) {
+		/* Muted, the louder side counts as the floor. */
+		louder = max3(db[0], db[1], BF_OUT_WHEEL_FLOOR);
+		step = dir * bf_out_wheel_step(louder);
+		if (fast || i)
+			step *= 2;
+		bf_out_wheel_click(db, step);
+	}
+	for (ch = 0; ch < 2; ch++)
+		next[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 :
+			   bf_master_16bit(db[ch]);
+	bf_panel_out_wheel_write(chip, out, next[0], next[1]);
+	for (ch = 0; ch < 2; ch++) {
+		chip->panel_master_last[out][ch] = next[ch];
+		chip->panel_out_hdb[out][ch] = db[ch];
+	}
+	chip->panel_out_wheel_t = now;
+	chip->panel_out_wheel_dir = dir;
+	chip->panel_out_wheel_out = out;
+	chip->panel_out_resync = true;
+	mutex_unlock(&chip->mutex);
+}
+
+/* Once the wheel has rested, write the wheel's level to the 8-bit (and
+ * 16-bit) master, as TotalMix does at the end of a gesture.  The level
+ * already follows the firmware's step rule, so this normally changes
+ * nothing audible.  It fixes the rare click whose speed the driver
+ * guessed wrong: one zone step per such click, smoothed by the
+ * firmware.  Nothing is written below -64 dB, where the 8-bit codes the
+ * device takes are unknown, nor after a mixer application has set the
+ * master since (that write already set the device).
+ */
+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, chip->master[out][0],
+				(BF_REG_MASTER_16 + 2 * out) | flag);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][1],
+				(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	}
+unlock:
+	mutex_unlock(&chip->mutex);
+}
+
+/* IN-mode wheel: the gain of the SELECT-chosen channel(s) of the
+ * IN-selected pair, +/-1 dB per click (manual sec. 5.1: SELECT steps
+ * left/right/both, then the wheel changes the gain).  Writes the PANEL
+ * gain registers 0x1A 0x000A+mic (cap_select.pcap 2026-08-24 - the
+ * "ADC gain" family, which drives the same preamp as the GUI
+ * 0x0000+mic; the cache tracks the raw either way).  Opt has no
+ * preamp and SELECT None = no target.
+ */
+static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	u8 mics[2];
+	int n = 0;
+	int i;
+
+	if (chip->panel_in == 3 || chip->panel_select == 3)
+		return;
+	{
+		int base = chip->panel_in == 2 ? 2 : 0;
+
+		mics[0] = base + (chip->panel_select == 1 ? 1 : 0);
+		n = 1;
+		if (chip->panel_select == 2)
+			mics[n++] = base + 1;
+	}
+
+	mutex_lock(&chip->mutex);
+	for (i = 0; i < n; i++) {
+		int mic = mics[i];
+		int db = clamp((int)chip->gain[mic] + delta,
+				0, bf_gain_max_db(mic));
+		u8 raw = bf_gain_raw(mic, db);
+
+		bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic);
+		chip->gain[mic] = db;
+	}
+	mutex_unlock(&chip->mutex);
+}
+
+/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output
+ * Balance"): moves the stereo image of the OUT-selected output by
+ * attenuating ONE side, linear in raw (cap_pan_stereo.pcap: the varied
+ * side = fixed*(1-|pan|), ~0x9C raw step per click at 0 dB - the PAN
+ * of the stereo hardware output in TotalMix).  The balance position is
+ * derived from the L/R master ratio (the louder side is the fixed
+ * one), so the gesture needs no extra state - and the OUT wheel below
+ * moves both sides by the same dB to preserve an existing balance.
+ */
+static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = chip->panel_out == 3 ? 5 :
+		  chip->panel_out == 2 ? 1 : 0;
+	u16 l, r;
+	int bal;		/* -100..+100; + = image right (left varies) */
+	u16 fixed, varied;
+
+	mutex_lock(&chip->mutex);
+	/* Read under the lock so the L/R pair is consistent with the
+	 * master/mute/dim writers (they update chip->master[] under the
+	 * same mutex).
+	 */
+	l = chip->master[out][0];
+	r = chip->master[out][1];
+	/* Balance from the L/R ratio: the louder side is the fixed one. */
+	if (l >= r) {
+		bal = r ? -(100 - (100 * r) / l) : -100;
+		fixed = l;
+	} else {
+		bal = l ? (100 - (100 * l) / r) : 100;
+		fixed = r;
+	}
+	bal = clamp(bal + delta * 2, -100, 100);
+	varied = (u16)((u32)fixed * (100 - abs(bal)) / 100);
+	l = bal >= 0 ? varied : fixed;
+	r = bal >= 0 ? fixed : varied;
+
+	bf_panel_write_master(chip, out, l, r);
+	mutex_unlock(&chip->mutex);
+}
+
+/* SET press (byte3 0x42 flash): toggle 48V phantom on the
+ * SELECT-chosen mic(s) of the IN-selected pair.  The hardware only
+ * does this in standalone mode (online, TotalMix ignores SET - no USB
+ * write in the captures), but the driver IS the host: it writes the
+ * preamp state itself and the P48 LEDs follow (the tuxmix-core
+ * emulator, hardware-verified).  Restricted to IN mode + Ch1/2 (the
+ * phantom-capable pair); Opt/Ch3/4 and SELECT None = no target.
+ */
+static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
+{
+	u16 bits = 0;
+	int m;
+
+	if (chip->panel_mix || chip->panel_in != 1 ||
+	    chip->panel_select == 3)
+		return;
+	if (chip->panel_select != 1)
+		bits |= BF_PREAMP_48V_MIC1;
+	if (chip->panel_select != 0)
+		bits |= BF_PREAMP_48V_MIC2;
+
+	mutex_lock(&chip->mutex);
+	/* 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 (chip->panel_select == 2) {
+		if ((chip->preamp & bits) == bits)
+			chip->preamp &= ~bits;
+		else
+			chip->preamp |= bits;
+	} else {
+		chip->preamp ^= bits;
+	}
+	bf_preamp_state_write(chip);
+	for (m = 0; m < 4; m++)
+		chip->panel_mix_disp[m] = 0;
+	mutex_unlock(&chip->mutex);
+}
+
+static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl)
+{
+	if (chip->panel_kctl[ctl])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->panel_kctl[ctl]->id);
+}
+
+/* One 0x17 read + decode.  Called from the poll work; no locking needed -
+ * the worker is the only writer and the control get callbacks run under
+ * the ALSA controls lock (chip->panel_button/wheel are consumed there).
+ */
+static void bf_panel_tick(struct snd_usb_babyface *chip)
+{
+	u8 st[4];
+	int delta, in, out;
+	bool dim;
+	u8 cls, pcls;
+	int btn;
+	bool mix_flash, fader_now;
+
+	if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0)
+		return;	/* device gone / busy - retry next tick */
+
+	if (!chip->panel_seen) {
+		chip->panel_seen = true;
+		memcpy(chip->panel_prev, st, sizeof(st));
+		/* Seed the state controls from the first snapshot. */
+		in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);
+		if (in)
+			chip->panel_in = in;
+		out = bf_panel_out_decode(st[1] & 0x07);
+		if (out)
+			chip->panel_out = out;
+		chip->panel_mix = !!(st[0] & 0x80);
+		chip->panel_saw_fader = (st[2] >> 4) == 0x0;
+		chip->panel_dim = !!(st[1] & 0x20);
+		return;
+	}
+
+	/* The udev alsactl restore (~100 ms after probe) clobbers the host
+	 * SELECT with a stale stored value (the control is VOLATILE but
+	 * this alsactl stores/restores it anyway) - re-assert the device's
+	 * power-on state (nothing selected, cycle ARMED) for the first
+	 * ~3 s so the boot always starts in sync.
+	 */
+	if (time_is_after_jiffies(chip->panel_start + 3 * HZ))
+		chip->panel_select = 3;
+
+	/* Button flash (byte3 over the 0x40 idle base). */
+	btn = bf_panel_button_decode(st[3]);
+	if (btn)
+		chip->panel_button = btn;
+
+	/* Wheel: signed 4-bit wrap delta of the byte2 low nibble - only
+	 * while the mode class is unchanged.  A mode switch (IN 0x4x ->
+	 * fader 0x0x on a MIX press, or the OUT counter carrying 0x8F ->
+	 * 0x90 - the OUT counter is a full byte, cap_set2.pcap) must not
+	 * be read as a wheel jump.  Class: 0 = fader (0x0x), 1 = OUT
+	 * (0x8x/0x9x), 2 = IN (0x4x/0x5x/0x6x).
+	 */
+	cls = (st[2] >> 4) == 0x8 || (st[2] >> 4) == 0x9 ? 1 :
+	      (st[2] >> 4) == 0x0 ? 0 : 2;
+	pcls = (chip->panel_prev[2] >> 4) == 0x8 ||
+	       (chip->panel_prev[2] >> 4) == 0x9 ? 1 :
+	       (chip->panel_prev[2] >> 4) == 0x0 ? 0 : 2;
+	delta = (int)(st[2] & 0x0f) - (int)(chip->panel_prev[2] & 0x0f);
+	if (delta > 8)
+		delta -= 16;
+	else if (delta < -8)
+		delta += 16;
+	if (delta && cls == pcls) {
+		chip->panel_wheel = clamp(chip->panel_wheel + delta,
+					  SHRT_MIN, SHRT_MAX);
+		bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL);
+		/* Wheel by mode (LINUX-VALIDATION sec. 12, the TotalMix
+		 * emulator): MIX -> monitoring level, OUT (0x8x/0x9x) -> the
+		 * selected output master (or its balance while SELECT is
+		 * held), IN (0x4x/0x5x/0x6x) -> the SELECT-chosen preamp
+		 * gain.
+		 */
+		if (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 != chip->panel_in) {
+		chip->panel_in = in;
+		/* The card CLEARS its L/R/both selection on an IN pair
+		 * switch (user-verified 2026-08-27): re-sync the host-
+		 * tracked SELECT so SET / the wheel / MIX target nothing
+		 * until the user picks a channel again.  This is the main
+		 * anti-desync hook (the physical state is not readable).
+		 */
+		if (chip->panel_select != 3) {
+			chip->panel_select = 3;
+			bf_panel_notify(chip, BF_PANEL_KCTL_SELECT);
+		}
+		/* An IN-pair switch disarms the device's SELECT cycle: the
+		 * next press only re-arms it (no step), the one after that
+		 * cycles (device behavior, 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 != chip->panel_out) {
+		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) {
+		if (!chip->panel_select_armed) {
+			/* Disarmed (IN switch since the last step): the
+			 * press only re-arms the cycle - the device steps on
+			 * the NEXT press (user-verified 2026-08-28).
+			 */
+			chip->panel_select_armed = true;
+		} else {
+			chip->panel_select = (chip->panel_select + 1) & 3;
+		}
+		bf_panel_notify(chip, BF_PANEL_KCTL_SELECT);
+	}
+	/* 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;
+
+	/* SET (A) press: host-side 48V phantom toggle on the
+	 * SELECT-chosen mic(s) (see bf_panel_set_phantom).
+	 */
+	if (st[3] == BF_PANEL_FLASH_SET &&
+	    chip->panel_prev[3] != BF_PANEL_FLASH_SET)
+		bf_panel_set_phantom(chip);
+
+	/* DIM press: toggle the host-side dim, same host-in-the-loop
+	 * arrangement as SET above.  Decoding the press without acting on
+	 * it made the button look dead with the driver alone.
+	 */
+	if (st[3] == BF_PANEL_FLASH_DIM &&
+	    chip->panel_prev[3] != BF_PANEL_FLASH_DIM)
+		bf_panel_toggle_dim(chip);
+
+	/* MIX (fader mode) - HOST-latched, like TotalMix (cap_mix.pcap,
+	 * cap_select2.pcap): the raw press readback is `0D 0D 41 44` -
+	 * byte3 flash 0x44, NO engaged bit, byte2 still in the current
+	 * mode.  The host acks the flash with `0x17 0x8480 0x8C80` -> the
+	 * device latches fader mode (byte0/1 gain the 0x80 bit, byte2 =
+	 * 0x00+n counter) and STAYS there after the physical release; the
+	 * SECOND 0x44 flash exits it (`0x17 0x0400 0x8000` + `0x8080`).
+	 * A mode button (IN/OUT/SET) pressed during MIX makes the device
+	 * leave fader mode by itself -> same exit writes (the user: IN
+	 * must return to gain control).  `panel_saw_fader` gates the
+	 * device-driven exit so a pre-ack readback (byte2 still 0x4x
+	 * while the 0x44 flash shows) never ends MIX before it started.
+	 */
+	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 (chip->panel_mix) {
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+			chip->panel_mix = false;
+			chip->panel_saw_fader = false;
+		} else {
+			int ref, out;
+			int m;
+
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
+			chip->panel_mix = true;
+			/* Seed the monitoring level at the reference
+			 * crosspoint's current value so the first wheel
+			 * click doesn't jump from -inf (the reference =
+			 * the first SELECT-chosen channel of the IN pair;
+			 * Opt = the AS1/2 pair).
+			 */
+			out = chip->panel_out == 3 ? 5 :
+			      chip->panel_out == 2 ? 1 : 0;
+			ref = chip->panel_in == 3 ? 4 :
+			      (chip->panel_in == 2 ? 2 : 0) +
+			      (chip->panel_select == 1 ? 1 : 0);
+			chip->panel_mix_raw = chip->xpoint[out][ref][0];
+			/* Seed the VU display shadow at the CURRENT level
+			 * (cap_panel.pcap: TotalMix writes the display value of
+			 * the current fader on engage - 10 in that session -
+			 * not a hard 0; cap_mix's 0 was because the fader sat
+			 * at the bottom).  Only the channels the wheel can move.
+			 */
+			for (m = 0; m < 4; m++)
+				chip->panel_mix_disp[m] = 0;
+			if (ref < 4) {
+				int db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
+				int disp = bf_mix_display(db2);
+
+				bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp,
+						BF_REG_PANEL_GAIN + ref);
+				chip->panel_mix_disp[ref] = disp;
+			}
+		}
+		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+	}
+	if (fader_now) {
+		chip->panel_saw_fader = true;
+	} else if (chip->panel_mix && 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);
+		chip->panel_mix = false;
+		chip->panel_saw_fader = false;
+		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+	}
+
+	dim = !!(st[1] & 0x20);
+	if (dim != chip->panel_dim) {
+		chip->panel_dim = dim;
+		bf_panel_notify(chip, BF_PANEL_KCTL_DIM);
+	}
+
+	memcpy(chip->panel_prev, st, sizeof(st));
+}
+
+void babyface_panel_work(struct work_struct *work)
+{
+	struct snd_usb_babyface *chip = container_of(work,
+			struct snd_usb_babyface, panel_work.work);
+	unsigned int ms;
+
+	if (chip->shutdown)
+		return;
+	bf_panel_tick(chip);
+	chip->panel_poll_t = ktime_get();
+	/* Poll fast for a moment after an OUT wheel click. */
+	ms = time_before(jiffies, chip->panel_fast_until) ?
+	     BF_PANEL_FAST_POLL_MS : chip->panel_poll_ms;
+	schedule_delayed_work(&chip->panel_work, msecs_to_jiffies(ms));
+}
+
+void babyface_panel_start(struct snd_usb_babyface *chip)
+{
+	chip->panel_seen = false;
+	/* The device boots with NOTHING selected (the SELECT cycle starts
+	 * at none -> AN1 -> AN2 -> both -> none) - the unreadable selection
+	 * must start there too, or every later SET is off by one channel
+	 * (host at AN1 while the LEDs show nothing -> first SELECT makes
+	 * the device blink AN1 but the host believes AN2).
+	 */
+	chip->panel_select = 3;	/* none */
+	chip->panel_select_armed = true;
+	chip->panel_start = jiffies;
+	chip->panel_fast_until = jiffies;
+	chip->panel_poll_t = 0;
+	/* No master holds 0xffff: the wheel starts from the masters. */
+	memset(chip->panel_master_last, 0xff, sizeof(chip->panel_master_last));
+	schedule_delayed_work(&chip->panel_work, 0);
+}
+
+void babyface_panel_stop(struct snd_usb_babyface *chip)
+{
+	cancel_delayed_work_sync(&chip->panel_work);
+}
+
+/* -- controls -------------------------- */
+
+/* The button/wheel controls hold the LATEST state and are NOT consumed
+ * on read: wireplumber subscribes to every notifying control and reads
+ * it, so a clear-on-get would let another reader eat the event.  Each
+ * consumer tracks its own baseline and acts on changes (the button is a
+ * last-press code, the wheel an accumulated signed delta).  VOLATILE
+ * keeps alsactl from caching them.
+ */
+static int bf_panel_button_info(struct snd_kcontrol *kctl,
+				struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = BF_PANEL_BTN_DIM;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_panel_button_get(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->panel_button;
+	return 0;
+}
+
+static int bf_panel_wheel_info(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = SHRT_MIN;
+	uinfo->value.integer.max = SHRT_MAX;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_panel_wheel_get(struct snd_kcontrol *kctl,
+			      struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->panel_wheel;
+	return 0;
+}
+
+static int bf_panel_in_info(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_in_texts);
+}
+
+static int bf_panel_in_get(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = chip->panel_in;
+	return 0;
+}
+
+static int bf_panel_out_info(struct snd_kcontrol *kctl,
+			     struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_out_texts);
+}
+
+static int bf_panel_out_get(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = chip->panel_out;
+	return 0;
+}
+
+static int bf_panel_select_info(struct snd_kcontrol *kctl,
+				struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_select_texts);
+}
+
+static int bf_panel_select_get(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = chip->panel_select;
+	return 0;
+}
+
+/* Writable so software (or the user, after a driver reload) can
+ * re-sync the host-tracked SELECT state to the physical card - the
+ * L/R/both/none state is NOT in the 0x17 readback, so a reload starts
+ * at "Left" while the card may sit at any position; a desync makes
+ * SET / the wheel / MIX target the wrong channel.  Writing the
+ * physical state re-aligns the emulation (TotalMix parity: it also
+ * lets software select channels directly).
+ */
+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 ret = 0;
+
+	if (v > 3)
+		return -EINVAL;
+	if (v != chip->panel_select) {
+		chip->panel_select = v;
+		bf_panel_notify(chip, BF_PANEL_KCTL_SELECT);
+		ret = 1;
+	}
+	return ret;
+}
+
+/* Shared boolean get - private_value selects mix (0) / dim (1). */
+static int bf_panel_bool_get(struct snd_kcontrol *kctl,
+			     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] =
+		kctl->private_value ? chip->panel_dim : chip->panel_mix;
+	return 0;
+}
+
+int babyface_create_panel(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int err;
+
+	memset(chip->panel_kctl, 0, sizeof(chip->panel_kctl));
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Button",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_button_info,
+		.get = bf_panel_button_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_BUTTON] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Wheel",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_wheel_info,
+		.get = bf_panel_wheel_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_WHEEL] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel In",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_in_info,
+		.get = bf_panel_in_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_IN] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Out",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_out_info,
+		.get = bf_panel_out_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_OUT] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Mix",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = snd_ctl_boolean_mono_info,
+		.get = bf_panel_bool_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_MIX] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Dim",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = snd_ctl_boolean_mono_info,
+		.get = bf_panel_bool_get,
+		.private_value = 1,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_DIM] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Select",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_WRITE |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_select_info,
+		.get = bf_panel_select_get,
+		.put = bf_panel_select_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_SELECT] = kctl;
+
+	return 0;
+}
+
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index ccf3dbf36..8cf68e404 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -1272,6 +1272,7 @@ static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
 static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
 static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT;
 static int nurbs = BF_NURBS_DEFAULT;
+static int panel_poll_ms = BF_PANEL_POLL_MS_DEFAULT;
 
 module_param_array(index, int, NULL, 0444);
 MODULE_PARM_DESC(index, "Index value for the Babyface Pro sound card.");
@@ -1281,6 +1282,8 @@ module_param(frames_per_urb, int, 0644);
 MODULE_PARM_DESC(frames_per_urb, "Audio frames per URB, 8..1024 (16 = low-latency floor, 256 = default).");
 module_param(nurbs, int, 0644);
 MODULE_PARM_DESC(nurbs, "URBs in flight per direction, 1..16 (16 = low-latency).");
+module_param(panel_poll_ms, int, 0644);
+MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz).");
 
 /* -- USB driver ------------------------- */
 
@@ -1366,10 +1369,12 @@ static int babyface_probe(struct usb_interface *intf,
 	chip->alt = BF_ALT_1;
 	chip->frame_bytes = 56;
 	chip->preamp = BF_PREAMP_BASE;
+	chip->panel_poll_ms = clamp(panel_poll_ms, 10, 1000);
 	mutex_init(&chip->mutex);
 	spin_lock_init(&chip->lock);
 	atomic_set(&chip->urb_err, 0);
 	INIT_WORK(&chip->stream_work, babyface_stream_work);
+	INIT_DELAYED_WORK(&chip->panel_work, babyface_panel_work);
 	chip->card->private_free = babyface_private_free;
 
 	/* Model-neutral on purpose.  The FS and the original (2015)
@@ -1524,12 +1529,23 @@ 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.
+	 */
+	babyface_panel_start(chip);
+
 	usb_set_intfdata(intf, chip);
 	dev_info(&intf->dev,
 		 "Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n",
@@ -1569,6 +1585,7 @@ static void babyface_disconnect(struct usb_interface *intf)
 
 	chip->shutdown = true;
 	cancel_work_sync(&chip->stream_work);
+	babyface_panel_stop(chip);
 	/* Balance the probe()-time usb_disable_autosuspend(): the usb_device
 	 * outlives this interface claim (a usbfs detach re-probes without
 	 * the physical device ever disconnecting), so leaving autosuspend
@@ -1605,6 +1622,7 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
 			snd_pcm_suspend_all(sdev->device_data);
 	}
 	cancel_work_sync(&chip->stream_work);
+	babyface_panel_stop(chip);
 	mutex_lock(&chip->mutex);
 	if (chip->streaming)
 		babyface_stream_kill(chip);
@@ -1635,6 +1653,8 @@ static int babyface_resume(struct usb_interface *intf)
 	err = babyface_restore_state(chip);
 out:
 	mutex_unlock(&chip->mutex);
+	if (!err)
+		babyface_panel_start(chip);
 	return err;
 }
 
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 9822db312..ba1225de6 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -45,6 +45,7 @@
  *     bf_clock_write / bf_pitch_write.
  */
 
+#include <linux/ktime.h>
 #include <linux/module.h>
 #include <linux/mutex.h>
 #include <linux/unaligned.h>
@@ -182,6 +183,53 @@
  */
 #define BF_LOOPBACK_CHANNELS		30
 
+/* Front-panel poll interval default - Windows polls the 5-register
+ * status set at ~50 cycles/s (20 ms); match that.  Tunable via the
+ * panel_poll_ms module param for reviewers/distros who want a slower
+ * (or faster) rate than the Windows-matching default.
+ */
+#define BF_PANEL_POLL_MS_DEFAULT	20
+
+/* Front-panel readback (babyfacepro-ctl.c): 0x17 read at wIdx 0x0000 - the index
+ * the Windows driver polls (cap_buttons2.pcap).  byte0 = preamp 48V/PAD,
+ * byte1 = OUT sel + DIM/MIX bits, byte2 = IN sel + wheel counter,
+ * byte3 = button flash (see babyfacepro-ctl.c for the full layout).
+ */
+#define BF_REG_PANEL_READ		0x0000
+#define BF_PANEL_IN_SHIFT		4
+#define BF_PANEL_IN_CH12		0x04
+#define BF_PANEL_IN_CH34		0x05
+#define BF_PANEL_IN_OPT			0x06
+/* OUT selection - the gain-display-mode encoding (cap_dim.pcap);
+ * babyfacepro-ctl.c also accepts the base-mode 0x01/0x02 (cap_buttons.pcap).
+ */
+#define BF_PANEL_OUT_CH12		0x04
+#define BF_PANEL_OUT_PHONES		0x05
+#define BF_PANEL_OUT_OPT		0x06
+#define BF_PANEL_FLASH_IN		0x41
+#define BF_PANEL_FLASH_SET		0x42
+#define BF_PANEL_FLASH_MIX		0x44
+#define BF_PANEL_FLASH_OUT		0x48
+#define BF_PANEL_FLASH_SELECT		0x50
+#define BF_PANEL_FLASH_DIM		0x60
+#define BF_PANEL_BTN_NONE		0
+#define BF_PANEL_BTN_IN			1
+#define BF_PANEL_BTN_SET		2
+#define BF_PANEL_BTN_MIX		3
+#define BF_PANEL_BTN_OUT		4
+#define BF_PANEL_BTN_SELECT		5
+#define BF_PANEL_BTN_DIM		6
+
+/* The front-panel gain/display family (0x1A, wIdx 0x000A + mic 0-3;
+ * cap_panel/cap_mix.pcap): in gain mode the wheel writes the "ADC
+ * gain" here (drives the same preamp as the GUI 0x0000+mic); in MIX
+ * (fader) mode the same registers carry the VU DISPLAY shadow -
+ * TotalMix writes the monitoring level display value (0..~31) and the
+ * card lights the input VU segments accordingly (hardware-verified
+ * 2026-08-26 live: sweeping 0x1A values moved the input VU).
+ */
+#define BF_REG_PANEL_GAIN		0x000a
+
 /* The "cross" register block within each output: the L-registers sit at
  * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
  * source pairs that can be cross-linked).  bf_crosspoint_clear_cross()
@@ -309,6 +357,54 @@ struct snd_usb_babyface {
 	u16 dim_saved[2];		/* pre-DIM Phones master (out 1 L/R) */
 	bool dim;			/* DIM engaged (fixed -20 dB on Phones) */
 	struct snd_kcontrol *dim_kctl;      /* for snd_ctl_notify */
+
+	/* front panel (babyfacepro-ctl.c) - 0x17 readback poll */
+	struct delayed_work panel_work;
+	unsigned int panel_poll_ms;	/* front-panel poll interval, module param */
+	u8 panel_prev[4];		/* last 0x17 snapshot */
+	bool panel_seen;		/* first snapshot taken */
+	bool panel_select_armed;	/* device SELECT cycle armed (IN switch disarms) */
+	unsigned long panel_start;	/* jiffies at panel_start (boot re-assert) */
+	int panel_button;		/* latched button event (consumed on get) */
+	int panel_wheel;		/* accumulated wheel delta (consumed on get) */
+	int panel_in;			/* enum: 0 unknown, 1 Ch1/2, 2 Ch3/4, 3 Opt */
+	int panel_out;			/* enum: 0 unknown, 1 Ch1/2, 2 Phones, 3 Opt */
+	bool panel_mix;			/* MIX engaged - HOST-latched (like TotalMix):
+					 * set by the 0x44 flash ack, NOT by the readback
+					 * 0x80 bit (the raw press has none)
+					 */
+	bool panel_dim;			/* DIM sticky (byte1 bit 0x20) */
+	bool panel_saw_fader;		/* device observed in fader mode (byte2 0x0x)
+					 * - gates the device-driven MIX exit
+					 */
+	int panel_select;		/* SELECT state: 0 L, 1 R, 2 both, 3 none
+					 * (host-tracked - not in the readback)
+					 */
+	int panel_sel_hold;		/* consecutive ticks with byte3 = 0x50
+					 * (SELECT held > 200 ms = the OUT-balance
+					 * gesture; a tap flashes only ~100-150 ms,
+					 * selhold_probe2 - no engaged bit)
+					 */
+	/* The OUT wheel's own level per side, in half-dB, valid while the
+	 * master still holds the value the wheel last wrote (the 16-bit
+	 * value can't hold every half-dB step, nor the device's range
+	 * below -64 dB, nor its mute).  0xffff = nothing written yet.
+	 */
+	u16 panel_master_last[6][2];
+	s16 panel_out_hdb[6][2];
+	/* OUT wheel acceleration and resync at rest (babyfacepro-ctl.c) */
+	ktime_t panel_out_wheel_t;	/* estimated time of the last count */
+	ktime_t panel_poll_t;		/* when the previous poll ran */
+	unsigned long panel_fast_until;	/* jiffies: poll fast until then */
+	int panel_out_wheel_dir;	/* its direction, +1 or -1 */
+	int panel_out_wheel_out;	/* the output it moved */
+	bool panel_out_resync;		/* 8-bit write due once at rest */
+	u16 panel_mix_raw;		/* MIX-mode monitoring level (fader raw) */
+	u8 panel_mix_disp[4];		/* MIX-mode VU display shadow per mic
+					 * (0x1A 0x000A+mic - written on change
+					 * so the input VU follows the wheel)
+					 */
+	struct snd_kcontrol *panel_kctl[7]; /* for snd_ctl_notify */
 };
 
 /* The mixer state cached across interface re-probes/resume (see
@@ -394,6 +490,10 @@ u8 bf_gain_raw(int mic, int db);
 int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
 void bf_panel_toggle_dim(struct snd_usb_babyface *chip);
 int babyface_create_flags(struct snd_usb_babyface *chip);
+int babyface_create_panel(struct snd_usb_babyface *chip);
+void babyface_panel_start(struct snd_usb_babyface *chip);
+void babyface_panel_stop(struct snd_usb_babyface *chip);
+void babyface_panel_work(struct work_struct *work);
 
 /* Master gain-law helpers - shared with the front-panel wheels once
  * the front panel lands.
-- 
2.55.0
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.