mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Ismaïl Bahloul" <i.bahloul01@gmail.com>
To: linux-sound@vger.kernel.org
Cc: tiwai@suse.com, perex@perex.cz, linux-usb@vger.kernel.org,
	alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org,
	corbet@lwn.net, skhan@linuxfoundation.org, rdunlap@infradead.org,
	linux-doc@vger.kernel.org,
	"Ismaïl Bahloul" <i.bahloul01@gmail.com>,
	"David Fredman" <davfre@gmail.com>
Subject: [PATCH v7 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
Date: Tue, 27 Oct 2026 12:31:34 +0100	[thread overview]
Message-ID: <20261027113140.134257-4-i.bahloul01@gmail.com> (raw)
In-Reply-To: <20261027113140.134257-1-i.bahloul01@gmail.com>

Add the input side of the mixer: phantom power and pad, the instrument
ref level, the four preamp gains, and phase invert, stereo split and
trim.  The mic gain byte is packed: bits 0-4 count 3 dB steps and bits
5-7 hold the 0-2 dB remainder.  Phase, split and trim have no
registers of their own - the device implements them on the crosspoint
registers of the AN1/2 monitor bus - and trim combines with the
current fader value on the crosspoint fader curve, which this patch
adds.

bf_state_apply_flags() re-applies phase, split and trim on top of a
restored crosspoint state.  A failed preamp write leaves the cached
state as it was.

Co-developed-by: David Fredman <davfre@gmail.com>
Signed-off-by: David Fredman <davfre@gmail.com>
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 626 ++++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     | 101 +++-
 sound/usb/babyfacepro/babyfacepro.h     |  80 +++
 3 files changed, 806 insertions(+), 1 deletion(-)

diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 9b41dcde5..60604242b 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -654,3 +654,629 @@ 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++) {
+		u16 plain = chip->xpoint[out][mic][0];
+		u16 value = invert ? (u16)~plain : plain;
+
+		/* The AN1/2 block is written without the rotating bits, as
+		 * bf_xpoint_write() does.
+		 */
+		flag = 0;
+		if (out) {
+			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_XPOINT_BASE_L +
+				       BF_REG_XPOINT_STRIDE * out + s->idx_l) |
+				      flag);
+		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 (its block of the map),
+ * the same scope as CUE/mute/solo. `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];
+	u16 l = split ? 0x2000 : 0x1000;
+	u16 r = split ? 0x0000 : 0x1000;
+	int ret;
+
+	/* The AN1/2 block.  The same values also went to a "standard map"
+	 * half at 0x0068, which is AS1/2's submix, not part of AN1/2: it
+	 * overwrote AS1/2's own crosspoint for this playback pair.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			      BF_REG_XPOINT_BASE_L + s->idx_l);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			       BF_REG_XPOINT_BASE_R + 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;
+}
+
+/* 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).  It
+ * writes the trim alone on the MASTER curve (0x2000 = 0 dB,
+ * `bf_master_16bit`) into the AN1/2 block, all 4 registers of 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.
+ *
+ * It also wrote the fader and the trim summed, on the fader curve, to a
+ * "standard map" at 0x0068.  That block is AS1/2's submix (see
+ * bf_xpoint_write), so those writes never reached AN1/2: they only
+ * overwrote AS1/2's own crosspoints for these inputs, and are gone.
+ *
+ * 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.
+ */
+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];
+	u16 trim_raw = bf_master_16bit(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;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			       BF_REG_LOWMAP_BASE_R + 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;
+}
+
+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);
+	u16 old = chip->preamp;
+	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) {
+		/* Do not leave the cache claiming a state the device
+		 * never took.
+		 */
+		chip->preamp = old;
+		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 de1659307..c2835e2d6 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -255,7 +255,21 @@ int bf_cold_init(struct snd_usb_babyface *chip)
  */
 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);
@@ -286,6 +300,68 @@ int babyface_restore_state(struct snd_usb_babyface *chip)
 	return bf_pitch_write(chip, chip->pitch);
 }
 
+/* Re-apply the per-input state that sits on top of the crosspoints
+ * (phase invert, stereo split, trim) 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 ret;
+
+	/* Re-apply any engaged Phase invert - the crosspoint restore loop
+	 * above already re-wrote xpoint[][] as PLAIN values, so a phase
+	 * negation needs to be re-asserted on top, the same way the ON
+	 * state itself is applied (bf_phase_apply).
+	 */
+	{
+		int mic;
+
+		for (mic = 0; mic < 4; mic++) {
+			if (!chip->phase[mic])
+				continue;
+			ret = bf_phase_apply(chip, mic, true);
+			if (ret < 0)
+				return ret;
+		}
+	}
+
+	/* Re-apply any engaged stereo split (fixed constants, no fader
+	 * dependency - see bf_split_apply's own comment).
+	 */
+	{
+		int pb;
+
+		for (pb = 0; pb < 6; pb++) {
+			if (!chip->split[pb])
+				continue;
+			ret = bf_split_apply(chip, pb, true);
+			if (ret < 0)
+				return ret;
+		}
+	}
+
+	/* Re-apply any non-zero Trim (fader+trim combined, same reasoning
+	 * as phase - see bf_trim_apply's own comment). Only the pair's
+	 * even index needs to fire this (it always writes both channels),
+	 * and bf_trim_put keeps both entries of a pair equal, so the even
+	 * index holds the value that is really on the wire even when the
+	 * odd channel's control was the one the user touched.
+	 */
+	{
+		int mic;
+
+		for (mic = 0; mic < 4; mic += 2) {
+			if (!chip->trim[mic])
+				continue;
+			ret = bf_trim_apply(chip, mic, chip->trim[mic] * 2);
+			if (ret < 0)
+				return ret;
+		}
+	}
+	return 0;
+}
+
 /* 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.
@@ -1335,6 +1411,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) {
@@ -1374,6 +1451,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);
@@ -1429,6 +1507,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];
+
 	/* The 0x16 clear zeroed the mixer registers: apply the factory
 	 * default routing to keep the outputs live out of the box.  alsactl
 	 * restores the user's settings once the card is registered.
@@ -1494,6 +1581,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 a3b118bed..156a8c7dd 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -119,6 +119,45 @@
  */
 #define BF_SETTINGS_CLOCK_INTERNAL	0x0001
 
+/* 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) */
@@ -262,6 +301,37 @@ struct snd_usb_babyface {
 	u16 master[6][2];		/* cached masters, in BF_MASTER_STEP_* */
 	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 */
 };
 
 struct bf_rate {
@@ -290,6 +360,7 @@ extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
 
 /* Mixer-state persistence across interface re-probes. */
 int babyface_restore_state(struct snd_usb_babyface *chip);
+int bf_state_apply_flags(struct snd_usb_babyface *chip);
 
 /* babyfacepro-ctl.c */
 extern const u16 bf_flag_cycle[4];
@@ -302,6 +373,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 (babyfacepro-ctl.c). */
 int bf_master_16bit(int half_db);	/* dBx2 -> 16-bit master */
-- 
2.56.0


  parent reply	other threads:[~2026-10-27 11:32 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 11:31 [PATCH v7 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
2026-10-27 11:31 ` Ismaïl Bahloul [this message]
2026-10-27 11:31 ` [PATCH v7 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-10-27 11:31 ` [PATCH v7 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul

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=20261027113140.134257-4-i.bahloul01@gmail.com \
    --to=i.bahloul01@gmail.com \
    --cc=alsa-devel@alsa-project.org \
    --cc=corbet@lwn.net \
    --cc=davfre@gmail.com \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-sound@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    --cc=perex@perex.cz \
    --cc=rdunlap@infradead.org \
    --cc=skhan@linuxfoundation.org \
    --cc=tiwai@suse.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®