[RFC PATCH v5 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim

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 mic-input side of the mixer: phantom power, pad, instrument
ref level, the four preamp gains, and the phase/stereo-split/trim
controls layered on the AN1/2 monitor bus crosspoint registers.

The phase/split/trim registration loops and the crosspoint fader
curve they share with the front-panel MIX wheel (added by a later
patch) were split out of the same source function as the crosspoint
matrix; the curve is introduced here because bf_trim_apply() is its
first user.

Co-developed-by: David Fredman <[email protected]>
Signed-off-by: David Fredman <[email protected]>
Signed-off-by: Ismaïl Bahloul <[email protected]>
---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 752 ++++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     |  51 +-
 sound/usb/babyfacepro/babyfacepro.h     |  85 +++
 3 files changed, 887 insertions(+), 1 deletion(-)

diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index a3d3252f5..8ee711b81 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -705,3 +705,755 @@ int babyface_create_masters(struct snd_usb_babyface *chip)
 	return 0;
 }
 
+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;
+	/* Boost's 0x21 commit value (0x0003) is NOT a persisted register
+	 * bit - PROTOCOL.md's "Ref level" section found it only in the
+	 * one-shot 0x21 value alongside the 0x17 state write, so it has
+	 * to be re-sent alongside EVERY preamp write (phantom/PAD toggles
+	 * included), or Boost would silently degrade to plain -10dBV the
+	 * next time anything else touches this shared byte.
+	 */
+	return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT,
+			       chip->ref_level == BF_REF_LEVEL_BOOST ?
+			       0x0003 : 0x0000, 0x0000);
+}
+
+/* Phase (polarity) invert (AN1-4 only, PROTOCOL.md "Phase toggle",
+ * hardware-verified 2026-08-23): NEGATE (bitwise NOT, not two's
+ * complement) the L crosspoint register on every output pair's
+ * standard map, plus the AN1/2 low-map shadow specifically (the same
+ * single low-map register set CUE/mute/solo already use for that
+ * monitor bus - see this driver's own bf_ms_put for the address
+ * pattern). `chip->xpoint[out][mic][0]` is deliberately left holding
+ * the PLAIN value the user actually set - only the value WRITTEN to
+ * hardware is negated - so the crosspoint control's own readback still
+ * reports the real fader position while phase is engaged.
+ *
+ * KNOWN LIMITATION, same class TuxMix's own USB backend already has
+ * in `usb.rs::set_phase` (not fixed there either, as of this writing):
+ * this negates the CURRENT register value once, at toggle time. A
+ * later `bf_xpoint_put` on the same [out][mic] slot (i.e. the user
+ * drags that fader again while phase is engaged) writes the plain
+ * value, silently un-inverting phase until the user re-toggles it.
+ * Making the crosspoint hot path itself phase-aware would close this
+ * properly, but touches every one of the 84 crosspoint controls'
+ * write path - out of scope for this pass; flagged rather than
+ * silently shipped.
+ */
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert)
+{
+	const struct bf_source *s = &bf_sources[mic];
+	int out, ret;
+	u16 flag;
+
+	for (out = 0; out < 6; out++) {
+		unsigned int blk = bf_xpoint_block[out];
+		u16 plain = chip->xpoint[out][mic][0];
+		u16 value = invert ? (u16)~plain : plain;
+
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+				      (BF_REG_CROSS_BASE_L +
+				       BF_REG_CROSS_STRIDE * blk + s->idx_l) |
+				      flag);
+		if (ret < 0)
+			return ret;
+
+		if (out == 0) {
+			ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+					      s->idx_l);
+			if (ret < 0)
+				return ret;
+		}
+	}
+	return 0;
+}
+
+static int bf_phase_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_phase_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;
+
+	ucontrol->value.integer.value[0] = chip->phase[mic];
+	return 0;
+}
+
+static int bf_phase_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;
+	bool invert = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (invert == chip->phase[mic])
+		goto out;
+	ret = bf_phase_apply(chip, mic, invert);
+	if (ret < 0)
+		goto out;
+	chip->phase[mic] = invert;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+/* Stereo split (PROTOCOL.md "Stereo split", cap_ctrl3.pcap, hardware-
+ * verified): a playback pair's signal into the AN1/2 monitor bus goes
+ * hard-split (L=0x2000/R=0x0000, "split-mono") instead of the normal
+ * stereo pair (L=R=0x1000, -6 dB each side) - fixed constants, not
+ * derived from the current fader value (unlike Phase, there's nothing
+ * to preserve), matching TuxMix's own `usb.rs::set_stereo_split`
+ * exactly. Only reaches the AN1/2 destination (low map + that output's
+ * standard crosspoint block) - same scope as CUE/mute/solo's own
+ * low-map-only reach. `chip->xpoint[][]` is deliberately left
+ * untouched, same reasoning as Phase.
+ */
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split)
+{
+	const struct bf_source *s = &bf_sources[8 + pb];
+	unsigned int blk = bf_xpoint_block[0]; /* AN1/2 output */
+	u16 l = split ? 0x2000 : 0x1000;
+	u16 r = split ? 0x0000 : 0x1000;
+	int ret;
+
+	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;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			      (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+			       s->idx_l));
+	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));
+}
+
+static int bf_split_get(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int pb = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->split[pb];
+	return 0;
+}
+
+static int bf_split_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int pb = kctl->private_value;
+	bool split = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	mutex_lock(&chip->mutex);
+	if (split == chip->split[pb])
+		goto out;
+	ret = bf_split_apply(chip, pb, split);
+	if (ret < 0)
+		goto out;
+	chip->split[pb] = split;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+/* bf_fader_raw_to_db2()/bf_fader_db2_to_raw() (the crosspoint fader
+ * curve) are defined further down in this file, alongside the
+ * front-panel wheel code that was their first user - forward-declared
+ * here rather than moved, to keep this diff to additions only.
+ */
+static int bf_fader_raw_to_db2(u16 raw);
+static u16 bf_fader_db2_to_raw(int db2);
+
+static int bf_trim_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo);
+static int bf_trim_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol);
+static int bf_trim_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol);
+
+/* Input Trim (T button, AN1-4): PROTOCOL.md "Trim (T) write for the
+ * AN1/2 pair" (cap_trim2/3/4.pcap, hardware-verified) - the analog
+ * input's own gain-trim, applied through the crosspoint registers
+ * exactly like a fader (there is no separate trim register). Two
+ * different curves combine: the low map holds the trim ALONE on the
+ * MASTER curve (0x2000 = 0 dB, `bf_master_16bit`); the standard map
+ * holds fader+trim SUMMED on the FADER curve (`bf_fader_db2_to_raw`).
+ * Always writes all 8 registers for the pair (both AN1+AN2 or both
+ * AN3+AN4, matching the vendor software's linked-strip behaviour);
+ * `mic` may be either channel of the pair, and the base is derived
+ * (`mic & ~1`) so the write always lands on the correct pair's
+ * registers regardless of which channel's control triggered it.
+ * Destination is always the AN1/2 monitor bus, the same scope the
+ * MS-processor and CUE writes have, and the same one the vendor
+ * software's Trim reaches.
+ *
+ * Trim is a genuinely SHARED value per pair on real hardware (one
+ * write always touches both channels' registers) but is exposed as 2
+ * per-channel ALSA controls, one per input strip. `bf_trim_put` keeps
+ * the pair's two cache entries equal and notifies the sibling control,
+ * so the cache never claims a per-channel split the hardware cannot
+ * represent - and `bf_state_apply_flags`, which replays the pair from
+ * its even index, always replays the value that is actually on the
+ * wire.
+ *
+ * ONE KNOWN LIMITATION, kept rather than silently hidden:
+ *    Same class as Phase (see `bf_phase_apply`'s own comment):
+ *    `chip->xpoint[0][mic][0]` is read here for the CURRENT fader
+ *    value but never written back - the standard-map register ends up
+ *    holding fader+trim while the cache still holds the plain fader,
+ *    so a later `bf_xpoint_put` on the same slot writes the plain
+ *    value, silently dropping trim from the combined register until
+ *    this is re-applied. Not fixed for the same reason Phase wasn't:
+ *    touches the shared 84-crosspoint write path, out of scope here.
+ */
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2)
+{
+	int base = mic & ~1;
+	int sib = base + 1;
+	const struct bf_source *sb = &bf_sources[base];
+	const struct bf_source *ss = &bf_sources[sib];
+	unsigned int blk = bf_xpoint_block[0]; /* AN1/2 output */
+	u16 trim_raw = bf_master_16bit(trim_db2);
+	int fader_db2 = bf_fader_raw_to_db2(chip->xpoint[0][base][0]);
+	u16 standard_raw = bf_fader_db2_to_raw(fader_db2 + trim_db2);
+	int ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_L + sb->idx_l);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_R + sb->idx_r);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_L + ss->idx_l);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_R + ss->idx_r);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+			      (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+			       sb->idx_l));
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+			      (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+			       sb->idx_r));
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+			      (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+			       ss->idx_l));
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+			       (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+				ss->idx_r));
+}
+
+/* Trim's control value is dB as well, -65..+6. */
+static const DECLARE_TLV_DB_SCALE(bf_trim_tlv, -6500, 100, 0);
+
+static int bf_trim_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 = -65;
+	uinfo->value.integer.max = 6;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_trim_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;
+
+	ucontrol->value.integer.value[0] = chip->trim[mic];
+	return 0;
+}
+
+static int bf_trim_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 sib = mic ^ 1;
+	int db = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	if (db < -65 || db > 6)
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if (db == chip->trim[mic])
+		goto out;
+	ret = bf_trim_apply(chip, mic, db * 2);
+	if (ret < 0)
+		goto out;
+	/* One register per pair on the wire, so both channels of the pair
+	 * really did change: mirror the cache (the state restore replays
+	 * the pair from the even index) and tell user space about the
+	 * sibling control.
+	 */
+	chip->trim[mic] = db;
+	chip->trim[sib] = db;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	if (ret == 1 && chip->trim_kctl[sib])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->trim_kctl[sib]->id);
+	return ret;
+}
+
+/* Phase, stereo split and input trim - registered together since all
+ * three are per-input-strip controls layered on the same AN1/2 monitor
+ * bus crosspoint registers the matrix in babyface_create_xpoints()
+ * already created controls for.
+ */
+int babyface_create_trim(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int src, err;
+
+	for (src = 0; src < 4; src++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Phase Switch",
+			.index = src,
+			.info = bf_phase_info,
+			.get = bf_phase_get,
+			.put = bf_phase_put,
+			.private_value = src,
+		}, chip);
+		strscpy(kctl->id.name, bf_sources[src].name, sizeof(kctl->id.name));
+		strlcat(kctl->id.name, " Phase Switch", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (src = 0; src < 6; src++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Stereo Split Switch",
+			.index = src,
+			.info = bf_phase_info, /* plain boolean, same shape */
+			.get = bf_split_get,
+			.put = bf_split_put,
+			.private_value = src,
+		}, chip);
+		strscpy(kctl->id.name, bf_sources[8 + src].name, sizeof(kctl->id.name));
+		strlcat(kctl->id.name, " Stereo Split Switch", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (src = 0; src < 4; src++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Trim Volume",
+			.index = src,
+			.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+				  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+			.info = bf_trim_info,
+			.get = bf_trim_get,
+			.put = bf_trim_put,
+			.tlv.p = bf_trim_tlv,
+			.private_value = src,
+		}, chip);
+		chip->trim_kctl[src] = kctl;
+		strscpy(kctl->id.name, bf_sources[src].name, sizeof(kctl->id.name));
+		strlcat(kctl->id.name, " Trim Volume", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+	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 x2 (half-dB grid): the MIX wheel steps +/-0.5 dB per click
+ * on this curve (cap_mix.pcap).  0x0000 = -inf (digital mute),
+ * 0x0003 = -62 dB, ... 0x2D41 = +6 dB.  Raw values interpolate linearly
+ * between the 1-dB points.  This table's own front-panel MIX-mode user
+ * (the wheel readback) is added by a later patch in this series; it is
+ * introduced here because bf_trim_apply() above already needs it to
+ * combine trim with the current fader value on the same curve.
+ */
+#define BF_FADER_DB2_INF	(-130)	/* -65 dB = the wheel's -inf floor */
+
+static const struct bf_fader_pt {
+	s16 db2;	/* dB x 2 */
+	u16 raw;
+} bf_fader_curve[] = {
+	{ -124, 0x0003 }, { -122, 0x0004 }, { -120, 0x0005 },
+	{ -118, 0x0006 }, { -116, 0x0007 }, { -114, 0x0008 },
+	{ -112, 0x0009 }, { -110, 0x000a }, { -108, 0x000b },
+	{ -106, 0x000d }, { -104, 0x000e }, { -102, 0x0010 },
+	{ -100, 0x0012 }, {  -98, 0x0014 }, {  -96, 0x0017 },
+	{  -94, 0x0019 }, {  -92, 0x001d }, {  -90, 0x0020 },
+	{  -88, 0x0024 }, {  -86, 0x0029 }, {  -84, 0x002e },
+	{  -82, 0x0033 }, {  -80, 0x003a }, {  -78, 0x0041 },
+	{  -76, 0x0049 }, {  -74, 0x0051 }, {  -72, 0x005b },
+	{  -70, 0x0067 }, {  -68, 0x0073 }, {  -66, 0x0081 },
+	{  -64, 0x0091 }, {  -62, 0x00a3 }, {  -60, 0x00b7 },
+	{  -58, 0x00cd }, {  -56, 0x00e6 }, {  -54, 0x0102 },
+	{  -52, 0x0122 }, {  -50, 0x0145 }, {  -48, 0x016d },
+	{  -46, 0x019a }, {  -44, 0x01cc }, {  -42, 0x0204 },
+	{  -40, 0x0243 }, {  -38, 0x028a }, {  -36, 0x02d9 },
+	{  -34, 0x0332 }, {  -32, 0x0396 }, {  -30, 0x0406 },
+	{  -28, 0x0483 }, {  -26, 0x0510 }, {  -24, 0x05af },
+	{  -22, 0x0660 }, {  -20, 0x0727 }, {  -18, 0x0807 },
+	{  -16, 0x0902 }, {  -14, 0x0a1b }, {  -12, 0x0b57 },
+	{  -10, 0x0cb9 }, {   -8, 0x0e47 }, {   -6, 0x1004 },
+	{   -4, 0x11f9 }, {   -2, 0x142a }, {    0, 0x16a0 },
+	{    2, 0x1963 }, {    4, 0x1c7c }, {    6, 0x1ff6 },
+	{    8, 0x23dc }, {   10, 0x283d }, {   12, 0x2d41 },
+};
+
+/* Fader raw -> dBx2 (linear interpolation; raw 0 = -inf). */
+static int bf_fader_raw_to_db2(u16 raw)
+{
+	int i;
+
+	if (raw == 0 || raw < bf_fader_curve[0].raw)
+		return BF_FADER_DB2_INF;
+	for (i = 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) {
+		if (raw <= bf_fader_curve[i + 1].raw) {
+			u32 num = (u32)(raw - bf_fader_curve[i].raw) *
+				  (u32)(bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2);
+			u32 den = bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw;
+
+			return bf_fader_curve[i].db2 + (int)((num + den / 2) / den);
+		}
+	}
+	return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].db2;
+}
+
+/* dBx2 -> fader raw (linear interpolation; below -62 dB = mute 0). */
+static u16 bf_fader_db2_to_raw(int db2)
+{
+	int i;
+
+	if (db2 <= bf_fader_curve[0].db2)
+		return db2 < bf_fader_curve[0].db2 ? 0 : bf_fader_curve[0].raw;
+	for (i = 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) {
+		if (db2 <= bf_fader_curve[i + 1].db2) {
+			u32 num = (u32)(db2 - bf_fader_curve[i].db2) *
+				  (u32)(bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw);
+			u32 den = bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2;
+
+			return bf_fader_curve[i].raw + (u16)((num + den / 2) / den);
+		}
+	}
+	return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].raw;
+}
+
+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;
+}
+
+/* Ref Level (Instr 3/4) - see the constants' own comment in the
+ * header. A single shared 3-state switch, not per-channel (the
+ * protocol has no independent bits for IN3 vs IN4).
+ */
+static const char *const bf_reflevel_texts[] = {
+	"+4dBu", "-10dBV", "Boost", NULL
+};
+
+static int bf_reflevel_info(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 3, bf_reflevel_texts);
+}
+
+static int bf_reflevel_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->ref_level;
+	return 0;
+}
+
+static int bf_reflevel_put(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	unsigned int item = ucontrol->value.enumerated.item[0];
+	u16 old_preamp;
+	int old_ref_level;
+	int ret = 0;
+
+	if (item > BF_REF_LEVEL_BOOST)
+		return -EINVAL;
+
+	mutex_lock(&chip->mutex);
+	if ((int)item == chip->ref_level)
+		goto out;
+	old_preamp = chip->preamp;
+	old_ref_level = chip->ref_level;
+	chip->preamp = (chip->preamp & ~BF_PREAMP_REF_MASK) |
+		       (item == BF_REF_LEVEL_4DBU ? BF_PREAMP_REF_4DBU : 0);
+	chip->ref_level = item;
+	ret = bf_preamp_state_write(chip);
+	if (ret < 0) {
+		chip->preamp = old_preamp;
+		chip->ref_level = old_ref_level;
+		goto out;
+	}
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+/* Gain scales.
+ *
+ * The mic preamps (AN1/2) span 0-65 dB in 1 dB steps, carried in a
+ * packed byte rather than a plain count:
+ *
+ *	coarse = min(db / 3, 20)	bits 0-4, 3 dB per step
+ *	fine   = db - 3 * coarse	bits 5-7, the 0-2 dB remainder
+ *	value  = (fine << 5) | coarse
+ *
+ * Above 60 dB coarse saturates at 20 and fine continues 3, 4, 5, so
+ * 65 dB is 0xb4.  Decoded from USBPcap captures of TotalMix on
+ * Windows (bbf-gain2/3/4.pcap, 48 writes, all matching).
+ *
+ * Bits 5-7 were previously read as a transaction counter and written
+ * with a rotating 0x20/0x00/0x40, which both discarded the fine part
+ * of the setting and applied 0-2 dB of error depending on where the
+ * rotation happened to be.
+ *
+ * The Hi-Z instrument inputs (AN3/4) are not packed: the value is the
+ * gain in 0.5 dB units, 0-9 dB over 0-18.
+ */
+int bf_gain_max_db(int mic)
+{
+	return mic < 2 ? BF_GAIN_MAX_DB : 9;
+}
+
+int bf_gain_db(int mic, u8 raw)
+{
+	if (mic >= 2)
+		return raw / 2;
+	return 3 * (raw & BF_GAIN_COARSE_MASK) + (raw >> BF_GAIN_FINE_SHIFT);
+}
+
+u8 bf_gain_raw(int mic, int db)
+{
+	int coarse, fine;
+
+	if (mic >= 2)
+		return db * 2;
+	coarse = min(db / 3, BF_GAIN_COARSE_MAX);
+	fine = db - 3 * coarse;
+	return (u8)((fine << BF_GAIN_FINE_SHIFT) | coarse);
+}
+
+/* The preamp control's value already IS the gain in dB (0..65 for the mic
+ * inputs, 0..9 for the instrument ones), and the hardware really does
+ * resolve every one of those steps - bf_gain_raw() packs it into the
+ * register's coarse and fine fields.
+ */
+static const DECLARE_TLV_DB_SCALE(bf_gain_tlv, 0, 100, 0);
+
+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 packed register value is derived
+	 * at write time (bf_gain_raw).
+	 */
+	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;
+	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);
+
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, (u16)raw,
+			      BF_REG_GAIN + mic);
+	if (ret < 0)
+		goto out;
+	chip->gain[mic] = db;
+	ret = 1;
+out:
+	mutex_unlock(&chip->mutex);
+	return ret;
+}
+
+/* Preamp controls: phantom power, pad, instrument ref level and the
+ * four mic/instrument gains.  Registered separately from the output
+ * masters (babyface_create_masters()) since they cover a different
+ * part of the signal path (mic input, not output routing).
+ */
+int babyface_create_preamp(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int i, err;
+
+	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;
+	}
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Instrument Ref Level",
+		.info = bf_reflevel_info,
+		.get = bf_reflevel_get,
+		.put = bf_reflevel_put,
+	}, 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,
+			.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+				  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+			.info = bf_gain_info,
+			.get = bf_gain_get,
+			.put = bf_gain_put,
+			.tlv.p = bf_gain_tlv,
+			.private_value = i,
+		}, chip);
+		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
index 50b6a31a5..3a564f853 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -279,7 +279,21 @@ static DEFINE_MUTEX(bf_saved_mutex);
  */
 int babyface_restore_state(struct snd_usb_babyface *chip)
 {
-	int out, src, ret;
+	int out, src, mic, ret;
+
+	/* Preamp state + commit. */
+	ret = bf_preamp_state_write(chip);
+	if (ret < 0)
+		return ret;
+
+	/* The four input gains. */
+	for (mic = 0; mic < 4; mic++) {
+		ret = bf_vendor_write(chip, BF_REQ_GAIN,
+				      (u16)bf_gain_raw(mic, chip->gain[mic]),
+				      BF_REG_GAIN + mic);
+		if (ret < 0)
+			return ret;
+	}
 
 	/* Masters (8-bit = the real volume) + mutes. */
 	ret = bf_apply_masters(chip);
@@ -337,10 +351,16 @@ void bf_state_save(struct snd_usb_babyface *chip)
 		list_add_tail(&s->list, &bf_saved_list);
 	}
 
+	s->preamp = chip->preamp;
+	memcpy(s->gain, chip->gain, sizeof(s->gain));
 	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));
+	memcpy(s->phase, chip->phase, sizeof(s->phase));
+	memcpy(s->trim, chip->trim, sizeof(s->trim));
+	memcpy(s->split, chip->split, sizeof(s->split));
+	s->ref_level = chip->ref_level;
 	mutex_unlock(&bf_saved_mutex);
 }
 
@@ -359,10 +379,16 @@ int bf_state_restore(struct snd_usb_babyface *chip)
 	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->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));
+		memcpy(chip->phase, s->phase, sizeof(chip->phase));
+		memcpy(chip->trim, s->trim, sizeof(chip->trim));
+		memcpy(chip->split, s->split, sizeof(chip->split));
+		chip->ref_level = s->ref_level;
 		ret = 1;
 		break;
 	}
@@ -1118,6 +1144,7 @@ static int babyface_probe(struct usb_interface *intf,
 	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) {
@@ -1152,6 +1179,7 @@ static int babyface_probe(struct usb_interface *intf,
 	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);
@@ -1207,6 +1235,15 @@ static int babyface_probe(struct usb_interface *intf,
 		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.
@@ -1283,6 +1320,18 @@ static int babyface_probe(struct usb_interface *intf,
 		goto error;
 	}
 
+	err = babyface_create_trim(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "phase/split/trim control creation failed: %d\n", err);
+		goto error;
+	}
+
+	err = babyface_create_preamp(chip);
+	if (err < 0) {
+		dev_err(&intf->dev, "preamp control creation failed: %d\n", err);
+		goto error;
+	}
+
 	err = snd_card_register(chip->card);
 	if (err < 0) {
 		dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 6125e763f..505ec1927 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -114,6 +114,45 @@
 #define BF_SETTINGS_CLOCK_INTERNAL	0x0001
 #define BF_SETTINGS_CLOCK_OPTICAL	0x0004
 
+/* 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_REG_PREAMP			0x003f
+#define BF_REG_GAIN			0x0000	/* + mic 0-3 (bReq 0x1a) */
+#define BF_PREAMP_REF_4DBU		0x000c
+#define BF_PREAMP_REF_MASK		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
+
+/* Ref Level (Instr 3/4) - PROTOCOL.md "Ref level (Instr 3/4) - LABELED"
+ * (cap_reflevel2.pcap, hardware-verified): a single shared 3-state
+ * switch for the Instrument pair.  +4dBu/-10dBV are bits 2-3 of the
+ * preamp byte (BF_PREAMP_REF_MASK); Boost shares -10dBV's bits and is
+ * distinguished only by the 0x21 commit value (0x0003, not the usual
+ * 0x0000) - not a persisted register bit, so it must be tracked
+ * host-side (chip->ref_level) and re-asserted on every preamp write,
+ * not just the one that engaged it (see bf_preamp_state_write).
+ */
+#define BF_REF_LEVEL_4DBU		0
+#define BF_REF_LEVEL_MINUS10DBV		1
+#define BF_REF_LEVEL_BOOST		2
+
+/* Preamp gain: 0-65 dB in 1 dB steps, packed coarse/fine (see the
+ * gain-scale comment above bf_gain_max_db).
+ */
+#define BF_GAIN_MAX_DB			65
+/* Mic gain is packed: bits 0-4 coarse (3 dB), bits 5-7 the 0-2 dB rest. */
+#define BF_GAIN_COARSE_MASK		0x1f
+#define BF_GAIN_COARSE_MAX		20
+#define BF_GAIN_FINE_SHIFT		5
+
 /* 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) */
@@ -216,6 +255,37 @@ struct snd_usb_babyface {
 	u16 master[6][2];		/* cached 16-bit masters */
 	bool muted[6];
 	u16 xpoint[6][14][2];		/* cached crosspoints (out, src, L/R) */
+	u16 preamp;			/* 48V/PAD bits, base 0x0c */
+	u8 gain[4];			/* preamp gain in dB 0-65/9 (raw derived
+					 * at write: mic packed coarse/fine,
+					 * instr 0.5 dB/step)
+					 */
+	bool phase[4];			/* polarity invert, AN1-4 (bf_sources 0-3);
+					 * xpoint[][0..3][0] stays the PLAIN
+					 * value, only the wire write is
+					 * negated - see bf_phase_put's own
+					 * comment for the known limitation
+					 * this implies.
+					 */
+	int trim[4];			/* Trim (T), dB (-65..+6), AN1-4;
+					 * one shared register per pair, so
+					 * both entries of a pair are kept
+					 * equal; same "wire-only" caveat as
+					 * phase - see bf_trim_apply's own
+					 * comment.
+					 */
+	bool split[6];			/* stereo split, playback pairs PB1-PB6
+					 * (bf_sources idx 8-13); fixed
+					 * constants (PROTOCOL.md "Stereo
+					 * split"), not derived from the
+					 * fader - xpoint[][] is left
+					 * untouched, same as phase.
+					 */
+	int ref_level;			/* Instr 3/4 ref level, one of the
+					 * BF_REF_LEVEL_* values
+					 * (0 = +4dBu, the default)
+					 */
+	struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
 };
 
 /* The mixer state cached across interface re-probes/resume (see
@@ -226,10 +296,16 @@ struct snd_usb_babyface {
 struct bf_saved {
 	struct list_head list;
 	char key[32];
+	u16 preamp;
+	u8 gain[4];
 	u8 flag_cnt;
 	u16 master[6][2];
 	bool muted[6];
 	u16 xpoint[6][14][2];
+	bool phase[4];
+	int trim[4];
+	bool split[6];
+	int ref_level;
 };
 
 struct bf_rate {
@@ -273,6 +349,15 @@ 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);
+int babyface_create_trim(struct snd_usb_babyface *chip);
+int babyface_create_preamp(struct snd_usb_babyface *chip);
+int bf_preamp_state_write(struct snd_usb_babyface *chip);
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert);
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split);
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2);
+int bf_gain_max_db(int mic);
+int bf_gain_db(int mic, u8 raw);
+u8 bf_gain_raw(int mic, int db);
 
 /* Master gain-law helpers - shared with the front-panel wheels once
  * the front panel lands.
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.