[RFC PATCH v5 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
From: Ismaïl Bahloul
Date: Fri Sep 18 2026 - 08:01:59 EST
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 <davfre@xxxxxxxxx>
Signed-off-by: David Fredman <davfre@xxxxxxxxx>
Signed-off-by: Ismaïl Bahloul <i.bahloul01@xxxxxxxxx>
---
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