Linux USB
 help / color / mirror / Atom feed
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,
	"Ismaïl Bahloul" <i.bahloul01@gmail.com>
Subject: [RFC PATCH 1/1] ALSA: usb: add RME Babyface Pro FS driver (proprietary mode)
Date: Sat, 29 Aug 2026 11:03:33 +0100	[thread overview]
Message-ID: <20260829100333.32933-2-i.bahloul01@gmail.com> (raw)
In-Reply-To: <20260829100333.32933-1-i.bahloul01@gmail.com>

The RME Babyface Pro FS presents two USB personalities: a
class-compliant one already handled by snd-usb-audio, and a
proprietary one (VID 0x2a39, PID 0x3fc0) whose PCM stream runs on
INTERRUPT endpoints (interface 5, ep 0x01 OUT / 0x82 IN) instead of
the class-compliant isochronous path. snd-usb-audio's PCM engine is
isochronous-only and has no interrupt-transfer path, so this mode
cannot be handled as a quirk; it needs a standalone driver, modeled
on snd-usb-caiaq (the existing in-tree precedent for interrupt-based
USB audio streaming).

The proprietary mode is the one worth supporting: it exposes the
full channel count and the hardware DSP mixer that TotalMix FX
drives on Windows/Mac, none of which is reachable in class-compliant
mode. The vendor protocol (control requests, register map, front
panel readback) was reverse-engineered from Windows USB captures and
validated against real hardware; the capture analysis and calibrated
laws are documented alongside the userspace reference implementation
at https://github.com/ismail-bahloul/TuxMix (not part of this
series).

What's included:
 - Interrupt-URB PCM streaming, full-duplex, 2-12 channels, S24_LE,
   9 sample rates 32-192 kHz across 3 USB bandwidth classes.
 - ALSA mixer: 6 output masters + mutes, the 6x14 crosspoint routing
   matrix, 4 mic/instrument preamp gains with phantom power and PAD,
   pitch/varispeed, loopback, and several device-specific toggles
   (AN 1>2, input link, MS processor, DIM, width, FX send).
 - Front-panel emulation: the device has no onboard DSP for its own
   panel, so the host mirrors TotalMix's role, translating physical
   wheel/button events into mixer writes and exposing the decoded
   panel state as read-only ALSA controls.
 - Hardware 3-band + low-cut parametric EQ (4 analog-input strips),
   computed in fixed-point (no FPU use) and uploaded as coefficient
   blocks.
 - Mixer-state persistence across interface re-probes (a userspace
   client can usbfs-claim the interface, silently detaching this
   driver) and system suspend/resume, since the device firmware has
   no state readback of its own.

Split across two files matching the driver's two natural halves:
babyfacepro.c (vendor protocol/cold-init, PCM streaming, state
persistence, card lifecycle) and babyfacepro-ctl.c (the ALSA control
surface: mixer, front panel, EQ).

Validated on real hardware: full-duplex streaming across the whole
rate x period matrix with a signal-integrity tap, start/stop stress
(30 cycles), mixer-state restore across an interface unbind/rebind,
and a mid-stream disconnect, all via the automated regression suite
kept with the driver's development tree. sparse (C=1/C=2), W=1, and
checkpatch --strict are all clean; the driver also builds in-tree
against linux-next with W=1.

Known limitations, stated up front:
 - USB autosuspend is not supported yet and is explicitly disabled
   (usb_disable_autosuspend at probe, balanced at disconnect) rather
   than shipped untested: the front-panel poll and keepalive work
   items run continuously and nothing pairs usb_autopm_get/put around
   the stream. S3 suspend/resume works and is tested; full autosuspend
   (pausing the panel/keepalive work + autopm pairing) is a follow-up.
 - A few protocol items are not fully pinned down but do not affect
   the shipped controls (the relevant paths are hardware-verified);
   documented as open in PROTOCOL.md: the preamp readback index
   semantics (0x003F vs 0x0000), a width strip-ownership edge case,
   and the exact high-frequency warping of the EQ coefficient
   computation vs TotalMix's curve.
 - The latency profile is selected at load time via the frames_per_urb
   / nurbs module params (default 256 frames/URB, matching TotalMix's
   256-sample buffer; frames_per_urb=16 nurbs=16 gives a 0.33 ms
   monitoring-grade floor). Changing profile currently means a module
   reload; a runtime reconfiguration (RME's Fireface USB Settings-style
   switch) is a post-merge follow-up.

Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
 MAINTAINERS                             |    6 +
 sound/usb/Kconfig                       |   18 +
 sound/usb/Makefile                      |    2 +-
 sound/usb/babyfacepro/Makefile          |    2 +
 sound/usb/babyfacepro/babyfacepro-ctl.c | 2782 +++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     | 1449 ++++++++++++
 sound/usb/babyfacepro/babyfacepro.h     |  391 ++++
 7 files changed, 4649 insertions(+), 1 deletion(-)
 create mode 100644 sound/usb/babyfacepro/Makefile
 create mode 100644 sound/usb/babyfacepro/babyfacepro-ctl.c
 create mode 100644 sound/usb/babyfacepro/babyfacepro.c
 create mode 100644 sound/usb/babyfacepro/babyfacepro.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 7291238bc..3a1357492 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23653,6 +23653,12 @@ F:	include/dt-bindings/power/thead,th1520-power.h
 F:	include/dt-bindings/reset/thead,th1520-reset.h
 F:	include/linux/firmware/thead/thead,th1520-aon.h
 
+RME BABYFACE PRO FS DRIVER (PROPRIETARY MODE)
+M:	Ismaïl Bahloul <i.bahloul01@gmail.com>
+L:	alsa-devel@alsa-project.org (moderated for non-subscribers)
+S:	Maintained
+F:	sound/usb/babyfacepro/
+
 RNBD BLOCK DRIVERS
 M:	Md. Haris Iqbal <haris.iqbal@ionos.com>
 M:	Jack Wang <jinpu.wang@ionos.com>
diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig
index b4588915e..14d759814 100644
--- a/sound/usb/Kconfig
+++ b/sound/usb/Kconfig
@@ -204,6 +204,24 @@ config SND_USB_AUDIO_QMI
 	  To compile this driver as a module, choose M here: the module
 	  will be called snd-usb-audio-qmi.
 
+config SND_USB_BABYFACE_PRO
+	tristate "RME Babyface Pro FS (proprietary mode)"
+	select SND_PCM
+	help
+	  Say Y here to include support for the RME Babyface Pro FS in
+	  its proprietary mode (VID 0x2a39, PID 0x3fc0).
+
+	  The proprietary mode streams PCM over interrupt endpoints
+	  (interface 5, ep 0x01/0x82) instead of the class-compliant
+	  isochronous path handled by snd-usb-audio, so this driver is
+	  standalone (snd-usb-caiaq-style interrupt streaming).  It
+	  exposes the PCM stream plus the output masters, mutes, mic
+	  phantom/PAD and preamp gains as standard ALSA controls.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called snd-usb-babyface-pro.
+
+
 source "sound/usb/line6/Kconfig"
 
 endif	# SND_USB
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87..2f83f5881 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -35,5 +35,5 @@ obj-$(CONFIG_SND_USB_UA101) += snd-usbmidi-lib.o
 obj-$(CONFIG_SND_USB_USX2Y) += snd-usbmidi-lib.o
 obj-$(CONFIG_SND_USB_US122L) += snd-usbmidi-lib.o
 
-obj-$(CONFIG_SND) += misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/
+obj-$(CONFIG_SND) += misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/ babyfacepro/
 obj-$(CONFIG_SND_USB_LINE6)	+= line6/
diff --git a/sound/usb/babyfacepro/Makefile b/sound/usb/babyfacepro/Makefile
new file mode 100644
index 000000000..40badfd14
--- /dev/null
+++ b/sound/usb/babyfacepro/Makefile
@@ -0,0 +1,2 @@
+snd-usb-babyface-pro-y := babyfacepro.o babyfacepro-ctl.o
+obj-$(CONFIG_SND_USB_BABYFACE_PRO) += snd-usb-babyface-pro.o
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
new file mode 100644
index 000000000..dfdff9485
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -0,0 +1,2782 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RME Babyface Pro FS — proprietary-mode USB audio driver
+ *
+ * ALSA control surface: mixer (masters, preamp, crosspoints, flags,
+ * 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,
+ * and babyfacepro.c for the core driver (protocol, PCM streaming,
+ * state persistence, card lifecycle).
+ */
+#include <linux/log2.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#include "babyfacepro.h"
+
+const struct bf_source bf_sources[14] = {
+	{ "AN1",     0,  0 },
+	{ "AN2",     1,  1 },
+	{ "AN3",     2,  2 },
+	{ "AN4",     3,  3 },
+	{ "AS1/2",   4,  5 },
+	{ "ADAT3/4", 6,  7 },
+	{ "ADAT5/6", 8,  9 },
+	{ "ADAT7/8", 10, 11 },
+	{ "PB1",    12, 13 },
+	{ "PB2",    14, 15 },
+	{ "PB3",    16, 17 },
+	{ "PB4",    18, 19 },
+	{ "PB5",    20, 21 },
+	{ "PB6",    22, 23 },
+};
+
+/* Crosspoint-map output order vs the master-map order — HARDWARE-
+ * VERIFIED 2026-08-24: the block that feeds the Phones is the FIRST
+ * crosspoint block (0x34), while the Phones master is the SECOND
+ * (0x03E2/0x0006).  The crosspoint map lists the Phones first (the
+ * monitor output); the master map lists AN1/2 first.  Control index =
+ * the canonical order (AN1/2=0, PH3/4=1, ...) so the crosspoint and
+ * master controls line up; this table maps to the register block.
+ */
+const u8 bf_xpoint_block[6] = { 1, 0, 2, 3, 4, 5 };
+
+/* Master-register output order — the master map lists AN1/2 first
+ * (0x03E0) and the Phones master SECOND (0x03E2, HARDWARE-VERIFIED
+ * 2026-08-24); the crosspoint blocks are in the opposite order
+ * (Phones = block 0x34 first, hence bf_xpoint_block above).  Control
+ * index → canonical output (AN1/2=0, PH3/4=1, ...) = the master
+ * register position directly: the names 'AN1/2 Playback Volume' etc.
+ * must match the register they write (corrected 2026-08-26 — the
+ * previous {1,0,...} swap made 'AN1/2' drive the Phones and 'PH3/4'
+ * drive the AN1/2 analog out).
+ */
+static const u8 bf_master_out[6] = { 0, 1, 2, 3, 4, 5 };
+
+/* The 16-bit master value → the 8-bit companion code (0.5 dB/step).
+ * Integer-only: half_db = 12·log2(v/0x2000) via ilog2 + an 8-bit
+ * fractional-octave table (12·log2(1 + n/256), ~0.05 dB resolution —
+ * fine enough for the ±0.5 dB panel wheel to track the round-trip).
+ */
+static const u8 bf_lg2_frac[256] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+	2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+	3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4,
+	4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
+	5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+	6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+	6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+	7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8,
+	8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+	8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+	9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10,
+	10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+	10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+	11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+	11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+};
+
+/* 16-bit master → dB×2 (12 half-dB per octave; 0x2000 = 0 dB).
+ * Shared by the 8-bit companion and the front-panel OUT wheel.
+ */
+int bf_master_half_db(u16 vol16)
+{
+	unsigned int k, frac;
+
+	vol16 = clamp(vol16, 1, 0x4000);
+	k = ilog2(vol16);
+	frac = ((vol16 - (1u << k)) << 8) >> k;
+	return 12 * (int)k - 156 + bf_lg2_frac[frac];
+}
+
+/* dB×2 → 16-bit master (0x2000·2^(half_db/12), rounded).  The
+ * inverse of bf_master_half_db — the 12th-root table 2^(n/12).
+ */
+static const u16 bf_twelfth[12] = {
+	0x1000, 0x10f4, 0x11f6, 0x1307, 0x1429, 0x155c,
+	0x16a1, 0x17f9, 0x1966, 0x1ae9, 0x1c82, 0x1e34,
+};
+
+int bf_master_16bit(int half_db)
+{
+	int k = half_db / 12;
+	int n = half_db % 12;
+	u32 v;
+
+	if (n < 0) {
+		n += 12;
+		k--;
+	}
+	v = (u32)bf_twelfth[n] << 1;	/* 0x2000·2^(n/12) */
+	if (k >= 0) {
+		v <<= k;
+	} else {
+		v += 1u << (-k - 1);	/* round-half-up */
+		v >>= -k;
+	}
+	return (u16)clamp(v, 1, 0x4000);
+}
+
+u8 bf_master_8bit(u16 vol16)
+{
+	if (vol16 == 0)
+		return BF_MASTER_MUTE;
+	return (u8)clamp(0xf3 + bf_master_half_db(vol16), BF_MASTER_8_MIN, 0xff);
+}
+
+/* The cold-init register clear zeroes the mixer registers TotalMix
+ * re-uploads afterwards.  The kernel driver has no saved scene (no
+ * readback for faders), so it applies TotalMix's factory default:
+ * every source routed to every output at unity, masters at 0 dB and
+ * unmuted — the user/TuxMix can restore its own scene on top.
+ */
+int babyface_write_default_mixer(struct snd_usb_babyface *chip)
+{
+	int out, src, ret;
+	u16 flag;
+
+	/* Output masters: 0 dB (0x2000) + the unmute companion (0xf3). */
+	for (out = 0; out < 6; out++) {
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_UNMUTE,
+				      BF_REG_MASTER_8 + 2 * out);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_UNMUTE,
+				      BF_REG_MASTER_8 + 2 * out + 1);
+		if (ret < 0)
+			return ret;
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_MASTER_0DB,
+				      (BF_REG_MASTER_16 + 2 * out) | flag);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_MASTER_0DB,
+				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+		if (ret < 0)
+			return ret;
+		chip->master[out][0] = BF_MASTER_0DB;
+		chip->master[out][1] = BF_MASTER_0DB;
+		chip->muted[out] = false;
+	}
+
+	/* Every source into every output pair, L and R, at 0 dB (the
+	 * standard map; the low map is only a shadow).  The addresses use
+	 * the source's idx_l/idx_r on the canonical block — writing the raw
+	 * index on both bases would put PB1 R on the L side and PB1 L on
+	 * the R side (L+R on both = mono).  The "cross" registers
+	 * (L-reg idx_r / R-reg idx_l) are left at 0; the restore at stream
+	 * start re-writes the same addresses from the cache.
+	 */
+	for (out = 0; out < 6; out++) {
+		unsigned int blk = bf_xpoint_block[out];
+
+		for (src = 0; src < 14; src++) {
+			flag = bf_flag_cycle[chip->flag_cnt];
+			chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+			ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_FADER_0DB,
+					      (BF_REG_CROSS_BASE_L +
+					       BF_REG_CROSS_STRIDE * blk +
+					       bf_sources[src].idx_l) | flag);
+			if (ret < 0)
+				return ret;
+			ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_FADER_0DB,
+					      (BF_REG_CROSS_BASE_R +
+					       BF_REG_CROSS_STRIDE * blk +
+					       bf_sources[src].idx_r) | flag);
+			if (ret < 0)
+				return ret;
+		}
+		ret = bf_crosspoint_clear_cross(chip, blk);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Mirror the defaults into the control cache (14 controls/output). */
+	for (out = 0; out < 6; out++)
+		for (src = 0; src < 14; src++) {
+			chip->xpoint[out][src][0] = BF_FADER_0DB;
+			chip->xpoint[out][src][1] = BF_FADER_0DB;
+		}
+
+	/* Host settings word: clock Internal (0x0001). */
+	return bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0001,
+			       BF_REG_KEEPALIVE_SETTINGS);
+}
+
+/* The device resets its output masters to mute when a stream session
+ * starts (hardware-verified 2026-08-24: after a stream start the
+ * output stays silent until a master write lands — only a write
+ * un-mutes the 8-bit register).  Re-apply the six output masters +
+ * mutes from the cache; also used by the PM restore path.
+ */
+int bf_apply_masters(struct snd_usb_babyface *chip)
+{
+	int out, ret;
+	u16 flag;
+
+	for (out = 0; out < 6; out++) {
+		u16 l = chip->muted[out] ? 0 : chip->master[out][0];
+		u16 r = chip->muted[out] ? 0 : chip->master[out][1];
+		u8 l8 = chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(l);
+		u8 r8 = chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(r);
+
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, l8,
+				      BF_REG_MASTER_8 + 2 * out);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, r8,
+				      BF_REG_MASTER_8 + 2 * out + 1);
+		if (ret < 0)
+			return ret;
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+				      (BF_REG_MASTER_16 + 2 * out) | flag);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+/* ── mixer controls ──────────────────────── */
+
+/* dB TLV for the output masters: 0x2000 = 0 dB, 0x4000 = +6 dB
+ * (CALIBRATION.md) with the hardware 20*log10(v/0x2000) law — the raw
+ * 16-bit value IS the linear amplitude.  WirePlumber needs this to map
+ * the volume 1:1 to the hardware control instead of applying a software
+ * volume on top (which left the output ~30 dB down).
+ */
+static const DECLARE_TLV_DB_RANGE(bf_master_tlv,
+	0, 0x2000, TLV_DB_LINEAR_ITEM(-6500, 0),
+	0x2000, 0x4000, TLV_DB_LINEAR_ITEM(0, 600)
+);
+
+static int bf_master_info(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 2;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 0x4000;	/* +6 dB = 2 × 0dB(0x2000) */
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_master_get(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = bf_master_out[kctl->private_value];
+
+	ucontrol->value.integer.value[0] = chip->master[out][0];
+	ucontrol->value.integer.value[1] = chip->master[out][1];
+	return 0;
+}
+
+static int bf_master_put(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = bf_master_out[kctl->private_value];
+	u16 l = ucontrol->value.integer.value[0];
+	u16 r = ucontrol->value.integer.value[1];
+	u16 flag;
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (l == chip->master[out][0] && r == chip->master[out][1])
+		goto out;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+	/* The 8-bit register is the real volume; the 16-bit is its
+	 * companion (kept in sync like TotalMix).
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l),
+			      BF_REG_MASTER_8 + 2 * out);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r),
+			      BF_REG_MASTER_8 + 2 * out + 1);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			      (BF_REG_MASTER_16 + 2 * out) | flag);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	if (ret < 0)
+		goto out;
+
+	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 point. */
+	if (chip->dim && out == 1) {
+		chip->dim_saved[0] = l;
+		chip->dim_saved[1] = r;
+	}
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_mute_info(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 2;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_mute_get(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = bf_master_out[kctl->private_value];
+
+	/* ALSA convention: 1 = enabled (sound on) = not muted. */
+	ucontrol->value.integer.value[0] = !chip->muted[out];
+	ucontrol->value.integer.value[1] = !chip->muted[out];
+	return 0;
+}
+
+static int bf_mute_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = bf_master_out[kctl->private_value];
+	bool muted = !ucontrol->value.integer.value[0];
+	u16 flag;
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (muted == chip->muted[out])
+		goto out;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+	if (muted) {
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+				      BF_REG_MASTER_8 + 2 * out);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+				      BF_REG_MASTER_8 + 2 * out + 1);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+				      (BF_REG_MASTER_16 + 2 * out) | flag);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+		if (ret < 0)
+			goto out;
+	} else {
+		/* Unmute restores the cached volume (TotalMix keeps the
+		 * pre-mute fader value host-side), 8-bit + 16-bit.
+		 */
+		ret = bf_vendor_write(chip, BF_REQ_GAIN,
+				      bf_master_8bit(chip->master[out][0]),
+				      BF_REG_MASTER_8 + 2 * out);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN,
+				      bf_master_8bit(chip->master[out][1]),
+				      BF_REG_MASTER_8 + 2 * out + 1);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->master[out][0],
+				      (BF_REG_MASTER_16 + 2 * out) | flag);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->master[out][1],
+				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+		if (ret < 0)
+			goto out;
+	}
+	chip->muted[out] = muted;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+int bf_preamp_state_write(struct snd_usb_babyface *chip)
+{
+	int ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, chip->preamp, BF_REG_PREAMP);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+}
+
+/* ── crosspoint matrix (6 outputs × 14 sources) ────────────── */
+
+static int bf_xpoint_info(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 2;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = BF_FADER_TOP;	/* +6 dB fader top */
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_xpoint_get(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value >> 8;
+	int src = kctl->private_value & 0xff;
+
+	ucontrol->value.integer.value[0] = chip->xpoint[out][src][0];
+	ucontrol->value.integer.value[1] = chip->xpoint[out][src][1];
+	return 0;
+}
+
+static int bf_xpoint_put(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value >> 8;
+	int src = kctl->private_value & 0xff;
+	unsigned int blk = bf_xpoint_block[out];
+	const struct bf_source *s = &bf_sources[src];
+	u16 l = ucontrol->value.integer.value[0];
+	u16 r = ucontrol->value.integer.value[1];
+	u16 flag;
+	int ret = 0;
+
+	if (l > BF_FADER_TOP || r > BF_FADER_TOP)
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if (l == chip->xpoint[out][src][0] && r == chip->xpoint[out][src][1])
+		goto out;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+	/* L register = 0x0034 + 0x34·blk + idx, R = 0x004E + 0x34·blk + idx
+	 * (mono sources use the same idx on both sides).
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			      (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+			       s->idx_l) | flag);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			      (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+			       s->idx_r) | flag);
+	if (ret < 0)
+		goto out;
+
+	chip->xpoint[out][src][0] = l;
+	chip->xpoint[out][src][1] = r;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+int babyface_create_xpoints(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int out, src, err;
+
+	for (out = 0; out < 6; out++) {
+		for (src = 0; src < 14; src++) {
+			kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+				.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+				.name = "Playback Volume",
+				.index = out * 14 + src,
+				.info = bf_xpoint_info,
+				.get = bf_xpoint_get,
+				.put = bf_xpoint_put,
+				.private_value = (out << 8) | src,
+			}, chip);
+			/* Name the control by its source: "AN1 Playback Volume",
+			 * "PB1 Playback Volume"... with a unique index.
+			 */
+			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);
+			if (err < 0)
+				return err;
+		}
+	}
+	return 0;
+}
+
+/* ── flags / special controls (pitch, loopback, link, width, FX) ── */
+
+static int bf_switch_info(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_pitch_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 = -50;		/* -5.0 % */
+	uinfo->value.integer.max = 50;		/* +5.0 % */
+	uinfo->value.integer.step = 1;		/* 0.1 % */
+	return 0;
+}
+
+static int bf_pitch_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->pitch;
+	return 0;
+}
+
+static int bf_pitch_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int p = ucontrol->value.integer.value[0];
+	u32 dds24, dds16;
+	u16 frac, b1, b2;
+	int ret = 0;
+
+	if (p < -50 || p > 50)
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if (p == chip->pitch)
+		goto out;
+
+	/* The 0x1B DDS quad (16.8 fixed point, banked).  p is 0.1 % steps:
+	 * DDS_24 = round(50000·256/(1+p/1000)) = round(12800000000/(1000+p)).
+	 */
+	dds24 = (12800000000u + (u32)(1000 + p) / 2) / (u32)(1000 + p);
+	dds16 = dds24 >> 8;
+	frac = dds24 & 0xff;
+	b1 = (u16)((dds16 * 72562ull + 50000) / 100000);
+	b2 = (u16)((dds16 * 2 + 1) / 3);
+
+	ret = bf_vendor_write(chip, BF_REQ_DDS, (u16)dds16, (frac << 8) | 0);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_DDS, b1, 0x0001);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_DDS, b2, 0x0002);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_DDS, 0x7cff, 0x0003);
+	if (ret < 0)
+		goto out;
+	/* Every quad must be followed by the clock keepalive. */
+	ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0001,
+			      BF_REG_KEEPALIVE_SETTINGS);
+	if (ret < 0)
+		goto out;
+
+	chip->pitch = p;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_loopback_get(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->loopback[out];
+	ucontrol->value.integer.value[1] = chip->loopback[out];
+	return 0;
+}
+
+/* Write the full 30-channel loopback map: pair (2·out, 2·out+1) at
+ * `on` (0x0001/0x0000), all other channels cleared — exactly what
+ * TotalMix sends on every loopback toggle (cap_loopback2.pcap).  The
+ * full-map write is also the reliable OFF (the old per-pair write
+ * sometimes failed to disengage on the hardware).
+ */
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out,
+			  bool on)
+{
+	int ch, ret;
+
+	for (ch = 0; ch < BF_LOOPBACK_CHANNELS; ch++) {
+		u16 val = (on && (ch == out * 2 || ch == out * 2 + 1))
+			  ? 0x0001 : 0x0000;
+
+		ret = bf_vendor_write(chip, BF_REQ_LOOPBACK, val, ch);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+static int bf_loopback_put(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value;
+	bool on = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (on == chip->loopback[out])
+		goto out;
+	ret = bf_loopback_write_map(chip, out, on);
+	if (ret < 0)
+		goto out;
+	/* Single-active model (TotalMix writes one pair at 0x0001, the
+	 * rest 0x0000): toggling one output clears the others.
+	 */
+	memset(chip->loopback, 0, sizeof(chip->loopback));
+	chip->loopback[out] = on;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_an12_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->an12;
+	return 0;
+}
+
+static int bf_an12_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool an12 = ucontrol->value.integer.value[0];
+	u16 v;
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (an12 == chip->an12)
+		goto out;
+	v = (chip->linked ? 0x0400 : 0x0000) | (an12 ? 0x1000 : 0x0000);
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+	if (ret < 0)
+		goto out;
+	chip->an12 = an12;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_link_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->linked;
+	return 0;
+}
+
+static int bf_link_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool linked = ucontrol->value.integer.value[0];
+	u16 v;
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (linked == chip->linked)
+		goto out;
+	v = (linked ? 0x0400 : 0x0000) | (chip->an12 ? 0x1000 : 0x0000);
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+	if (ret < 0)
+		goto out;
+	chip->linked = linked;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_ms_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->ms_proc;
+	return 0;
+}
+
+/* MS-proc: engage per the cap_ms2.pcap ON pattern — write 0x0000 to
+ * ALL FOUR AN2 (side) crosspoints: standard map 0x0035/0x004F (L/R)
+ * + low map 0x0001/0x001B (L/R) — the side path is muted (ear-
+ * verified 2026-08-26 with the mic on AN2: MS ON = silence); release
+ * restores the cached fader values (host-side, like TotalMix).
+ * (The 0x1000/0x0004 writes are the DISENGAGE restore values seen in
+ * cap_ms2 — the driver had them inverted on the engage path.)
+ */
+static int bf_ms_put(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool on = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (on == chip->ms_proc)
+		goto out;
+	if (on) {
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+		if (ret < 0)
+			goto out;
+	} else {
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->xpoint[1][1][0], 0x0001);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->xpoint[1][1][0], 0x0035);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->xpoint[1][1][1], 0x001b);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->xpoint[1][1][1], 0x004f);
+		if (ret < 0)
+			goto out;
+	}
+	chip->ms_proc = on;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+/* DIM — cap_dim2.pcap: an absolute -20 dB on the Phones master
+ * (out 1: 8-bit 0xCB / 16-bit 0x0333) regardless of the current level,
+ * plus the 0x17 wVal=0x2000 wIdx=0x2000 flag; release restores the
+ * pre-DIM master host-side.  The master cache keeps the real volume.
+ */
+static int bf_dim_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->dim;
+	return 0;
+}
+
+static int bf_dim_put(struct snd_kcontrol *kctl,
+		      struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool on = ucontrol->value.integer.value[0];
+	u16 flag;
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (on == chip->dim)
+		goto out;
+	if (on) {
+		chip->dim_saved[0] = chip->master[1][0];
+		chip->dim_saved[1] = chip->master[1][1];
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+				      BF_REG_MASTER_8 + 2 * 1);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+				      BF_REG_MASTER_8 + 2 * 1 + 1);
+		if (ret < 0)
+			goto out;
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+				      (BF_REG_MASTER_16 + 2 * 1) | flag);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+				      (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000);
+		if (ret < 0)
+			goto out;
+	} else {
+		ret = bf_vendor_write(chip, BF_REQ_GAIN,
+				      bf_master_8bit(chip->dim_saved[0]),
+				      BF_REG_MASTER_8 + 2 * 1);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN,
+				      bf_master_8bit(chip->dim_saved[1]),
+				      BF_REG_MASTER_8 + 2 * 1 + 1);
+		if (ret < 0)
+			goto out;
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->dim_saved[0],
+				      (BF_REG_MASTER_16 + 2 * 1) | flag);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				      chip->dim_saved[1],
+				      (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+		if (ret < 0)
+			goto out;
+		ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x0000, 0x2000);
+		if (ret < 0)
+			goto out;
+	}
+	chip->dim = on;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_width_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 = -100;
+	uinfo->value.integer.max = 100;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_width_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->width;
+	return 0;
+}
+
+static int bf_width_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int w = ucontrol->value.integer.value[0];
+	u16 l, r;
+	int ret = 0;
+
+	if (w < -100 || w > 100)
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if (w == chip->width)
+		goto out;
+	/* Width spread: L = 0x1000·(1+w), R = 0x1000·(1−w), L+R = 0x2000.
+	 * TotalMix writes the strip's src pair on BOTH maps (cap_width3-7,
+	 * PROTOCOL.md “Width strip mapping”): the low map (0x0000+src L /
+	 * 0x001A+src R) and the std block-0 map (0x0034+src L /
+	 * 0x004E+src R) — the stereo pair spreads L/R in opposition, the
+	 * mirror src (AN2) gets the swapped values.
+	 */
+	l = (u16)(((0x2000 * (100 + w) / 2) + 50) / 100);
+	r = 0x2000 - l;
+	/* Low map: AN1 L=0x0000, R=0x001A; AN2 L=0x0001, R=0x001B. */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+	if (ret < 0)
+		goto out;
+	/* Std block-0 map (item 0b, the missing half): AN1 L=0x0034,
+	 * R=0x004E; AN2 L=0x0035, R=0x004F.  (The playback strips PB2-6
+	 * target block n−2 — 0x00AE family — reserved for the per-strip
+	 * controls.)
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0034);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x004e);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0035);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x004f);
+	if (ret < 0)
+		goto out;
+	chip->width = w;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int bf_fx_send_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 = 0x1000;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_fx_send_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->fx_send;
+	return 0;
+}
+
+static int bf_fx_send_put(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	u16 v = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	if (v > 0x1000)
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if (v == chip->fx_send)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0138);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0153);
+	if (ret < 0)
+		goto out;
+	chip->fx_send = v;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+int babyface_create_flags(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int i, err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Varispeed Pitch",
+		.info = bf_pitch_info,
+		.get = bf_pitch_get,
+		.put = bf_pitch_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	for (i = 0; i < 6; i++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Loopback Switch",
+			.index = i,
+			.info = bf_mute_info,
+			.get = bf_loopback_get,
+			.put = bf_loopback_put,
+			.private_value = i,
+		}, chip);
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "AN 1>2 Switch",
+		.info = bf_switch_info,
+		.get = bf_an12_get,
+		.put = bf_an12_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "AN1/2 Link Switch",
+		.info = bf_switch_info,
+		.get = bf_link_get,
+		.put = bf_link_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "MS Processor Switch",
+		.info = bf_switch_info,
+		.get = bf_ms_get,
+		.put = bf_ms_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Dim Switch",
+		.info = bf_switch_info,
+		.get = bf_dim_get,
+		.put = bf_dim_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Width",
+		.info = bf_width_info,
+		.get = bf_width_get,
+		.put = bf_width_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "FX Send Volume",
+		.info = bf_fx_send_info,
+		.get = bf_fx_send_get,
+		.put = bf_fx_send_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	return 0;
+}
+
+static int bf_bool_info(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_phantom_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->preamp & kctl->private_value);
+	return 0;
+}
+
+static int bf_phantom_put(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	u16 bit = kctl->private_value;
+	bool on = ucontrol->value.integer.value[0];
+	bool cur = !!(chip->preamp & bit);
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (on == cur)
+		goto out;
+	chip->preamp = on ? (chip->preamp | bit) : (chip->preamp & ~bit);
+	ret = bf_preamp_state_write(chip);
+	if (ret < 0)
+		goto out;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+/* Gain scales: the mic preamps (AN1/2) span 0-65 dB over raw 0-20
+ * (3.25 dB/step); the Hi-Z instrument inputs (AN3/4) are digitally
+ * limited to 9 dB over raw 0-18 (0.5 dB/step) — manual §10, raw
+ * ranges verified from cap_gain12/cap_gain34.pcap.  Shared by the GUI
+ * controls and the front-panel gain wheel.
+ */
+int bf_gain_max_db(int mic)
+{
+	return mic < 2 ? BF_GAIN_MAX_DB : 9;
+}
+
+int bf_gain_db(int mic, u8 raw)
+{
+	return mic < 2 ? (raw * 13) / 4 : raw / 2;
+}
+
+u8 bf_gain_raw(int mic, int db)
+{
+	return mic < 2 ? (db * 8 + 13) / 26 : db * 2;
+}
+
+static int bf_gain_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_gain_max_db(kctl->private_value);
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_gain_get(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+
+	/* chip->gain[] tracks the dB (the raw is derived at write time —
+	 * the 3.25 dB/step mic grid would otherwise make a ±1 dB wheel
+	 * stick on a raw boundary).
+	 */
+	ucontrol->value.integer.value[0] = chip->gain[mic];
+	return 0;
+}
+
+static int bf_gain_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+	int db = ucontrol->value.integer.value[0];
+	u8 raw, counter;
+	int ret = 0;
+
+	if (db < 0 || db > bf_gain_max_db(mic))
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if (db == chip->gain[mic])
+		goto out;
+	raw = bf_gain_raw(mic, db);
+	counter = (chip->gain_cycle % 3 == 0) ? 0x20 :
+		  (chip->gain_cycle % 3 == 1) ? 0x00 : 0x40;
+	chip->gain_cycle = (chip->gain_cycle + 1) % 3;
+
+	ret = bf_vendor_write(chip, BF_REQ_GAIN,
+			      (u16)((raw & 0x1f) | counter),
+			      BF_REG_GAIN + mic);
+	if (ret < 0)
+		goto out;
+	chip->gain[mic] = db;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+int babyface_create_controls(struct snd_usb_babyface *chip)
+{
+	static const char * const out_names[6] = {
+		"AN1/2", "PH3/4", "AS1/2", "ADAT3/4", "ADAT5/6", "ADAT7/8"
+	};
+	struct snd_kcontrol *kctl;
+	int i, err;
+
+	for (i = 0; i < 6; i++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = out_names[i],
+			.index = i,
+			.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+				  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+			.info = bf_master_info,
+			.get = bf_master_get,
+			.put = bf_master_put,
+			.tlv.p = bf_master_tlv,
+			.private_value = i,
+		}, chip);
+		strlcat(kctl->id.name, " Playback Volume", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = out_names[i],
+			.index = i,
+			.info = bf_mute_info,
+			.get = bf_mute_get,
+			.put = bf_mute_put,
+			.private_value = i,
+		}, chip);
+		strlcat(kctl->id.name, " Playback Switch", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+
+		dev_dbg(&chip->dev->dev, "output %d = %s\n", i, out_names[i]);
+	}
+
+	for (i = 0; i < 2; i++) {
+		u16 bit = i == 0 ? BF_PREAMP_48V_MIC1 : BF_PREAMP_48V_MIC2;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Phantom Power Mic 1",
+			.index = i,
+			.info = bf_bool_info,
+			.get = bf_phantom_get,
+			.put = bf_phantom_put,
+			.private_value = bit,
+		}, chip);
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (i = 0; i < 2; i++) {
+		u16 bit = i == 0 ? BF_PREAMP_PAD_MIC1 : BF_PREAMP_PAD_MIC2;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Pad Mic 1",
+			.index = i,
+			.info = bf_bool_info,
+			.get = bf_phantom_get,
+			.put = bf_phantom_put,
+			.private_value = bit,
+		}, chip);
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (i = 0; i < 4; i++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Mic 1 Capture Volume",
+			.index = i,
+			.info = bf_gain_info,
+			.get = bf_gain_get,
+			.put = bf_gain_put,
+			.private_value = i,
+		}, chip);
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+	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).
+ * dB stored ×2 (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 × 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 → dB×2 (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;
+}
+
+/* dB×2 → 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;
+}
+
+/* MIX-mode VU display law — monitoring dB×2 → 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 §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 §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 §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 = chip->master[out][0];
+	u16 r = chip->master[out][1];
+	int bal;		/* −100..+100; + = image right (left varies) */
+	u16 fixed, varied;
+
+	mutex_lock(&chip->mutex);
+	/* 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 §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 §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 §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);
+
+	/* 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;
+}
+
+#define BF_EQ_Q27		(1 << 27)
+#define BF_EQ_LC_OFF		0x04000000
+#define BF_EQ_BLOCK_LEN		64
+
+/* atan(2^-i) x 2^27 (CORDIC). */
+static const s64 bf_atan_tab[28] = {
+	0x6487ED5, 0x3B58CE1, 0x1F5B760, 0xFEADD5,
+	0x7FD56F, 0x3FFAAB, 0x1FFF55, 0xFFFEB,
+	0x7FFFD, 0x40000, 0x20000, 0x10000,
+	0x8000, 0x4000, 0x2000, 0x1000,
+	0x800, 0x400, 0x200, 0x100,
+	0x80, 0x40, 0x20, 0x10,
+	0x8, 0x4, 0x2, 0x1,
+};
+
+/* ---- fixed-point helpers (Q27 in/out, s64 intermediates) ---- */
+
+/* sin/cos of an angle in [0, pi/2] (Q27).  Simultaneous CORDIC, 28
+ * iterations (~1e-8 residual).  eq_selftest.c verifies the whole
+ * pipeline against the double-precision reference.
+ */
+static void bf_sincos(s64 ang, s64 *sn, s64 *cs)
+{
+	s64 x = 0x4DBA76D;	/* 1/1.64676 x 2^27 (CORDIC gain) */
+	s64 y = 0;
+	s64 z = ang;
+	int i;
+
+	for (i = 0; i < 28; i++) {
+		s64 d = z >= 0 ? 1 : -1;
+		s64 nx = x - d * (y >> i);
+		s64 ny = y + d * (x >> i);
+
+		x = nx;
+		y = ny;
+		z -= d * bf_atan_tab[i];
+	}
+	*cs = x;
+	*sn = y;
+}
+
+/* 2^u for u in Q27, u in [-2, 2] (gain-amplitude range). */
+static s64 bf_exp2(s64 u)
+{
+	s64 n = u >> 27;
+	s64 r = u - (n << 27);
+	s64 rl = (r * 0x58B90C0 + (1 << 26)) >> 27;	/* r.ln2 */
+	s64 e = BF_EQ_Q27;
+	s64 term = BF_EQ_Q27;
+	int k;
+
+	for (k = 1; k <= 10; k++) {
+		term = ((term * rl + (1 << 26)) >> 27) / k;
+		e += term;
+	}
+	return n >= 0 ? e << n : e >> -n;
+}
+
+/* The 5 stored words (c0..c3 + shared c4) for one band.
+ * type: 1 bell, 2 low shelf, 3 high shelf.  freq_hz, fs in Hz;
+ * q100 = Q x 100; gain_x10 = dB x 10.  fs is the stream rate.
+ */
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+		      s32 gain_x10, s32 fs)
+{
+	s64 f = freq_hz;
+	s64 w0, c, s, alpha, A, sq;
+	s64 b0, b1, b2, a0, a1, a2;
+	s64 pi = 0x1921FB54;	/* pi, Q27 */
+	s64 hpi = 0xC90FDAA;	/* pi/2, Q27 */
+	s64 t;
+	int both = 0, cflip = 0;
+
+	if (gain_x10 == 0 || q100 <= 0) {
+		/* Inactive band: identity words (also guards the alpha
+		 * division below against the default Q=0 the controls start
+		 * with — a user setting gain before Q used to hit a kernel
+		 * divide-by-zero oops).
+		 */
+		w[0] = 0;
+		w[1] = 0;
+		w[2] = 0;
+		w[3] = 0;
+		return;
+	}
+
+	/* w0 = 2.pi.f/fs (Q27), reduced to [0, pi/2]. */
+	w0 = (f * BF_EQ_Q27) / fs;
+	w0 = (w0 * 0x3243F6A9) >> 27;	/* x 2.pi */
+	t = w0;
+	if (t > pi) {
+		t -= pi;
+		both = 1;
+	}
+	if (t > hpi) {
+		t = pi - t;
+		cflip = 1;
+	}
+	bf_sincos(t, &s, &c);
+	if (both) {
+		s = -s;
+		c = -c;
+	}
+	if (cflip)
+		c = -c;
+
+	alpha = (s * 100 + q100) / (2 * (s64)q100);	/* sin(w0)/(2Q) */
+	/* A = 10^(g/40), sqrt(A): g = gain_x10/10 dB */
+	A = bf_exp2((s64)gain_x10 * 0x11021E);
+	sq = bf_exp2((s64)gain_x10 * 0x8810F);
+
+	if (type == 1) {
+		s64 ta = (alpha * A + (1 << 26)) >> 27;
+
+		b0 = BF_EQ_Q27 + ta;
+		b1 = -2 * c;
+		b2 = BF_EQ_Q27 - ta;
+		a0 = BF_EQ_Q27 + (alpha * BF_EQ_Q27 + A / 2) / A;
+		a1 = -2 * c;
+		a2 = BF_EQ_Q27 - (alpha * BF_EQ_Q27 + A / 2) / A;
+	} else {
+		s64 ap1 = A + BF_EQ_Q27;
+		s64 am1 = A - BF_EQ_Q27;
+		s64 cp0 = (am1 * c + (1 << 26)) >> 27;	/* (A-1).c */
+		s64 cp1 = (ap1 * c + (1 << 26)) >> 27;	/* (A+1).c */
+		s64 ab = (2 * sq * alpha + (1 << 26)) >> 27;
+
+		if (type == 2) {	/* low shelf */
+			b0 = (A * (ap1 - cp0 + ab) + (1 << 26)) >> 27;
+			b1 = (2 * A * (am1 - cp1) + (1 << 26)) >> 27;
+			b2 = (A * (ap1 - cp0 - ab) + (1 << 26)) >> 27;
+			a0 = ap1 + cp0 + ab;
+			a1 = -2 * (am1 + cp1);
+			a2 = ap1 + cp0 - ab;
+		} else {		/* high shelf */
+			b0 = (A * (ap1 + cp0 + ab) + (1 << 26)) >> 27;
+			b1 = (-2 * A * (am1 + cp1) + (1 << 26)) >> 27;
+			b2 = (A * (ap1 + cp0 - ab) + (1 << 26)) >> 27;
+			a0 = ap1 - cp0 + ab;
+			a1 = -2 * (am1 - cp1);
+			a2 = ap1 - cp0 - ab;
+		}
+	}
+
+	w[0] = (s32)((a1 * BF_EQ_Q27 + a0 / 2) / a0);
+	w[1] = (s32)((a2 * BF_EQ_Q27 + a0 / 2) / a0);
+	w[2] = (s32)((b1 * BF_EQ_Q27 + b0 / 2) / b0);
+	w[3] = (s32)((b2 * BF_EQ_Q27 + b0 / 2) / b0);
+	w[4] = (s32)((b0 * BF_EQ_Q27 + a0 / 2) / a0);
+}
+
+/* ---- low cut ---- */
+
+/* Slope byte: 2^n-1 (n poles) -> 6/12/18/24 dB per oct; 0 = off. */
+static u8 bf_eq_lc_slope_byte(s32 slope_db)
+{
+	switch (slope_db) {
+	case 6:  return 0x01;
+	case 12: return 0x03;
+	case 18: return 0x07;
+	case 24: return 0x0F;
+	}
+	return 0;
+}
+
+/* The 0x38 low-cut frequency word: round(K.f'.(11656)/(11656+f')) with
+ * K = 11508, f' = f x slope-compensation factor (cap_eq9 fit, 0.003%;
+ * the slope factor keeps the composite -3 dB point constant).
+ */
+static u32 bf_eq_lc_freq_raw(s32 freq_hz, s32 slope_db)
+{
+	s64 f, word;
+
+	if (freq_hz <= 0)
+		return BF_EQ_LC_OFF;
+	f = freq_hz;
+	switch (slope_db) {
+	case 6:
+		f = f * 15267 / 10000;
+		break;
+
+	case 18:
+		f = f * 8061 / 10000;
+		break;
+
+	case 24:
+		f = f * 6977 / 10000;
+		break;
+	}
+	word = (11508 * f * 11656 + (11656 + f) / 2) / (11656 + f);
+	return (u32)word;
+}
+
+/* ---- block build + bulk write ---- */
+
+static void bf_eq_build_block(u8 *b, int ch, u8 slope,
+			      const s32 bands[3][4], s32 shared, u32 lc)
+{
+	int slot, k;
+
+	memset(b, 0, BF_EQ_BLOCK_LEN);
+	b[0] = ch;
+	b[1] = slope;
+	b[2] = ch;
+	b[3] = 0x80;	/* EQ engine active */
+	for (slot = 0; slot < 3; slot++) {
+		for (k = 0; k < 4; k++) {
+			put_unaligned_le32((u32)bands[slot][k],
+					   b + 0x04 + slot * 0x10 + 4 * k);
+		}
+	}
+	put_unaligned_le32((u32)shared, b + 0x34);
+	put_unaligned_le32(lc, b + 0x38);
+}
+
+/* Upload one 64-byte block on bulk OUT ep 0x0A (interface 1). */
+static int bf_eq_upload(struct snd_usb_babyface *chip, const u8 *block)
+{
+	u8 *buf;
+	int ret, len;
+
+	/* usb_bulk_msg DMA-maps the buffer: it must not be on the stack
+	 * (usb_hcd_map_urb_for_dma returns -EAGAIN for stack buffers).
+	 */
+	buf = kmemdup(block, BF_EQ_BLOCK_LEN, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+	ret = usb_bulk_msg(chip->dev, usb_sndbulkpipe(chip->dev, 0x0a),
+			   buf, BF_EQ_BLOCK_LEN, &len, 1000);
+	kfree(buf);
+	if (ret < 0)
+		dev_err(&chip->dev->dev, "EQ bulk upload failed: %d\n", ret);
+	return ret;
+}
+
+/* Write the L+R block pair for one strip (channel base = strip x 2). */
+static int bf_eq_write_strip(struct snd_usb_babyface *chip, int strip)
+{
+	struct bf_eq_channel *e = &chip->eq[strip];
+	u8 b[BF_EQ_BLOCK_LEN];
+	s32 identity[3][4] = { { 0 }, { 0 }, { 0 } };
+	s32 shared = e->on ? e->shared : BF_EQ_Q27;
+	u32 lc = e->on ? e->lc_raw : BF_EQ_LC_OFF;
+	/* The header slope byte (b[1]) is only valid while the low cut is
+	 * engaged: a stale slope with 0x38 = off made the device apply a
+	 * garbage-frequency cut (ear-verified: "low cut off" left only
+	 * highs).  cap_eq7: byte1 = 0x00 + 0x38 = 0x04000000 when off.
+	 */
+	u8 slope = (e->on && e->lc_hz > 0) ? e->slope : 0;
+	int ch, ret;
+
+	for (ch = 0; ch < 2; ch++) {
+		bf_eq_build_block(b, strip * 2 + ch, slope,
+				  e->on ? e->words : identity, shared, lc);
+		ret = bf_eq_upload(chip, b);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+/* Recompute one strip's words + low cut from its params, re-upload.
+ * Lock-free by convention: every caller must already hold chip->mutex
+ * (bf_eq_put() and bf_eq_reupload() do) — asserting it here catches a
+ * future caller that forgets, instead of a silent self-deadlock.
+ */
+static void bf_eq_update_strip(struct snd_usb_babyface *chip, int strip)
+{
+	struct bf_eq_channel *e = &chip->eq[strip];
+	s32 fs = chip->rate ? chip->rate : 48000;
+	s32 last_c4 = BF_EQ_Q27;
+	int band, i;
+
+	lockdep_assert_held(&chip->mutex);
+
+	for (band = 0; band < 3; band++) {
+		s32 w[5];
+
+		bf_eq_band_words(w, e->band_type[band], e->band_freq[band],
+				 e->band_q[band], e->band_gain[band], fs);
+		for (i = 0; i < 4; i++)
+			e->words[band][i] = w[i];
+		if (e->band_type[band] && e->band_gain[band])
+			last_c4 = w[4];	/* shared scale: the last band */
+	}
+	e->shared = last_c4;
+	e->lc_raw = bf_eq_lc_freq_raw(e->lc_hz, e->slope_db);
+	e->slope = bf_eq_lc_slope_byte(e->slope_db);
+	bf_eq_write_strip(chip, strip);
+}
+
+/* Recompute + re-upload all four strips (rate change). Caller must
+ * hold chip->mutex — bf_eq_update_strip()/bf_eq_write_strip() are
+ * lock-free by convention (see bf_eq_put()) and the only caller,
+ * babyface_pcm_hw_params(), already holds the lock across the rate
+ * change; locking here too self-deadlocked it (hung-task: "blocked
+ * on a mutex likely owned by" itself, hit via regress.sh's rate
+ * sweep).
+ */
+void bf_eq_reupload(struct snd_usb_babyface *chip)
+{
+	int strip;
+
+	for (strip = 0; strip < 4; strip++)
+		bf_eq_update_strip(chip, strip);
+}
+
+/* ---- ALSA controls (4 strips x 19 controls) ---- */
+
+#define EQ_STRIP(pv)	((pv) >> 8)
+#define EQ_PARAM(pv)	((pv) & 0xff)
+/* params: 0 enable, 1-3 type, 4-6 freq, 7-9 q, 10-12 gain, 13 lc freq, 14 lc slope */
+
+static const char *const bf_eq_type_texts[] = {
+	"Off", "Bell", "Low Shelf", "High Shelf", NULL
+};
+
+static const char *const bf_eq_slope_texts[] = {
+	"6 dB/oct", "12 dB/oct", "18 dB/oct", "24 dB/oct", NULL
+};
+
+static int bf_eq_info(struct snd_kcontrol *kctl,
+		      struct snd_ctl_elem_info *uinfo)
+{
+	int param = EQ_PARAM(kctl->private_value);
+
+	if (param == 0) {
+		uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+		uinfo->count = 1;
+		return 0;
+	}
+	if (param == 1 || param == 2 || param == 3)
+		return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_type_texts);
+	if (param == 14)
+		return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_slope_texts);
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = (param == 10 || param == 11 ||
+				    param == 12) ? -240 :
+				   (param == 7 || param == 8 ||
+				    param == 9) ? 5 : 0;
+	uinfo->value.integer.max = (param == 7 || param == 8 ||
+				    param == 9) ? 1000 :
+				   (param == 10 || param == 11 ||
+				    param == 12) ? 240 : 20000;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_eq_get(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	struct bf_eq_channel *e = &chip->eq[EQ_STRIP(kctl->private_value)];
+	int param = EQ_PARAM(kctl->private_value);
+	int band = (param - 1) % 3;
+	s32 *v = NULL;
+
+	switch (param) {
+	case 0:
+		v = (s32 *)&e->on;
+		break;
+
+	case 1:
+	case 2:
+	case 3:
+		v = &e->band_type[band];
+		break;
+
+	case 4:
+	case 5:
+	case 6:
+		v = &e->band_freq[band];
+		break;
+
+	case 7:
+	case 8:
+	case 9:
+		v = &e->band_q[band];
+		break;
+
+	case 10:
+	case 11:
+	case 12:
+		v = &e->band_gain[band];
+		break;
+
+	case 13:
+		v = &e->lc_hz;
+		break;
+
+	case 14:
+		v = &e->slope_db;
+		break;
+	}
+	if (param == 14) {
+		/* Inverse of put's index->dB map: slope_db stores the raw
+		 * 6/12/18/24 dB/oct value, but an ENUMERATED control's .get
+		 * must return the enum item index (0-3), same as .put
+		 * receives — returning the raw dB value here (the bug this
+		 * replaces) fed back an out-of-range index to every ALSA
+		 * consumer (confirmed via amixer: writing index 1 read back
+		 * as value 12, not 1).
+		 */
+		s32 slope = v ? *v : 6;
+
+		ucontrol->value.integer.value[0] =
+			slope >= 24 ? 3 : slope >= 18 ? 2 : slope >= 12 ? 1 : 0;
+	} else {
+		ucontrol->value.integer.value[0] = v ? *v : 0;
+	}
+	return 0;
+}
+
+static int bf_eq_put(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int strip = EQ_STRIP(kctl->private_value);
+	int param = EQ_PARAM(kctl->private_value);
+	struct bf_eq_channel *e = &chip->eq[strip];
+	int band = (param - 1) % 3;
+	s32 nv = (s32)ucontrol->value.integer.value[0];
+	s32 *v = NULL;
+	int ret = 0;
+
+	switch (param) {
+	case 0:
+		v = (s32 *)&e->on;
+		break;
+
+	case 1:
+	case 2:
+	case 3:
+		v = &e->band_type[band];
+		break;
+
+	case 4:
+	case 5:
+	case 6:
+		v = &e->band_freq[band];
+		break;
+
+	case 7:
+	case 8:
+	case 9:
+		v = &e->band_q[band];
+		break;
+
+	case 10:
+	case 11:
+	case 12:
+		v = &e->band_gain[band];
+		break;
+
+	case 13:
+		v = &e->lc_hz;
+		break;
+
+	case 14:
+		v = &e->slope_db;
+		break;
+	}
+	if (param == 14)	/* slope enum items are 6/12/18/24 */
+		nv = nv == 0 ? 6 : nv == 1 ? 12 : nv == 2 ? 18 : 24;
+
+	mutex_lock(&chip->mutex);
+	if (v && *v != nv) {
+		*v = nv;
+		bf_eq_update_strip(chip, strip);
+		ret = 1;
+	}
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+int babyface_create_eq(struct snd_usb_babyface *chip)
+{
+	static const char *const names[4] = { "AN1", "AN2", "AN3", "AN4" };
+	static const char *const params[] = {
+		"EQ Enable",
+		"EQ Band 1 Type", "EQ Band 2 Type", "EQ Band 3 Type",
+		"EQ Band 1 Freq", "EQ Band 2 Freq", "EQ Band 3 Freq",
+		"EQ Band 1 Q", "EQ Band 2 Q", "EQ Band 3 Q",
+		"EQ Band 1 Gain", "EQ Band 2 Gain", "EQ Band 3 Gain",
+		"EQ Low Cut Freq", "EQ Low Cut Slope",
+	};
+	int strip, i, err;
+
+	for (strip = 0; strip < 4; strip++) {
+		for (i = 0; i < 15; i++) {
+			struct snd_kcontrol *kctl;
+			char name[64];
+
+			snprintf(name, sizeof(name), "%s %s", names[strip],
+				 params[i]);
+			kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+				.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+				.name = "EQ",
+				.index = 0,
+				.info = bf_eq_info,
+				.get = bf_eq_get,
+				.put = bf_eq_put,
+				.private_value = (strip << 8) | i,
+			}, chip);
+			if (!kctl)
+				return -ENOMEM;
+			strscpy(kctl->id.name, name, sizeof(kctl->id.name));
+			err = snd_ctl_add(chip->card, kctl);
+			if (err < 0)
+				return err;
+		}
+	}
+	return 0;
+}
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
new file mode 100644
index 000000000..44ad80584
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -0,0 +1,1449 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RME Babyface Pro FS — proprietary-mode USB audio driver
+ *
+ * Core driver: USB vendor requests + cold init, interrupt-URB PCM
+ * streaming, mixer-state persistence across re-probes/resume, and
+ * the card lifecycle (probe/disconnect/PM/module entry).
+ *
+ * See babyfacepro.h for the shared device state and register map,
+ * and babyfacepro-ctl.c for the ALSA control surface (mixer, front
+ * panel, DSP EQ).
+ */
+#include <linux/log2.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#include "babyfacepro.h"
+
+/* The transaction-flag counter cycle on 16-bit writes. */
+const u16 bf_flag_cycle[4] = { 0xc000, 0x4000, 0x8000, 0x0000 };
+
+/* ── sample-rate / alt classes ──────────────────── */
+
+static const struct bf_rate bf_rates[] = {
+	{  32000, BF_ALT_1, 56,  8 },
+	{  44100, BF_ALT_1, 56,  8 },
+	{  48000, BF_ALT_1, 56,  8 },
+	{  64000, BF_ALT_1, 56,  8 },
+	{  88200, BF_ALT_1, 56,  8 },
+	{  96000, BF_ALT_2, 40, 16 },
+	{ 128000, BF_ALT_2, 40, 16 },
+	{ 176400, BF_ALT_3, 32, 32 },
+	{ 192000, BF_ALT_3, 32, 32 },
+};
+
+static const unsigned int bf_rate_list[ARRAY_SIZE(bf_rates)] = {
+	32000, 44100, 48000, 64000, 88200,
+	96000, 128000, 176400, 192000,
+};
+
+const struct snd_pcm_hw_constraint_list bf_rates_constraint = {
+	.count = ARRAY_SIZE(bf_rate_list),
+	.list = bf_rate_list,
+	.mask = 0,
+};
+
+const struct bf_rate *bf_rate_lookup(unsigned int rate)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(bf_rates); i++)
+		if (bf_rates[i].rate == rate)
+			return &bf_rates[i];
+	return NULL;
+}
+
+/* ── vendor requests ─────────────────────── */
+
+int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx)
+{
+	return usb_control_msg_send(chip->dev, 0, req,
+				    USB_DIR_OUT | USB_TYPE_VENDOR |
+				    USB_RECIP_DEVICE,
+				    val, idx, NULL, 0, 1000, GFP_KERNEL);
+}
+
+int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf)
+{
+	return usb_control_msg_recv(chip->dev, 0, req,
+				    USB_DIR_IN | USB_TYPE_VENDOR |
+				    USB_RECIP_DEVICE,
+				    0, idx, buf, 4, 1000, GFP_KERNEL);
+}
+
+/* The cold-start session init (cap_coldplug.pcap), verbatim from the
+ * user-space reference (protocol::streaming_init).  Without it the
+ * firmware never validates a stream.
+ */
+int bf_cold_init(struct snd_usb_babyface *chip)
+{
+	int ret, i;
+
+	for (i = 0; i <= 0x3d; i++) {
+		if (i == 0x1e || i == 0x1f)
+			continue;
+		ret = bf_vendor_write(chip, BF_REQ_REG_CLEAR, 0x0000, i);
+		if (ret < 0)
+			return ret;
+	}
+	/* 48-kHz DDS clock quads (banked 0x1B). */
+	ret = bf_vendor_write(chip, BF_REQ_DDS, 0xc350, 0x0000);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_DDS, 0x8db8, 0xd201);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_DDS, 0x8234, 0xd302);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_DDS, 0x7cff, 0xf803);
+	if (ret < 0)
+		return ret;
+	/* 0x1C status — the hardware-validated reference (protocol::
+	 * streaming_init) sends it as an OUT write; Windows reads it.
+	 * Both are tolerated; match the validated path.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_STATUS_2, 0x0000, 0x0000);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0021, BF_REG_KEEPALIVE_INIT);
+	if (ret < 0)
+		return ret;
+	/* 0x17 wIdx=0x0000 does NOT touch the preamp state (0x003F). */
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x000c, 0x0000);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+	if (ret < 0)
+		return ret;
+	for (i = 0; i < 2; i++) {
+		ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0000, 0x3000);
+		if (ret < 0)
+			return ret;
+	}
+	for (i = 0; i < 3; i++) {
+		ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0800, 0x0800);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+/* The 0x16 cold-init clear covers only 0x00-0x3D — the "cross"
+ * registers of a block (L-reg odd / R-reg even of the stereo
+ * sources) survive from the previous session and would sum L+R into
+ * BOTH channels of the output (mono).  Zero them explicitly: 10 odd
+ * L-registers (5,7,…23) + 10 even R-registers (4,6,…22).
+ */
+int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip,
+			      unsigned int blk)
+{
+	int ret, k;
+	u16 flag;
+
+	for (k = 5; k < 24; k += 2) {
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+				      (BF_REG_CROSS_BASE_L +
+				       BF_REG_CROSS_STRIDE * blk + k) | flag);
+		if (ret < 0)
+			return ret;
+	}
+	for (k = 4; k < 24; k += 2) {
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+				      (BF_REG_CROSS_BASE_R +
+				       BF_REG_CROSS_STRIDE * blk + k) | flag);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+/* ── mixer-state persistence across interface re-probes ────────
+ * A userspace client can claim the proprietary interface via usbfs
+ * (USBDEVFS_DISCONNECT_CLAIM — seen with PipeWire grabbing the
+ * device when a stream targets the sink, and with the TuxMix
+ * user-space daemon's libusb).  That detaches us and the card
+ * disappears for the duration; on release the interface re-probes.
+ * The device keeps its registers across the detach, but our cold
+ * init clears them — so save the mixer state at disconnect and
+ * restore it at the next probe.
+ */
+
+static LIST_HEAD(bf_saved_list);
+static DEFINE_MUTEX(bf_saved_mutex);
+
+/* Re-apply the whole cached mixer state after a resume (the device
+ * lost its registers across a system suspend — TotalMix does the same
+ * re-apply).  Caller holds chip->mutex.
+ */
+int babyface_restore_state(struct snd_usb_babyface *chip)
+{
+	int out, src, mic, ret;
+	u16 flag;
+
+	/* Preamp state + commit. */
+	ret = bf_preamp_state_write(chip);
+	if (ret < 0)
+		return ret;
+
+	/* The four mic gains (the counter restarts). */
+	for (mic = 0; mic < 4; mic++) {
+		u8 counter = (mic % 3 == 0) ? 0x20 : (mic % 3 == 1) ? 0x00 : 0x40;
+
+		ret = bf_vendor_write(chip, BF_REQ_GAIN,
+				      (u16)((bf_gain_raw(mic, chip->gain[mic]) & 0x1f) |
+					    counter),
+				      BF_REG_GAIN + mic);
+		if (ret < 0)
+			return ret;
+	}
+	chip->gain_cycle = 1;
+
+	/* Masters (8-bit = the real volume) + mutes. */
+	ret = bf_apply_masters(chip);
+	if (ret < 0)
+		return ret;
+
+	/* Crosspoints (canonical out → register block). */
+	for (out = 0; out < 6; out++) {
+		unsigned int blk = bf_xpoint_block[out];
+
+		for (src = 0; src < 14; src++) {
+			flag = bf_flag_cycle[chip->flag_cnt];
+			chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+			ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+					      chip->xpoint[out][src][0],
+					      (BF_REG_CROSS_BASE_L +
+					       BF_REG_CROSS_STRIDE * blk +
+					       bf_sources[src].idx_l) | flag);
+			if (ret < 0)
+				return ret;
+			ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+					      chip->xpoint[out][src][1],
+					      (BF_REG_CROSS_BASE_R +
+					       BF_REG_CROSS_STRIDE * blk +
+					       bf_sources[src].idx_r) | flag);
+			if (ret < 0)
+				return ret;
+		}
+		ret = bf_crosspoint_clear_cross(chip, blk);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Pitch (the DDS quad) + the clock keepalive. */
+	if (chip->pitch) {
+		u32 dds24 = (12800000000u + (u32)(1000 + chip->pitch) / 2) /
+			    (u32)(1000 + chip->pitch);
+		u16 dds16 = dds24 >> 8;
+		u16 frac = dds24 & 0xff;
+
+		ret = bf_vendor_write(chip, BF_REQ_DDS, dds16, (frac << 8) | 0);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_DDS,
+				      (u16)((dds16 * 72562ull + 50000) / 100000), 0x0001);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_DDS, (u16)((dds16 * 2 + 1) / 3),
+				      0x0002);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_DDS, 0x7cff, 0x0003);
+		if (ret < 0)
+			return ret;
+	}
+	return bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0001,
+			       BF_REG_KEEPALIVE_SETTINGS);
+}
+
+/* Re-apply the non-master flags (loopback / AN1>2 / link / width /
+ * FX send / MS) after a state restore.  The write patterns mirror the
+ * corresponding _put() handlers.  Caller holds chip->mutex.
+ */
+int bf_state_apply_flags(struct snd_usb_babyface *chip)
+{
+	int out, ret, on_out = -1;
+	u16 l, r;
+
+	/* Loopback: the full 30-channel map from the cached state (the
+	 * single-active invariant keeps at most one pair at 0x0001).
+	 */
+	for (out = 0; out < 6; out++) {
+		if (chip->loopback[out]) {
+			on_out = out;
+			break;
+		}
+	}
+	ret = bf_loopback_write_map(chip, on_out, on_out >= 0);
+	if (ret < 0)
+		return ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP,
+			      (chip->linked ? 0x0400 : 0x0000) |
+			      (chip->an12 ? 0x1000 : 0x0000), 0x1000);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+	if (ret < 0)
+		return ret;
+
+	l = (u16)(((0x2000 * (100 + chip->width) / 2) + 50) / 100);
+	r = 0x2000 - l;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+	if (ret < 0)
+		return ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0138);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0153);
+	if (ret < 0)
+		return ret;
+
+	if (chip->ms_proc) {
+		/* Same ON pattern as bf_ms_put (cap_ms2.pcap): mute the AN2
+		 * (side) crosspoints, both maps.
+		 */
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Re-apply an engaged DIM (the fixed -20 dB Phones pair + flag). */
+	if (chip->dim) {
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+				      BF_REG_MASTER_8 + 2 * 1);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+				      BF_REG_MASTER_8 + 2 * 1 + 1);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+				      (BF_REG_MASTER_16 + 2 * 1) |
+				      bf_flag_cycle[chip->flag_cnt]);
+		if (ret < 0)
+			return ret;
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+				      (BF_REG_MASTER_16 + 2 * 1 + 1) |
+				      bf_flag_cycle[chip->flag_cnt]);
+		if (ret < 0)
+			return ret;
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+void bf_state_save(struct snd_usb_babyface *chip)
+{
+	struct bf_saved *s;
+	const char *key = chip->dev->serial ? chip->dev->serial :
+			  dev_name(&chip->dev->dev);
+	bool found = false;
+
+	mutex_lock(&bf_saved_mutex);
+	list_for_each_entry(s, &bf_saved_list, list) {
+		if (strcmp(s->key, key))
+			continue;
+		found = true;
+		break;
+	}
+	if (!found) {
+		s = kzalloc_obj(*s, GFP_KERNEL);
+		if (!s) {
+			mutex_unlock(&bf_saved_mutex);
+			return;
+		}
+		strscpy(s->key, key, sizeof(s->key));
+		list_add_tail(&s->list, &bf_saved_list);
+	}
+
+	s->preamp = chip->preamp;
+	memcpy(s->gain, chip->gain, sizeof(s->gain));
+	s->gain_cycle = chip->gain_cycle;
+	s->flag_cnt = chip->flag_cnt;
+	memcpy(s->master, chip->master, sizeof(s->master));
+	memcpy(s->muted, chip->muted, sizeof(s->muted));
+	memcpy(s->xpoint, chip->xpoint, sizeof(s->xpoint));
+	s->pitch = chip->pitch;
+	memcpy(s->loopback, chip->loopback, sizeof(s->loopback));
+	s->an12 = chip->an12;
+	s->linked = chip->linked;
+	s->ms_proc = chip->ms_proc;
+	s->width = chip->width;
+	s->fx_send = chip->fx_send;
+	s->dim = chip->dim;
+	mutex_unlock(&bf_saved_mutex);
+}
+
+/* Copy a saved state (if any) into a freshly probed chip and push it
+ * to the device.  Returns 1 when restored, -ENOENT when there is none,
+ * or a negative error from the vendor writes.
+ */
+int bf_state_restore(struct snd_usb_babyface *chip)
+{
+	struct bf_saved *s;
+	const char *key = chip->dev->serial ? chip->dev->serial :
+			  dev_name(&chip->dev->dev);
+	int ret = -ENOENT;
+
+	mutex_lock(&bf_saved_mutex);
+	list_for_each_entry(s, &bf_saved_list, list) {
+		if (strcmp(s->key, key))
+			continue;
+		chip->preamp = s->preamp;
+		memcpy(chip->gain, s->gain, sizeof(chip->gain));
+		chip->gain_cycle = s->gain_cycle;
+		chip->flag_cnt = s->flag_cnt;
+		memcpy(chip->master, s->master, sizeof(chip->master));
+		memcpy(chip->muted, s->muted, sizeof(chip->muted));
+		memcpy(chip->xpoint, s->xpoint, sizeof(chip->xpoint));
+		chip->pitch = s->pitch;
+		memcpy(chip->loopback, s->loopback, sizeof(chip->loopback));
+		chip->an12 = s->an12;
+		chip->linked = s->linked;
+		chip->ms_proc = s->ms_proc;
+		chip->width = s->width;
+		chip->fx_send = s->fx_send;
+		chip->dim = s->dim;
+		ret = 1;
+		break;
+	}
+	mutex_unlock(&bf_saved_mutex);
+	if (ret != 1)
+		return ret;
+
+	mutex_lock(&chip->mutex);
+	ret = babyface_restore_state(chip);
+	if (ret == 0)
+		ret = bf_state_apply_flags(chip);
+	mutex_unlock(&chip->mutex);
+	return ret ? ret : 1;
+}
+
+void bf_state_purge(void)
+{
+	struct bf_saved *s, *tmp;
+
+	mutex_lock(&bf_saved_mutex);
+	list_for_each_entry_safe(s, tmp, &bf_saved_list, list) {
+		list_del(&s->list);
+		kfree(s);
+	}
+	mutex_unlock(&bf_saved_mutex);
+}
+
+/* ── stream (interrupt URBs, caiaq-style) ──────────────── */
+
+static bool babyface_capture_copy(struct snd_usb_babyface *chip,
+				  struct snd_pcm_substream *subs,
+				  const u8 *data, unsigned int frames)
+{
+	struct snd_pcm_runtime *rt = subs->runtime;
+	unsigned int buf_frames = rt->buffer_size;
+	unsigned int words = chip->frame_bytes / 4;
+	unsigned int chans = rt->channels;
+	unsigned int pos, f, i;
+	unsigned long new_period;
+	bool crossed = false;
+	u8 *dst;
+
+	spin_lock(&chip->lock);
+	pos = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] % buf_frames;
+	for (f = 0; f < frames; f++) {
+		const __le32 *w = (const __le32 *)(data + f * chip->frame_bytes);
+
+		dst = rt->dma_area + frames_to_bytes(rt, pos);
+		for (i = 0; i < chans; i++) {
+			/* Channel map: app ch0-3 = device words 0-3 (AN1-4);
+			 * app ch4-9 = words 6-11 (ADAT/SPDIF); app ch10/11 =
+			 * words 12/13 = a FIXED-GAIN playback tap (observed
+			 * 2026-08-25: the playback echoes there at ~−27 dB,
+			 * independent of the output masters — NOT the output
+			 * bus; the ADAT/SPDIF range is words 6-11 only).  The
+			 * device words 4/5 are a fixed marker, not audio —
+			 * skipped.  At 96/192 kHz the frame has fewer words;
+			 * missing ones read as zero.
+			 */
+			static const u8 map[12] = { 0, 1, 2, 3, 6, 7, 8, 9,
+						   10, 11, 12, 13 };
+			u8 wi = i < 12 ? map[i] : 0xff;
+			s32 s = 0;
+
+			if (wi < words) {
+				/* 24-bit sample in bytes 1-3; arithmetic shift
+				 * sign-extends from bit 23.  S24_LE container.
+				 */
+				s = (s32)le32_to_cpu(w[wi]) >> 8;
+			}
+			put_unaligned_le32((u32)s, dst + i * 4);
+		}
+		pos++;
+		if (pos >= buf_frames)
+			pos = 0;
+	}
+	chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] += frames;
+	new_period = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] / rt->period_size;
+	if (new_period != chip->prev_period[SNDRV_PCM_STREAM_CAPTURE]) {
+		chip->prev_period[SNDRV_PCM_STREAM_CAPTURE] = new_period;
+		crossed = true;
+	}
+	spin_unlock(&chip->lock);
+
+	return crossed;
+}
+
+static bool babyface_playback_copy(struct snd_usb_babyface *chip,
+				   struct snd_pcm_substream *subs,
+				   u8 *data, unsigned int frames)
+{
+	struct snd_pcm_runtime *rt = subs->runtime;
+	unsigned int buf_frames = rt->buffer_size;
+	unsigned int words = chip->frame_bytes / 4;
+	unsigned int chans = rt->channels;
+	unsigned int pos, f, i;
+	unsigned long new_period;
+	bool crossed = false;
+	const u8 *src;
+
+	spin_lock(&chip->lock);
+	/* Clamp to what the app has actually written: the in-flight URBs
+	 * (nurbs × frames_per_urb) can exceed the app ring, and without
+	 * this the driver advances hw_ptr past appl_ptr — the ALSA core
+	 * then flags a spurious XRUN on the next app interaction even
+	 * though the app refills on schedule (seen at period 16-128 /
+	 * 96-192 kHz with nurbs=16).  The device just repeats the last
+	 * frames (stale audio) instead of corrupting the stream state.
+	 * NB: subtract the unbounded counters directly — modulo arithmetic
+	 * is ambiguous at exact buffer multiples (appl=512, hw=0 → both
+	 * wrap to 0).
+	 */
+	{
+		snd_pcm_sframes_t data =
+			(snd_pcm_sframes_t)(rt->control->appl_ptr -
+					    chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK]);
+		if (data < 0)
+			data = 0;
+		if (data > (snd_pcm_sframes_t)buf_frames)
+			data = (snd_pcm_sframes_t)buf_frames;
+		if ((unsigned int)data < frames)
+			frames = (unsigned int)data;
+	}
+	pos = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] % buf_frames;
+	for (f = 0; f < frames; f++) {
+		__le32 *w = (__le32 *)(data + f * chip->frame_bytes);
+
+		src = rt->dma_area + frames_to_bytes(rt, pos);
+		/* App ch n feeds the device word n (PB1-6 = words 0-11);
+		 * words 12/13 stay zero.  At 96/192 kHz the frame is
+		 * shorter — the extra app channels are dropped.
+		 */
+		for (i = 0; i < chans && i < words; i++) {
+			u32 s = get_unaligned_le32(src + i * 4);
+
+			/* 24-bit sample into bytes 1-3, byte 0 = 0. */
+			w[i] = cpu_to_le32((s & 0x00ffffff) << 8);
+		}
+		for (; i < words; i++)
+			w[i] = 0;
+		pos++;
+		if (pos >= buf_frames)
+			pos = 0;
+	}
+	chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] += frames;
+	new_period = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] / rt->period_size;
+	if (new_period != chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK]) {
+		chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK] = new_period;
+		crossed = true;
+	}
+	spin_unlock(&chip->lock);
+
+	return crossed;
+}
+
+static void babyface_complete_in(struct urb *urb)
+{
+	struct snd_usb_babyface *chip = urb->context;
+	struct snd_pcm_substream *subs;
+	unsigned long flags;
+	unsigned int frames;
+	bool crossed = false;
+	int ret;
+
+	if (urb->status < 0) {
+		if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+		    urb->status == -ECONNRESET)
+			return;		/* killed */
+		dev_dbg_ratelimited(&chip->dev->dev, "IN urb status %d\n",
+				    urb->status);
+		if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+			schedule_work(&chip->stream_work);
+		goto resubmit;
+	}
+	atomic_set(&chip->urb_err, 0);
+
+	subs = READ_ONCE(chip->subs[SNDRV_PCM_STREAM_CAPTURE]);
+	if (subs) {
+		snd_pcm_stream_lock_irqsave(subs, flags);
+		if (snd_pcm_running(subs)) {
+			frames = urb->actual_length / chip->frame_bytes;
+			if (frames)
+				crossed = babyface_capture_copy(chip, subs,
+								urb->transfer_buffer,
+								frames);
+		}
+		snd_pcm_stream_unlock_irqrestore(subs, flags);
+		if (crossed)
+			snd_pcm_period_elapsed(subs);
+	}
+resubmit:
+	ret = usb_submit_urb(urb, GFP_ATOMIC);
+	if (ret < 0) {
+		dev_err_ratelimited(&chip->dev->dev,
+				    "IN resubmit failed: %d\n", ret);
+		if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+			schedule_work(&chip->stream_work);
+	}
+}
+
+static void babyface_complete_out(struct urb *urb)
+{
+	struct snd_usb_babyface *chip = urb->context;
+	struct snd_pcm_substream *subs;
+	unsigned long flags;
+	unsigned int frames;
+	bool crossed = false;
+	int ret;
+
+	if (urb->status < 0) {
+		if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+		    urb->status == -ECONNRESET)
+			return;		/* killed */
+		dev_dbg_ratelimited(&chip->dev->dev, "OUT urb status %d\n",
+				    urb->status);
+		if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+			schedule_work(&chip->stream_work);
+		goto resubmit;
+	}
+	atomic_set(&chip->urb_err, 0);
+
+	subs = READ_ONCE(chip->subs[SNDRV_PCM_STREAM_PLAYBACK]);
+	if (subs) {
+		snd_pcm_stream_lock_irqsave(subs, flags);
+		if (snd_pcm_running(subs)) {
+			frames = chip->frames_per_urb;
+			crossed = babyface_playback_copy(chip, subs,
+							 urb->transfer_buffer, frames);
+		}
+		snd_pcm_stream_unlock_irqrestore(subs, flags);
+		if (crossed)
+			snd_pcm_period_elapsed(subs);
+	} else {
+		/* No consumer: silence the OUT frames. */
+		memset(urb->transfer_buffer, 0, urb->transfer_buffer_length);
+	}
+resubmit:
+	ret = usb_submit_urb(urb, GFP_ATOMIC);
+	if (ret < 0) {
+		dev_err_ratelimited(&chip->dev->dev,
+				    "OUT resubmit failed: %d\n", ret);
+		if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+			schedule_work(&chip->stream_work);
+	}
+}
+
+void babyface_stream_kill(struct snd_usb_babyface *chip)
+{
+	int i;
+
+	for (i = 0; i < chip->nurbs; i++) {
+		usb_kill_urb(chip->urbs_in[i]);
+		usb_kill_urb(chip->urbs_out[i]);
+	}
+	chip->streaming = false;
+}
+
+/* Stream start/stop run in process context (control transfers sleep).
+ * The trigger only toggles stream_users and schedules this work.
+ */
+
+/* Stop both PCM substreams (if running) so apps blocked in read/write
+ * wake with a clean error: XRUN for a recoverable stream error, or
+ * DISCONNECTED when the card is going away.
+ */
+void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state)
+{
+	int s;
+
+	for (s = 0; s < 2; s++) {
+		struct snd_pcm_substream *subs = READ_ONCE(chip->subs[s]);
+
+		if (subs && snd_pcm_running(subs))
+			snd_pcm_stop(subs, state);
+	}
+}
+
+/* Re-count stream_users from the substream running states.  The apps
+ * can recover (re-prepare + trigger) while the stream work runs, so a
+ * hard `= 0` would wipe a fresh increment and leave a RUNNING
+ * substream with no URBs (hang).  Called on the error paths with the
+ * mutex held.
+ */
+static void bf_recount_users(struct snd_usb_babyface *chip)
+{
+	unsigned long flags;
+	int s, users = 0;
+
+	for (s = 0; s < 2; s++) {
+		struct snd_pcm_substream *subs = READ_ONCE(chip->subs[s]);
+
+		if (subs && snd_pcm_running(subs))
+			users++;
+	}
+	spin_lock_irqsave(&chip->lock, flags);
+	chip->stream_users = users;
+	spin_unlock_irqrestore(&chip->lock, flags);
+}
+
+void babyface_stream_work(struct work_struct *work)
+{
+	struct snd_usb_babyface *chip =
+		container_of(work, struct snd_usb_babyface, stream_work);
+	unsigned int urbsize = chip->frame_bytes * chip->frames_per_urb;
+	unsigned long flags;
+	int i, ret;
+	int users;
+
+	mutex_lock(&chip->mutex);
+
+	if (chip->shutdown) {
+		mutex_unlock(&chip->mutex);
+		return;
+	}
+
+	/* Persistent URB errors (bad link, device wedged): stop the stream
+	 * and wake the apps with -EPIPE.  stream_users is re-counted from
+	 * the (now stopped) substreams so an app recovery (prepare+start)
+	 * re-arms the session from a clean slate.
+	 */
+	if (atomic_read(&chip->urb_err) >= BF_URB_ERR_STOP) {
+		dev_err(&chip->dev->dev,
+			"stream error: %d consecutive bad URBs, stopping (apps re-arm)\n",
+			BF_URB_ERR_STOP);
+		babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+		if (chip->streaming)
+			babyface_stream_kill(chip);
+		bf_recount_users(chip);
+		atomic_set(&chip->urb_err, 0);
+		mutex_unlock(&chip->mutex);
+		return;
+	}
+
+	spin_lock_irqsave(&chip->lock, flags);
+	users = chip->stream_users;
+	spin_unlock_irqrestore(&chip->lock, flags);
+
+	if (users > 0 && !chip->streaming) {
+		/* The firmware only validates a stream session that is
+		 * preceded by the full cold-init (the user-space reference
+		 * sends streaming_init at every session start — without it
+		 * the outputs stay silent).  The 0x16 clear wipes the mixer
+		 * registers, so the cached state is re-applied after the arm.
+		 */
+		ret = bf_cold_init(chip);
+		if (ret < 0)
+			goto err;
+
+		/* Stream trigger pair (cap_audio): 0x10 0x8000 + 0x1D. */
+		ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0000, 0x8000);
+		if (ret < 0)
+			goto err;
+		ret = bf_vendor_write(chip, BF_REQ_SESSION_START, 0x0000, 0x0000);
+		if (ret < 0)
+			goto err;
+
+		for (i = 0; i < chip->nurbs; i++) {
+			usb_fill_int_urb(chip->urbs_in[i], chip->dev,
+					 usb_rcvintpipe(chip->dev, BF_EP_IN),
+					 chip->buf_in[i], urbsize,
+					 babyface_complete_in, chip, 1);
+			usb_fill_int_urb(chip->urbs_out[i], chip->dev,
+					 usb_sndintpipe(chip->dev, BF_EP_OUT),
+					 chip->buf_out[i], urbsize,
+					 babyface_complete_out, chip, 1);
+		}
+		for (i = 0; i < chip->nurbs; i++) {
+			ret = usb_submit_urb(chip->urbs_in[i], GFP_KERNEL);
+			if (ret < 0)
+				goto err;
+			ret = usb_submit_urb(chip->urbs_out[i], GFP_KERNEL);
+			if (ret < 0)
+				goto err;
+		}
+		/* Session arm (cap_audio frame 5829, after the URBs). */
+		ret = bf_vendor_write(chip, BF_REQ_SESSION_ARM, 0x0000, 0xc000);
+		if (ret < 0)
+			goto err;
+
+		/* The cold init above cleared the mixer registers; push the
+		 * cached state back (preamp, gains, masters, crosspoints,
+		 * pitch) so the session starts at the user's levels.
+		 */
+		ret = babyface_restore_state(chip);
+		if (ret < 0)
+			goto err;
+
+		/* The 0x16 clear also wipes the flag registers (loopback,
+		 * AN1>2, stereo link, width, FX send, MS) — re-apply them.
+		 */
+		ret = bf_state_apply_flags(chip);
+		if (ret < 0)
+			goto err;
+
+		chip->streaming = true;
+		dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
+			chip->frames_per_urb, chip->nurbs);
+	} else if (users == 0 && chip->streaming) {
+		babyface_stream_kill(chip);
+		dev_dbg(&chip->dev->dev, "stream stopped\n");
+	}
+
+	mutex_unlock(&chip->mutex);
+	return;
+
+err:
+	dev_err(&chip->dev->dev, "failed to start stream: %d\n", ret);
+	babyface_stream_kill(chip);
+	/* The apps already got a successful trigger — wake them with an
+	 * XRUN so a failed start (device wedged, cold-init error) does not
+	 * leave them hung in read/write with no URBs in flight.
+	 */
+	babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+	bf_recount_users(chip);
+	mutex_unlock(&chip->mutex);
+}
+
+/* ── PCM ─────────────────────────── */
+
+static const struct snd_pcm_hardware babyface_pcm_hw = {
+	.info = SNDRV_PCM_INFO_INTERLEAVED |
+		SNDRV_PCM_INFO_BLOCK_TRANSFER,
+	.formats = SNDRV_PCM_FMTBIT_S24_LE,
+	.rate_min = 32000,
+	.rate_max = 192000,
+	.channels_min = 2,
+	.channels_max = 12,
+	.buffer_bytes_max = 1 << 20,
+	.period_bytes_max = 1 << 18,
+	.periods_min = 2,
+	.periods_max = 16,
+};
+
+static int babyface_pcm_open(struct snd_pcm_substream *subs)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	struct snd_pcm_runtime *rt = subs->runtime;
+	unsigned long flags;
+	int ret;
+
+	rt->hw = babyface_pcm_hw;
+	ret = snd_pcm_hw_constraint_list(rt, 0, SNDRV_PCM_HW_PARAM_RATE,
+					 &bf_rates_constraint);
+	if (ret < 0)
+		return ret;
+	/* One URB delivers frames_per_urb frames per interrupt; a period must
+	 * span at least one URB so a completion crosses at most one period
+	 * boundary.  Constrain in frames (not bytes) so the minimum period
+	 * does not balloon at low channel counts: 2 ch @ 48 kHz → 256
+	 * frames (5.3 ms) instead of 1536 frames from a 12-ch byte clamp.
+	 */
+	ret = snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+					   chip->frames_per_urb, 1 << 18);
+	if (ret < 0)
+		return ret;
+
+	spin_lock_irqsave(&chip->lock, flags);
+	chip->subs[subs->stream] = subs;
+	spin_unlock_irqrestore(&chip->lock, flags);
+	return 0;
+}
+
+static int babyface_pcm_close(struct snd_pcm_substream *subs)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	unsigned long flags;
+
+	/* Wait for the stream stop work so the URB callbacks (which
+	 * touch subs) are done before the substream can be freed.
+	 */
+	flush_work(&chip->stream_work);
+	spin_lock_irqsave(&chip->lock, flags);
+	chip->subs[subs->stream] = NULL;
+	spin_unlock_irqrestore(&chip->lock, flags);
+	return 0;
+}
+
+static int babyface_pcm_hw_params(struct snd_pcm_substream *subs,
+				  struct snd_pcm_hw_params *params)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	const struct bf_rate *r;
+	int ret = 0;
+
+	r = bf_rate_lookup(params_rate(params));
+	if (!r)
+		return -EINVAL;
+
+	/* The stream URBs must be at least one alt packet wide: the device
+	 * delivers its IN data in alt-sized packets (448/640/1024 B for
+	 * alt 1/2/3), and a smaller URB buffer makes the host controller
+	 * discard the transfer with -EOVERFLOW (babble) — seen at
+	 * 176.4/192 kHz with frames_per_urb below 32.  Return a clean
+	 * error instead of a silently dead capture stream.
+	 */
+	if (chip->frames_per_urb < r->min_fpu) {
+		dev_err(&chip->dev->dev,
+			"rate %u Hz needs frames_per_urb >= %u (module has %u)\n",
+			r->rate, r->min_fpu, chip->frames_per_urb);
+		return -EINVAL;
+	}
+
+	mutex_lock(&chip->mutex);
+	if (r->rate != chip->rate) {
+		/* Both directions share one clock, so a rate change must not
+		 * race live transfers.  Stop the URBs, re-point the bandwidth
+		 * class and let the stream work restart the session at the
+		 * new rate — the other running substream briefly sees a rate
+		 * step (PipeWire re-negotiates via its resampler) instead of
+		 * this open failing with -EBUSY (which killed the PW sink).
+		 */
+		if (chip->streaming) {
+			unsigned long flags;
+
+			babyface_stream_kill(chip);
+			spin_lock_irqsave(&chip->lock, flags);
+			if (chip->stream_users > 0)
+				schedule_work(&chip->stream_work);
+			spin_unlock_irqrestore(&chip->lock, flags);
+		}
+		ret = usb_set_interface(chip->dev, BF_IFACE, r->alt);
+		if (ret < 0)
+			goto out;
+		chip->rate = r->rate;
+		chip->alt = r->alt;
+		chip->frame_bytes = r->frame_bytes;
+		/* The DSP EQ coefficients depend on fs: re-upload. */
+		bf_eq_reupload(chip);
+		dev_dbg(&chip->dev->dev, "rate %u Hz (alt %u)\n",
+			chip->rate, chip->alt);
+	}
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+static int babyface_pcm_hw_free(struct snd_pcm_substream *subs)
+{
+	/* The device buffer is host-side; nothing to release here. */
+	return 0;
+}
+
+static int babyface_pcm_prepare(struct snd_pcm_substream *subs)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	unsigned long flags;
+
+	spin_lock_irqsave(&chip->lock, flags);
+	chip->hw_ptr[subs->stream] = 0;
+	chip->prev_period[subs->stream] = 0;
+	spin_unlock_irqrestore(&chip->lock, flags);
+	return 0;
+}
+
+static int babyface_pcm_trigger(struct snd_pcm_substream *subs, int cmd)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	unsigned long flags;
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+		spin_lock_irqsave(&chip->lock, flags);
+		chip->hw_ptr[subs->stream] = 0;
+		chip->prev_period[subs->stream] = 0;
+		/* stream_users is shared by the two substreams (separate
+		 * locks) — serialize the ++/-- so a concurrent trigger on
+		 * the other direction can't lose an increment (which would
+		 * stop the stream while a substream still runs).
+		 */
+		if (chip->stream_users++ == 0)
+			schedule_work(&chip->stream_work);
+		spin_unlock_irqrestore(&chip->lock, flags);
+		return 0;
+	case SNDRV_PCM_TRIGGER_STOP:
+		spin_lock_irqsave(&chip->lock, flags);
+		if (chip->stream_users > 0 && --chip->stream_users == 0)
+			schedule_work(&chip->stream_work);
+		spin_unlock_irqrestore(&chip->lock, flags);
+		return 0;
+	}
+	return -EINVAL;
+}
+
+static snd_pcm_uframes_t babyface_pcm_pointer(struct snd_pcm_substream *subs)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	unsigned long flags;
+	snd_pcm_uframes_t pos;
+
+	spin_lock_irqsave(&chip->lock, flags);
+	pos = chip->hw_ptr[subs->stream] % subs->runtime->buffer_size;
+	spin_unlock_irqrestore(&chip->lock, flags);
+	return pos;
+}
+
+static const struct snd_pcm_ops babyface_pcm_ops = {
+	.open = babyface_pcm_open,
+	.close = babyface_pcm_close,
+	.ioctl = snd_pcm_lib_ioctl,
+	.hw_params = babyface_pcm_hw_params,
+	.hw_free = babyface_pcm_hw_free,
+	.prepare = babyface_pcm_prepare,
+	.trigger = babyface_pcm_trigger,
+	.pointer = babyface_pcm_pointer,
+};
+
+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 FS sound card.");
+module_param_array(id, charp, NULL, 0444);
+MODULE_PARM_DESC(id, "ID string for the Babyface Pro FS sound card.");
+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 ───────────────────────── */
+
+static void babyface_private_free(struct snd_card *card)
+{
+	struct snd_usb_babyface *chip = card->private_data;
+	unsigned int urbsize;
+	int i;
+
+	if (!chip)
+		return;
+
+	/* The URB arrays are NULL when the probe failed before allocating
+	 * them (snd_card_free runs private_free on any probe error).
+	 */
+	if (chip->urbs_in) {
+		urbsize = chip->frame_bytes * chip->frames_per_urb;
+		for (i = 0; i < chip->nurbs; i++) {
+			if (chip->urbs_in[i]) {
+				usb_kill_urb(chip->urbs_in[i]);
+				usb_free_urb(chip->urbs_in[i]);
+			}
+			if (chip->urbs_out[i]) {
+				usb_kill_urb(chip->urbs_out[i]);
+				usb_free_urb(chip->urbs_out[i]);
+			}
+			usb_free_coherent(chip->dev, urbsize, chip->buf_in[i],
+					  chip->dma_in[i]);
+			usb_free_coherent(chip->dev, urbsize, chip->buf_out[i],
+					  chip->dma_out[i]);
+		}
+	}
+	kfree(chip->urbs_in);
+	kfree(chip->urbs_out);
+	kfree(chip->buf_in);
+	kfree(chip->buf_out);
+	kfree(chip->dma_in);
+	kfree(chip->dma_out);
+	usb_put_dev(chip->dev);
+}
+
+static int babyface_probe(struct usb_interface *intf,
+			  const struct usb_device_id *usb_id)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	struct snd_usb_babyface *chip;
+	struct snd_card *card;
+	struct snd_pcm *pcm;
+	unsigned int urbsize;
+	u8 st[4];
+	int i, err;
+
+	if (intf->cur_altsetting->desc.bInterfaceNumber != BF_IFACE) {
+		/* Only the proprietary audio interface is ours; the MIDI
+		 * (standard class) and bulk interfaces stay unclaimed so
+		 * snd-usb-audio can take the MIDI one.
+		 */
+		return -ENODEV;
+	}
+
+	frames_per_urb = clamp(frames_per_urb, 8, 1024) & ~7;
+	nurbs = clamp(nurbs, 1, 16);
+	panel_poll_ms = clamp(panel_poll_ms, 10, 1000);
+
+	err = snd_card_new(&intf->dev, index[0], id[0], THIS_MODULE,
+			   sizeof(*chip), &card);
+	if (err < 0) {
+		dev_err(&intf->dev, "snd_card_new failed: %d\n", err);
+		return err;
+	}
+	chip = card->private_data;
+	chip->card = card;
+
+	chip->dev = usb_get_dev(dev);
+	/* USB autosuspend is untested: babyface_suspend()/_resume() don't
+	 * check PMSG_IS_AUTO, and nothing in this driver holds a PM
+	 * reference while streaming or while the panel poll/keepalive
+	 * timers are running, so an autosuspend request could race a
+	 * live stream or panel tick. Disable it explicitly rather than
+	 * ship an untested code path — full autosuspend support (correct
+	 * autopm_get/put pairing around the stream and the panel/keepalive
+	 * work) is a deliberate follow-up, not an oversight.
+	 */
+	usb_disable_autosuspend(chip->dev);
+	chip->iface = intf;
+	chip->nurbs = nurbs;
+	chip->frames_per_urb = frames_per_urb;
+	chip->panel_poll_ms = panel_poll_ms;
+	chip->rate = 48000;
+	chip->alt = BF_ALT_1;
+	chip->frame_bytes = 56;
+	chip->preamp = BF_PREAMP_BASE;
+	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;
+
+	strscpy(chip->card->driver, "BabyfaceProFS",
+		sizeof(chip->card->driver));
+	strscpy(chip->card->shortname, "Babyface Pro FS",
+		sizeof(chip->card->shortname));
+	snprintf(chip->card->longname, sizeof(chip->card->longname),
+		 "RME Babyface Pro FS (proprietary mode) at %s",
+		 dev_name(&dev->dev));
+	strscpy(chip->card->mixername, "Babyface Pro FS",
+		sizeof(chip->card->mixername));
+
+	/* alt 1 = the default 48-kHz bandwidth class. */
+	err = usb_set_interface(dev, BF_IFACE, BF_ALT_1);
+	if (err < 0) {
+		dev_err(&intf->dev, "usb_set_interface failed: %d\n", err);
+		goto error;
+	}
+
+	err = bf_cold_init(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "cold init failed: %d\n", err);
+		goto error;
+	}
+
+	/* Sync the preamp state from the 0x17 readback (byte 0 mirrors
+	 * the 48V/PAD bits; it persists across power cycles).
+	 */
+	err = bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PREAMP, st);
+	if (err < 0)
+		dev_dbg(&intf->dev, "preamp readback failed: %d\n", err);
+	else
+		chip->preamp = st[0];
+
+	/* Restore the mixer state saved at the last disconnect (if any);
+	 * the device keeps its registers across a usbfs detach, but the
+	 * cold init above cleared them, so push the user's settings back.
+	 */
+	err = bf_state_restore(chip);
+	if (err == -ENOENT) {
+		/* No saved state: the 0x16 clear zeroed the mixer registers,
+		 * so restore the factory default routing to keep the outputs
+		 * live out of the box.
+		 */
+		err = babyface_write_default_mixer(chip);
+		if (err < 0) {
+			dev_err(&intf->dev, "default mixer restore failed: %d\n", err);
+			goto error;
+		}
+	} else if (err < 0) {
+		dev_err(&intf->dev, "mixer state restore failed: %d\n", err);
+		goto error;
+	}
+
+	urbsize = chip->frame_bytes * chip->frames_per_urb;
+	chip->urbs_in = kcalloc(chip->nurbs, sizeof(*chip->urbs_in), GFP_KERNEL);
+	chip->urbs_out = kcalloc(chip->nurbs, sizeof(*chip->urbs_out), GFP_KERNEL);
+	chip->buf_in = kcalloc(chip->nurbs, sizeof(*chip->buf_in), GFP_KERNEL);
+	chip->buf_out = kcalloc(chip->nurbs, sizeof(*chip->buf_out), GFP_KERNEL);
+	chip->dma_in = kcalloc(chip->nurbs, sizeof(*chip->dma_in), GFP_KERNEL);
+	chip->dma_out = kcalloc(chip->nurbs, sizeof(*chip->dma_out), GFP_KERNEL);
+	if (!chip->urbs_in || !chip->urbs_out || !chip->buf_in ||
+	    !chip->buf_out || !chip->dma_in || !chip->dma_out)
+		goto error;
+
+	for (i = 0; i < chip->nurbs; i++) {
+		chip->urbs_in[i] = usb_alloc_urb(0, GFP_KERNEL);
+		chip->urbs_out[i] = usb_alloc_urb(0, GFP_KERNEL);
+		chip->buf_in[i] = usb_alloc_coherent(dev, urbsize, GFP_KERNEL,
+						     &chip->dma_in[i]);
+		chip->buf_out[i] = usb_alloc_coherent(dev, urbsize, GFP_KERNEL,
+						      &chip->dma_out[i]);
+		if (!chip->urbs_in[i] || !chip->urbs_out[i] ||
+		    !chip->buf_in[i] || !chip->buf_out[i])
+			goto error;
+	}
+
+	err = snd_pcm_new(chip->card, "Babyface Pro FS", 0, 1, 1, &pcm);
+	if (err < 0) {
+		dev_err(&intf->dev, "snd_pcm_new failed: %d\n", err);
+		goto error;
+	}
+	pcm->private_data = chip;
+	strscpy(pcm->name, "Babyface Pro FS", sizeof(pcm->name));
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &babyface_pcm_ops);
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &babyface_pcm_ops);
+
+	/* The PCM buffer is host-side (the URB callbacks copy in/out of
+	 * it); vmalloc is the standard choice for that.
+	 */
+	err = snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC,
+					     NULL, 0, 1 << 20);
+	if (err < 0) {
+		dev_err(&intf->dev, "buffer allocation failed: %d\n", err);
+		goto error;
+	}
+
+	err = babyface_create_controls(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "control creation failed: %d\n", err);
+		goto error;
+	}
+
+	err = babyface_create_xpoints(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "crosspoint creation failed: %d\n", err);
+		goto error;
+	}
+
+	err = babyface_create_flags(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "flag control creation failed: %d\n", err);
+		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 = babyface_create_eq(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "EQ control creation failed: %d\n", err);
+		goto error;
+	}
+
+	/* The DSP coefficient stream (EQ, bulk ep 0x0A) lives on interface
+	 * 1, which has a single altsetting (alt 0) already active in the
+	 * default configuration — the endpoint is scheduled, no
+	 * SET_INTERFACE or interface claim is needed (the earlier
+	 * -EAGAIN was the on-stack transfer buffer, and SET_INTERFACE on
+	 * interface 1 wedged the iface-5 audio stream — playback URBs
+	 * never completed).
+	 */
+
+	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 FS: card %i, %u frames/URB, %u URBs/direction\n",
+		 chip->card->number, chip->frames_per_urb, chip->nurbs);
+	return 0;
+
+error:
+	usb_set_intfdata(intf, NULL);
+	snd_card_free(chip->card);
+	return err;
+}
+
+static void babyface_disconnect(struct usb_interface *intf)
+{
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+	if (!chip)
+		return;
+
+	/* Idempotence guard: a disconnect can race a re-probe (usbfs
+	 * detach/re-attach) — tear the card down exactly once.
+	 */
+	usb_set_intfdata(intf, NULL);
+	if (chip->shutdown)
+		return;
+
+	/* Keep the mixer state for the next probe: a userspace usbfs
+	 * claim (PipeWire sink grab, TuxMix daemon) detaches us and the
+	 * cold init of the re-probe would otherwise wipe the settings.
+	 */
+	bf_state_save(chip);
+
+	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
+	 * disabled here would wrongly affect whatever claims the device next.
+	 */
+	usb_enable_autosuspend(chip->dev);
+	/* Wake apps blocked in read/write: the card is going away. */
+	dev_info(&chip->dev->dev, "disconnect: stopping PCM substreams\n");
+	babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_DISCONNECTED);
+	mutex_lock(&chip->mutex);
+	if (chip->streaming)
+		babyface_stream_kill(chip);
+	mutex_unlock(&chip->mutex);
+
+	snd_card_disconnect(chip->card);
+	/* NEVER snd_card_free() here: it blocks until the last user
+	 * closes the card, and an open client (e.g. PipeWire) deadlocks
+	 * the disconnect (seen live: pipewire stuck in snd_card_free,
+	 * D state).  free_when_closed frees on the last close.
+	 */
+	snd_card_free_when_closed(chip->card);
+}
+
+static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+	struct snd_device *sdev;
+
+	if (!chip)
+		return 0;
+	list_for_each_entry(sdev, &chip->card->devices, list) {
+		if (sdev->type == SNDRV_DEV_PCM)
+			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);
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int babyface_resume(struct usb_interface *intf)
+{
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+	int err;
+
+	if (!chip)
+		return 0;
+
+	/* The device lost its state across the suspend; re-run the cold
+	 * init and re-apply the cached mixer state.  Suspended PCM
+	 * substreams are woken by the core — apps get -ESTRPIPE and
+	 * restart (the trigger re-arms the stream).
+	 */
+	mutex_lock(&chip->mutex);
+	err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
+	if (err < 0)
+		goto out;
+	err = bf_cold_init(chip);
+	if (err < 0)
+		goto out;
+	err = babyface_restore_state(chip);
+out:
+	mutex_unlock(&chip->mutex);
+	if (!err)
+		babyface_panel_start(chip);
+	return err;
+}
+
+static const struct usb_device_id babyface_ids[] = {
+	{ USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) },
+	{ }
+};
+MODULE_DEVICE_TABLE(usb, babyface_ids);
+
+static struct usb_driver babyface_driver = {
+	.name = "snd-usb-babyface-pro",
+	.probe = babyface_probe,
+	.disconnect = babyface_disconnect,
+	.suspend = babyface_suspend,
+	.resume = babyface_resume,
+	.id_table = babyface_ids,
+};
+
+static int __init babyface_init(void)
+{
+	return usb_register(&babyface_driver);
+}
+
+static void __exit babyface_exit(void)
+{
+	bf_state_purge();
+	usb_deregister(&babyface_driver);
+}
+
+module_init(babyface_init);
+module_exit(babyface_exit);
+
+MODULE_AUTHOR("Ismaïl Bahloul <i.bahloul01@gmail.com>");
+MODULE_DESCRIPTION("RME Babyface Pro FS (proprietary mode) USB audio driver");
+MODULE_LICENSE("GPL");
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
new file mode 100644
index 000000000..df90f48ed
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -0,0 +1,391 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * RME Babyface Pro FS — proprietary-mode USB audio driver
+ *
+ * The Babyface Pro FS presents two personalities on the USB bus: a
+ * class-compliant one (handled by snd-usb-audio) and a proprietary one
+ * (VID 0x2a39 / PID 0x3fc0) whose PCM stream runs on INTERRUPT
+ * endpoints (interface 5, ep 0x01 OUT / 0x82 IN).  Isochronous
+ * transfers are rejected there with EINVAL, and snd-usb-audio has no
+ * interrupt-PCM path, so this driver is standalone (snd-usb-caiaq-style
+ * interrupt streaming) instead of an snd-usb-audio quirk.
+ *
+ * The protocol (vendor requests + 14×32-bit frame layout) was
+ * reverse-engineered from Windows captures and validated on hardware —
+ * tools/usbdump/PROTOCOL.md is the authoritative reference.
+ *
+ * Stream notes (hardware-validated 2026-08):
+ *   - frames_per_urb is tunable 8..1024 (multiple of 8) but must be at
+ *     least one alt packet wide — the device delivers IN data in
+ *     alt-sized packets (448/640/1024 B for alt 1/2/3), smaller URBs
+ *     get -EOVERFLOW (babble).  So frames_per_urb >= 8/16/32 for
+ *     alt 1/2/3; the driver rejects violating rates in hw_params.
+ *   - Validated sweep 256→128→64→32→16 (≤ 128 kHz): with nurbs=8 the
+ *     period floor is 32 frames (0.67 ms @ 48 kHz) without glitches;
+ *     nurbs=16 drops it to 16 frames (0.33 ms).  Soaks (5-15 min,
+ *     2026-08-25) refine this: period 32 is the zero-glitch floor
+ *     (0 xruns both directions); period 16 is rock-solid on playback
+ *     but the capture side drops ~1 buffer per 7 s (0.67 ms each —
+ *     any scheduler hiccup overruns a 0.33 ms ring) — fine for
+ *     monitoring, not for clean recording.  Defaults (256×8) match
+ *     the RME TotalMix 256-sample buffer; the low-latency profile is
+ *     16×16.
+ *   - The device only advances the stream while BOTH endpoints have a
+ *     pending URB — IN and OUT are always submitted as a pair.
+ *   - Sample rate = SET_INTERFACE(5, alt) only; the alt is a bandwidth
+ *     class (alt 1 = 32/44.1/48/64/88.2 kHz, alt 2 = 96/128 kHz,
+ *     alt 3 = 176.4/192 kHz), not a 1:1 rate code.
+ */
+
+#include <linux/log2.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#define USB_VENDOR_RME			0x2a39
+#define USB_PRODUCT_BABYFACE_PRO_FS	0x3fc0
+
+/* The proprietary audio interface (interface 5, interrupt endpoints). */
+#define BF_IFACE			5
+#define BF_EP_OUT			0x01
+#define BF_EP_IN			0x82
+
+#define BF_ALT_1			1	/* 32/44.1/48/64/88.2 kHz, 448-B packets */
+#define BF_ALT_2			2	/* 96/128 kHz, 640-B packets */
+#define BF_ALT_3			3	/* 176.4/192 kHz, 1024-B packets */
+
+/* Default stream geometry — conservative, matches the RME TotalMix
+ * 256-sample buffer.  Both are tunable via module params; the
+ * low-latency profile (validated) is frames_per_urb=16 nurbs=16.
+ */
+#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 × 32-bit words per frame */
+
+/* Consecutive URB errors (CRC/babble/protocol or a failed resubmit)
+ * before the stream is stopped and the apps get a clean -EPIPE.
+ */
+#define BF_URB_ERR_STOP			3
+
+/* Vendor requests (bmRequestType 0x40, value in wValue, no data phase). */
+#define BF_REQ_KEEPALIVE		0x10	/* settings word / stream trigger */
+#define BF_REQ_STATUS			0x11	/* read 4 B */
+#define BF_REQ_CROSSPOINT		0x12	/* 16-bit crosspoint / master */
+#define BF_REQ_SESSION_STOP		0x13	/* disarm — never sent mid-run */
+#define BF_REQ_SESSION_ARM		0x14
+#define BF_REQ_REG_CLEAR		0x16	/* cold-init register clear */
+#define BF_REQ_PREAMP			0x17	/* 48V/PAD state + readback */
+#define BF_REQ_GAIN			0x1a	/* 8-bit gain / master companion */
+#define BF_REQ_DDS			0x1b	/* clock quads */
+#define BF_REQ_STATUS_2			0x1c	/* read 4 B */
+#define BF_REQ_SESSION_START		0x1d
+#define BF_REQ_PREAMP_COMMIT		0x21	/* commit after 0x17 */
+#define BF_REQ_LOOPBACK			0x15	/* per-output-channel flag */
+
+/* Loopback map width (captured 2026-08-25, cap_loopback2.pcap):
+ * TotalMix writes the FULL 30-channel 0x15 map on every toggle (ON =
+ * the pair at 0x0001 + the other 28 at 0x0000; OFF = all 0x0000).
+ * wIdx = 2×out_index: AN1/2 = 0/1, PH3/4 = 2/3, AS1/2 = 4/5, …
+ */
+#define BF_LOOPBACK_CHANNELS		30
+
+/* Register addresses. */
+#define BF_REG_PREAMP			0x003f
+#define BF_REG_MASTER_16		0x03e0	/* + 2·out (bReq 0x12) */
+#define BF_REG_MASTER_8			0x0004	/* + 2·out (bReq 0x1a) */
+#define BF_REG_GAIN			0x0000	/* + mic 0-3 (bReq 0x1a) */
+#define BF_REG_CROSS_BASE_L		0x0034	/* + 0x34·out + src (bReq 0x12) */
+#define BF_REG_CROSS_BASE_R		0x004e	/* + 0x34·out + src */
+#define BF_REG_CROSS_STRIDE		0x0034
+#define BF_REG_KEEPALIVE_SETTINGS	0x05cf
+#define BF_REG_KEEPALIVE_INIT		0x05ff
+
+/* 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 =
+ * 0x0C set; −10dBV/Boost = clear; Boost additionally commits 0x21
+ * wVal 0x0003).  Keeping it always set = forcing the default +4dBu,
+ * which is correct for the driver (no ref-level control).
+ */
+#define BF_PREAMP_REF_4DBU		0x000c
+#define BF_PREAMP_BASE			BF_PREAMP_REF_4DBU
+#define BF_PREAMP_48V_MIC1		0x0001
+#define BF_PREAMP_48V_MIC2		0x0002
+#define BF_PREAMP_PAD_MIC1		0x0010
+#define BF_PREAMP_PAD_MIC2		0x0020
+
+/* Calibrated master value: 0 dB = 0x2000 (+6 dB = 0x4000).  See
+ * CALIBRATION.md.  The crosspoint fader curve is DIFFERENT (0 dB =
+ * 0x16a0, top 0x2d41 — see below).
+ */
+#define BF_MASTER_0DB			0x2000
+
+/* The 8-bit master is the REAL output volume (hardware-verified
+ * 2026-08-24: writing it changes the level, the 16-bit does not).
+ * Scale: 0.5 dB per step, 0xf3 = 0 dB (the scene-load default),
+ * bottom 0x73 = -64 dB (silence), top 0xff = +6 dB.  The 16-bit
+ * register is a companion kept in sync (TotalMix writes both).
+ * The mute value is 0x3B.
+ */
+#define BF_MASTER_8_0DB			0xf3
+#define BF_MASTER_8_MIN			0x73
+#define BF_MASTER_MUTE			0x3b
+#define BF_MASTER_UNMUTE		0xf3
+
+/* 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).
+ */
+#define BF_FADER_0DB			0x16a0
+#define BF_FADER_TOP			0x2d41
+
+/* The crosspoint matrix sources (14 controls per output). */
+struct bf_source {
+	const char *name;
+	u8 idx_l;
+	u8 idx_r;
+};
+
+/* Crosspoint-source order + register block maps (babyfacepro-ctl.c). */
+extern const struct bf_source bf_sources[14];
+extern const u8 bf_xpoint_block[6];
+
+/* Calibrated preamp gain: 65 dB over 20 raw steps (3.25 dB/step). */
+#define BF_GAIN_MAX_DB			65
+
+struct snd_usb_babyface {
+	struct snd_card *card;
+	struct usb_device *dev;
+	struct usb_interface *iface;
+
+	struct mutex mutex;		/* controls + stream geometry */
+	spinlock_t lock;		/* hw_ptr / subs */
+
+	/* stream */
+	struct urb **urbs_in;
+	struct urb **urbs_out;
+	void **buf_in;
+	void **buf_out;
+	dma_addr_t *dma_in;
+	dma_addr_t *dma_out;
+	unsigned int nurbs;
+	unsigned int frames_per_urb;
+	unsigned int frame_bytes;	/* 56/40/32 for alt 1/2/3 */
+	unsigned int rate;
+	unsigned int alt;
+	int stream_users;		/* PCM substreams sharing the stream */
+	bool streaming;			/* URBs actually in flight */
+	bool shutdown;
+	atomic_t urb_err;		/* consecutive bad URBs (stops the stream) */
+	struct work_struct stream_work;
+
+	struct snd_pcm_substream *subs[2];
+	unsigned long hw_ptr[2];
+	unsigned long prev_period[2];
+
+	/* mixer state (no gain readback exists — host-side mirror) */
+	u16 preamp;			/* 48V/PAD bits, base 0x0c */
+	u8 gain[4];			/* preamp gain in dB 0-65/9 (raw derived
+					 * at write: mic 3.25 dB/step, instr
+					 * 0.5 dB/step)
+					 */
+	u8 gain_cycle;			/* 0x20/0x00/0x40 transaction counter */
+	u8 flag_cnt;			/* 0xc000/0x4000/0x8000/0x0000 */
+	u16 master[6][2];		/* cached 16-bit masters */
+	bool muted[6];
+	u16 dim_saved[2];		/* pre-DIM Phones master (out 1 L/R) */
+	bool dim;			/* DIM engaged (fixed -20 dB on Phones) */
+	u16 xpoint[6][14][2];		/* cached crosspoints (out, src, L/R) */
+	int pitch;			/* varispeed in 0.1% (-500..+500) */
+	bool loopback[6];
+	bool an12;			/* AN 1>2 copy */
+	bool linked;			/* AN1/2 input link */
+	bool ms_proc;			/* MS processor engaged */
+	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 */
+	struct bf_eq_channel {
+		bool on;		/* EQ engaged (else identity blocks) */
+		s32 slope_db;		/* low-cut slope 6/12/18/24 (0 = off) */
+		s32 lc_hz;		/* low-cut freq, 0 = off */
+		u32 lc_raw;		/* cached 0x38 word */
+		u8 slope;		/* cached slope byte (2^n - 1) */
+		s32 band_type[3];	/* 0 off, 1 bell, 2 low shelf, 3 high shelf */
+		s32 band_freq[3];	/* Hz */
+		s32 band_q[3];		/* Q x 100 */
+		s32 band_gain[3];	/* dB x 10 */
+		s32 words[3][4];	/* cached c0..c3 */
+		s32 shared;		/* cached c4 (shared by the slots) */
+	} eq[4];
+
+	/* 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 bf_saved {
+	struct list_head list;
+	char key[32];
+	u16 preamp;
+	u8 gain[4];
+	u8 gain_cycle;
+	u8 flag_cnt;
+	u16 master[6][2];
+	bool muted[6];
+	u16 xpoint[6][14][2];
+	int pitch;
+	bool loopback[6];
+	bool an12;
+	bool linked;
+	bool ms_proc;
+	int width;
+	u16 fx_send;
+	bool dim;
+};
+
+struct bf_rate {
+	unsigned int rate;
+	unsigned int alt;
+	unsigned int frame_bytes;
+	unsigned int min_fpu;	/* frames/URB floor = one alt packet (448/640/1024 B) */
+};
+
+/* Sample-rate / alt classes (babyfacepro.c). */
+const struct bf_rate *bf_rate_lookup(unsigned int rate);
+
+/* ── shared driver state ────────────────────── */
+extern const u16 bf_flag_cycle[4];
+extern const struct bf_source bf_sources[14];
+
+/* babyfacepro-ctl.c — the DSP EQ (struct snd_usb_babyface is defined above). */
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+		      s32 gain_x10, s32 fs);
+void bf_eq_reupload(struct snd_usb_babyface *chip);
+int babyface_create_eq(struct snd_usb_babyface *chip);
+extern const u8 bf_xpoint_block[6];
+extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
+
+/* ── babyfacepro.c ──────────────────────── */
+int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx);
+int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf);
+int bf_cold_init(struct snd_usb_babyface *chip);
+int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip,
+			      unsigned int blk);
+const struct bf_rate *bf_rate_lookup(unsigned int rate);
+void babyface_stream_kill(struct snd_usb_babyface *chip);
+void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state);
+void babyface_stream_work(struct work_struct *work);
+
+/* ── babyfacepro-ctl.c ─────────────────────── */
+int babyface_write_default_mixer(struct snd_usb_babyface *chip);
+int bf_apply_masters(struct snd_usb_babyface *chip);
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
+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);
+int babyface_create_flags(struct snd_usb_babyface *chip);
+
+/* Master + gain law helpers — shared with the front-panel wheels. */
+int bf_master_half_db(u16 vol16);	/* 16-bit master → dB×2 */
+int bf_master_16bit(int half_db);	/* dB×2 → 16-bit master */
+u8 bf_master_8bit(u16 vol16);		/* 16-bit master → 8-bit companion */
+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);
+int bf_state_restore(struct snd_usb_babyface *chip);
+void bf_state_purge(void);
+int babyface_restore_state(struct snd_usb_babyface *chip);
+int bf_state_apply_flags(struct snd_usb_babyface *chip);
-- 
2.55.0


  reply	other threads:[~2026-08-29 10:03 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-29 10:03 [RFC PATCH 0/1] RME Babyface Pro FS driver (proprietary mode) Ismaïl Bahloul
2026-08-29 10:03 ` Ismaïl Bahloul [this message]
2026-08-31 14:11 ` 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=20260829100333.32933-2-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