[RFC PATCH v5 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing

Ismaïl Bahloul <[email protected]>
Newsgroups org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-sound,org.kernel.vger.linux-usb
Message-ID <[email protected]>
Adds the first mixer controls: the six output masters (volume + mute)
and the 6x14 crosspoint routing matrix, plus the cold-init default
mixer that wires every source to every output at unity so the card
makes sound without any user-space mixer at all.

Masters and crosspoints ship together because the default-mixer setup
has to write both in one pass - output masters with no signal routed
to them would just be a silent volume knob.  This is the first patch
in the series where the card produces controllable, routed audio.

Also introduces mixer-state persistence across interface re-probes
(a userspace usbfs claim, e.g. PipeWire or the TuxMix daemon, detaches
and re-probes us) and across suspend/resume's cold re-init, trimmed
for now to the masters and crosspoints this patch adds; later patches
in the series extend babyface_restore_state()/struct bf_saved the same
way as they add more mixer state to cache.

bf_flag_cycle[]/bf_vendor_write_cycle()/bf_crosspoint_clear_cross(),
previously sketched in the core file, move to babyfacepro-ctl.c since
they are crosspoint/mixer-only.

Co-developed-by: David Fredman <[email protected]>
Signed-off-by: David Fredman <[email protected]>
Signed-off-by: Ismaïl Bahloul <[email protected]>
---
 sound/usb/babyfacepro/Makefile          |   2 +-
 sound/usb/babyfacepro/babyfacepro-ctl.c | 707 ++++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     | 189 ++++++-
 sound/usb/babyfacepro/babyfacepro.h     | 121 +++-
 4 files changed, 1013 insertions(+), 6 deletions(-)
 create mode 100644 sound/usb/babyfacepro/babyfacepro-ctl.c

diff --git a/sound/usb/babyfacepro/Makefile b/sound/usb/babyfacepro/Makefile
index 5adc6d474..a50647a06 100644
--- a/sound/usb/babyfacepro/Makefile
+++ b/sound/usb/babyfacepro/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-snd-usb-babyface-pro-y := babyfacepro.o
+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..a3d3252f5
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -0,0 +1,707 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
+ *
+ * ALSA control surface: this patch adds the output masters (volume +
+ * mute) and the crosspoint routing matrix - the minimum needed for
+ * the card to produce controllable, routed audio.  They ship together
+ * because babyface_write_default_mixer() (the cold-init default
+ * mixer) has to set up both in one pass: masters with no signal
+ * routed to them would just be a silent volume knob.
+ *
+ * Preamp/gain, the routing-flag switches (loopback, AN1>2, link,
+ * clock source, width, FX send, DIM), varispeed pitch, the front
+ * panel and the DSP EQ are added by later patches in this series -
+ * see babyfacepro.h and the cover letter for the full plan.
+ *
+ * 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/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 flag-cycle counter, its writer and the per-block crosspoint
+ * "cross" register clear are crosspoint/mixer-only, so they live here
+ * rather than in the core driver file.
+ */
+
+/* The transaction-flag counter cycle on 16-bit writes. */
+const u16 bf_flag_cycle[4] = { 0xc000, 0x4000, 0x8000, 0x0000 };
+
+/* Write with the per-transaction flag-cycle word OR'd into idx.  The
+ * device wants the flag word (0xc000/0x4000/0x8000/0x0000, rotating)
+ * set on every 0x12/0x1a write; this is the hot path for the mixer
+ * puts, so it is factored out.
+ */
+int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx)
+{
+	u16 flag = bf_flag_cycle[chip->flag_cnt];
+
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+	return bf_vendor_write(chip, req, val, idx | flag);
+}
+
+/* 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;
+
+	for (k = BF_CROSS_L_FIRST; k <= BF_CROSS_L_LAST; k += 2) {
+		ret = bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000,
+					    BF_REG_CROSS_BASE_L +
+					    BF_REG_CROSS_STRIDE * blk + k);
+		if (ret < 0)
+			return ret;
+	}
+	for (k = BF_CROSS_R_FIRST; k <= BF_CROSS_R_LAST; k += 2) {
+		ret = bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000,
+					    BF_REG_CROSS_BASE_R +
+					    BF_REG_CROSS_STRIDE * blk + k);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+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 -> dBx2 (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];
+}
+
+/* dBx2 -> 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 routing -
+ * every source to every output at unity - so the card makes sound
+ * without any user-space mixer at all.
+ *
+ * The two analog masters (AN1/2, the main out; PH3/4, the headphone
+ * out) come up at -20 dB rather than TotalMix's 0 dB.  Routing all 14
+ * sources into an output at unity means they SUM, and this runs on
+ * every fresh load, before alsa-restore has had a chance to put the
+ * user's own levels back.  On monitors or headphones with no volume
+ * control of their own that is a real hazard, and the failure is
+ * asymmetric: a default that is too quiet is turned up in a second,
+ * one that is too loud cannot be taken back.  -20 dB is still plainly
+ * audible, and it is not an invented number - it is the exact
+ * 8-bit/16-bit pair the hardware's own DIM button writes.
+ *
+ * The other four outputs (AS1/2, ADAT3/4, ADAT5/6, ADAT7/8) are all
+ * digital, carried over the single optical port - nothing downstream
+ * of them can be damaged by a loud signal the way a speaker or a pair
+ * of headphones can, so there is no hazard to mitigate, only a
+ * digital feed that would otherwise arrive 20 dB quiet for no reason
+ * a downstream device could infer.  They keep TotalMix's own 0 dB
+ * default (raised 2026-09-15 after David Fredman pointed out the
+ * blanket -20 dB reached them too, on his report of the AN1/2/PH3/4
+ * default - issue #4).
+ */
+int babyface_write_default_mixer(struct snd_usb_babyface *chip)
+{
+	int out, src, ret;
+	u16 flag;
+
+	/* Output masters: the two analog outputs at -20 dB, the four
+	 * digital ones at 0 dB (see the comment above).  Unmuted either
+	 * way.
+	 */
+	for (out = 0; out < 6; out++) {
+		bool analog = out < 2;
+		u8 gain8 = analog ? BF_MASTER_MINUS20_8 : BF_MASTER_UNMUTE;
+		u16 gain16 = analog ? BF_MASTER_MINUS20_16 : BF_MASTER_0DB;
+
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, gain8,
+				      BF_REG_MASTER_8 + 2 * out);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_GAIN, gain8,
+				      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, gain16,
+				      (BF_REG_MASTER_16 + 2 * out) | flag);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, gain16,
+				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+		if (ret < 0)
+			return ret;
+		chip->master[out][0] = gain16;
+		chip->master[out][1] = gain16;
+		chip->muted[out] = false;
+	}
+
+	/* Every source into every output pair, L and R, at 0 dB (the
+	 * standard map, plus the low map on AN1/2 - see bf_xpoint_write's
+	 * own comment for why AN1/2 needs both).  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++) {
+			ret = bf_xpoint_write(chip, out, src, BF_FADER_0DB,
+					      BF_FADER_0DB);
+			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 - bf_settings_write() is Internal-only at
+	 * this point in the series (the clock-source control lands with
+	 * a later patch).
+	 */
+	return bf_settings_write(chip);
+}
+
+/* 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 x 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;
+
+	/* The control is declared 0..0x4000 (+6 dB); reject anything outside
+	 * so the 16-bit companion register and the cache stay in spec (the
+	 * ALSA core only enforces this with CONFIG_SND_CTL_INPUT_VALIDATION).
+	 */
+	if (l > 0x4000 || r > 0x4000)
+		return -EINVAL;
+
+	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;
+	/* The DIM engaged/dim_saved re-base hook lands with the DIM
+	 * switch (a later patch in this series) - chip->dim does not
+	 * exist yet at this point in the split.
+	 */
+	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;
+}
+
+/* -- crosspoint matrix (6 outputs x 14 sources) -------------- */
+
+/* The crosspoint fader is linear in amplitude: BF_FADER_0DB (0x16a0) is
+ * unity and BF_FADER_TOP (0x2d41) is exactly twice that, i.e. +6 dB - see
+ * bf_fader_curve, whose whole span follows raw = BF_FADER_0DB * 10^(dB/20).
+ * Raw 0 is off.
+ */
+static const DECLARE_TLV_DB_LINEAR(bf_xpoint_tlv, TLV_DB_GAIN_MUTE, 600);
+
+/* Write a crosspoint slot on the wire: the standard map always, and -
+ * for the AN1/2 output only - the low map as well.
+ *
+ * HARDWARE-VERIFIED 2026-09-14: AN1/2 is not just another output with
+ * a redundant "shadow" register, despite what this file used to say.
+ * Sweeping only the standard map (BF_REG_CROSS_BASE_*) into AN1/2
+ * produced no audible change at all, off through +6 dB, with two
+ * independent sources (a generated tone via PB1, a live mic via AN2);
+ * the exact same code path targeting any other output (verified on
+ * PH3/4) tracked the fader correctly, off to +6 dB within 0.6 dB.
+ * PROTOCOL.md's "Scene load" capture explains why: the vendor software
+ * always writes BOTH the standard map and the low map
+ * (BF_REG_LOWMAP_BASE_*) together for AN1/2's own crosspoints, at the
+ * same value - the low map is what actually feeds that output's sum;
+ * the standard map alone is not enough. Every other output only has a
+ * standard map.
+ *
+ * bf_split_apply() already knew this (it writes both for AN1/2's
+ * playback pairs); this generalises the same pattern to the plain
+ * fader.
+ */
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+		    u16 l, u16 r)
+{
+	unsigned int blk = bf_xpoint_block[out];
+	const struct bf_source *s = &bf_sources[src];
+	u16 flag;
+	int ret;
+
+	if (out == 0) {
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+				      BF_REG_LOWMAP_BASE_L + s->idx_l);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+				      BF_REG_LOWMAP_BASE_R + s->idx_r);
+		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_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+			       s->idx_l) | flag);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			       (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+				s->idx_r) | flag);
+}
+
+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;
+	u16 l = ucontrol->value.integer.value[0];
+	u16 r = ucontrol->value.integer.value[1];
+	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;
+
+	ret = bf_xpoint_write(chip, out, src, l, r);
+	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,
+				.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+					  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+				.info = bf_xpoint_info,
+				.get = bf_xpoint_get,
+				.put = bf_xpoint_put,
+				.tlv.p = bf_xpoint_tlv,
+				.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;
+}
+
+int babyface_create_masters(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]);
+	}
+
+	return 0;
+}
+
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 2d71286e3..50b6a31a5 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -3,13 +3,16 @@
  * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
  *
  * Core driver: USB vendor requests + cold init, interrupt-URB PCM
- * streaming, and the card lifecycle (probe/disconnect/module entry).
- * No mixer controls, front-panel support or suspend/resume yet - see
- * babyfacepro.h and the cover letter for the rest of the series.
+ * streaming, mixer-state persistence across re-probes/resume, and the
+ * card lifecycle (probe/disconnect/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 - added by
+ * later patches in this series).
  */
 #include <linux/math64.h>
 #include <linux/module.h>
 #include <linux/mutex.h>
+#include <linux/slab.h>
 #include <linux/string.h>
 #include <linux/unaligned.h>
 #include <linux/usb.h>
@@ -252,6 +255,139 @@ int bf_cold_init(struct snd_usb_babyface *chip)
 	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.
+ *
+ * Only the masters and the crosspoint matrix exist at this point in
+ * the series; preamp/gain and pitch are re-applied here too once the
+ * later patches that introduce them land.
+ */
+int babyface_restore_state(struct snd_usb_babyface *chip)
+{
+	int out, src, ret;
+
+	/* Masters (8-bit = the real volume) + mutes. */
+	ret = bf_apply_masters(chip);
+	if (ret < 0)
+		return ret;
+
+	/* Crosspoints (canonical out -> register block; AN1/2 also needs
+	 * the low map, see bf_xpoint_write's own comment).
+	 */
+	for (out = 0; out < 6; out++) {
+		unsigned int blk = bf_xpoint_block[out];
+
+		for (src = 0; src < 14; src++) {
+			ret = bf_xpoint_write(chip, out, src,
+					      chip->xpoint[out][src][0],
+					      chip->xpoint[out][src][1]);
+			if (ret < 0)
+				return ret;
+		}
+		ret = bf_crosspoint_clear_cross(chip, blk);
+		if (ret < 0)
+			return ret;
+	}
+	/* The rate (family register), then the varispeed ratio, which is
+	 * sticky in the device and so must be re-sent even at pitch 0 in
+	 * case something else wrote it while we were detached.
+	 */
+	ret = bf_clock_write(chip);
+	if (ret < 0)
+		return ret;
+	return bf_pitch_write(chip, chip->pitch);
+}
+
+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->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));
+	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->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));
+		ret = 1;
+		break;
+	}
+	mutex_unlock(&bf_saved_mutex);
+	if (ret != 1)
+		return ret;
+
+	mutex_lock(&chip->mutex);
+	ret = babyface_restore_state(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);
+}
+
 /* -- PCM data path ------------------------- */
 
 static bool babyface_capture_copy(struct snd_usb_babyface *chip,
@@ -640,6 +776,14 @@ void babyface_stream_work(struct work_struct *work)
 		if (ret < 0)
 			goto err;
 
+		/* The cold init above cleared the mixer registers; push the
+		 * cached masters and crosspoints back so the session starts
+		 * at the user's levels.
+		 */
+		ret = babyface_restore_state(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);
@@ -1063,6 +1207,26 @@ static int babyface_probe(struct usb_interface *intf,
 		goto error;
 	}
 
+	/* 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;
+	}
+
 	/* Keep the allocation size independent of the active USB mode. */
 	urbsize = BF_WORDS_PER_FRAME * sizeof(u32) * chip->frames_per_urb;
 	chip->urbs_in = kcalloc(chip->nurbs, sizeof(*chip->urbs_in), GFP_KERNEL);
@@ -1107,6 +1271,18 @@ static int babyface_probe(struct usb_interface *intf,
 		goto error;
 	}
 
+	err = babyface_create_masters(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "master 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 = snd_card_register(chip->card);
 	if (err < 0) {
 		dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
@@ -1144,6 +1320,12 @@ static void babyface_disconnect(struct usb_interface *intf)
 	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);
 	/* Balance the probe()-time usb_disable_autosuspend(): the usb_device
@@ -1189,6 +1371,7 @@ static int __init babyface_init(void)
 
 static void __exit babyface_exit(void)
 {
+	bf_state_purge();
 	usb_deregister(&babyface_driver);
 }
 
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index de0353a59..6125e763f 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -12,8 +12,12 @@
  * snd-usb-audio quirk.
  *
  * This file and babyfacepro.c cover the card lifecycle (probe,
- * disconnect) and the PCM stream only - no mixer controls, no
- * front-panel support, no suspend/resume yet.  Those are added by
+ * disconnect), the PCM stream, and mixer-state persistence across
+ * re-probes/resume.  babyfacepro-ctl.c adds the ALSA control surface,
+ * starting with the output masters (volume + mute) and the crosspoint
+ * routing matrix - the minimum needed for the card to produce
+ * controllable, routed audio.  Preamp/gain, the routing-flag switches,
+ * varispeed pitch, front-panel support and suspend/resume are added by
  * later patches in this series, each with its own register map and
  * device-state additions; see the cover letter for the full series
  * plan.
@@ -81,8 +85,10 @@
  * panel and DSP EQ patches each add their own as those features land.
  */
 #define BF_REQ_KEEPALIVE		0x10	/* settings word / stream trigger */
+#define BF_REQ_CROSSPOINT		0x12	/* 16-bit crosspoint / master */
 #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
@@ -108,6 +114,72 @@
 #define BF_SETTINGS_CLOCK_INTERNAL	0x0001
 #define BF_SETTINGS_CLOCK_OPTICAL	0x0004
 
+/* Register addresses (masters + crosspoint matrix). */
+#define BF_REG_MASTER_16		0x03e0	/* + 2*out (bReq 0x12) */
+#define BF_REG_MASTER_8			0x0004	/* + 2*out (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
+/* Low map (the AN1/2 monitor bus's own per-source registers, one set
+ * shared across every output - not one per output block like the
+ * standard crosspoint map above). NOT a shadow/mirror of the standard
+ * map for AN1/2: it is what that output actually sums from, and the
+ * standard map alone has no audible effect on it (hardware-verified
+ * 2026-09-14, see bf_xpoint_write's own comment and KERNEL-DRIVER.md).
+ */
+#define BF_REG_LOWMAP_BASE_L		0x0000	/* + idx_l */
+#define BF_REG_LOWMAP_BASE_R		0x001a	/* + idx_r */
+
+/* The "cross" register block within each output: the L-registers sit at
+ * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
+ * source pairs that can be cross-linked).  bf_crosspoint_clear_cross()
+ * zeroes them because the cold-init clear does not cover them.
+ */
+#define BF_CROSS_L_FIRST		5
+#define BF_CROSS_L_LAST			23
+#define BF_CROSS_R_FIRST		4
+#define BF_CROSS_R_LAST			22
+
+/* 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
+/* -20 dB master, 8-bit and 16-bit: the exact pair the hardware DIM
+ * button writes (cap_dim2.pcap), reused as the power-on default.
+ */
+#define BF_MASTER_MINUS20_8		0xcb
+#define BF_MASTER_MINUS20_16		0x0333
+
+/* 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];
+
 struct snd_usb_babyface {
 	struct snd_card *card;
 	struct usb_device *dev;
@@ -138,6 +210,26 @@ struct snd_usb_babyface {
 	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) */
+	u8 flag_cnt;			/* 0xc000/0x4000/0x8000/0x0000 */
+	u16 master[6][2];		/* cached 16-bit masters */
+	bool muted[6];
+	u16 xpoint[6][14][2];		/* cached crosspoints (out, src, L/R) */
+};
+
+/* The mixer state cached across interface re-probes/resume (see
+ * babyfacepro.c's own comment on bf_state_save/bf_state_restore).
+ * Grows alongside struct snd_usb_babyface as later patches in this
+ * series add more mixer state to persist.
+ */
+struct bf_saved {
+	struct list_head list;
+	char key[32];
+	u8 flag_cnt;
+	u16 master[6][2];
+	bool muted[6];
+	u16 xpoint[6][14][2];
 };
 
 struct bf_rate {
@@ -163,3 +255,28 @@ 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);
 extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
+
+/* Mixer-state persistence across interface re-probes/resume. */
+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);
+
+/* -- babyfacepro-ctl.c ----------------------- */
+extern const u16 bf_flag_cycle[4];
+int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx);
+int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip,
+			      unsigned int blk);
+int babyface_write_default_mixer(struct snd_usb_babyface *chip);
+int bf_apply_masters(struct snd_usb_babyface *chip);
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+		    u16 l, u16 r);
+int babyface_create_masters(struct snd_usb_babyface *chip);
+int babyface_create_xpoints(struct snd_usb_babyface *chip);
+
+/* Master gain-law helpers - shared with the front-panel wheels once
+ * the front panel lands.
+ */
+int bf_master_half_db(u16 vol16);	/* 16-bit master -> dBx2 */
+int bf_master_16bit(int half_db);	/* dBx2 -> 16-bit master */
+u8 bf_master_8bit(u16 vol16);		/* 16-bit master -> 8-bit companion */
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.