From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=209.85.216.45 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793100762; b=cwT45CC02jnHUeo1NKpKOtco3tGXMcJr53Ol3uqWsuZl1dwwXFSvNlRzuglxbKnP2xVp uTNTMfxpNhlBGoC6jrUleCX5WprOKgFQ5V30jCCDKuGoWGXc1AjUe590aPAK8cqOFuC1c AbnV7nq63tNLjHPm4eFl2xsFhyTILIkdc6Jb6ggIJFI4vjme8dr9NbG1pSwcTlOX/JL7W kgV5pIKxGnt2DizXIrwewE85KIxinDBXwFP2/VcYQ05ZHnmc7BVNtAf6x8jZWkX+OLLaX bapcXUnxhhCXcLvMXHpX7f2tETNv6MlnvpcVQHkW3f428bwJnHXnz9FSqLLAdqX1NNg== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793100762; h=DMARC-Filter:Received:DKIM-Signature:X-Google-DKIM-Signature: X-Forwarded-Encrypted:X-Gm-Message-State:X-Gm-Gg:X-Received:Received: From:To:Cc:Subject:Date:Message-ID:X-Mailer:In-Reply-To:References: MIME-Version:Content-Type:Content-Transfer-Encoding; bh=g9ZX0tY+g5dn8C4r41SHQkDTfFlY8VsjBEYLPtbp1g0=; b=zWgrNgNjL38g0AtYoli3aszK4J61npHDF7Qyswg3qFKV5np0hkcSv12aXeXilPSRxuw0 w1l3CvwO+EJoxqQHlTnMlC2RUXNRycOTxGn4U254VLoJb2pviTyup6e6XsH8IP4z9FH1q oM3P5PKYHInkBgcGSiXyyjSri9gpcJySnHOGRUzNuC/++jQuSUdhgPKZYKnUC9RQMWy4O G2V0bBI9ZWD4+BeUeevPdPAxbmCfFgeNXL02KG8HX7P0N66ITa+p9N/K2O9FUyi/nDHEC ppvGszAO5YIW9IATSRMzKfVF5F5q2Kv9L2vXNwqXBGNSFecvhwXisIHMy45r4r9tzCw== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=CI9vSqYN; arc=none smtp.remote-ip=209.85.216.45 Received: from mail-pj1-f45.google.com (mail-pj1-f45.google.com [209.85.216.45]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1AE8E606A31B for ; Tue, 27 Oct 2026 11:32:39 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org 1AE8E606A31B Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=CI9vSqYN Received: by mail-pj1-f45.google.com with SMTP id 98e67ed59e1d1-3ad787b79ffso2258194a91.1 for ; Tue, 27 Oct 2026 04:32:39 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1793100759; x=1793705559; darn=vger.kernel.org; h=content-transfer-encoding:content-type:mime-version:references :in-reply-to:message-id:date:subject:cc:to:from:from:to:cc:subject :date:message-id:reply-to:content-type; bh=g9ZX0tY+g5dn8C4r41SHQkDTfFlY8VsjBEYLPtbp1g0=; b=CI9vSqYNnd750rHV5R4JTUEtS9bRxRDu9YHA+TBW6WOp5om/KvQI+Qy1U26K+4JqiB UVeF6ZJ+XNTgHiXBPoDA5sYr6XzYBBlKK5OsSP3tpU0k1cGorIxuECIB7Afq0HP4VuO1 /j2tWDi0jy9iewpu0aeWyDam+Tt6lkGAEOUxconz6LAGhr4hmL6AG1FSQDu8EfD2GMBE A5Cd8Cfenj6xgMQMW/2Pm/cwqUnwGpZl7Z/HUdOxcdM7I9+TTrmRy5fS60wrWzO5uZaE CykNWSiRRKMnVfmNV3qfVxYInaLO6gjyGOKIYysksmqVNPSmhP167yHtSM8UwquXufhy A9KQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1793100759; x=1793705559; h=content-transfer-encoding:content-type:mime-version:references :in-reply-to:message-id:date:subject:cc:to:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=g9ZX0tY+g5dn8C4r41SHQkDTfFlY8VsjBEYLPtbp1g0=; b=tjfmrgiyRxTj5GKBmUKgAfibfpfqiv2Sm/y02yuu31IVTJY1iUAATiCQSeVMbAJiIl oW9QRkZGdMZWRSDQKVykwHTd+dASISki8HvWahGW4sBoKf5Us/qjPGV4eVcRHX3vg/D6 pZlZd24XH2QnVad1naUJk8zc1SjI+zMYtiEWz5pZIajlYNL+VnKVY4pIYxI23PNEdpnB oPJDblD8tqEMQZZsWb4dVaB7ZVYMRS4fbh/86OIGCwe6NADHIbXi2ISNAoCx/bhy6BwO QHkl3PXJQgBkQhkkdU67sJJBIyHOZ7qvbthkc3lnVtq9OYbx0gnpaU4sHFlD6fxgGLb1 4utg== X-Forwarded-Encrypted: i=1; AKwUvBy5LB6VkIgKT2OQSwFfCd2TW3IzoX6spvcxGdtJwPggx1KBXRgMPNXl/lBmBbKtWAWfucj2KOZxOTE=@vger.kernel.org X-Gm-Message-State: AFq9FYK8D2BDddNVmJ9SoHFp1abOg7k3cvFKs9p9vowdtpid5IVa/vud j/mwOWd4Q7KjQLj0g91rdLiyJCaMaUZam53g38f/m0OGi8aKS2EwuKTG X-Gm-Gg: AYBFou2Y51W4f8T91axwMv6/2VCMGgpDSOFf6WM8OPHvPUYXpVGi7DItwnEMspl9PvH 3xaa36egomlCy/ldUQ/UFLL1xpZWd4i/fTqRipam9D/T6uJr2NJYRWUa8mXn3IxnBN0U1oQGO+A 0oGwZNXr9uJU3fGtAk3ylnoJSHxidWzQxyqWtUUY27tWCbu7uHer/K1u32fnz/kFb0C7AjfSfqE VUd1kd4wnSSlv9etoyl756onTtTzG+edWc9E6lF/kQZfaEu77S67RSqOswc5ORW0I1WndaVk9OP 0CD6ZJShLgYVw57GloSOwX+yIsgZmeW5vnD/CaZgf38G58voXFWRVTVyj+JcfzY49xaP5uX8+3Q gysvQEI0cwMNlrmyDT42nXX84D7JAR9wl6lMg6C2DX2fCrb0f5ITWhzBGosJ7N6rtrzEoKye8fV l1FV4K1GuCNzlYZIjhWwGgl09VW62h/ivjTG1CtAGz6CUb/3Dwdgvn7x3EMKE3Fhjg7V3DtJT8s vk8C2OPkYqPAOlOXNWWmcm9wYV4PkjoEDC0KNUe3xamoz6kuCPGBWnhhZ91GGMOhnayEf1IaOgI cCe96BLkGHh1tJCQ12XN3eeqqvATIrIhZlKbF0tH7WaPaEyiR5kqaHhWl1gdvAosQupvT/PzIQU X X-Received: by 2002:a17:90a:e18d:b0:3a7:9045:fe29 with SMTP id 98e67ed59e1d1-3ade9b2cf0fmr7436783a91.4.1793100758977; Tue, 27 Oct 2026 04:32:38 -0700 (PDT) Received: from cachyos (2a01cb0c82caf900a186612f304bfea5.ipv6.abo.wanadoo.fr. [2a01:cb0c:82ca:f900:a186:612f:304b:fea5]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3ae4041fafcsm4523962a91.0.2026.10.27.04.32.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Oct 2026 04:32:38 -0700 (PDT) From: =?UTF-8?q?Isma=C3=AFl=20Bahloul?= 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, =?UTF-8?q?Isma=C3=AFl=20Bahloul?= , David Fredman 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 Message-ID: <20261027113140.134257-4-i.bahloul01@gmail.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261027113140.134257-1-i.bahloul01@gmail.com> References: <20261027113140.134257-1-i.bahloul01@gmail.com> X-Mailing-List: linux-doc@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: David Fredman Signed-off-by: Ismaïl Bahloul --- 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