From: "Ismaïl Bahloul" <i.bahloul01@gmail.com>
To: linux-sound@vger.kernel.org
Cc: linux-usb@vger.kernel.org, alsa-devel@alsa-project.org,
perex@perex.cz, tiwai@suse.com, linux-kernel@vger.kernel.org
Subject: [RFC PATCH v4 2/3] ALSA: usb: babyfacepro: add the front-panel poll and controls
Date: Mon, 14 Sep 2026 18:23:33 +0100 [thread overview]
Message-ID: <20260914172334.49074-3-i.bahloul01@gmail.com> (raw)
In-Reply-To: <20260914172334.49074-1-i.bahloul01@gmail.com>
The device has no DSP of its own for its front panel: on Windows the
vendor software is in the loop, translating physical button and wheel
events into mixer writes. This driver does the same.
A delayed work polls the 0x17 readback (interval settable with the
panel_poll_ms module parameter, 10-1000 ms, default 20) and mirrors
the decoded state into read-only ALSA controls: the button event, the
wheel delta, the IN/OUT/MIX selection, DIM and SELECT. The driver
itself acts on three of them, the way the vendor software does: SET
toggles phantom power on the selected input, DIM toggles the host-side
dim, and the wheel drives the master, gain or balance depending on the
mode the panel is in, including the MIX-mode VU display.
Anything else a user wants a button to do can be built on the
read-only controls from user space.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 885 +++++++++++++++++++++++-
sound/usb/babyfacepro/babyfacepro.c | 22 +-
sound/usb/babyfacepro/babyfacepro.h | 88 +++
3 files changed, 993 insertions(+), 2 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 61bfe1f12..2b05e9e99 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -3,7 +3,7 @@
* RME Babyface Pro FS - proprietary-mode USB audio driver
*
* ALSA control surface: mixer (masters, preamp, crosspoints, flags,
- * gains)
+ * gains), front-panel poll + controls, and the hardware DSP EQ
* (3-band + low cut).
*
* See babyfacepro.h for the shared device state and register map,
@@ -1411,6 +1411,25 @@ static int bf_dim_put(struct snd_kcontrol *kctl,
return ret;
}
+/* DIM press on the front panel. The device has no DSP of its own for
+ * this, so the host does it, exactly as it already does for the SET
+ * button's phantom toggle.
+ */
+void bf_panel_toggle_dim(struct snd_usb_babyface *chip)
+{
+ bool on;
+ int ret;
+
+ mutex_lock(&chip->mutex);
+ on = !chip->dim;
+ ret = bf_dim_apply(chip, on);
+ mutex_unlock(&chip->mutex);
+
+ if (ret == 0 && chip->dim_kctl)
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->dim_kctl->id);
+}
+
static int bf_width_info(struct snd_kcontrol *kctl,
struct snd_ctl_elem_info *uinfo)
{
@@ -1619,6 +1638,7 @@ int babyface_create_flags(struct snd_usb_babyface *chip)
.get = bf_dim_get,
.put = bf_dim_put,
}, chip);
+ chip->dim_kctl = kctl;
err = snd_ctl_add(chip->card, kctl);
if (err < 0)
return err;
@@ -1956,6 +1976,75 @@ int babyface_create_controls(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. Two encodings seen in captures: the
+ * gain-display mode 0x04/0x05/0x06 (cap_dim.pcap, cap_buttons2.pcap)
+ * and the base mode 0x01/0x02/0x00 (cap_buttons.pcap; 0x01 is also the
+ * idle byte1 of cap_padpan.pcap and the live device). Accept both;
+ * 0x00 is ambiguous (could be Opt or no selection) so keep previous.
+ */
+static int bf_panel_out_decode(u8 v)
+{
+ switch (v) {
+ case BF_PANEL_OUT_CH12: return 1;
+ case BF_PANEL_OUT_PHONES: return 2;
+ case BF_PANEL_OUT_OPT: return 3;
+ case 0x01: return 1; /* base-mode Ch 1/2 */
+ case 0x02: return 2; /* base-mode Phones */
+ default: return 0;
+ }
+}
+
/* -- 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).
@@ -2033,4 +2122,798 @@ static u16 bf_fader_db2_to_raw(int db2)
return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].raw;
}
+/* 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;
+ }
+}
+
+/* OUT-mode wheel: the master fader of the OUT-selected output, +/-0.5 dB
+ * per click (cap_set2/cap_dim.pcap: the wheel writes the 16-bit master
+ * 0x03E0+2*out on the master curve 0x2000*2^(dB/6); the driver keeps
+ * the 8-bit companion in sync like bf_master_put - the 8-bit is the
+ * real volume). BOTH sides move by the same dB so an existing
+ * balance (hold-SELECT) is preserved. Same output mapping as the MIX
+ * wheel (Phones = canon 1, Opt = ADAT7/8 = canon 5, else AN1/2).
+ */
+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 hl, hr;
+ u16 l, r;
+
+ mutex_lock(&chip->mutex);
+ hl = bf_master_half_db(chip->master[out][0]) + delta;
+ hr = bf_master_half_db(chip->master[out][1]) + delta;
+ l = bf_master_16bit(clamp(hl, -128, 12));
+ r = bf_master_16bit(clamp(hr, -128, 12));
+ bf_panel_write_master(chip, out, l, r);
+ 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);
+ }
+
+ /* 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);
+
+ if (chip->shutdown)
+ return;
+ bf_panel_tick(chip);
+ schedule_delayed_work(&chip->panel_work,
+ msecs_to_jiffies(chip->panel_poll_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;
+ 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 2fba16e54..7a708342d 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -8,7 +8,7 @@
*
* See babyfacepro.h for the shared device state and register map,
* and babyfacepro-ctl.c for the ALSA control surface (mixer, front
- * state persistence and card lifecycle).
+ * panel, DSP EQ).
*/
#include <linux/log2.h>
#include <linux/math64.h>
@@ -1168,6 +1168,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.");
@@ -1177,6 +1178,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 ------------------------- */
@@ -1260,6 +1263,7 @@ static int babyface_probe(struct usb_interface *intf,
chip->iface = intf;
chip->nurbs = clamp(nurbs, 1, 16);
chip->frames_per_urb = clamp(frames_per_urb, 8, 1024) & ~7;
+ chip->panel_poll_ms = clamp(panel_poll_ms, 10, 1000);
chip->rate = 48000;
chip->alt = BF_ALT_1;
chip->frame_bytes = 56;
@@ -1268,6 +1272,7 @@ static int babyface_probe(struct usb_interface *intf,
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)
@@ -1409,12 +1414,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",
@@ -1454,6 +1470,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
@@ -1490,6 +1507,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);
@@ -1520,6 +1538,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 113963956..bf370d826 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -68,6 +68,13 @@
#define BF_FRAMES_PER_URB_DEFAULT 256
#define BF_NURBS_DEFAULT 8
+/* 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
+
#define BF_WORDS_PER_FRAME 14 /* 14 x 32-bit words per frame */
/* Consecutive URB errors (CRC/babble/protocol or a failed resubmit)
@@ -145,6 +152,36 @@
#define BF_CROSS_R_FIRST 4
#define BF_CROSS_R_LAST 22
+/* 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
+
/* Preamp state byte (0x17, wIdx 0x003F - full state, verified).
* NOTE 2026-08-26 (cap_reflevel3.pcap): the 0x0C "base" is NOT a
* constant - it is the Instr 3/4 REF-LEVEL bits (bits 2-3, +4dBu =
@@ -198,6 +235,16 @@
#define BF_MASTER_MINUS20_8 0xcb
#define BF_MASTER_MINUS20_16 0x0333
+/* 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
+
/* Crosspoint fader curve: 0 dB = 0x16a0, +6 dB = 0x2d41 (fader curve,
* DIFFERENT from the master 0x4000 top - see CALIBRATION.md).
*/
@@ -300,7 +347,43 @@ struct snd_usb_babyface {
int width; /* width knob -100..+100 */
u16 fx_send; /* FX send level 0..0x1000 */
+ /* DSP EQ (babyfacepro-ctl.c) - 4 analog-input strips, params kept in state */
+
+ /* 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)
+ */
+ 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 */
struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
+ struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */
};
struct bf_saved {
@@ -366,6 +449,7 @@ int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert);
int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split);
int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2);
+void bf_panel_toggle_dim(struct snd_usb_babyface *chip);
int bf_preamp_state_write(struct snd_usb_babyface *chip);
int babyface_create_controls(struct snd_usb_babyface *chip);
int babyface_create_xpoints(struct snd_usb_babyface *chip);
@@ -379,6 +463,10 @@ int bf_gain_max_db(int mic);
int bf_gain_db(int mic, u8 raw); u8 bf_gain_raw(int mic, int db);
/* -- babyfacepro-ctl.c ----------------------- */
+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);
/* -- babyfacepro.c ------------------------ */
void bf_state_save(struct snd_usb_babyface *chip);
--
2.55.0
next prev parent reply other threads:[~2026-09-14 17:23 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-14 17:23 [RFC PATCH v4 0/3] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-09-14 17:23 ` [RFC PATCH v4 1/3] " Ismaïl Bahloul
2026-09-14 17:23 ` Ismaïl Bahloul [this message]
2026-09-14 17:23 ` [RFC PATCH v4 3/3] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-09-16 13:38 ` [RFC PATCH v4 0/3] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Takashi Iwai
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=20260914172334.49074-3-i.bahloul01@gmail.com \
--to=i.bahloul01@gmail.com \
--cc=alsa-devel@alsa-project.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-sound@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=perex@perex.cz \
--cc=tiwai@suse.com \
/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
all inboxes | Powered by JetHome®