* [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode)
@ 2026-09-18 11:39 Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
` (7 more replies)
0 siblings, 8 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Hi Takashi,
This is v5, re-split per your v4 review: still too coarse, start with
a minimalistic core (probe/disconnect/PCM only), add mixers
piece-by-piece, suspend/resume near the end, and add proper "big
picture" documentation for human reviewers.
Beyond the restructuring, this revision also folds in three
hardware-verified fixes found by a contributor (David Fredman,
credited per-patch via Co-developed-by/Signed-off-by) since v4 went
out:
- The sample rate is now set through the family register the
firmware actually keys off, rather than the previous approach,
which could leave the device on the wrong rate after certain
family transitions. A running stream also now locks the rate: a
second client asking for a conflicting rate is refused at
hw_params rather than silently retuning a live stream out from
under it (patches 1, 2, 4).
- The front-panel OUT selector's base-mode decode (Ch 1/2 vs Phones
vs Opt) was wrong on selector values 0/1/2, corrected against the
physical LEDs on both a non-FS unit and the Pro FS (patch 6).
- Turning the front-panel wheel with OUT selected produced audible
zipper noise: the firmware moves the analog output level itself on
each click with its own ~12 ms smoothing, and the driver was also
writing the analog (8-bit) master on every poll from a flat
1 dB/click count, fighting the firmware's own move each time. The
driver now writes only the digital (16-bit) master during the
gesture, follows the firmware's actual level- and speed-dependent
step size, and reconciles the real master once the wheel is at
rest, the way TotalMix does (patch 6).
Every patch was built in-tree on its own against next-20260911 and is
checkpatch --strict clean of errors; five WARNING-level notices remain
across the series when checkpatch is run against the patch files
themselves (the same two `ang` "misspellings" in the EQ patch noted in
v4, plus one new `retuned` "misspelling" from the rate fix above -
all three are correctly spelled, BIT() is separately declined as a
CHECK in the EQ patch because it returns unsigned long, wrong for that
file's signed Q27 fixed-point math - and two routine "does MAINTAINERS
need updating?" reminders on the patches that add files, answered
once in patch 1's
MAINTAINERS entry for the whole series).
How this v5 is split, and why:
1. Core: probe, disconnect, PCM stream only. No mixer, no front
panel, no DSP EQ, no suspend/resume. This alone is a working
(if silent, no routing yet) capture/playback driver.
2. Output masters + the crosspoint routing matrix, together: a
driver with masters but no routing would still be silent, since
the factory-default routing this patch also adds is what actually
connects a source to an output. This is the first patch that
produces controllable, audible output.
3. Mic preamp: phantom power, pad, instrument ref level, the four
gains, plus phase/stereo-split/trim (input-strip controls layered
on the crosspoint registers the previous patch introduced).
4. Routing flags and varispeed pitch, together: loopback, AN 1>2,
sample clock source, AN1/2 link, MS processor, DIM, width, FX
send, and pitch. These started life as one function registering
all of them in a single pass; splitting pitch out on its own would
have meant an artificial function split with no real benefit.
5. S3 suspend/resume.
6. The front-panel poll and controls (buttons, wheel, IN/OUT/SELECT,
MIX, DIM) plus the host-side emulation of TotalMix's own role for
the wheel/button behaviour.
7. The hardware 3-band + low-cut DSP EQ.
8. A new documentation patch (Documentation/sound/cards/
babyface-pro.rst): why the proprietary mode needs a standalone
driver, the vendor protocol's write-only/no-readback shape, the
asynchronous stream model, why mixer state has to be cached and
replayed, and why the driver emulates TotalMix for the front
panel. This is meant to give a reviewer the big picture before
reading patches 1-7, not to restate what their own commit messages
and code comments already cover.
Two places needed real judgement calls rather than a mechanical split
along control names, both explained above and in the affected patches'
own commit messages: masters+crosspoint (patch 2) and flags+pitch
(patch 4) are each one patch instead of further split, because the
underlying code was never separable along those lines without
inventing an artificial seam - the same reasoning v4's own cover
letter gave for not re-splitting the core+mixer patch further at the
time.
Known limitations, unchanged from v4:
- USB autosuspend is not supported, explicitly disabled at probe.
S3 suspend/resume works and is tested.
- A few protocol details remain open (documented, not blocking): the
preamp readback index semantics, a width strip-ownership edge case,
whether DIM's scope is reassignable with TotalMix's Main Out
setting, and the exact high-frequency warping of the EQ
coefficients against the vendor curve.
- The latency profile (frames_per_urb, nurbs) is a module parameter,
not runtime-reconfigurable.
Validation: the same full-duplex rate x period sweep, start/stop
stress, mixer-restore-across-unbind/rebind and mid-stream-disconnect
regression suite as v4, run against the final tree - 40 of 40. Each of
the 8 patches was additionally checked out on its own and built
in-tree, to confirm the split itself introduces no broken intermediate
state. The three fixes above were separately hardware-verified: all
nine rate/family combinations plus varispeed and the rate-lock
refusal (patches 1/2/4); LED-correlated captures of the OUT selector
across both a non-FS unit and the Pro FS (patch 6); and zipper-noise-
free wheel turns against a looped-back level measurement (patch 6) -
each detailed in the affected patch's own commit message.
Disclosure: as with previous versions, the reverse-engineering and
substantial portions of the kernel implementation were written with
heavy assistance from AI coding assistants. All of it was verified by
hand on real hardware; the human authors remain responsible for the
result via Signed-off-by.
Thanks for reading,
Ismaïl
Ismaïl Bahloul (8):
ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
ALSA: usb: babyfacepro: add output masters and crosspoint routing
ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
ALSA: usb: babyfacepro: add routing flags and varispeed pitch
ALSA: usb: babyfacepro: add S3 suspend/resume
ALSA: usb: babyfacepro: add the front-panel poll and controls
ALSA: usb: babyfacepro: add the hardware DSP EQ
Documentation: sound: add the Babyface Pro proprietary-mode design doc
Documentation/sound/cards/babyface-pro.rst | 163 +
Documentation/sound/cards/index.rst | 1 +
MAINTAINERS | 6 +
sound/usb/Kconfig | 20 +
sound/usb/Makefile | 2 +-
sound/usb/babyfacepro/Makefile | 4 +
sound/usb/babyfacepro/babyfacepro-ctl.c | 3775 ++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 1700 +++++++++
sound/usb/babyfacepro/babyfacepro.h | 522 +++
9 files changed, 6192 insertions(+), 1 deletion(-)
create mode 100644 Documentation/sound/cards/babyface-pro.rst
create mode 100644 sound/usb/babyfacepro/Makefile
create mode 100644 sound/usb/babyfacepro/babyfacepro-ctl.c
create mode 100644 sound/usb/babyfacepro/babyfacepro.c
create mode 100644 sound/usb/babyfacepro/babyfacepro.h
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
` (6 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
Minimal first step of the series: card lifecycle (probe/disconnect),
interrupt-URB PCM streaming, and the sample-rate/varispeed clock
mechanism the stream depends on. No mixer controls, front-panel
support, DSP EQ or suspend/resume yet - each is added by its own
later patch.
The sample rate is the family register (request 0x10, index 0x0030:
32k/44.1k/48k family) with SET_INTERFACE(5, alt) selecting the x1/x2/x4
speed on top of it; the 0x1B DDS quad is varispeed, a pitch ratio
applied independently of the family. A running stream now locks the
rate: a second client asking for a different one is refused at
hw_params rather than silently retuning the device out from under a
live stream, matching the vendor drivers' own behaviour.
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>
---
MAINTAINERS | 6 +
sound/usb/Kconfig | 20 +
sound/usb/Makefile | 2 +-
sound/usb/babyfacepro/Makefile | 4 +
sound/usb/babyfacepro/babyfacepro.c | 1200 +++++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.h | 165 ++++
6 files changed, 1396 insertions(+), 1 deletion(-)
create mode 100644 sound/usb/babyfacepro/Makefile
create mode 100644 sound/usb/babyfacepro/babyfacepro.c
create mode 100644 sound/usb/babyfacepro/babyfacepro.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 207a6e2db..31ed00692 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23828,6 +23828,12 @@ F: include/dt-bindings/power/thead,th1520-power.h
F: include/dt-bindings/reset/thead,th1520-reset.h
F: include/linux/firmware/thead/thead,th1520-aon.h
+RME BABYFACE PRO DRIVER (PROPRIETARY MODE)
+M: Ismaïl Bahloul <i.bahloul01@gmail.com>
+L: alsa-devel@alsa-project.org (moderated for non-subscribers)
+S: Maintained
+F: sound/usb/babyfacepro/
+
RNBD BLOCK DRIVERS
M: Md. Haris Iqbal <haris.iqbal@ionos.com>
M: Jack Wang <jinpu.wang@ionos.com>
diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig
index b4588915e..f58eedf3d 100644
--- a/sound/usb/Kconfig
+++ b/sound/usb/Kconfig
@@ -204,6 +204,26 @@ config SND_USB_AUDIO_QMI
To compile this driver as a module, choose M here: the module
will be called snd-usb-audio-qmi.
+config SND_USB_BABYFACE_PRO
+ tristate "RME Babyface Pro / Pro FS (proprietary mode)"
+ select SND_PCM
+ help
+ Say Y here to include support for the RME Babyface Pro and
+ Babyface Pro FS in their proprietary mode (VID 0x2a39,
+ PID 0x3fc0). The two models share the same USB IDs and
+ descriptors and are handled identically.
+
+ The proprietary mode streams PCM over interrupt endpoints
+ (interface 5, ep 0x01/0x82) instead of the class-compliant
+ isochronous path handled by snd-usb-audio, so this driver is
+ standalone (snd-usb-caiaq-style interrupt streaming). This
+ first patch covers probe, disconnect and the PCM stream only;
+ later patches in the series add the mixer, front panel and
+ DSP EQ as standard ALSA controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called snd-usb-babyface-pro.
+
source "sound/usb/line6/Kconfig"
endif # SND_USB
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87..2f83f5881 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -35,5 +35,5 @@ obj-$(CONFIG_SND_USB_UA101) += snd-usbmidi-lib.o
obj-$(CONFIG_SND_USB_USX2Y) += snd-usbmidi-lib.o
obj-$(CONFIG_SND_USB_US122L) += snd-usbmidi-lib.o
-obj-$(CONFIG_SND) += misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/
+obj-$(CONFIG_SND) += misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/ babyfacepro/
obj-$(CONFIG_SND_USB_LINE6) += line6/
diff --git a/sound/usb/babyfacepro/Makefile b/sound/usb/babyfacepro/Makefile
new file mode 100644
index 000000000..5adc6d474
--- /dev/null
+++ b/sound/usb/babyfacepro/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+snd-usb-babyface-pro-y := babyfacepro.o
+
+obj-$(CONFIG_SND_USB_BABYFACE_PRO) += snd-usb-babyface-pro.o
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
new file mode 100644
index 000000000..2d71286e3
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -0,0 +1,1200 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
+ *
+ * Core driver: USB vendor requests + cold init, interrupt-URB PCM
+ * streaming, and the card lifecycle (probe/disconnect/module entry).
+ * No mixer controls, front-panel support or suspend/resume yet - see
+ * babyfacepro.h and the cover letter for the rest of the series.
+ */
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#include "babyfacepro.h"
+
+/* -- sample-rate / alt classes -------------------- */
+
+/* rate = family_base << (alt - 1): three rate families (32k / 44.1k /
+ * 48k), three interface speeds. Measured on hardware, all nine rates,
+ * through the family register alone (2026-09-16).
+ *
+ * 64 kHz is the 32 kHz family at x2, not a 64 kHz base at x1. Both clock
+ * 64 kHz at pitch 0, but a 64 kHz base on alt 1 needs 64000 x 56 B =
+ * 3584 kB/s, which is exactly alt 1's 448-byte packet ceiling: any
+ * positive varispeed then runs off the end of the bus, and the device
+ * delivers the mirrored rate instead (asked +5 %, got -5 %). At x2 the
+ * frame is 40 B, so the same 64 kHz costs 2560 kB/s against a 5120 kB/s
+ * ceiling. Same reasoning puts 128 kHz on alt 3 rather than alt 2.
+ */
+static const struct bf_rate bf_rates[] = {
+ { 32000, BF_ALT_1, 56, 8 },
+ { 44100, BF_ALT_1, 56, 8 },
+ { 48000, BF_ALT_1, 56, 8 },
+ { 64000, BF_ALT_2, 40, 16 },
+ { 88200, BF_ALT_2, 40, 16 },
+ { 96000, BF_ALT_2, 40, 16 },
+ { 128000, BF_ALT_3, 32, 32 },
+ { 176400, BF_ALT_3, 32, 32 },
+ { 192000, BF_ALT_3, 32, 32 },
+};
+
+static const unsigned int bf_rate_list[ARRAY_SIZE(bf_rates)] = {
+ 32000, 44100, 48000, 64000, 88200,
+ 96000, 128000, 176400, 192000,
+};
+
+const struct snd_pcm_hw_constraint_list bf_rates_constraint = {
+ .count = ARRAY_SIZE(bf_rate_list),
+ .list = bf_rate_list,
+ .mask = 0,
+};
+
+const struct bf_rate *bf_rate_lookup(unsigned int rate)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(bf_rates); i++)
+ if (bf_rates[i].rate == rate)
+ return &bf_rates[i];
+ return NULL;
+}
+
+unsigned int bf_rate_family(const struct bf_rate *r)
+{
+ switch (r->rate >> (r->alt - 1)) {
+ case 32000:
+ return 0;
+ case 44100:
+ return 1;
+ default:
+ return 2;
+ }
+}
+
+/* -- vendor requests ----------------------- */
+
+/* Timeout for every vendor control transfer (ms). */
+#define BF_CTL_TIMEOUT 1000
+
+int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx)
+{
+ return usb_control_msg_send(chip->dev, 0, req,
+ USB_DIR_OUT | USB_TYPE_VENDOR |
+ USB_RECIP_DEVICE,
+ val, idx, NULL, 0, BF_CTL_TIMEOUT, GFP_KERNEL);
+}
+
+int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf)
+{
+ return usb_control_msg_recv(chip->dev, 0, req,
+ USB_DIR_IN | USB_TYPE_VENDOR |
+ USB_RECIP_DEVICE,
+ 0, idx, buf, 4, BF_CTL_TIMEOUT, GFP_KERNEL);
+}
+
+/* Sends the BF_REG_KEEPALIVE_SETTINGS word (PROTOCOL.md: "keepalive
+ * 0x10 0x05CF wVal = host settings-state register" - a single shared
+ * word, not independent per-setting writes). Hardcoded to Internal
+ * clock for now; the clock-source control and the chip->clock_optical
+ * bit it composes in land with a later patch in this series, at which
+ * point this grows the same way babyface_restore_state() does further
+ * down the series.
+ */
+int bf_settings_write(struct snd_usb_babyface *chip)
+{
+ return bf_vendor_write(chip, BF_REQ_KEEPALIVE, BF_SETTINGS_CLOCK_INTERNAL,
+ BF_REG_KEEPALIVE_SETTINGS);
+}
+
+/* Set the sample rate: the family register. The firmware derives its
+ * own DDS word from it; SET_INTERFACE(5, alt) (done in hw_params) is the
+ * x1/x2/x4 speed. This is exactly what the RME Windows driver sends on
+ * a rate change (USBPcap, 2026-09-14, all fourteen changes): the family,
+ * then the settings word, nothing else. Measured on this driver's
+ * target hardware for all nine rates with no quad in play (2026-09-16),
+ * and to -1 ppm at 44.1 kHz against an NTP-disciplined clock.
+ *
+ * An earlier version of this branch had this register and measured it as
+ * dead. It was not: bf_cold_init()'s 0x0021 -> 0x05ff write is this
+ * same register (0x05ff = 0x05cf | 0x0030) hardcoded to the 48 kHz
+ * family, and it followed the write. See bf_cold_init().
+ */
+int bf_clock_write(struct snd_usb_babyface *chip)
+{
+ const struct bf_rate *r = bf_rate_lookup(chip->rate);
+ int ret;
+
+ if (!r)
+ return -EINVAL;
+ ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE,
+ bf_rate_family(r) << 4, BF_REG_RATE_FAMILY);
+ if (ret < 0)
+ return ret;
+ return bf_settings_write(chip);
+}
+
+/* Varispeed: the 0x1B DDS quad. It is a pitch RATIO applied on top of
+ * the family rate, not a clock, so it carries no base rate and is the
+ * same four words at every sample rate (measured 2026-09-16: a 48 kHz
+ * quad over the 44.1 kHz family gives 44.1 kHz; the same quad at +5 %
+ * gives 46305). It is sticky: the device keeps the last ratio across
+ * family writes, so pitch 0 must send the x1.0 quad rather than nothing.
+ *
+ * Only bank 2 is read on the Babyface Pro (one bank at a time swapped
+ * between two quads: only bank 2 moved the clock). It is the ratio's
+ * period as a 24-bit word, 1.0 = 2^14 x 25 MHz / 48000 = 8533333.33, and
+ * the firmware's constant is exact: sent exact it measures -1 ppm, sent
+ * as RME's own Q16-truncated word (0x8234d3) it measures +14 ppm. So
+ * bank 2 is computed exactly. Banks 0, 1 and 3 are sent as the RME
+ * host software computes them - bank 0 the x1.0 period 12800000 scaled
+ * by the pitch, banks 1 and 3 that value times fixed Q16 constants
+ * (0xb9c2, 0xa3d7), truncated - so that a unit which reads a different
+ * bank (the FS is not measured) sees exactly what Windows gives it.
+ * Together this reproduces the cold-plug capture's quad at pitch 0 in
+ * banks 0, 1 and 3, and improves on it in bank 2.
+ *
+ * Every quad must be followed by the settings keepalive or it does not
+ * apply. pitch is in 0.1 % steps, -50..50.
+ */
+int bf_pitch_write(struct snd_usb_babyface *chip, int pitch)
+{
+ const u64 num0 = 12800000ULL * 1000; /* bank 0: 12800000 / (1 + p) */
+ const u64 num2 = 409600000000000ULL; /* bank 2: 4.096e11 * 1000 */
+ u32 den0 = 1000 + pitch;
+ u32 den2 = 48000 * den0;
+ u64 b0 = div_u64(num0 + den0 / 2, den0);
+ u64 b[4];
+ int k, ret;
+
+ b[0] = b0;
+ b[1] = (b0 * 47554) >> 16;
+ b[2] = div_u64(num2 + den2 / 2, den2);
+ b[3] = (b0 * 41943) >> 16;
+
+ for (k = 0; k < 4; k++) {
+ ret = bf_vendor_write(chip, BF_REQ_DDS, (b[k] >> 8) & 0xffff,
+ ((b[k] & 0xff) << 8) | k);
+ if (ret < 0)
+ return ret;
+ }
+ return bf_settings_write(chip);
+}
+
+/* The cold-start session init (cap_coldplug.pcap), verbatim from the
+ * user-space reference (protocol::streaming_init). Without it the
+ * firmware never validates a stream.
+ */
+int bf_cold_init(struct snd_usb_babyface *chip)
+{
+ const struct bf_rate *r;
+ int ret, i;
+
+ for (i = 0; i <= 0x3d; i++) {
+ if (i == 0x1e || i == 0x1f)
+ continue;
+ ret = bf_vendor_write(chip, BF_REQ_REG_CLEAR, 0x0000, i);
+ if (ret < 0)
+ return ret;
+ }
+ /* The varispeed quad, where the coldplug capture sends its x1.0
+ * quad. At pitch 0 banks 0, 1 and 3 match the capture; bank 2 is
+ * the exact ratio (see bf_pitch_write).
+ */
+ ret = bf_pitch_write(chip, chip->pitch);
+ if (ret < 0)
+ return ret;
+ /* 0x1C status - the hardware-validated reference (protocol::
+ * streaming_init) sends it as an OUT write; Windows reads it.
+ * Both are tolerated; match the validated path.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_STATUS_2, 0x0000, 0x0000);
+ if (ret < 0)
+ return ret;
+ /* The settings word and the family register in one write (0x05ff =
+ * 0x05cf | 0x0030). The coldplug capture hardcodes 0x0021 here:
+ * family 48 kHz, clock internal. Sent for the rate actually in use,
+ * this IS the rate write; hardcoded, it was the thing that reset every
+ * earlier family write to 48 kHz.
+ */
+ r = bf_rate_lookup(chip->rate);
+ if (!r)
+ return -EINVAL;
+ ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE,
+ (bf_rate_family(r) << 4) | BF_SETTINGS_CLOCK_INTERNAL,
+ BF_REG_KEEPALIVE_INIT);
+ if (ret < 0)
+ return ret;
+ /* 0x17 wIdx=0x0000 does NOT touch the preamp state (0x003F). */
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x000c, 0x0000);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ return ret;
+ for (i = 0; i < 2; i++) {
+ ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0000, 0x3000);
+ if (ret < 0)
+ return ret;
+ }
+ for (i = 0; i < 3; i++) {
+ ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0800, 0x0800);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+/* -- PCM data path ------------------------- */
+
+static bool babyface_capture_copy(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs,
+ u8 *data, unsigned int frames)
+{
+ struct snd_pcm_runtime *rt = subs->runtime;
+ unsigned int buf_frames = rt->buffer_size;
+ unsigned int words = chip->frame_bytes / 4;
+ unsigned int chans = rt->channels;
+ unsigned int pos, f, i;
+ unsigned long new_period;
+ bool crossed = false;
+ u8 *dst;
+
+ spin_lock(&chip->lock);
+ pos = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] % buf_frames;
+ for (f = 0; f < frames; f++) {
+ const __le32 *w = (const __le32 *)(data + f * chip->frame_bytes);
+
+ dst = rt->dma_area + frames_to_bytes(rt, pos);
+ for (i = 0; i < chans; i++) {
+ /* Channel map: app ch0-3 = device words 0-3 (AN1-4);
+ * app ch4-9 = words 6-11 (ADAT/SPDIF); app ch10/11 =
+ * words 12/13 = a FIXED-GAIN playback tap (observed
+ * 2026-08-25: the playback echoes there at ~-27 dB,
+ * independent of the output masters - NOT the output
+ * bus; the ADAT/SPDIF range is words 6-11 only). The
+ * device words 4/5 are a fixed marker, not audio -
+ * skipped. At 96/192 kHz the frame has fewer words;
+ * missing ones read as zero.
+ */
+ static const u8 map[12] = { 0, 1, 2, 3, 6, 7, 8, 9,
+ 10, 11, 12, 13 };
+ u8 wi = i < 12 ? map[i] : 0xff;
+ s32 s = 0;
+
+ if (wi < words) {
+ /* 24-bit sample in bits 31-8 of the 32-bit word;
+ * S32_LE with 24 msbits, so copy the word as-is.
+ */
+ s = (s32)le32_to_cpu(w[wi]);
+ }
+ put_unaligned_le32((u32)s, dst + i * 4);
+ }
+ pos++;
+ if (pos >= buf_frames)
+ pos = 0;
+ }
+ chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] += frames;
+ new_period = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] / rt->period_size;
+ if (new_period != chip->prev_period[SNDRV_PCM_STREAM_CAPTURE]) {
+ chip->prev_period[SNDRV_PCM_STREAM_CAPTURE] = new_period;
+ crossed = true;
+ }
+ spin_unlock(&chip->lock);
+
+ return crossed;
+}
+
+static bool babyface_playback_copy(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs,
+ u8 *data, unsigned int frames)
+{
+ struct snd_pcm_runtime *rt = subs->runtime;
+ unsigned int buf_frames = rt->buffer_size;
+ unsigned int words = chip->frame_bytes / 4;
+ unsigned int chans = rt->channels;
+ unsigned int pos, f, i;
+ unsigned long new_period;
+ bool crossed = false;
+ const u8 *src;
+
+ spin_lock(&chip->lock);
+ /* Never copy frames the app has not prepared. With several URBs in
+ * flight the device can transiently be ahead of the app ring at low
+ * periods, so hw_ptr may reach past appl_ptr; reading beyond appl_ptr
+ * would trip a spurious XRUN. The device just repeats the last
+ * frames instead. (Both counters are unbounded, so clamp on the
+ * signed difference.)
+ */
+ {
+ snd_pcm_sframes_t avail =
+ (snd_pcm_sframes_t)(rt->control->appl_ptr -
+ chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK]);
+
+ if (avail < 0)
+ avail = 0;
+ else if (avail > (snd_pcm_sframes_t)buf_frames)
+ avail = buf_frames;
+ if ((snd_pcm_uframes_t)avail < frames)
+ frames = (unsigned int)avail;
+ }
+ pos = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] % buf_frames;
+ for (f = 0; f < frames; f++) {
+ __le32 *w = (__le32 *)(data + f * chip->frame_bytes);
+
+ src = rt->dma_area + frames_to_bytes(rt, pos);
+ /* App ch n feeds the device word n (PB1-6 = words 0-11);
+ * words 12/13 stay zero. At 96/192 kHz the frame is
+ * shorter - the extra app channels are dropped.
+ */
+ for (i = 0; i < chans && i < words; i++) {
+ u32 s = get_unaligned_le32(src + i * 4);
+
+ /* S32_LE msbits=24: the app's 24 valid bits are already
+ * left-justified (bits 31-8) - copy the word as-is.
+ */
+ w[i] = cpu_to_le32(s);
+ }
+ for (; i < words; i++)
+ w[i] = 0;
+ pos++;
+ if (pos >= buf_frames)
+ pos = 0;
+ }
+ chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] += frames;
+ new_period = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] / rt->period_size;
+ if (new_period != chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK]) {
+ chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK] = new_period;
+ crossed = true;
+ }
+ spin_unlock(&chip->lock);
+
+ return crossed;
+}
+
+static void babyface_complete_in(struct urb *urb)
+{
+ struct snd_usb_babyface *chip = urb->context;
+ struct snd_pcm_substream *subs;
+ unsigned long flags;
+ unsigned int frames;
+ bool crossed = false;
+ int ret;
+
+ if (urb->status < 0) {
+ if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+ urb->status == -ECONNRESET)
+ return; /* killed */
+ dev_dbg_ratelimited(&chip->dev->dev, "IN urb status %d\n",
+ urb->status);
+ if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+ schedule_work(&chip->stream_work);
+ goto resubmit;
+ }
+ atomic_set(&chip->urb_err, 0);
+
+ subs = READ_ONCE(chip->subs[SNDRV_PCM_STREAM_CAPTURE]);
+ if (subs) {
+ snd_pcm_stream_lock_irqsave(subs, flags);
+ if (snd_pcm_running(subs)) {
+ frames = urb->actual_length / chip->frame_bytes;
+ if (frames)
+ crossed = babyface_capture_copy(chip, subs,
+ urb->transfer_buffer,
+ frames);
+ }
+ snd_pcm_stream_unlock_irqrestore(subs, flags);
+ if (crossed)
+ snd_pcm_period_elapsed(subs);
+ }
+resubmit:
+ ret = usb_submit_urb(urb, GFP_ATOMIC);
+ if (ret < 0) {
+ dev_err_ratelimited(&chip->dev->dev,
+ "IN resubmit failed: %d\n", ret);
+ if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+ schedule_work(&chip->stream_work);
+ }
+}
+
+static void babyface_complete_out(struct urb *urb)
+{
+ struct snd_usb_babyface *chip = urb->context;
+ struct snd_pcm_substream *subs;
+ unsigned long flags;
+ unsigned int frames;
+ bool crossed = false;
+ int ret;
+
+ if (urb->status < 0) {
+ if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+ urb->status == -ECONNRESET)
+ return; /* killed */
+ dev_dbg_ratelimited(&chip->dev->dev, "OUT urb status %d\n",
+ urb->status);
+ if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+ schedule_work(&chip->stream_work);
+ goto resubmit;
+ }
+ atomic_set(&chip->urb_err, 0);
+
+ subs = READ_ONCE(chip->subs[SNDRV_PCM_STREAM_PLAYBACK]);
+ if (subs) {
+ snd_pcm_stream_lock_irqsave(subs, flags);
+ if (snd_pcm_running(subs)) {
+ frames = chip->frames_per_urb;
+ crossed = babyface_playback_copy(chip, subs,
+ urb->transfer_buffer, frames);
+ }
+ snd_pcm_stream_unlock_irqrestore(subs, flags);
+ if (crossed)
+ snd_pcm_period_elapsed(subs);
+ } else {
+ /* No consumer: silence the OUT frames. */
+ memset(urb->transfer_buffer, 0, urb->transfer_buffer_length);
+ }
+resubmit:
+ ret = usb_submit_urb(urb, GFP_ATOMIC);
+ if (ret < 0) {
+ dev_err_ratelimited(&chip->dev->dev,
+ "OUT resubmit failed: %d\n", ret);
+ if (atomic_inc_return(&chip->urb_err) >= BF_URB_ERR_STOP)
+ schedule_work(&chip->stream_work);
+ }
+}
+
+void babyface_stream_kill(struct snd_usb_babyface *chip)
+{
+ int i;
+
+ for (i = 0; i < chip->nurbs; i++) {
+ usb_kill_urb(chip->urbs_in[i]);
+ usb_kill_urb(chip->urbs_out[i]);
+ }
+ chip->streaming = false;
+}
+
+/* Stream start/stop run in process context (control transfers sleep).
+ * The trigger only toggles stream_users and schedules this work.
+ */
+
+/* Stop both PCM substreams (if running) so apps blocked in read/write
+ * wake with a clean error: XRUN for a recoverable stream error, or
+ * DISCONNECTED when the card is going away.
+ */
+void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state)
+{
+ int s;
+
+ for (s = 0; s < 2; s++) {
+ struct snd_pcm_substream *subs = READ_ONCE(chip->subs[s]);
+
+ if (subs && snd_pcm_running(subs))
+ snd_pcm_stop(subs, state);
+ }
+}
+
+/* Re-count stream_users from the substream running states. The apps
+ * can recover (re-prepare + trigger) while the stream work runs, so a
+ * hard `= 0` would wipe a fresh increment and leave a RUNNING
+ * substream with no URBs (hang). Called on the error paths with the
+ * mutex held.
+ */
+static void bf_recount_users(struct snd_usb_babyface *chip)
+{
+ unsigned long flags;
+ int s, users = 0;
+
+ for (s = 0; s < 2; s++) {
+ struct snd_pcm_substream *subs = READ_ONCE(chip->subs[s]);
+
+ if (subs && snd_pcm_running(subs))
+ users++;
+ }
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->stream_users = users;
+ spin_unlock_irqrestore(&chip->lock, flags);
+}
+
+/* Stream model (big picture):
+ *
+ * The PCM stream is entirely asynchronous. A trigger(START/STOP) does
+ * not touch the hardware; it only changes the shared stream_users
+ * counter (0..2, one per running substream, counted under chip->lock)
+ * and schedules stream_work. The work runs in process context (the
+ * control transfers and usb_submit_urb() calls must sleep):
+ *
+ * - 1st user (users 0 -> 1): cold-init + the session trigger pair,
+ * then the interrupt URBs (nurbs in each direction) are submitted
+ * and the session is armed.
+ * - last user (users 1 -> 0): the URBs are killed and the session
+ * disarmed.
+ *
+ * So the device has exactly one live stream regardless of how many
+ * substreams run, and a playback+capture pair shares it. An app that
+ * triggers while the work is running just increments stream_users; the
+ * work's re-count on the error path (bf_recount_users) reflects the
+ * RUNNING state so a recovered app re-arms from a clean slate.
+ */
+void babyface_stream_work(struct work_struct *work)
+{
+ struct snd_usb_babyface *chip =
+ container_of(work, struct snd_usb_babyface, stream_work);
+ unsigned int urbsize;
+ unsigned long flags;
+ int i, ret;
+ int users;
+
+ mutex_lock(&chip->mutex);
+ urbsize = chip->frame_bytes * chip->frames_per_urb;
+
+ if (chip->shutdown) {
+ mutex_unlock(&chip->mutex);
+ return;
+ }
+
+ /* Persistent URB errors (bad link, device wedged): stop the stream
+ * and wake the apps with -EPIPE. stream_users is re-counted from
+ * the (now stopped) substreams so an app recovery (prepare+start)
+ * re-arms the session from a clean slate.
+ */
+ if (atomic_read(&chip->urb_err) >= BF_URB_ERR_STOP) {
+ dev_err(&chip->dev->dev,
+ "stream error: %d consecutive bad URBs, stopping (apps re-arm)\n",
+ BF_URB_ERR_STOP);
+ babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ bf_recount_users(chip);
+ atomic_set(&chip->urb_err, 0);
+ mutex_unlock(&chip->mutex);
+ return;
+ }
+
+ spin_lock_irqsave(&chip->lock, flags);
+ users = chip->stream_users;
+ spin_unlock_irqrestore(&chip->lock, flags);
+
+ if (users > 0 && !chip->streaming) {
+ /* The firmware only validates a stream session that is
+ * preceded by the full cold-init (the user-space reference
+ * sends streaming_init at every session start - without it
+ * the outputs stay silent).
+ */
+ ret = bf_cold_init(chip);
+ if (ret < 0)
+ goto err;
+
+ /* Stream trigger pair (cap_audio): 0x10 0x8000 + 0x1D. */
+ ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0000, 0x8000);
+ if (ret < 0)
+ goto err;
+ ret = bf_vendor_write(chip, BF_REQ_SESSION_START, 0x0000, 0x0000);
+ if (ret < 0)
+ goto err;
+
+ for (i = 0; i < chip->nurbs; i++) {
+ /* Do not replay data from the previous stream/format. */
+ memset(chip->buf_out[i], 0, urbsize);
+ usb_fill_int_urb(chip->urbs_in[i], chip->dev,
+ usb_rcvintpipe(chip->dev, BF_EP_IN),
+ chip->buf_in[i], urbsize,
+ babyface_complete_in, chip, 1);
+ usb_fill_int_urb(chip->urbs_out[i], chip->dev,
+ usb_sndintpipe(chip->dev, BF_EP_OUT),
+ chip->buf_out[i], urbsize,
+ babyface_complete_out, chip, 1);
+ /* The buffers come from usb_alloc_coherent(), so
+ * they are already DMA-mapped: hand the HCD the
+ * mapping instead of letting it map them again.
+ * Without this, usb_hcd_map_urb_for_dma() calls
+ * dma_map_single() on a coherent allocation, which
+ * fails with -EAGAIN on any host where that
+ * allocation is a vmap (IOMMU-backed dma-iommu,
+ * e.g. amd_iommu in its default translated mode).
+ */
+ chip->urbs_in[i]->transfer_dma = chip->dma_in[i];
+ chip->urbs_in[i]->transfer_flags |=
+ URB_NO_TRANSFER_DMA_MAP;
+ chip->urbs_out[i]->transfer_dma = chip->dma_out[i];
+ chip->urbs_out[i]->transfer_flags |=
+ URB_NO_TRANSFER_DMA_MAP;
+ }
+ for (i = 0; i < chip->nurbs; i++) {
+ ret = usb_submit_urb(chip->urbs_in[i], GFP_KERNEL);
+ if (ret < 0)
+ goto err;
+ ret = usb_submit_urb(chip->urbs_out[i], GFP_KERNEL);
+ if (ret < 0)
+ goto err;
+ }
+ /* Session arm (cap_audio frame 5829, after the URBs). */
+ ret = bf_vendor_write(chip, BF_REQ_SESSION_ARM, 0x0000, 0xc000);
+ if (ret < 0)
+ goto err;
+
+ chip->streaming = true;
+ dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
+ chip->frames_per_urb, chip->nurbs);
+ } else if (users == 0 && chip->streaming) {
+ babyface_stream_kill(chip);
+ dev_dbg(&chip->dev->dev, "stream stopped\n");
+ }
+
+ mutex_unlock(&chip->mutex);
+ return;
+
+err:
+ dev_err(&chip->dev->dev, "failed to start stream: %d\n", ret);
+ babyface_stream_kill(chip);
+ /* The apps already got a successful trigger - wake them with an
+ * XRUN so a failed start (device wedged, cold-init error) does not
+ * leave them hung in read/write with no URBs in flight.
+ */
+ babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+ bf_recount_users(chip);
+ mutex_unlock(&chip->mutex);
+}
+
+/* -- PCM --------------------------- */
+
+static const struct snd_pcm_hardware babyface_pcm_hw = {
+ .info = SNDRV_PCM_INFO_INTERLEAVED |
+ SNDRV_PCM_INFO_BLOCK_TRANSFER,
+ .formats = SNDRV_PCM_FMTBIT_S32_LE,
+ .rate_min = 32000,
+ .rate_max = 192000,
+ .channels_min = 2,
+ .channels_max = 12,
+ .buffer_bytes_max = 1 << 20,
+ .period_bytes_max = 1 << 18,
+ .periods_min = 2,
+ .periods_max = 16,
+};
+
+/* Is the OTHER direction actually transferring? Not "is it open": a client
+ * that has the device open but has not started must still be able to pick the
+ * rate, so that an application opening playback and capture before starting
+ * either one can take both to 96 kHz.
+ */
+static bool bf_other_running(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs)
+{
+ struct snd_pcm_substream *other = READ_ONCE(chip->subs[!subs->stream]);
+ unsigned long flags;
+ bool running;
+
+ if (!other)
+ return false;
+ snd_pcm_stream_lock_irqsave(other, flags);
+ running = snd_pcm_running(other);
+ snd_pcm_stream_unlock_irqrestore(other, flags);
+ return running;
+}
+
+static int babyface_pcm_open(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ struct snd_pcm_runtime *rt = subs->runtime;
+ unsigned int locked;
+ unsigned long flags;
+ int ret;
+
+ rt->hw = babyface_pcm_hw;
+ ret = snd_pcm_hw_constraint_list(rt, 0, SNDRV_PCM_HW_PARAM_RATE,
+ &bf_rates_constraint);
+ if (ret < 0)
+ return ret;
+ /* The stream is 24-bit audio in a 32-bit container, left-justified
+ * (the device word carries the sample in bits 31-8). Declare the
+ * valid-bit width so user-space knows to ignore the low 8 bits.
+ */
+ ret = snd_pcm_hw_constraint_msbits(rt, 0, 32, 24);
+ if (ret < 0)
+ return ret;
+ /* One URB delivers frames_per_urb frames per interrupt; a period must
+ * span at least one URB so a completion crosses at most one period
+ * boundary. Constrain in frames (not bytes) so the minimum period
+ * does not balloon at low channel counts: 2 ch @ 48 kHz -> 256
+ * frames (5.3 ms) instead of 1536 frames from a 12-ch byte clamp.
+ */
+ ret = snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+ chip->frames_per_urb, 1 << 18);
+ if (ret < 0)
+ return ret;
+
+ /* Both directions share one clock, so while audio is flowing the rate
+ * belongs to whoever started it. Offer that rate alone: a client
+ * arriving later then negotiates down to it, and the sound server
+ * resamples, rather than the device being retuned underneath a running
+ * stream. Advertising the constraint here rather than failing in
+ * hw_params() is what keeps a PipeWire sink alive - it picks the rate
+ * on offer instead of asking for one that has to be refused.
+ *
+ * This is what the vendor drivers do. RME's settings panel greys the
+ * sample rate out during record/playback, and their manual is explicit
+ * that all active ASIO clients share one rate, with WDM (or CoreAudio)
+ * doing any conversion - never the driver.
+ */
+ mutex_lock(&chip->mutex);
+ locked = chip->streaming ? chip->rate : 0;
+ mutex_unlock(&chip->mutex);
+ if (locked) {
+ ret = snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_RATE,
+ locked, locked);
+ if (ret < 0)
+ return ret;
+ }
+
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->subs[subs->stream] = subs;
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+}
+
+static int babyface_pcm_close(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ unsigned long flags;
+
+ /* Wait for the stream stop work so the URB callbacks (which
+ * touch subs) are done before the substream can be freed.
+ */
+ flush_work(&chip->stream_work);
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->subs[subs->stream] = NULL;
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+}
+
+static int babyface_pcm_hw_params(struct snd_pcm_substream *subs,
+ struct snd_pcm_hw_params *params)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ const struct bf_rate *r;
+ int ret = 0;
+
+ r = bf_rate_lookup(params_rate(params));
+ if (!r)
+ return -EINVAL;
+
+ /* The stream URBs must be at least one alt packet wide: the device
+ * delivers its IN data in alt-sized packets (448/640/1024 B for
+ * alt 1/2/3), and a smaller URB buffer makes the host controller
+ * discard the transfer with -EOVERFLOW (babble) - seen at
+ * 176.4/192 kHz with frames_per_urb below 32. Return a clean
+ * error instead of a silently dead capture stream.
+ */
+ if (chip->frames_per_urb < r->min_fpu) {
+ dev_err(&chip->dev->dev,
+ "rate %u Hz needs frames_per_urb >= %u (module has %u)\n",
+ r->rate, r->min_fpu, chip->frames_per_urb);
+ return -EINVAL;
+ }
+
+ mutex_lock(&chip->mutex);
+ if (r->rate != chip->rate) {
+ /* The constraint in open() normally means nobody asks for a
+ * rate other than the running one. It can still happen, when
+ * this substream was opened before the other one started: the
+ * constraint is evaluated at open, so it did not apply then.
+ * Refuse rather than retune - the alternative is a running
+ * stream silently changing pitch, with no error and no xrun,
+ * because a negotiated rate is never revised for a live
+ * substream.
+ */
+ if (bf_other_running(chip, subs)) {
+ dev_dbg(&chip->dev->dev,
+ "rate %u Hz refused: %u Hz stream running\n",
+ r->rate, chip->rate);
+ ret = -EBUSY;
+ goto out;
+ }
+ /* Nothing is transferring, so the clock is free. Stop any
+ * armed URBs, re-point the bandwidth class and let the stream
+ * work restart the session at the new rate.
+ */
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ ret = usb_set_interface(chip->dev, BF_IFACE, r->alt);
+ if (ret < 0) {
+ babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+ bf_recount_users(chip);
+ goto out;
+ }
+ chip->rate = r->rate;
+ chip->alt = r->alt;
+ chip->frame_bytes = r->frame_bytes;
+ /* Publish the new geometry before restarting either stream. */
+ schedule_work(&chip->stream_work);
+ dev_dbg(&chip->dev->dev, "rate %u Hz (alt %u)\n",
+ chip->rate, chip->alt);
+ }
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int babyface_pcm_hw_free(struct snd_pcm_substream *subs)
+{
+ /* The device buffer is host-side; nothing to release here. */
+ return 0;
+}
+
+static int babyface_pcm_prepare(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ unsigned long flags;
+
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->hw_ptr[subs->stream] = 0;
+ chip->prev_period[subs->stream] = 0;
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+}
+
+static int babyface_pcm_trigger(struct snd_pcm_substream *subs, int cmd)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ unsigned long flags;
+
+ switch (cmd) {
+ case SNDRV_PCM_TRIGGER_START:
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->hw_ptr[subs->stream] = 0;
+ chip->prev_period[subs->stream] = 0;
+ /* stream_users is shared by the two substreams (separate
+ * locks) - serialize the ++/-- so a concurrent trigger on
+ * the other direction can't lose an increment (which would
+ * stop the stream while a substream still runs).
+ */
+ if (chip->stream_users++ == 0)
+ schedule_work(&chip->stream_work);
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+ case SNDRV_PCM_TRIGGER_STOP:
+ spin_lock_irqsave(&chip->lock, flags);
+ if (chip->stream_users > 0 && --chip->stream_users == 0)
+ schedule_work(&chip->stream_work);
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static snd_pcm_uframes_t babyface_pcm_pointer(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ unsigned long flags;
+ snd_pcm_uframes_t pos;
+
+ spin_lock_irqsave(&chip->lock, flags);
+ pos = chip->hw_ptr[subs->stream] % subs->runtime->buffer_size;
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return pos;
+}
+
+static const struct snd_pcm_ops babyface_pcm_ops = {
+ .open = babyface_pcm_open,
+ .close = babyface_pcm_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .hw_params = babyface_pcm_hw_params,
+ .hw_free = babyface_pcm_hw_free,
+ .prepare = babyface_pcm_prepare,
+ .trigger = babyface_pcm_trigger,
+ .pointer = babyface_pcm_pointer,
+};
+
+static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
+static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
+static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT;
+static int nurbs = BF_NURBS_DEFAULT;
+
+module_param_array(index, int, NULL, 0444);
+MODULE_PARM_DESC(index, "Index value for the Babyface Pro sound card.");
+module_param_array(id, charp, NULL, 0444);
+MODULE_PARM_DESC(id, "ID string for the Babyface Pro sound card.");
+module_param(frames_per_urb, int, 0644);
+MODULE_PARM_DESC(frames_per_urb, "Audio frames per URB, 8..1024 (16 = low-latency floor, 256 = default).");
+module_param(nurbs, int, 0644);
+MODULE_PARM_DESC(nurbs, "URBs in flight per direction, 1..16 (16 = low-latency).");
+
+/* -- USB driver ------------------------- */
+
+static void babyface_private_free(struct snd_card *card)
+{
+ struct snd_usb_babyface *chip = card->private_data;
+ unsigned int urbsize;
+ int i;
+
+ if (!chip)
+ return;
+
+ /* The URB arrays are NULL when the probe failed before allocating
+ * them (snd_card_free runs private_free on any probe error).
+ */
+ if (chip->urbs_in) {
+ urbsize = BF_WORDS_PER_FRAME * sizeof(u32) * chip->frames_per_urb;
+ for (i = 0; i < chip->nurbs; i++) {
+ if (chip->urbs_in[i]) {
+ usb_kill_urb(chip->urbs_in[i]);
+ usb_free_urb(chip->urbs_in[i]);
+ }
+ if (chip->urbs_out[i]) {
+ usb_kill_urb(chip->urbs_out[i]);
+ usb_free_urb(chip->urbs_out[i]);
+ }
+ usb_free_coherent(chip->dev, urbsize, chip->buf_in[i],
+ chip->dma_in[i]);
+ usb_free_coherent(chip->dev, urbsize, chip->buf_out[i],
+ chip->dma_out[i]);
+ }
+ }
+ kfree(chip->urbs_in);
+ kfree(chip->urbs_out);
+ kfree(chip->buf_in);
+ kfree(chip->buf_out);
+ kfree(chip->dma_in);
+ kfree(chip->dma_out);
+ usb_put_dev(chip->dev);
+}
+
+static int babyface_probe(struct usb_interface *intf,
+ const struct usb_device_id *usb_id)
+{
+ struct usb_device *dev = interface_to_usbdev(intf);
+ struct snd_usb_babyface *chip;
+ struct snd_card *card;
+ struct snd_pcm *pcm;
+ unsigned int urbsize;
+ int i, err;
+
+ if (intf->cur_altsetting->desc.bInterfaceNumber != BF_IFACE) {
+ /* Only the proprietary audio interface is ours; the MIDI
+ * (standard class) and bulk interfaces stay unclaimed so
+ * snd-usb-audio can take the MIDI one.
+ */
+ return -ENODEV;
+ }
+
+ err = snd_card_new(&intf->dev, index[0], id[0], THIS_MODULE,
+ sizeof(*chip), &card);
+ if (err < 0) {
+ dev_err(&intf->dev, "snd_card_new failed: %d\n", err);
+ return err;
+ }
+ chip = card->private_data;
+ chip->card = card;
+
+ chip->dev = usb_get_dev(dev);
+ /* USB autosuspend is untested: nothing in this driver holds a PM
+ * reference while streaming, so an autosuspend request could race
+ * a live stream. Disable it explicitly rather than ship an
+ * untested code path - full autosuspend support (correct
+ * autopm_get/put pairing around the stream) is a deliberate
+ * follow-up, not an oversight.
+ */
+ usb_disable_autosuspend(chip->dev);
+ chip->iface = intf;
+ chip->nurbs = clamp(nurbs, 1, 16);
+ chip->frames_per_urb = clamp(frames_per_urb, 8, 1024) & ~7;
+ chip->rate = 48000;
+ chip->alt = BF_ALT_1;
+ chip->frame_bytes = 56;
+ mutex_init(&chip->mutex);
+ spin_lock_init(&chip->lock);
+ atomic_set(&chip->urb_err, 0);
+ INIT_WORK(&chip->stream_work, babyface_stream_work);
+ chip->card->private_free = babyface_private_free;
+
+ /* Model-neutral on purpose. The FS and the original (2015)
+ * Babyface Pro share VID:PID 2a39:3fc0, report the same bcdDevice
+ * and the same iProduct shape, and neither says "FS" anywhere, so
+ * there is nothing to tell them apart at probe time - and the
+ * driver is reported to work unmodified on both. card->driver in
+ * particular is what alsa-lib configs and UCM profiles match on,
+ * so it has to be right before this reaches a released kernel.
+ */
+ strscpy(chip->card->driver, "BabyfacePro",
+ sizeof(chip->card->driver));
+ strscpy(chip->card->shortname, "Babyface Pro",
+ sizeof(chip->card->shortname));
+ snprintf(chip->card->longname, sizeof(chip->card->longname),
+ "RME Babyface Pro (proprietary mode) at %s",
+ dev_name(&dev->dev));
+
+ /* If the id was not passed as a module option, derive it from the
+ * shortname with the whitespace removed, the way snd-usb-caiaq
+ * does. Letting the core derive it instead yields the last word
+ * of the shortname ("Pro"), which is no use as an hw: name.
+ */
+ if (*chip->card->id == '\0') {
+ char cid[sizeof(chip->card->id)];
+ const char *c;
+ size_t len;
+
+ memset(cid, 0, sizeof(cid));
+ for (c = chip->card->shortname, len = 0;
+ *c && len < sizeof(cid) - 1; c++)
+ if (*c != ' ')
+ cid[len++] = *c;
+ snd_card_set_id(chip->card, cid);
+ }
+ strscpy(chip->card->mixername, "Babyface Pro",
+ sizeof(chip->card->mixername));
+
+ /* alt 1 = the default 48-kHz bandwidth class. */
+ err = usb_set_interface(dev, BF_IFACE, BF_ALT_1);
+ if (err < 0) {
+ dev_err(&intf->dev, "usb_set_interface failed: %d\n", err);
+ goto error;
+ }
+
+ err = bf_cold_init(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "cold init failed: %d\n", err);
+ goto error;
+ }
+
+ /* Keep the allocation size independent of the active USB mode. */
+ urbsize = BF_WORDS_PER_FRAME * sizeof(u32) * chip->frames_per_urb;
+ chip->urbs_in = kcalloc(chip->nurbs, sizeof(*chip->urbs_in), GFP_KERNEL);
+ chip->urbs_out = kcalloc(chip->nurbs, sizeof(*chip->urbs_out), GFP_KERNEL);
+ chip->buf_in = kcalloc(chip->nurbs, sizeof(*chip->buf_in), GFP_KERNEL);
+ chip->buf_out = kcalloc(chip->nurbs, sizeof(*chip->buf_out), GFP_KERNEL);
+ chip->dma_in = kcalloc(chip->nurbs, sizeof(*chip->dma_in), GFP_KERNEL);
+ chip->dma_out = kcalloc(chip->nurbs, sizeof(*chip->dma_out), GFP_KERNEL);
+ if (!chip->urbs_in || !chip->urbs_out || !chip->buf_in ||
+ !chip->buf_out || !chip->dma_in || !chip->dma_out)
+ goto error;
+
+ for (i = 0; i < chip->nurbs; i++) {
+ chip->urbs_in[i] = usb_alloc_urb(0, GFP_KERNEL);
+ chip->urbs_out[i] = usb_alloc_urb(0, GFP_KERNEL);
+ chip->buf_in[i] = usb_alloc_coherent(dev, urbsize, GFP_KERNEL,
+ &chip->dma_in[i]);
+ chip->buf_out[i] = usb_alloc_coherent(dev, urbsize, GFP_KERNEL,
+ &chip->dma_out[i]);
+ if (!chip->urbs_in[i] || !chip->urbs_out[i] ||
+ !chip->buf_in[i] || !chip->buf_out[i])
+ goto error;
+ }
+
+ err = snd_pcm_new(chip->card, "Babyface Pro", 0, 1, 1, &pcm);
+ if (err < 0) {
+ dev_err(&intf->dev, "snd_pcm_new failed: %d\n", err);
+ goto error;
+ }
+ pcm->private_data = chip;
+ strscpy(pcm->name, "Babyface Pro", sizeof(pcm->name));
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &babyface_pcm_ops);
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &babyface_pcm_ops);
+
+ /* The PCM buffer is host-side (the URB callbacks copy in/out of
+ * it); vmalloc is the standard choice for that.
+ */
+ err = snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC,
+ NULL, 0, 1 << 20);
+ if (err < 0) {
+ dev_err(&intf->dev, "buffer allocation 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);
+ goto error;
+ }
+
+ usb_set_intfdata(intf, chip);
+ dev_info(&intf->dev,
+ "Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n",
+ chip->card->number, chip->frames_per_urb, chip->nurbs);
+ return 0;
+
+error:
+ usb_set_intfdata(intf, NULL);
+ /* Balance the probe()-time usb_disable_autosuspend(): disconnect()
+ * is never called for a failed probe, so the disable would leak and
+ * leave autosuspend off on this usb_device until a physical unplug.
+ */
+ usb_enable_autosuspend(chip->dev);
+ snd_card_free(chip->card);
+ return err;
+}
+
+static void babyface_disconnect(struct usb_interface *intf)
+{
+ struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+ if (!chip)
+ return;
+
+ /* Idempotence guard: a disconnect can race a re-probe (usbfs
+ * detach/re-attach) - tear the card down exactly once.
+ */
+ usb_set_intfdata(intf, NULL);
+ if (chip->shutdown)
+ return;
+
+ chip->shutdown = true;
+ cancel_work_sync(&chip->stream_work);
+ /* Balance the probe()-time usb_disable_autosuspend(): the usb_device
+ * outlives this interface claim (a usbfs detach re-probes without
+ * the physical device ever disconnecting), so leaving autosuspend
+ * disabled here would wrongly affect whatever claims the device next.
+ */
+ usb_enable_autosuspend(chip->dev);
+ /* Wake apps blocked in read/write: the card is going away. */
+ dev_info(&chip->dev->dev, "disconnect: stopping PCM substreams\n");
+ babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_DISCONNECTED);
+ mutex_lock(&chip->mutex);
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ mutex_unlock(&chip->mutex);
+
+ snd_card_disconnect(chip->card);
+ /* NEVER snd_card_free() here: it blocks until the last user
+ * closes the card, and an open client (e.g. PipeWire) deadlocks
+ * the disconnect (seen live: pipewire stuck in snd_card_free,
+ * D state). free_when_closed frees on the last close.
+ */
+ snd_card_free_when_closed(chip->card);
+}
+
+static const struct usb_device_id babyface_ids[] = {
+ { USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) },
+ { }
+};
+MODULE_DEVICE_TABLE(usb, babyface_ids);
+
+static struct usb_driver babyface_driver = {
+ .name = "snd-usb-babyface-pro",
+ .probe = babyface_probe,
+ .disconnect = babyface_disconnect,
+ .id_table = babyface_ids,
+};
+
+static int __init babyface_init(void)
+{
+ return usb_register(&babyface_driver);
+}
+
+static void __exit babyface_exit(void)
+{
+ usb_deregister(&babyface_driver);
+}
+
+module_init(babyface_init);
+module_exit(babyface_exit);
+
+MODULE_AUTHOR("Ismaïl Bahloul <i.bahloul01@gmail.com>");
+MODULE_DESCRIPTION("RME Babyface Pro / Pro FS (proprietary mode) USB audio driver");
+MODULE_LICENSE("GPL");
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
new file mode 100644
index 000000000..de0353a59
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
+ *
+ * The Babyface Pro (and Pro FS) present two personalities on the USB
+ * bus: a class-compliant one (handled by snd-usb-audio) and a
+ * proprietary one (VID 0x2a39 / PID 0x3fc0) whose PCM stream runs on
+ * INTERRUPT endpoints (interface 5, ep 0x01 OUT / 0x82 IN).
+ * Isochronous transfers are rejected there with EINVAL, and
+ * snd-usb-audio has no interrupt-PCM path, so this driver is
+ * standalone (snd-usb-caiaq-style interrupt streaming) instead of an
+ * snd-usb-audio quirk.
+ *
+ * This file and babyfacepro.c cover the card lifecycle (probe,
+ * disconnect) and the PCM stream only - no mixer controls, no
+ * front-panel support, no suspend/resume yet. Those are added by
+ * later patches in this series, each with its own register map and
+ * device-state additions; see the cover letter for the full series
+ * plan.
+ *
+ * The protocol (vendor requests + 14x32-bit frame layout) was
+ * reverse-engineered from Windows captures and validated on hardware -
+ * tools/usbdump/PROTOCOL.md in the driver's development repository is
+ * the authoritative reference.
+ *
+ * Stream notes (hardware-validated 2026-08):
+ * - frames_per_urb is tunable 8..1024 (multiple of 8) but must be at
+ * least one alt packet wide - the device delivers IN data in
+ * alt-sized packets (448/640/1024 B for alt 1/2/3), smaller URBs
+ * get -EOVERFLOW (babble). So frames_per_urb >= 8/16/32 for
+ * alt 1/2/3; the driver rejects violating rates in hw_params.
+ * - The device only advances the stream while BOTH endpoints have a
+ * pending URB - IN and OUT are always submitted as a pair.
+ * - SET_INTERFACE(5, alt) selects single/double/quad speed and USB
+ * packet capacity; the BASE rate is the family register (request
+ * 0x10, index 0x0030: 32k / 44.1k / 48k family), from which the
+ * firmware derives its own DDS word. Three families times three
+ * speeds give all nine rates. The 0x1B DDS quad is varispeed: a
+ * pitch RATIO applied on top of the family rate, sticky until
+ * rewritten, with the 48 kHz cold-plug quad meaning x1.0. See
+ * bf_clock_write / bf_pitch_write.
+ */
+
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#define USB_VENDOR_RME 0x2a39
+#define USB_PRODUCT_BABYFACE_PRO_FS 0x3fc0
+
+/* The proprietary audio interface (interface 5, interrupt endpoints). */
+#define BF_IFACE 5
+#define BF_EP_OUT 0x01
+#define BF_EP_IN 0x82
+
+#define BF_ALT_1 1 /* x1: 32/44.1/48 kHz, 448-B packets */
+#define BF_ALT_2 2 /* x2: 64/88.2/96 kHz, 640-B packets */
+#define BF_ALT_3 3 /* x4: 128/176.4/192 kHz, 1024-B packets */
+
+/* Default stream geometry - conservative, matches the RME TotalMix
+ * 256-sample buffer. Both are tunable via module params; the
+ * low-latency profile (validated) is frames_per_urb=16 nurbs=16.
+ */
+#define BF_FRAMES_PER_URB_DEFAULT 256
+#define BF_NURBS_DEFAULT 8
+
+#define BF_WORDS_PER_FRAME 14 /* 14 x 32-bit words per frame */
+
+/* Consecutive URB errors (CRC/babble/protocol or a failed resubmit)
+ * before the stream is stopped and the apps get a clean -EPIPE.
+ */
+#define BF_URB_ERR_STOP 3
+
+/* Vendor requests (bmRequestType 0x40, value in wValue, no data phase).
+ * Only the subset the core probe/PCM path needs; the mixer, front
+ * panel and DSP EQ patches each add their own as those features land.
+ */
+#define BF_REQ_KEEPALIVE 0x10 /* settings word / stream trigger */
+#define BF_REQ_REG_CLEAR 0x16 /* cold-init register clear */
+#define BF_REQ_PREAMP 0x17 /* 48V/PAD state + readback */
+#define BF_REQ_DDS 0x1b /* clock quads */
+#define BF_REQ_STATUS_2 0x1c /* read 4 B */
+#define BF_REQ_SESSION_START 0x1d
+#define BF_REQ_SESSION_ARM 0x14
+#define BF_REQ_PREAMP_COMMIT 0x21 /* commit after 0x17 */
+
+#define BF_REG_RATE_FAMILY 0x0030 /* family << 4 (bReq 0x10) */
+/* 0x05ff = 0x05cf | 0x0030: the settings word and the family register
+ * written together (measured 2026-09-16: bits 4-5 of the value land in
+ * the family register). The cold-plug capture writes 0x0021 here, i.e.
+ * family 48 kHz + clock internal - which is why replaying it after a
+ * family write used to reset the rate to 48 kHz.
+ */
+#define BF_REG_KEEPALIVE_INIT 0x05ff
+#define BF_REG_KEEPALIVE_SETTINGS 0x05cf
+
+/* Host settings-state word carried by the BF_REG_KEEPALIVE_SETTINGS
+ * keepalive (PROTOCOL.md "keepalive 0x10 0x05CF wVal = host settings-
+ * state register", hardware-verified 2026-08-22/23). Only the
+ * Internal value is used until the clock-source control lands with a
+ * later patch in this series.
+ */
+#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
+#define BF_SETTINGS_CLOCK_OPTICAL 0x0004
+
+struct snd_usb_babyface {
+ struct snd_card *card;
+ struct usb_device *dev;
+ struct usb_interface *iface;
+
+ struct mutex mutex; /* controls + stream geometry */
+ spinlock_t lock; /* hw_ptr / subs */
+
+ /* stream */
+ struct urb **urbs_in;
+ struct urb **urbs_out;
+ void **buf_in;
+ void **buf_out;
+ dma_addr_t *dma_in;
+ dma_addr_t *dma_out;
+ unsigned int nurbs;
+ unsigned int frames_per_urb;
+ unsigned int frame_bytes; /* 56/40/32 for alt 1/2/3 */
+ unsigned int rate;
+ unsigned int alt;
+ int pitch; /* varispeed in 0.1% (-500..+500) */
+ int stream_users; /* PCM substreams sharing the stream */
+ bool streaming; /* URBs actually in flight */
+ bool shutdown;
+ atomic_t urb_err; /* consecutive bad URBs (stops the stream) */
+ struct work_struct stream_work;
+
+ struct snd_pcm_substream *subs[2];
+ unsigned long hw_ptr[2];
+ unsigned long prev_period[2];
+};
+
+struct bf_rate {
+ unsigned int rate;
+ unsigned int alt;
+ unsigned int frame_bytes;
+ unsigned int min_fpu; /* frames/URB floor = one alt packet (448/640/1024 B) */
+};
+
+/* -- babyfacepro.c ------------------------ */
+const struct bf_rate *bf_rate_lookup(unsigned int rate);
+/* The family register value for a rate: 0 / 1 / 2 for the 32k / 44.1k /
+ * 48k family (rate >> (alt - 1)).
+ */
+unsigned int bf_rate_family(const struct bf_rate *r);
+int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx);
+int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf);
+int bf_settings_write(struct snd_usb_babyface *chip);
+int bf_clock_write(struct snd_usb_babyface *chip);
+int bf_pitch_write(struct snd_usb_babyface *chip, int pitch);
+int bf_cold_init(struct snd_usb_babyface *chip);
+void babyface_stream_kill(struct snd_usb_babyface *chip);
+void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state);
+void babyface_stream_work(struct work_struct *work);
+extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
` (5 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
Adds the first mixer controls: the six output masters (volume + mute)
and the 6x14 crosspoint routing matrix, plus the cold-init default
mixer that wires every source to every output at unity so the card
makes sound without any user-space mixer at all.
Masters and crosspoints ship together because the default-mixer setup
has to write both in one pass - output masters with no signal routed
to them would just be a silent volume knob. This is the first patch
in the series where the card produces controllable, routed audio.
Also introduces mixer-state persistence across interface re-probes
(a userspace usbfs claim, e.g. PipeWire or the TuxMix daemon, detaches
and re-probes us) and across suspend/resume's cold re-init, trimmed
for now to the masters and crosspoints this patch adds; later patches
in the series extend babyface_restore_state()/struct bf_saved the same
way as they add more mixer state to cache.
bf_flag_cycle[]/bf_vendor_write_cycle()/bf_crosspoint_clear_cross(),
previously sketched in the core file, move to babyfacepro-ctl.c since
they are crosspoint/mixer-only.
Co-developed-by: David Fredman <davfre@gmail.com>
Signed-off-by: David Fredman <davfre@gmail.com>
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/Makefile | 2 +-
sound/usb/babyfacepro/babyfacepro-ctl.c | 707 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 189 ++++++-
sound/usb/babyfacepro/babyfacepro.h | 121 +++-
4 files changed, 1013 insertions(+), 6 deletions(-)
create mode 100644 sound/usb/babyfacepro/babyfacepro-ctl.c
diff --git a/sound/usb/babyfacepro/Makefile b/sound/usb/babyfacepro/Makefile
index 5adc6d474..a50647a06 100644
--- a/sound/usb/babyfacepro/Makefile
+++ b/sound/usb/babyfacepro/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
-snd-usb-babyface-pro-y := babyfacepro.o
+snd-usb-babyface-pro-y := babyfacepro.o babyfacepro-ctl.o
obj-$(CONFIG_SND_USB_BABYFACE_PRO) += snd-usb-babyface-pro.o
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
new file mode 100644
index 000000000..a3d3252f5
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -0,0 +1,707 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
+ *
+ * ALSA control surface: this patch adds the output masters (volume +
+ * mute) and the crosspoint routing matrix - the minimum needed for
+ * the card to produce controllable, routed audio. They ship together
+ * because babyface_write_default_mixer() (the cold-init default
+ * mixer) has to set up both in one pass: masters with no signal
+ * routed to them would just be a silent volume knob.
+ *
+ * Preamp/gain, the routing-flag switches (loopback, AN1>2, link,
+ * clock source, width, FX send, DIM), varispeed pitch, the front
+ * panel and the DSP EQ are added by later patches in this series -
+ * see babyfacepro.h and the cover letter for the full plan.
+ *
+ * See babyfacepro.h for the shared device state and register map,
+ * and babyfacepro.c for the core driver (protocol, PCM streaming,
+ * state persistence, card lifecycle).
+ */
+#include <linux/log2.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#include "babyfacepro.h"
+
+/* The flag-cycle counter, its writer and the per-block crosspoint
+ * "cross" register clear are crosspoint/mixer-only, so they live here
+ * rather than in the core driver file.
+ */
+
+/* The transaction-flag counter cycle on 16-bit writes. */
+const u16 bf_flag_cycle[4] = { 0xc000, 0x4000, 0x8000, 0x0000 };
+
+/* Write with the per-transaction flag-cycle word OR'd into idx. The
+ * device wants the flag word (0xc000/0x4000/0x8000/0x0000, rotating)
+ * set on every 0x12/0x1a write; this is the hot path for the mixer
+ * puts, so it is factored out.
+ */
+int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx)
+{
+ u16 flag = bf_flag_cycle[chip->flag_cnt];
+
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ return bf_vendor_write(chip, req, val, idx | flag);
+}
+
+/* The 0x16 cold-init clear covers only 0x00-0x3D - the "cross"
+ * registers of a block (L-reg odd / R-reg even of the stereo
+ * sources) survive from the previous session and would sum L+R into
+ * BOTH channels of the output (mono). Zero them explicitly: 10 odd
+ * L-registers (5,7,...23) + 10 even R-registers (4,6,...22).
+ */
+int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip,
+ unsigned int blk)
+{
+ int ret, k;
+
+ for (k = BF_CROSS_L_FIRST; k <= BF_CROSS_L_LAST; k += 2) {
+ ret = bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000,
+ BF_REG_CROSS_BASE_L +
+ BF_REG_CROSS_STRIDE * blk + k);
+ if (ret < 0)
+ return ret;
+ }
+ for (k = BF_CROSS_R_FIRST; k <= BF_CROSS_R_LAST; k += 2) {
+ ret = bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000,
+ BF_REG_CROSS_BASE_R +
+ BF_REG_CROSS_STRIDE * blk + k);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+const struct bf_source bf_sources[14] = {
+ { "AN1", 0, 0 },
+ { "AN2", 1, 1 },
+ { "AN3", 2, 2 },
+ { "AN4", 3, 3 },
+ { "AS1/2", 4, 5 },
+ { "ADAT3/4", 6, 7 },
+ { "ADAT5/6", 8, 9 },
+ { "ADAT7/8", 10, 11 },
+ { "PB1", 12, 13 },
+ { "PB2", 14, 15 },
+ { "PB3", 16, 17 },
+ { "PB4", 18, 19 },
+ { "PB5", 20, 21 },
+ { "PB6", 22, 23 },
+};
+
+/* Crosspoint-map output order vs the master-map order - HARDWARE-
+ * VERIFIED 2026-08-24: the block that feeds the Phones is the FIRST
+ * crosspoint block (0x34), while the Phones master is the SECOND
+ * (0x03E2/0x0006). The crosspoint map lists the Phones first (the
+ * monitor output); the master map lists AN1/2 first. Control index =
+ * the canonical order (AN1/2=0, PH3/4=1, ...) so the crosspoint and
+ * master controls line up; this table maps to the register block.
+ */
+const u8 bf_xpoint_block[6] = { 1, 0, 2, 3, 4, 5 };
+
+/* Master-register output order - the master map lists AN1/2 first
+ * (0x03E0) and the Phones master SECOND (0x03E2, HARDWARE-VERIFIED
+ * 2026-08-24); the crosspoint blocks are in the opposite order
+ * (Phones = block 0x34 first, hence bf_xpoint_block above). Control
+ * index -> canonical output (AN1/2=0, PH3/4=1, ...) = the master
+ * register position directly: the names 'AN1/2 Playback Volume' etc.
+ * must match the register they write (corrected 2026-08-26 - the
+ * previous {1,0,...} swap made 'AN1/2' drive the Phones and 'PH3/4'
+ * drive the AN1/2 analog out).
+ */
+static const u8 bf_master_out[6] = { 0, 1, 2, 3, 4, 5 };
+
+/* The 16-bit master value -> the 8-bit companion code (0.5 dB/step).
+ * Integer-only: half_db = 12*log2(v/0x2000) via ilog2 + an 8-bit
+ * fractional-octave table (12*log2(1 + n/256), ~0.05 dB resolution -
+ * fine enough for the +/-0.5 dB panel wheel to track the round-trip).
+ */
+static const u8 bf_lg2_frac[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+};
+
+/* 16-bit master -> dBx2 (12 half-dB per octave; 0x2000 = 0 dB).
+ * Shared by the 8-bit companion and the front-panel OUT wheel.
+ */
+int bf_master_half_db(u16 vol16)
+{
+ unsigned int k, frac;
+
+ vol16 = clamp(vol16, 1, 0x4000);
+ k = ilog2(vol16);
+ frac = ((vol16 - (1u << k)) << 8) >> k;
+ return 12 * (int)k - 156 + bf_lg2_frac[frac];
+}
+
+/* dBx2 -> 16-bit master (0x2000*2^(half_db/12), rounded). The
+ * inverse of bf_master_half_db - the 12th-root table 2^(n/12).
+ */
+static const u16 bf_twelfth[12] = {
+ 0x1000, 0x10f4, 0x11f6, 0x1307, 0x1429, 0x155c,
+ 0x16a1, 0x17f9, 0x1966, 0x1ae9, 0x1c82, 0x1e34,
+};
+
+int bf_master_16bit(int half_db)
+{
+ int k = half_db / 12;
+ int n = half_db % 12;
+ u32 v;
+
+ if (n < 0) {
+ n += 12;
+ k--;
+ }
+ v = (u32)bf_twelfth[n] << 1; /* 0x2000*2^(n/12) */
+ if (k >= 0) {
+ v <<= k;
+ } else {
+ v += 1u << (-k - 1); /* round-half-up */
+ v >>= -k;
+ }
+ return (u16)clamp(v, 1, 0x4000);
+}
+
+u8 bf_master_8bit(u16 vol16)
+{
+ if (vol16 == 0)
+ return BF_MASTER_MUTE;
+ return (u8)clamp(0xf3 + bf_master_half_db(vol16), BF_MASTER_8_MIN, 0xff);
+}
+
+/* The cold-init register clear zeroes the mixer registers TotalMix
+ * re-uploads afterwards. The kernel driver has no saved scene (no
+ * readback for faders), so it applies TotalMix's factory routing -
+ * every source to every output at unity - so the card makes sound
+ * without any user-space mixer at all.
+ *
+ * The two analog masters (AN1/2, the main out; PH3/4, the headphone
+ * out) come up at -20 dB rather than TotalMix's 0 dB. Routing all 14
+ * sources into an output at unity means they SUM, and this runs on
+ * every fresh load, before alsa-restore has had a chance to put the
+ * user's own levels back. On monitors or headphones with no volume
+ * control of their own that is a real hazard, and the failure is
+ * asymmetric: a default that is too quiet is turned up in a second,
+ * one that is too loud cannot be taken back. -20 dB is still plainly
+ * audible, and it is not an invented number - it is the exact
+ * 8-bit/16-bit pair the hardware's own DIM button writes.
+ *
+ * The other four outputs (AS1/2, ADAT3/4, ADAT5/6, ADAT7/8) are all
+ * digital, carried over the single optical port - nothing downstream
+ * of them can be damaged by a loud signal the way a speaker or a pair
+ * of headphones can, so there is no hazard to mitigate, only a
+ * digital feed that would otherwise arrive 20 dB quiet for no reason
+ * a downstream device could infer. They keep TotalMix's own 0 dB
+ * default (raised 2026-09-15 after David Fredman pointed out the
+ * blanket -20 dB reached them too, on his report of the AN1/2/PH3/4
+ * default - issue #4).
+ */
+int babyface_write_default_mixer(struct snd_usb_babyface *chip)
+{
+ int out, src, ret;
+ u16 flag;
+
+ /* Output masters: the two analog outputs at -20 dB, the four
+ * digital ones at 0 dB (see the comment above). Unmuted either
+ * way.
+ */
+ for (out = 0; out < 6; out++) {
+ bool analog = out < 2;
+ u8 gain8 = analog ? BF_MASTER_MINUS20_8 : BF_MASTER_UNMUTE;
+ u16 gain16 = analog ? BF_MASTER_MINUS20_16 : BF_MASTER_0DB;
+
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, gain8,
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, gain8,
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, gain16,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, gain16,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+ chip->master[out][0] = gain16;
+ chip->master[out][1] = gain16;
+ chip->muted[out] = false;
+ }
+
+ /* Every source into every output pair, L and R, at 0 dB (the
+ * standard map, plus the low map on AN1/2 - see bf_xpoint_write's
+ * own comment for why AN1/2 needs both). The addresses use the
+ * source's idx_l/idx_r on the canonical block - writing the raw
+ * index on both bases would put PB1 R on the L side and PB1 L on
+ * the R side (L+R on both = mono). The "cross" registers
+ * (L-reg idx_r / R-reg idx_l) are left at 0; the restore at stream
+ * start re-writes the same addresses from the cache.
+ */
+ for (out = 0; out < 6; out++) {
+ unsigned int blk = bf_xpoint_block[out];
+
+ for (src = 0; src < 14; src++) {
+ ret = bf_xpoint_write(chip, out, src, BF_FADER_0DB,
+ BF_FADER_0DB);
+ if (ret < 0)
+ return ret;
+ }
+ ret = bf_crosspoint_clear_cross(chip, blk);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Mirror the defaults into the control cache (14 controls/output). */
+ for (out = 0; out < 6; out++)
+ for (src = 0; src < 14; src++) {
+ chip->xpoint[out][src][0] = BF_FADER_0DB;
+ chip->xpoint[out][src][1] = BF_FADER_0DB;
+ }
+
+ /* Host settings word - bf_settings_write() is Internal-only at
+ * this point in the series (the clock-source control lands with
+ * a later patch).
+ */
+ return bf_settings_write(chip);
+}
+
+/* The device resets its output masters to mute when a stream session
+ * starts (hardware-verified 2026-08-24: after a stream start the
+ * output stays silent until a master write lands - only a write
+ * un-mutes the 8-bit register). Re-apply the six output masters +
+ * mutes from the cache; also used by the PM restore path.
+ */
+int bf_apply_masters(struct snd_usb_babyface *chip)
+{
+ int out, ret;
+ u16 flag;
+
+ for (out = 0; out < 6; out++) {
+ u16 l = chip->muted[out] ? 0 : chip->master[out][0];
+ u16 r = chip->muted[out] ? 0 : chip->master[out][1];
+ u8 l8 = chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(l);
+ u8 r8 = chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(r);
+
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, l8,
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, r8,
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+/* -- mixer controls ------------------------ */
+
+/* dB TLV for the output masters: 0x2000 = 0 dB, 0x4000 = +6 dB
+ * (CALIBRATION.md) with the hardware 20*log10(v/0x2000) law - the raw
+ * 16-bit value IS the linear amplitude. WirePlumber needs this to map
+ * the volume 1:1 to the hardware control instead of applying a software
+ * volume on top (which left the output ~30 dB down).
+ */
+static const DECLARE_TLV_DB_RANGE(bf_master_tlv,
+ 0, 0x2000, TLV_DB_LINEAR_ITEM(-6500, 0),
+ 0x2000, 0x4000, TLV_DB_LINEAR_ITEM(0, 600)
+);
+
+static int bf_master_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 0x4000; /* +6 dB = 2 x 0dB(0x2000) */
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_master_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+
+ ucontrol->value.integer.value[0] = chip->master[out][0];
+ ucontrol->value.integer.value[1] = chip->master[out][1];
+ return 0;
+}
+
+static int bf_master_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+ u16 l = ucontrol->value.integer.value[0];
+ u16 r = ucontrol->value.integer.value[1];
+ u16 flag;
+ int ret = 0;
+
+ /* The control is declared 0..0x4000 (+6 dB); reject anything outside
+ * so the 16-bit companion register and the cache stay in spec (the
+ * ALSA core only enforces this with CONFIG_SND_CTL_INPUT_VALIDATION).
+ */
+ if (l > 0x4000 || r > 0x4000)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (l == chip->master[out][0] && r == chip->master[out][1])
+ goto out;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+ /* The 8-bit register is the real volume; the 16-bit is its
+ * companion (kept in sync like TotalMix).
+ */
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ goto out;
+
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ chip->muted[out] = false;
+ /* The DIM engaged/dim_saved re-base hook lands with the DIM
+ * switch (a later patch in this series) - chip->dim does not
+ * exist yet at this point in the split.
+ */
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_mute_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_mute_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+
+ /* ALSA convention: 1 = enabled (sound on) = not muted. */
+ ucontrol->value.integer.value[0] = !chip->muted[out];
+ ucontrol->value.integer.value[1] = !chip->muted[out];
+ return 0;
+}
+
+static int bf_mute_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+ bool muted = !ucontrol->value.integer.value[0];
+ u16 flag;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (muted == chip->muted[out])
+ goto out;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+ if (muted) {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ goto out;
+ } else {
+ /* Unmute restores the cached volume (TotalMix keeps the
+ * pre-mute fader value host-side), 8-bit + 16-bit.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->master[out][0]),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->master[out][1]),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->master[out][0],
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->master[out][1],
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ goto out;
+ }
+ chip->muted[out] = muted;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* -- crosspoint matrix (6 outputs x 14 sources) -------------- */
+
+/* The crosspoint fader is linear in amplitude: BF_FADER_0DB (0x16a0) is
+ * unity and BF_FADER_TOP (0x2d41) is exactly twice that, i.e. +6 dB - see
+ * bf_fader_curve, whose whole span follows raw = BF_FADER_0DB * 10^(dB/20).
+ * Raw 0 is off.
+ */
+static const DECLARE_TLV_DB_LINEAR(bf_xpoint_tlv, TLV_DB_GAIN_MUTE, 600);
+
+/* Write a crosspoint slot on the wire: the standard map always, and -
+ * for the AN1/2 output only - the low map as well.
+ *
+ * HARDWARE-VERIFIED 2026-09-14: AN1/2 is not just another output with
+ * a redundant "shadow" register, despite what this file used to say.
+ * Sweeping only the standard map (BF_REG_CROSS_BASE_*) into AN1/2
+ * produced no audible change at all, off through +6 dB, with two
+ * independent sources (a generated tone via PB1, a live mic via AN2);
+ * the exact same code path targeting any other output (verified on
+ * PH3/4) tracked the fader correctly, off to +6 dB within 0.6 dB.
+ * PROTOCOL.md's "Scene load" capture explains why: the vendor software
+ * always writes BOTH the standard map and the low map
+ * (BF_REG_LOWMAP_BASE_*) together for AN1/2's own crosspoints, at the
+ * same value - the low map is what actually feeds that output's sum;
+ * the standard map alone is not enough. Every other output only has a
+ * standard map.
+ *
+ * bf_split_apply() already knew this (it writes both for AN1/2's
+ * playback pairs); this generalises the same pattern to the plain
+ * fader.
+ */
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+ u16 l, u16 r)
+{
+ unsigned int blk = bf_xpoint_block[out];
+ const struct bf_source *s = &bf_sources[src];
+ u16 flag;
+ int ret;
+
+ if (out == 0) {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ BF_REG_LOWMAP_BASE_L + s->idx_l);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ BF_REG_LOWMAP_BASE_R + s->idx_r);
+ if (ret < 0)
+ return ret;
+ }
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ s->idx_l) | flag);
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ s->idx_r) | flag);
+}
+
+static int bf_xpoint_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = BF_FADER_TOP; /* +6 dB fader top */
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_xpoint_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value >> 8;
+ int src = kctl->private_value & 0xff;
+
+ ucontrol->value.integer.value[0] = chip->xpoint[out][src][0];
+ ucontrol->value.integer.value[1] = chip->xpoint[out][src][1];
+ return 0;
+}
+
+static int bf_xpoint_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value >> 8;
+ int src = kctl->private_value & 0xff;
+ u16 l = ucontrol->value.integer.value[0];
+ u16 r = ucontrol->value.integer.value[1];
+ int ret = 0;
+
+ if (l > BF_FADER_TOP || r > BF_FADER_TOP)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (l == chip->xpoint[out][src][0] && r == chip->xpoint[out][src][1])
+ goto out;
+
+ ret = bf_xpoint_write(chip, out, src, l, r);
+ if (ret < 0)
+ goto out;
+
+ chip->xpoint[out][src][0] = l;
+ chip->xpoint[out][src][1] = r;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+int babyface_create_xpoints(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int out, src, err;
+
+ for (out = 0; out < 6; out++) {
+ for (src = 0; src < 14; src++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Playback Volume",
+ .index = out * 14 + src,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = bf_xpoint_info,
+ .get = bf_xpoint_get,
+ .put = bf_xpoint_put,
+ .tlv.p = bf_xpoint_tlv,
+ .private_value = (out << 8) | src,
+ }, chip);
+ /* Name the control by its source: "AN1 Playback Volume",
+ * "PB1 Playback Volume"... with a unique index.
+ */
+ strscpy(kctl->id.name, bf_sources[src].name,
+ sizeof(kctl->id.name));
+ strlcat(kctl->id.name, " Playback Volume",
+ sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+ }
+
+ return 0;
+}
+
+int babyface_create_masters(struct snd_usb_babyface *chip)
+{
+ static const char * const out_names[6] = {
+ "AN1/2", "PH3/4", "AS1/2", "ADAT3/4", "ADAT5/6", "ADAT7/8"
+ };
+ struct snd_kcontrol *kctl;
+ int i, err;
+
+ for (i = 0; i < 6; i++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = out_names[i],
+ .index = i,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = bf_master_info,
+ .get = bf_master_get,
+ .put = bf_master_put,
+ .tlv.p = bf_master_tlv,
+ .private_value = i,
+ }, chip);
+ strlcat(kctl->id.name, " Playback Volume", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = out_names[i],
+ .index = i,
+ .info = bf_mute_info,
+ .get = bf_mute_get,
+ .put = bf_mute_put,
+ .private_value = i,
+ }, chip);
+ strlcat(kctl->id.name, " Playback Switch", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ dev_dbg(&chip->dev->dev, "output %d = %s\n", i, out_names[i]);
+ }
+
+ return 0;
+}
+
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 2d71286e3..50b6a31a5 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -3,13 +3,16 @@
* RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
*
* Core driver: USB vendor requests + cold init, interrupt-URB PCM
- * streaming, and the card lifecycle (probe/disconnect/module entry).
- * No mixer controls, front-panel support or suspend/resume yet - see
- * babyfacepro.h and the cover letter for the rest of the series.
+ * streaming, mixer-state persistence across re-probes/resume, and the
+ * card lifecycle (probe/disconnect/module entry). See babyfacepro.h
+ * for the shared device state and register map, and babyfacepro-ctl.c
+ * for the ALSA control surface (mixer, front panel, DSP EQ - added by
+ * later patches in this series).
*/
#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mutex.h>
+#include <linux/slab.h>
#include <linux/string.h>
#include <linux/unaligned.h>
#include <linux/usb.h>
@@ -252,6 +255,139 @@ int bf_cold_init(struct snd_usb_babyface *chip)
return 0;
}
+/* -- mixer-state persistence across interface re-probes --------
+ * A userspace client can claim the proprietary interface via usbfs
+ * (USBDEVFS_DISCONNECT_CLAIM - seen with PipeWire grabbing the
+ * device when a stream targets the sink, and with the TuxMix
+ * user-space daemon's libusb). That detaches us and the card
+ * disappears for the duration; on release the interface re-probes.
+ * The device keeps its registers across the detach, but our cold
+ * init clears them - so save the mixer state at disconnect and
+ * restore it at the next probe.
+ */
+
+static LIST_HEAD(bf_saved_list);
+static DEFINE_MUTEX(bf_saved_mutex);
+
+/* Re-apply the whole cached mixer state after a resume (the device
+ * lost its registers across a system suspend - TotalMix does the same
+ * re-apply). Caller holds chip->mutex.
+ *
+ * Only the masters and the crosspoint matrix exist at this point in
+ * the series; preamp/gain and pitch are re-applied here too once the
+ * later patches that introduce them land.
+ */
+int babyface_restore_state(struct snd_usb_babyface *chip)
+{
+ int out, src, ret;
+
+ /* Masters (8-bit = the real volume) + mutes. */
+ ret = bf_apply_masters(chip);
+ if (ret < 0)
+ return ret;
+
+ /* Crosspoints (canonical out -> register block; AN1/2 also needs
+ * the low map, see bf_xpoint_write's own comment).
+ */
+ for (out = 0; out < 6; out++) {
+ unsigned int blk = bf_xpoint_block[out];
+
+ for (src = 0; src < 14; src++) {
+ ret = bf_xpoint_write(chip, out, src,
+ chip->xpoint[out][src][0],
+ chip->xpoint[out][src][1]);
+ if (ret < 0)
+ return ret;
+ }
+ ret = bf_crosspoint_clear_cross(chip, blk);
+ if (ret < 0)
+ return ret;
+ }
+ /* The rate (family register), then the varispeed ratio, which is
+ * sticky in the device and so must be re-sent even at pitch 0 in
+ * case something else wrote it while we were detached.
+ */
+ ret = bf_clock_write(chip);
+ if (ret < 0)
+ return ret;
+ return bf_pitch_write(chip, chip->pitch);
+}
+
+void bf_state_save(struct snd_usb_babyface *chip)
+{
+ struct bf_saved *s;
+ const char *key = chip->dev->serial ? chip->dev->serial :
+ dev_name(&chip->dev->dev);
+ bool found = false;
+
+ mutex_lock(&bf_saved_mutex);
+ list_for_each_entry(s, &bf_saved_list, list) {
+ if (strcmp(s->key, key))
+ continue;
+ found = true;
+ break;
+ }
+ if (!found) {
+ s = kzalloc_obj(*s, GFP_KERNEL);
+ if (!s) {
+ mutex_unlock(&bf_saved_mutex);
+ return;
+ }
+ strscpy(s->key, key, sizeof(s->key));
+ list_add_tail(&s->list, &bf_saved_list);
+ }
+
+ s->flag_cnt = chip->flag_cnt;
+ memcpy(s->master, chip->master, sizeof(s->master));
+ memcpy(s->muted, chip->muted, sizeof(s->muted));
+ memcpy(s->xpoint, chip->xpoint, sizeof(s->xpoint));
+ mutex_unlock(&bf_saved_mutex);
+}
+
+/* Copy a saved state (if any) into a freshly probed chip and push it
+ * to the device. Returns 1 when restored, -ENOENT when there is none,
+ * or a negative error from the vendor writes.
+ */
+int bf_state_restore(struct snd_usb_babyface *chip)
+{
+ struct bf_saved *s;
+ const char *key = chip->dev->serial ? chip->dev->serial :
+ dev_name(&chip->dev->dev);
+ int ret = -ENOENT;
+
+ mutex_lock(&bf_saved_mutex);
+ list_for_each_entry(s, &bf_saved_list, list) {
+ if (strcmp(s->key, key))
+ continue;
+ chip->flag_cnt = s->flag_cnt;
+ memcpy(chip->master, s->master, sizeof(chip->master));
+ memcpy(chip->muted, s->muted, sizeof(chip->muted));
+ memcpy(chip->xpoint, s->xpoint, sizeof(chip->xpoint));
+ ret = 1;
+ break;
+ }
+ mutex_unlock(&bf_saved_mutex);
+ if (ret != 1)
+ return ret;
+
+ mutex_lock(&chip->mutex);
+ ret = babyface_restore_state(chip);
+ mutex_unlock(&chip->mutex);
+ return ret ? ret : 1;
+}
+
+void bf_state_purge(void)
+{
+ struct bf_saved *s, *tmp;
+
+ mutex_lock(&bf_saved_mutex);
+ list_for_each_entry_safe(s, tmp, &bf_saved_list, list) {
+ list_del(&s->list);
+ kfree(s);
+ }
+ mutex_unlock(&bf_saved_mutex);
+}
+
/* -- PCM data path ------------------------- */
static bool babyface_capture_copy(struct snd_usb_babyface *chip,
@@ -640,6 +776,14 @@ void babyface_stream_work(struct work_struct *work)
if (ret < 0)
goto err;
+ /* The cold init above cleared the mixer registers; push the
+ * cached masters and crosspoints back so the session starts
+ * at the user's levels.
+ */
+ ret = babyface_restore_state(chip);
+ if (ret < 0)
+ goto err;
+
chip->streaming = true;
dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
chip->frames_per_urb, chip->nurbs);
@@ -1063,6 +1207,26 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ /* Restore the mixer state saved at the last disconnect (if any);
+ * the device keeps its registers across a usbfs detach, but the
+ * cold init above cleared them, so push the user's settings back.
+ */
+ err = bf_state_restore(chip);
+ if (err == -ENOENT) {
+ /* No saved state: the 0x16 clear zeroed the mixer registers,
+ * so restore the factory default routing to keep the outputs
+ * live out of the box.
+ */
+ err = babyface_write_default_mixer(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "default mixer restore failed: %d\n", err);
+ goto error;
+ }
+ } else if (err < 0) {
+ dev_err(&intf->dev, "mixer state restore failed: %d\n", err);
+ goto error;
+ }
+
/* Keep the allocation size independent of the active USB mode. */
urbsize = BF_WORDS_PER_FRAME * sizeof(u32) * chip->frames_per_urb;
chip->urbs_in = kcalloc(chip->nurbs, sizeof(*chip->urbs_in), GFP_KERNEL);
@@ -1107,6 +1271,18 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ err = babyface_create_masters(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "master control creation failed: %d\n", err);
+ goto error;
+ }
+
+ err = babyface_create_xpoints(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "crosspoint creation failed: %d\n", err);
+ goto error;
+ }
+
err = snd_card_register(chip->card);
if (err < 0) {
dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
@@ -1144,6 +1320,12 @@ static void babyface_disconnect(struct usb_interface *intf)
if (chip->shutdown)
return;
+ /* Keep the mixer state for the next probe: a userspace usbfs
+ * claim (PipeWire sink grab, TuxMix daemon) detaches us and the
+ * cold init of the re-probe would otherwise wipe the settings.
+ */
+ bf_state_save(chip);
+
chip->shutdown = true;
cancel_work_sync(&chip->stream_work);
/* Balance the probe()-time usb_disable_autosuspend(): the usb_device
@@ -1189,6 +1371,7 @@ static int __init babyface_init(void)
static void __exit babyface_exit(void)
{
+ bf_state_purge();
usb_deregister(&babyface_driver);
}
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index de0353a59..6125e763f 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -12,8 +12,12 @@
* snd-usb-audio quirk.
*
* This file and babyfacepro.c cover the card lifecycle (probe,
- * disconnect) and the PCM stream only - no mixer controls, no
- * front-panel support, no suspend/resume yet. Those are added by
+ * disconnect), the PCM stream, and mixer-state persistence across
+ * re-probes/resume. babyfacepro-ctl.c adds the ALSA control surface,
+ * starting with the output masters (volume + mute) and the crosspoint
+ * routing matrix - the minimum needed for the card to produce
+ * controllable, routed audio. Preamp/gain, the routing-flag switches,
+ * varispeed pitch, front-panel support and suspend/resume are added by
* later patches in this series, each with its own register map and
* device-state additions; see the cover letter for the full series
* plan.
@@ -81,8 +85,10 @@
* panel and DSP EQ patches each add their own as those features land.
*/
#define BF_REQ_KEEPALIVE 0x10 /* settings word / stream trigger */
+#define BF_REQ_CROSSPOINT 0x12 /* 16-bit crosspoint / master */
#define BF_REQ_REG_CLEAR 0x16 /* cold-init register clear */
#define BF_REQ_PREAMP 0x17 /* 48V/PAD state + readback */
+#define BF_REQ_GAIN 0x1a /* 8-bit gain / master companion */
#define BF_REQ_DDS 0x1b /* clock quads */
#define BF_REQ_STATUS_2 0x1c /* read 4 B */
#define BF_REQ_SESSION_START 0x1d
@@ -108,6 +114,72 @@
#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
#define BF_SETTINGS_CLOCK_OPTICAL 0x0004
+/* Register addresses (masters + crosspoint matrix). */
+#define BF_REG_MASTER_16 0x03e0 /* + 2*out (bReq 0x12) */
+#define BF_REG_MASTER_8 0x0004 /* + 2*out (bReq 0x1a) */
+#define BF_REG_CROSS_BASE_L 0x0034 /* + 0x34*out + src (bReq 0x12) */
+#define BF_REG_CROSS_BASE_R 0x004e /* + 0x34*out + src */
+#define BF_REG_CROSS_STRIDE 0x0034
+/* Low map (the AN1/2 monitor bus's own per-source registers, one set
+ * shared across every output - not one per output block like the
+ * standard crosspoint map above). NOT a shadow/mirror of the standard
+ * map for AN1/2: it is what that output actually sums from, and the
+ * standard map alone has no audible effect on it (hardware-verified
+ * 2026-09-14, see bf_xpoint_write's own comment and KERNEL-DRIVER.md).
+ */
+#define BF_REG_LOWMAP_BASE_L 0x0000 /* + idx_l */
+#define BF_REG_LOWMAP_BASE_R 0x001a /* + idx_r */
+
+/* The "cross" register block within each output: the L-registers sit at
+ * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
+ * source pairs that can be cross-linked). bf_crosspoint_clear_cross()
+ * zeroes them because the cold-init clear does not cover them.
+ */
+#define BF_CROSS_L_FIRST 5
+#define BF_CROSS_L_LAST 23
+#define BF_CROSS_R_FIRST 4
+#define BF_CROSS_R_LAST 22
+
+/* Calibrated master value: 0 dB = 0x2000 (+6 dB = 0x4000). See
+ * CALIBRATION.md. The crosspoint fader curve is DIFFERENT (0 dB =
+ * 0x16a0, top 0x2d41 - see below).
+ */
+#define BF_MASTER_0DB 0x2000
+
+/* The 8-bit master is the REAL output volume (hardware-verified
+ * 2026-08-24: writing it changes the level, the 16-bit does not).
+ * Scale: 0.5 dB per step, 0xf3 = 0 dB (the scene-load default),
+ * bottom 0x73 = -64 dB (silence), top 0xff = +6 dB. The 16-bit
+ * register is a companion kept in sync (TotalMix writes both).
+ * The mute value is 0x3B.
+ */
+#define BF_MASTER_8_0DB 0xf3
+#define BF_MASTER_8_MIN 0x73
+#define BF_MASTER_MUTE 0x3b
+#define BF_MASTER_UNMUTE 0xf3
+/* -20 dB master, 8-bit and 16-bit: the exact pair the hardware DIM
+ * button writes (cap_dim2.pcap), reused as the power-on default.
+ */
+#define BF_MASTER_MINUS20_8 0xcb
+#define BF_MASTER_MINUS20_16 0x0333
+
+/* Crosspoint fader curve: 0 dB = 0x16a0, +6 dB = 0x2d41 (fader curve,
+ * DIFFERENT from the master 0x4000 top - see CALIBRATION.md).
+ */
+#define BF_FADER_0DB 0x16a0
+#define BF_FADER_TOP 0x2d41
+
+/* The crosspoint matrix sources (14 controls per output). */
+struct bf_source {
+ const char *name;
+ u8 idx_l;
+ u8 idx_r;
+};
+
+/* Crosspoint-source order + register block maps (babyfacepro-ctl.c). */
+extern const struct bf_source bf_sources[14];
+extern const u8 bf_xpoint_block[6];
+
struct snd_usb_babyface {
struct snd_card *card;
struct usb_device *dev;
@@ -138,6 +210,26 @@ struct snd_usb_babyface {
struct snd_pcm_substream *subs[2];
unsigned long hw_ptr[2];
unsigned long prev_period[2];
+
+ /* mixer state (no gain readback exists - host-side mirror) */
+ u8 flag_cnt; /* 0xc000/0x4000/0x8000/0x0000 */
+ u16 master[6][2]; /* cached 16-bit masters */
+ bool muted[6];
+ u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
+};
+
+/* The mixer state cached across interface re-probes/resume (see
+ * babyfacepro.c's own comment on bf_state_save/bf_state_restore).
+ * Grows alongside struct snd_usb_babyface as later patches in this
+ * series add more mixer state to persist.
+ */
+struct bf_saved {
+ struct list_head list;
+ char key[32];
+ u8 flag_cnt;
+ u16 master[6][2];
+ bool muted[6];
+ u16 xpoint[6][14][2];
};
struct bf_rate {
@@ -163,3 +255,28 @@ void babyface_stream_kill(struct snd_usb_babyface *chip);
void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state);
void babyface_stream_work(struct work_struct *work);
extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
+
+/* Mixer-state persistence across interface re-probes/resume. */
+void bf_state_save(struct snd_usb_babyface *chip);
+int bf_state_restore(struct snd_usb_babyface *chip);
+void bf_state_purge(void);
+int babyface_restore_state(struct snd_usb_babyface *chip);
+
+/* -- babyfacepro-ctl.c ----------------------- */
+extern const u16 bf_flag_cycle[4];
+int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx);
+int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip,
+ unsigned int blk);
+int babyface_write_default_mixer(struct snd_usb_babyface *chip);
+int bf_apply_masters(struct snd_usb_babyface *chip);
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+ u16 l, u16 r);
+int babyface_create_masters(struct snd_usb_babyface *chip);
+int babyface_create_xpoints(struct snd_usb_babyface *chip);
+
+/* Master gain-law helpers - shared with the front-panel wheels once
+ * the front panel lands.
+ */
+int bf_master_half_db(u16 vol16); /* 16-bit master -> dBx2 */
+int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
+u8 bf_master_8bit(u16 vol16); /* 16-bit master -> 8-bit companion */
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
` (4 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
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@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 | 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
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (2 preceding siblings ...)
2026-09-18 11:39 ` [RFC PATCH v5 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
` (3 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Adds loopback, AN 1>2, sample clock source, AN1/2 link, MS processor,
DIM, width, FX send and varispeed pitch. Pitch is included here
rather than its own patch because the source function that created
these controls registered all of them, pitch included, in one pass.
bf_state_apply_flags() (re-applying this group after a cold-init, on
top of babyface_restore_state()) is introduced here too, since none
of this state existed for it to restore before this patch.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 660 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 210 +++++++-
sound/usb/babyfacepro/babyfacepro.h | 42 +-
3 files changed, 900 insertions(+), 12 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 8ee711b81..2b4dd471d 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -19,8 +19,10 @@
* state persistence, card lifecycle).
*/
#include <linux/log2.h>
+#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mutex.h>
+#include <linux/string.h>
#include <linux/unaligned.h>
#include <linux/usb.h>
#include <linux/workqueue.h>
@@ -1457,3 +1459,661 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
return 0;
}
+static int bf_switch_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_pitch_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 = -50; /* -5.0 % */
+ uinfo->value.integer.max = 50; /* +5.0 % */
+ uinfo->value.integer.step = 1; /* 0.1 % */
+ return 0;
+}
+
+static int bf_pitch_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->pitch;
+ return 0;
+}
+
+static int bf_pitch_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int p = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (p < -50 || p > 50)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (p == chip->pitch)
+ goto out;
+
+ /* The DDS quad is a ratio on top of the family rate, the same at
+ * every sample rate; bf_pitch_write() sends it with the settings
+ * keepalive that commits it.
+ */
+ ret = bf_pitch_write(chip, p);
+ if (ret < 0)
+ goto out;
+ chip->pitch = p;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_loopback_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->loopback[out];
+ ucontrol->value.integer.value[1] = chip->loopback[out];
+ return 0;
+}
+
+/* Write the full 30-channel loopback map: pair (2*out, 2*out+1) at
+ * `on` (0x0001/0x0000), all other channels cleared - exactly what
+ * TotalMix sends on every loopback toggle (cap_loopback2.pcap). The
+ * full-map write is also the reliable OFF (the old per-pair write
+ * sometimes failed to disengage on the hardware).
+ */
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out,
+ bool on)
+{
+ int ch, ret;
+
+ for (ch = 0; ch < BF_LOOPBACK_CHANNELS; ch++) {
+ u16 val = (on && (ch == out * 2 || ch == out * 2 + 1))
+ ? 0x0001 : 0x0000;
+
+ ret = bf_vendor_write(chip, BF_REQ_LOOPBACK, val, ch);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+static int bf_loopback_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value;
+ bool on = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == chip->loopback[out])
+ goto out;
+ ret = bf_loopback_write_map(chip, out, on);
+ if (ret < 0)
+ goto out;
+ /* Single-active model (TotalMix writes one pair at 0x0001, the
+ * rest 0x0000): toggling one output clears the others.
+ */
+ memset(chip->loopback, 0, sizeof(chip->loopback));
+ chip->loopback[out] = on;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_an12_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->an12;
+ return 0;
+}
+
+static int bf_an12_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool an12 = ucontrol->value.integer.value[0];
+ u16 v;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (an12 == chip->an12)
+ goto out;
+ v = (chip->linked ? 0x0400 : 0x0000) | (an12 ? 0x1000 : 0x0000);
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ goto out;
+ chip->an12 = an12;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Clock source (PROTOCOL.md "Clock source / no-lock state",
+ * hardware-verified 2026-08-22, clktest.c): NOT a register write at
+ * all - only the BF_REG_KEEPALIVE_SETTINGS word changes (bit 2 =
+ * Optical). Matches the naming TuxMix's ALSA backend already looks
+ * for ("Sample Clock Source", the same name found on the stock
+ * snd-usb-audio Class-Compliant driver) so it picks this control up
+ * with zero changes on that side.
+ */
+static const char *const bf_clock_texts[] = {
+ "Internal", "Optical In", NULL
+};
+
+static int bf_clock_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 2, bf_clock_texts);
+}
+
+static int bf_clock_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->clock_optical ? 1 : 0;
+ return 0;
+}
+
+static int bf_clock_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool optical = ucontrol->value.enumerated.item[0] != 0;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (optical == chip->clock_optical)
+ goto out;
+ chip->clock_optical = optical;
+ ret = bf_settings_write(chip);
+ if (ret < 0) {
+ chip->clock_optical = !optical;
+ goto out;
+ }
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_link_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->linked;
+ return 0;
+}
+
+static int bf_link_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool linked = ucontrol->value.integer.value[0];
+ u16 v;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (linked == chip->linked)
+ goto out;
+ v = (linked ? 0x0400 : 0x0000) | (chip->an12 ? 0x1000 : 0x0000);
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ goto out;
+ chip->linked = linked;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_ms_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->ms_proc;
+ return 0;
+}
+
+/* MS-proc: engage per the cap_ms2.pcap ON pattern - write 0x0000 to
+ * ALL FOUR AN2 (side) crosspoints: standard map 0x0035/0x004F (L/R)
+ * + low map 0x0001/0x001B (L/R) - the side path is muted (ear-
+ * verified 2026-08-26 with the mic on AN2: MS ON = silence); release
+ * restores the cached fader values (host-side, like TotalMix).
+ * (The 0x1000/0x0004 writes are the DISENGAGE restore values seen in
+ * cap_ms2 - the driver had them inverted on the engage path.)
+ */
+static int bf_ms_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool on = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == chip->ms_proc)
+ goto out;
+ if (on) {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+ if (ret < 0)
+ goto out;
+ } else {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][0], 0x0001);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][0], 0x0035);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][1], 0x001b);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][1], 0x004f);
+ if (ret < 0)
+ goto out;
+ }
+ chip->ms_proc = on;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* DIM - cap_dim2.pcap: an absolute -20 dB on the Phones master
+ * (out 1: 8-bit 0xCB / 16-bit 0x0333) regardless of the current level,
+ * plus the 0x17 wVal=0x2000 wIdx=0x2000 flag; release restores the
+ * pre-DIM master host-side. The master cache keeps the real volume.
+ */
+static int bf_dim_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->dim;
+ return 0;
+}
+
+/* Apply DIM on the wire. chip->mutex must be held: this is reached both
+ * from the ALSA control and from the front-panel poll, and taking the
+ * lock here instead would self-deadlock one of the two.
+ */
+static int bf_dim_apply(struct snd_usb_babyface *chip, bool on)
+{
+ u16 flag;
+ int ret;
+
+ lockdep_assert_held(&chip->mutex);
+ if (on) {
+ chip->dim_saved[0] = chip->master[1][0];
+ chip->dim_saved[1] = chip->master[1][1];
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MINUS20_8,
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MINUS20_8,
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ BF_MASTER_MINUS20_16,
+ (BF_REG_MASTER_16 + 2 * 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ BF_MASTER_MINUS20_16,
+ (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000);
+ if (ret < 0)
+ return ret;
+ } else {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->dim_saved[0]),
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->dim_saved[1]),
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->dim_saved[0],
+ (BF_REG_MASTER_16 + 2 * 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->dim_saved[1],
+ (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x0000, 0x2000);
+ if (ret < 0)
+ return ret;
+ }
+ chip->dim = on;
+ return 0;
+}
+
+static int bf_dim_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool on = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == chip->dim)
+ goto out;
+ ret = bf_dim_apply(chip, on);
+ if (ret == 0)
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* DIM press on the front panel. The device has no DSP of its own for
+ * this, so the host does it, exactly as it already does for the SET
+ * button's phantom toggle.
+ */
+void bf_panel_toggle_dim(struct snd_usb_babyface *chip)
+{
+ bool on;
+ int ret;
+
+ mutex_lock(&chip->mutex);
+ on = !chip->dim;
+ ret = bf_dim_apply(chip, on);
+ mutex_unlock(&chip->mutex);
+
+ if (ret == 0 && chip->dim_kctl)
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->dim_kctl->id);
+}
+
+static int bf_width_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 = -100;
+ uinfo->value.integer.max = 100;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_width_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->width;
+ return 0;
+}
+
+static int bf_width_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int w = ucontrol->value.integer.value[0];
+ u16 l, r;
+ int ret = 0;
+
+ if (w < -100 || w > 100)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (w == chip->width)
+ goto out;
+ /* Width spread: L = 0x1000*(1+w), R = 0x1000*(1-w), L+R = 0x2000.
+ * TotalMix writes the strip's src pair on BOTH maps (cap_width3-7,
+ * PROTOCOL.md "Width strip mapping"): the low map (0x0000+src L /
+ * 0x001A+src R) and the std block-0 map (0x0034+src L /
+ * 0x004E+src R) - the stereo pair spreads L/R in opposition, the
+ * mirror src (AN2) gets the swapped values.
+ */
+ l = (u16)(((0x2000 * (100 + w) / 2) + 50) / 100);
+ r = 0x2000 - l;
+ /* Low map: AN1 L=0x0000, R=0x001A; AN2 L=0x0001, R=0x001B. */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+ if (ret < 0)
+ goto out;
+ /* Std block-0 map (item 0b, the missing half): AN1 L=0x0034,
+ * R=0x004E; AN2 L=0x0035, R=0x004F. (The playback strips PB2-6
+ * target block n-2 - 0x00AE family - reserved for the per-strip
+ * controls.)
+ */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0034);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x004e);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0035);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x004f);
+ if (ret < 0)
+ goto out;
+ chip->width = w;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_fx_send_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 = 0x1000;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_fx_send_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->fx_send;
+ return 0;
+}
+
+static int bf_fx_send_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ u16 v = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (v > 0x1000)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (v == chip->fx_send)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0138);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0153);
+ if (ret < 0)
+ goto out;
+ chip->fx_send = v;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Routing flags + varispeed pitch: registered together since the
+ * source function that created them (pre-split) registered all of
+ * them in one pass.
+ */
+int babyface_create_flags(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int i, err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Varispeed Pitch",
+ .info = bf_pitch_info,
+ .get = bf_pitch_get,
+ .put = bf_pitch_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ for (i = 0; i < 6; i++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Loopback Switch",
+ .index = i,
+ .info = bf_mute_info,
+ .get = bf_loopback_get,
+ .put = bf_loopback_put,
+ .private_value = i,
+ }, 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 = "AN 1>2 Switch",
+ .info = bf_switch_info,
+ .get = bf_an12_get,
+ .put = bf_an12_put,
+ }, 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 = "Sample Clock Source",
+ .info = bf_clock_info,
+ .get = bf_clock_get,
+ .put = bf_clock_put,
+ }, 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 = "AN1/2 Link Switch",
+ .info = bf_switch_info,
+ .get = bf_link_get,
+ .put = bf_link_put,
+ }, 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 = "MS Processor Switch",
+ .info = bf_switch_info,
+ .get = bf_ms_get,
+ .put = bf_ms_put,
+ }, 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 = "Dim Switch",
+ .info = bf_switch_info,
+ .get = bf_dim_get,
+ .put = bf_dim_put,
+ }, chip);
+ chip->dim_kctl = kctl;
+ 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 = "Width",
+ .info = bf_width_info,
+ .get = bf_width_get,
+ .put = bf_width_put,
+ }, 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 = "FX Send Volume",
+ .info = bf_fx_send_info,
+ .get = bf_fx_send_get,
+ .put = bf_fx_send_put,
+ }, 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 3a564f853..c15fca214 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -103,17 +103,23 @@ int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf)
0, idx, buf, 4, BF_CTL_TIMEOUT, GFP_KERNEL);
}
-/* Sends the BF_REG_KEEPALIVE_SETTINGS word (PROTOCOL.md: "keepalive
- * 0x10 0x05CF wVal = host settings-state register" - a single shared
- * word, not independent per-setting writes). Hardcoded to Internal
- * clock for now; the clock-source control and the chip->clock_optical
- * bit it composes in land with a later patch in this series, at which
- * point this grows the same way babyface_restore_state() does further
- * down the series.
+/* Composes and sends the BF_REG_KEEPALIVE_SETTINGS word from every
+ * currently-tracked flag together (PROTOCOL.md: "keepalive 0x10 0x05CF
+ * wVal = host settings-state register" - a single shared word, not
+ * independent per-setting writes). Only clock source is tracked so
+ * far; the single call site this replaces (which used to hardcode
+ * 0x0001, i.e. "always Internal") is why this exists as its own
+ * function rather than an inline write at each call site - the next
+ * flag added to this word (EQ for Record / Optical-Out SPDIF, this
+ * driver's own upstream follow-up list) just OR's in here too, instead
+ * of every caller needing to remember every other bit.
*/
int bf_settings_write(struct snd_usb_babyface *chip)
{
- return bf_vendor_write(chip, BF_REQ_KEEPALIVE, BF_SETTINGS_CLOCK_INTERNAL,
+ u16 w = chip->clock_optical ? BF_SETTINGS_CLOCK_OPTICAL :
+ BF_SETTINGS_CLOCK_INTERNAL;
+
+ return bf_vendor_write(chip, BF_REQ_KEEPALIVE, w,
BF_REG_KEEPALIVE_SETTINGS);
}
@@ -231,7 +237,9 @@ int bf_cold_init(struct snd_usb_babyface *chip)
if (!r)
return -EINVAL;
ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE,
- (bf_rate_family(r) << 4) | BF_SETTINGS_CLOCK_INTERNAL,
+ (bf_rate_family(r) << 4) |
+ (chip->clock_optical ? BF_SETTINGS_CLOCK_OPTICAL :
+ BF_SETTINGS_CLOCK_INTERNAL),
BF_REG_KEEPALIVE_INIT);
if (ret < 0)
return ret;
@@ -327,6 +335,157 @@ int babyface_restore_state(struct snd_usb_babyface *chip)
return bf_pitch_write(chip, chip->pitch);
}
+/* Re-apply the non-master flags (loopback / AN1>2 / link / width /
+ * FX send / MS) 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 out, ret, on_out = -1;
+ u16 l, r;
+
+ /* Loopback: the full 30-channel map from the cached state (the
+ * single-active invariant keeps at most one pair at 0x0001).
+ */
+ for (out = 0; out < 6; out++) {
+ if (chip->loopback[out]) {
+ on_out = out;
+ break;
+ }
+ }
+ ret = bf_loopback_write_map(chip, on_out, on_out >= 0);
+ if (ret < 0)
+ return ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP,
+ (chip->linked ? 0x0400 : 0x0000) |
+ (chip->an12 ? 0x1000 : 0x0000), 0x1000);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ return ret;
+
+ l = (u16)(((0x2000 * (100 + chip->width) / 2) + 50) / 100);
+ r = 0x2000 - l;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+ if (ret < 0)
+ return ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0138);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0153);
+ if (ret < 0)
+ return ret;
+
+ if (chip->ms_proc) {
+ /* Same ON pattern as bf_ms_put (cap_ms2.pcap): mute the AN2
+ * (side) crosspoints, both maps.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Re-apply an engaged DIM (the fixed -20 dB Phones pair + flag). */
+ if (chip->dim) {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+ (BF_REG_MASTER_16 + 2 * 1) |
+ bf_flag_cycle[chip->flag_cnt]);
+ if (ret < 0)
+ return ret;
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+ (BF_REG_MASTER_16 + 2 * 1 + 1) |
+ bf_flag_cycle[chip->flag_cnt]);
+ if (ret < 0)
+ return ret;
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000);
+ if (ret < 0)
+ return 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;
+}
+
void bf_state_save(struct snd_usb_babyface *chip)
{
struct bf_saved *s;
@@ -361,6 +520,15 @@ void bf_state_save(struct snd_usb_babyface *chip)
memcpy(s->trim, chip->trim, sizeof(s->trim));
memcpy(s->split, chip->split, sizeof(s->split));
s->ref_level = chip->ref_level;
+ s->pitch = chip->pitch;
+ memcpy(s->loopback, chip->loopback, sizeof(s->loopback));
+ s->an12 = chip->an12;
+ s->linked = chip->linked;
+ s->ms_proc = chip->ms_proc;
+ s->clock_optical = chip->clock_optical;
+ s->width = chip->width;
+ s->fx_send = chip->fx_send;
+ s->dim = chip->dim;
mutex_unlock(&bf_saved_mutex);
}
@@ -389,6 +557,15 @@ int bf_state_restore(struct snd_usb_babyface *chip)
memcpy(chip->trim, s->trim, sizeof(chip->trim));
memcpy(chip->split, s->split, sizeof(chip->split));
chip->ref_level = s->ref_level;
+ chip->pitch = s->pitch;
+ memcpy(chip->loopback, s->loopback, sizeof(chip->loopback));
+ chip->an12 = s->an12;
+ chip->linked = s->linked;
+ chip->ms_proc = s->ms_proc;
+ chip->clock_optical = s->clock_optical;
+ chip->width = s->width;
+ chip->fx_send = s->fx_send;
+ chip->dim = s->dim;
ret = 1;
break;
}
@@ -398,6 +575,8 @@ int bf_state_restore(struct snd_usb_babyface *chip)
mutex_lock(&chip->mutex);
ret = babyface_restore_state(chip);
+ if (ret == 0)
+ ret = bf_state_apply_flags(chip);
mutex_unlock(&chip->mutex);
return ret ? ret : 1;
}
@@ -810,6 +989,13 @@ void babyface_stream_work(struct work_struct *work)
if (ret < 0)
goto err;
+ /* The 0x16 clear also wipes the flag registers (loopback,
+ * AN1>2, stereo link, width, FX send, MS) - re-apply them.
+ */
+ ret = bf_state_apply_flags(chip);
+ if (ret < 0)
+ goto err;
+
chip->streaming = true;
dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
chip->frames_per_urb, chip->nurbs);
@@ -1332,6 +1518,12 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ err = babyface_create_flags(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "flag 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 505ec1927..9822db312 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -107,9 +107,13 @@
/* Host settings-state word carried by the BF_REG_KEEPALIVE_SETTINGS
* keepalive (PROTOCOL.md "keepalive 0x10 0x05CF wVal = host settings-
- * state register", hardware-verified 2026-08-22/23). Only the
- * Internal value is used until the clock-source control lands with a
- * later patch in this series.
+ * state register", hardware-verified 2026-08-22/23): clock source is
+ * NOT a register write at all, only this flag word changes. Bit 2 =
+ * clock Optical (bit clear = Internal, the default); bits 6/10 (EQ for
+ * Record / Optical-Out SPDIF) are next in the driver's own upstream
+ * follow-up list, not wired to a control yet - the composer below only
+ * OR's in the clock bit today, structured so those can be added the
+ * same way later without another flag-stomping rewrite.
*/
#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
#define BF_SETTINGS_CLOCK_OPTICAL 0x0004
@@ -169,6 +173,15 @@
#define BF_REG_LOWMAP_BASE_L 0x0000 /* + idx_l */
#define BF_REG_LOWMAP_BASE_R 0x001a /* + idx_r */
+#define BF_REQ_LOOPBACK 0x15 /* per-output-channel flag */
+
+/* Loopback map width (captured 2026-08-25, cap_loopback2.pcap):
+ * TotalMix writes the FULL 30-channel 0x15 map on every toggle (ON =
+ * the pair at 0x0001 + the other 28 at 0x0000; OFF = all 0x0000).
+ * wIdx = 2xout_index: AN1/2 = 0/1, PH3/4 = 2/3, AS1/2 = 4/5, ...
+ */
+#define BF_LOOPBACK_CHANNELS 30
+
/* The "cross" register block within each output: the L-registers sit at
* odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
* source pairs that can be cross-linked). bf_crosspoint_clear_cross()
@@ -286,6 +299,16 @@ struct snd_usb_babyface {
* (0 = +4dBu, the default)
*/
struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
+ bool loopback[6];
+ bool an12; /* AN 1>2 copy */
+ bool linked; /* AN1/2 input link */
+ bool ms_proc; /* MS processor engaged */
+ bool clock_optical; /* clock source: false = Internal (default) */
+ int width; /* width knob -100..+100 */
+ u16 fx_send; /* FX send level 0..0x1000 */
+ u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */
+ bool dim; /* DIM engaged (fixed -20 dB on Phones) */
+ struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */
};
/* The mixer state cached across interface re-probes/resume (see
@@ -306,6 +329,15 @@ struct bf_saved {
int trim[4];
bool split[6];
int ref_level;
+ int pitch;
+ bool loopback[6];
+ bool an12;
+ bool linked;
+ bool ms_proc;
+ bool clock_optical;
+ int width;
+ u16 fx_send;
+ bool dim;
};
struct bf_rate {
@@ -337,6 +369,7 @@ void bf_state_save(struct snd_usb_babyface *chip);
int bf_state_restore(struct snd_usb_babyface *chip);
void bf_state_purge(void);
int babyface_restore_state(struct snd_usb_babyface *chip);
+int bf_state_apply_flags(struct snd_usb_babyface *chip);
/* -- babyfacepro-ctl.c ----------------------- */
extern const u16 bf_flag_cycle[4];
@@ -358,6 +391,9 @@ 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);
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
+void bf_panel_toggle_dim(struct snd_usb_babyface *chip);
+int babyface_create_flags(struct snd_usb_babyface *chip);
/* Master gain-law helpers - shared with the front-panel wheels once
* the front panel lands.
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (3 preceding siblings ...)
2026-09-18 11:39 ` [RFC PATCH v5 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
` (2 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Kills the stream and lets the PCM core suspend substreams (apps get
-ESTRPIPE and restart) on suspend; resume re-runs the cold init and
replays the cached mixer state. Front-panel poll start/stop around
suspend is added by the front-panel patch, once that poll exists.
USB autosuspend itself is out of scope (already disabled at probe in
the first patch of this series) - this is S3 system sleep only.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro.c | 48 +++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index c15fca214..ccf3dbf36 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -1592,6 +1592,52 @@ static void babyface_disconnect(struct usb_interface *intf)
snd_card_free_when_closed(chip->card);
}
+static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+ struct snd_device *sdev;
+
+ if (!chip)
+ return 0;
+ list_for_each_entry(sdev, &chip->card->devices, list) {
+ if (sdev->type == SNDRV_DEV_PCM)
+ snd_pcm_suspend_all(sdev->device_data);
+ }
+ cancel_work_sync(&chip->stream_work);
+ mutex_lock(&chip->mutex);
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ mutex_unlock(&chip->mutex);
+ return 0;
+}
+
+static int babyface_resume(struct usb_interface *intf)
+{
+ struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+ int err;
+
+ if (!chip)
+ return 0;
+
+ /* The device lost its state across the suspend; re-run the cold
+ * init and re-apply the cached mixer state. Suspended PCM
+ * substreams are woken by the core - apps get -ESTRPIPE and
+ * restart (the trigger re-arms the stream).
+ */
+ mutex_lock(&chip->mutex);
+ err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
+ if (err < 0)
+ goto out;
+ err = bf_cold_init(chip);
+ if (err < 0)
+ goto out;
+ err = babyface_restore_state(chip);
+out:
+ mutex_unlock(&chip->mutex);
+ return err;
+}
+
static const struct usb_device_id babyface_ids[] = {
{ USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) },
{ }
@@ -1602,6 +1648,8 @@ static struct usb_driver babyface_driver = {
.name = "snd-usb-babyface-pro",
.probe = babyface_probe,
.disconnect = babyface_disconnect,
+ .suspend = babyface_suspend,
+ .resume = babyface_resume,
.id_table = babyface_ids,
};
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (4 preceding siblings ...)
2026-09-18 11:39 ` [RFC PATCH v5 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
Adds the 0x17 readback poll (buttons, wheel, IN/OUT/SELECT, MIX, DIM)
as read-only ALSA controls, plus the host-side emulation of the
physical wheel/button behaviour (TotalMix's own role): the OUT/IN/MIX
wheels, SET's phantom toggle and DIM.
Suspend/resume now also stop/start the poll around a system sleep.
On an original Babyface Pro, front-panel OUT selector values 0/1/2
correspond to Ch 1/2, Phones and Opt; correct the mapping, which left
Ch 1/2 at the previous selection and assigned the other two values to
the wrong outputs. Verified against the physical LEDs and knob
targeting on both a non-FS unit and the Pro FS.
Turning the OUT wheel also made heavy zipper noise on the main and
headphone outputs. A loopback measurement (a tone from the headphone
output cabled into IN3/IN4) showed why: the firmware moves the analog
output level itself on each click, smoothed over about 12 ms, but the
driver also wrote the 8-bit (analog) master on every ~20 ms poll from
its own flat 1 dB/click count, overriding the firmware's own move each
time and saw-toothing the level by 1-3 dB while the wheel turned. The
firmware's actual step also depends on the level and on how fast the
wheel turns (0.5 to 3 dB per click, doubled below a ~62 ms gap between
clicks).
Write only the 16-bit (digital) master during the gesture, track the
wheel's own level per side in half-dB so the ALSA controls stay
accurate, poll the panel every 5 ms for 200 ms after a click to tell
fast clicks apart, and reconcile the real 8-bit master once the wheel
has rested for 150 ms, the way TotalMix does at the end of a gesture.
A muted output now stays muted during the gesture instead of being
unmuted by the 8-bit write, and an existing balance (hold-SELECT) is
preserved since both sides move by the louder side's step.
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 | 1081 +++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 20 +
sound/usb/babyfacepro/babyfacepro.h | 100 +++
3 files changed, 1201 insertions(+)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 2b4dd471d..eac97c895 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -2117,3 +2117,1084 @@ int babyface_create_flags(struct snd_usb_babyface *chip)
return 0;
}
+/* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */
+enum {
+ BF_PANEL_KCTL_BUTTON,
+ BF_PANEL_KCTL_WHEEL,
+ BF_PANEL_KCTL_IN,
+ BF_PANEL_KCTL_OUT,
+ BF_PANEL_KCTL_MIX,
+ BF_PANEL_KCTL_DIM,
+ BF_PANEL_KCTL_SELECT,
+ BF_PANEL_KCTL_NUM,
+};
+
+static const char *const bf_panel_in_texts[] = {
+ "Unknown", "Ch 1/2", "Ch 3/4", "Opt", NULL
+};
+
+static const char *const bf_panel_out_texts[] = {
+ "Unknown", "Ch 1/2", "Phones", "Opt", NULL
+};
+
+static const char *const bf_panel_select_texts[] = {
+ "Left", "Right", "Both", "None", NULL
+};
+
+/* byte3 button flash -> event code (0 = none). The idle byte3 is 0x40;
+ * a press flashes the value below the base for one or two poll frames.
+ */
+static int bf_panel_button_decode(u8 flash)
+{
+ switch (flash) {
+ case BF_PANEL_FLASH_IN: return BF_PANEL_BTN_IN;
+ case BF_PANEL_FLASH_SET: return BF_PANEL_BTN_SET;
+ case BF_PANEL_FLASH_MIX: return BF_PANEL_BTN_MIX;
+ case BF_PANEL_FLASH_OUT: return BF_PANEL_BTN_OUT;
+ case BF_PANEL_FLASH_SELECT: return BF_PANEL_BTN_SELECT;
+ case BF_PANEL_FLASH_DIM: return BF_PANEL_BTN_DIM;
+ default: return BF_PANEL_BTN_NONE;
+ }
+}
+
+/* (byte2 >> 4) & 7 = IN position 4/5/6 -> enum index (0 = not in range). */
+static int bf_panel_in_decode(u8 nib)
+{
+ switch (nib) {
+ case BF_PANEL_IN_CH12: return 1;
+ case BF_PANEL_IN_CH34: return 2;
+ case BF_PANEL_IN_OPT: return 3;
+ default: return 0;
+ }
+}
+
+/* byte1 & 7 = OUT position. 0/1/2 for physical Ch 1/2 / Phones / Opt,
+ * LED-correlated capture, hardware-verified on both an original
+ * Babyface Pro and the Pro FS.
+ */
+static int bf_panel_out_decode(u8 v)
+{
+ switch (v) {
+ case 0x00:
+ case BF_PANEL_OUT_CH12: return 1;
+ case 0x01:
+ case BF_PANEL_OUT_PHONES: return 2;
+ case 0x02:
+ case BF_PANEL_OUT_OPT: return 3;
+ default: return 0;
+ }
+}
+
+/* MIX-mode VU display law - monitoring dBx2 -> the 0x1A 0x000A display
+ * value. Piecewise-linear through the captured (dB, display) points
+ * (cap_mix.pcap 2026-08-23: (-62,0) (-54,1) (-48,2) (-42.5,3)
+ * (-35,4) (-28.4,5); cap_panel.pcap: (-7.4,10) (-6.7,11)
+ * (-4.6,12)) - a log-ish VU scale (coarse at the bottom, ~1.4 dB/step
+ * near 0). The -28..-8 dB middle is interpolated; the exact law is
+ * pending the cap_mixdisp.pcap full-range sweep (TODO 0g).
+ */
+static int bf_mix_display(int db2)
+{
+ static const struct {
+ s16 db2;
+ u8 disp;
+ } pts[] = {
+ { -124, 0 }, { -108, 1 }, { -96, 2 }, { -85, 3 },
+ { -70, 4 }, { -57, 5 }, { -15, 10 }, { -13, 11 },
+ { -9, 12 },
+ };
+ int i;
+
+ if (db2 <= pts[0].db2)
+ return 0;
+ for (i = 0; i < ARRAY_SIZE(pts) - 1; i++) {
+ if (db2 <= pts[i + 1].db2) {
+ u32 num = (u32)(db2 - pts[i].db2) *
+ (u32)(pts[i + 1].disp - pts[i].disp);
+ u32 den = pts[i + 1].db2 - pts[i].db2;
+
+ return pts[i].disp + (int)((num + den / 2) / den);
+ }
+ }
+ /* Above -4.6 dB: keep the last slope (2 dB/step) up to +6 dB. */
+ return pts[ARRAY_SIZE(pts) - 1].disp +
+ clamp((db2 - pts[ARRAY_SIZE(pts) - 1].db2) / 4, 0, 12);
+}
+
+/* The kernel driver plays the TotalMix role for the MIX button (the
+ * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs
+ * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT-
+ * chosen channel(s) of the IN-selected pair, into the OUT-selected
+ * output's crosspoint block - the STANDARD map only (cap_mix.pcap /
+ * cap_select2.pcap, no low-map mirror). Mirrors the change into the
+ * xpoint cache so the ALSA controls follow the wheel. Takes the mutex
+ * (the 0x12 writes cycle the transaction flag like the mixer puts).
+ */
+static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ /* Canonical output of the OUT selection (enum 1 = Ch1/2,
+ * 2 = Phones, 3 = Opt): AN1/2, PH3/4, ADAT7/8 (the optical
+ * output) respectively.
+ */
+ int out = chip->panel_out == 3 ? 5 :
+ chip->panel_out == 2 ? 1 : 0;
+ unsigned int blk = bf_xpoint_block[out];
+ u8 targets[2];
+ int n = 0;
+ int db2;
+ u16 raw, flag;
+ int i;
+
+ /* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT
+ * steps left/right/both; none = nothing selected = no-op wheel).
+ * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4.
+ */
+ if (chip->panel_in == 3) {
+ targets[0] = 4; /* Opt: the AS1/2 pair */
+ n = 1;
+ } else if (chip->panel_select != 3) {
+ int base = chip->panel_in == 2 ? 2 : 0;
+
+ targets[0] = base + (chip->panel_select == 1 ? 1 : 0);
+ n = 1;
+ if (chip->panel_select == 2)
+ targets[n++] = base + 1;
+ }
+
+ mutex_lock(&chip->mutex);
+ db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
+ db2 = clamp(db2 + delta, BF_FADER_DB2_INF, 12);
+ raw = bf_fader_db2_to_raw(db2);
+ chip->panel_mix_raw = raw;
+ for (i = 0; i < n; i++) {
+ const struct bf_source *s = &bf_sources[targets[i]];
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ s->idx_l) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ s->idx_r) | flag);
+ chip->xpoint[out][targets[i]][0] = raw;
+ chip->xpoint[out][targets[i]][1] = raw;
+ /* MIX-mode VU display shadow (0x1A 0x000A+mic): TotalMix
+ * mirrors the monitoring level into the panel display family
+ * (cap_mix/cap_panel.pcap) - the input VU segments follow it.
+ * Written only on change (the captures show TotalMix updating
+ * it on segment crossings). Law = bf_mix_display (TODO 0g
+ * pending the exact full-range capture).
+ */
+ if (targets[i] < 4) {
+ int disp = bf_mix_display(db2);
+
+ if (disp != chip->panel_mix_disp[targets[i]]) {
+ bf_vendor_write(chip, BF_REQ_GAIN,
+ (u16)disp,
+ BF_REG_PANEL_GAIN + targets[i]);
+ chip->panel_mix_disp[targets[i]] = disp;
+ }
+ }
+ }
+ mutex_unlock(&chip->mutex);
+}
+
+/* Write an output's L/R masters (8-bit companions + 16-bit with the
+ * transaction flag) and mirror into the cache - shared by the OUT
+ * volume wheel and the balance wheel. Caller holds the mutex.
+ */
+static void bf_panel_write_master(struct snd_usb_babyface *chip, int out,
+ u16 l, u16 r)
+{
+ u16 flag;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l),
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ chip->muted[out] = false;
+ /* A Phones change while DIM is engaged re-bases the restore. */
+ if (chip->dim && out == 1) {
+ chip->dim_saved[0] = l;
+ chip->dim_saved[1] = r;
+ }
+}
+
+/* Front-panel OUT wheel, measured on hardware 2026-09-17 with a tone
+ * looped from the headphone output into IN3/IN4:
+ *
+ * - The firmware moves an analog output's level by itself when the
+ * wheel turns, with its own ~12 ms smoothing, even when the host
+ * writes nothing. See bf_out_wheel_step() for the step size.
+ * - A host write to the 8-bit master (the analog gain) overrides that.
+ * Writing it on every poll from the host's own count made the level
+ * saw-tooth by 1-3 dB while turning, heard as heavy zipper noise.
+ * - The 16-bit master does not affect the analog level, but it is the
+ * digital outputs' level. TotalMix writes only the 16-bit during a
+ * wheel gesture, and the 8-bit once the wheel is at rest.
+ *
+ * So while the wheel turns, only the 16-bit is written, and the cache
+ * follows the firmware's own count so the ALSA controls read the real
+ * level. A muted output stays muted: only the cache moves. Both
+ * sides move by the louder side's step, so a balance (hold-SELECT) is
+ * kept. Same output mapping as the MIX wheel (Phones = canon 1,
+ * Opt = ADAT7/8 = canon 5, else AN1/2).
+ */
+static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out,
+ u16 l, u16 r)
+{
+ u16 flag;
+
+ if (!chip->muted[out]) {
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ }
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ /* A Phones change while DIM is engaged re-bases the restore. */
+ if (chip->dim && out == 1) {
+ chip->dim_saved[0] = l;
+ chip->dim_saved[1] = r;
+ }
+}
+
+/* A click that follows the previous one within this gap moves twice as
+ * far. In the recordings the firmware doubled clicks up to 58 ms apart
+ * and not from 67 ms. A normal poll every ~21 ms can't tell those
+ * apart, so after an OUT wheel click the panel is polled every
+ * BF_PANEL_FAST_POLL_MS for BF_PANEL_FAST_HOLD_MS, and each click is
+ * dated at the middle of the interval it was seen in. A wrong guess is
+ * fixed by the resync at rest below.
+ */
+#define BF_OUT_WHEEL_ACCEL_MS 62
+#define BF_PANEL_FAST_POLL_MS 5
+#define BF_PANEL_FAST_HOLD_MS 200
+
+/* Quiet time after the last click before the resync write. */
+#define BF_OUT_WHEEL_RESYNC_MS 150
+
+/* The device's own OUT wheel range: below -90 dB a side is muted.
+ * BF_OUT_WHEEL_MUTED is where a muted louder side is kept.
+ */
+#define BF_OUT_WHEEL_FLOOR (-180) /* half-dB */
+#define BF_OUT_WHEEL_MUTED (BF_OUT_WHEEL_FLOOR - 1)
+/* Lowest level the 8-bit master is known to take (BF_MASTER_8_MIN). */
+#define BF_OUT_WHEEL_8BIT_MIN (BF_MASTER_8_MIN - BF_MASTER_8_0DB)
+
+/**
+ * bf_out_wheel_step - the firmware's OUT wheel step, in half-dB
+ * @half_db: the louder side's level before the click, in half-dB
+ *
+ * Measured on the headphone output (a tone looped into IN3/IN4, the
+ * driver writing nothing):
+ *
+ * -9.5 dB and up 0.5 dB per click
+ * -10 to -25.5 dB 1 dB
+ * -26 to -41.5 dB 1.5 dB
+ * -42 to -57.5 dB 2 dB
+ * -58 dB and down 3 dB
+ *
+ * A fast click moves twice as far. Both sides move by the step of the
+ * louder one, so a balance is kept.
+ */
+static int bf_out_wheel_step(int half_db)
+{
+ if (half_db >= -19)
+ return 1;
+ if (half_db >= -51)
+ return 2;
+ if (half_db >= -83)
+ return 3;
+ if (half_db >= -115)
+ return 4;
+ return 6;
+}
+
+/* The wheel's view of one side: its own tracked level while the master
+ * is still what the wheel wrote, else the master's level.
+ */
+static int bf_out_wheel_level(struct snd_usb_babyface *chip, int out, int ch)
+{
+ u16 raw = chip->master[out][ch];
+
+ if (raw == chip->panel_master_last[out][ch])
+ return chip->panel_out_hdb[out][ch];
+ if (!raw)
+ return BF_OUT_WHEEL_MUTED;
+ return bf_master_half_db(raw);
+}
+
+/* One click on both sides. They move together, so a balance is kept.
+ * When the louder side would go below the floor, both stop there and the
+ * louder side mutes; from mute, turning up continues from the floor
+ * (-90 dB up one step is -87). A quieter side below the floor is muted
+ * but keeps its offset, so the balance comes back as the level rises.
+ */
+static void bf_out_wheel_click(int db[2], int step)
+{
+ int louder = max(db[0], db[1]);
+ int shift = step;
+
+ if (louder < BF_OUT_WHEEL_FLOOR) {
+ if (step < 0)
+ return;
+ shift = BF_OUT_WHEEL_FLOOR - louder + step;
+ } else if (louder + step < BF_OUT_WHEEL_FLOOR) {
+ shift = BF_OUT_WHEEL_MUTED - louder;
+ } else if (louder + step > 12) {
+ shift = 12 - louder;
+ }
+ db[0] += shift;
+ db[1] += shift;
+}
+
+static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ int out = chip->panel_out == 3 ? 5 :
+ chip->panel_out == 2 ? 1 : 0;
+ int dir = delta > 0 ? 1 : -1;
+ int clicks = abs(delta);
+ ktime_t now = ktime_get();
+ bool fast;
+ int ch, i, step, louder;
+ int db[2];
+ u16 next[2];
+
+ chip->panel_fast_until = jiffies +
+ msecs_to_jiffies(BF_PANEL_FAST_HOLD_MS);
+ /* The click came some time since the previous poll: take the
+ * middle. Clicks after the first in this poll came within one
+ * interval, so only the first can be slow. A reversal starts a
+ * new turn.
+ */
+ if (chip->panel_poll_t)
+ now = ktime_sub(now,
+ ktime_divns(ktime_sub(now, chip->panel_poll_t), 2));
+
+ mutex_lock(&chip->mutex);
+ fast = chip->panel_out_wheel_t &&
+ dir == chip->panel_out_wheel_dir &&
+ out == chip->panel_out_wheel_out &&
+ ktime_ms_delta(now, chip->panel_out_wheel_t) <
+ BF_OUT_WHEEL_ACCEL_MS;
+ for (ch = 0; ch < 2; ch++)
+ db[ch] = bf_out_wheel_level(chip, out, ch);
+ for (i = 0; i < clicks; i++) {
+ /* Muted, the louder side counts as the floor. */
+ louder = max3(db[0], db[1], BF_OUT_WHEEL_FLOOR);
+ step = dir * bf_out_wheel_step(louder);
+ if (fast || i)
+ step *= 2;
+ bf_out_wheel_click(db, step);
+ }
+ for (ch = 0; ch < 2; ch++)
+ next[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 :
+ bf_master_16bit(db[ch]);
+ bf_panel_out_wheel_write(chip, out, next[0], next[1]);
+ for (ch = 0; ch < 2; ch++) {
+ chip->panel_master_last[out][ch] = next[ch];
+ chip->panel_out_hdb[out][ch] = db[ch];
+ }
+ chip->panel_out_wheel_t = now;
+ chip->panel_out_wheel_dir = dir;
+ chip->panel_out_wheel_out = out;
+ chip->panel_out_resync = true;
+ mutex_unlock(&chip->mutex);
+}
+
+/* Once the wheel has rested, write the wheel's level to the 8-bit (and
+ * 16-bit) master, as TotalMix does at the end of a gesture. The level
+ * already follows the firmware's step rule, so this normally changes
+ * nothing audible. It fixes the rare click whose speed the driver
+ * guessed wrong: one zone step per such click, smoothed by the
+ * firmware. Nothing is written below -64 dB, where the 8-bit codes the
+ * device takes are unknown, nor after a mixer application has set the
+ * master since (that write already set the device).
+ */
+static void bf_panel_out_resync(struct snd_usb_babyface *chip)
+{
+ int out, ch;
+ int db[2];
+ u16 flag;
+
+ if (!chip->panel_out_resync ||
+ ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) <
+ BF_OUT_WHEEL_RESYNC_MS)
+ return;
+
+ mutex_lock(&chip->mutex);
+ chip->panel_out_resync = false;
+ out = chip->panel_out_wheel_out;
+ for (ch = 0; ch < 2; ch++) {
+ if (chip->master[out][ch] != chip->panel_master_last[out][ch])
+ goto unlock;
+ db[ch] = chip->panel_out_hdb[out][ch];
+ if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
+ goto unlock;
+ }
+ if (!chip->muted[out]) {
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[0],
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[1],
+ BF_REG_MASTER_8 + 2 * out + 1);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][0],
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][1],
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ }
+unlock:
+ mutex_unlock(&chip->mutex);
+}
+
+/* IN-mode wheel: the gain of the SELECT-chosen channel(s) of the
+ * IN-selected pair, +/-1 dB per click (manual sec. 5.1: SELECT steps
+ * left/right/both, then the wheel changes the gain). Writes the PANEL
+ * gain registers 0x1A 0x000A+mic (cap_select.pcap 2026-08-24 - the
+ * "ADC gain" family, which drives the same preamp as the GUI
+ * 0x0000+mic; the cache tracks the raw either way). Opt has no
+ * preamp and SELECT None = no target.
+ */
+static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ u8 mics[2];
+ int n = 0;
+ int i;
+
+ if (chip->panel_in == 3 || chip->panel_select == 3)
+ return;
+ {
+ int base = chip->panel_in == 2 ? 2 : 0;
+
+ mics[0] = base + (chip->panel_select == 1 ? 1 : 0);
+ n = 1;
+ if (chip->panel_select == 2)
+ mics[n++] = base + 1;
+ }
+
+ mutex_lock(&chip->mutex);
+ for (i = 0; i < n; i++) {
+ int mic = mics[i];
+ int db = clamp((int)chip->gain[mic] + delta,
+ 0, bf_gain_max_db(mic));
+ u8 raw = bf_gain_raw(mic, db);
+
+ bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic);
+ chip->gain[mic] = db;
+ }
+ mutex_unlock(&chip->mutex);
+}
+
+/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output
+ * Balance"): moves the stereo image of the OUT-selected output by
+ * attenuating ONE side, linear in raw (cap_pan_stereo.pcap: the varied
+ * side = fixed*(1-|pan|), ~0x9C raw step per click at 0 dB - the PAN
+ * of the stereo hardware output in TotalMix). The balance position is
+ * derived from the L/R master ratio (the louder side is the fixed
+ * one), so the gesture needs no extra state - and the OUT wheel below
+ * moves both sides by the same dB to preserve an existing balance.
+ */
+static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ int out = chip->panel_out == 3 ? 5 :
+ chip->panel_out == 2 ? 1 : 0;
+ u16 l, r;
+ int bal; /* -100..+100; + = image right (left varies) */
+ u16 fixed, varied;
+
+ mutex_lock(&chip->mutex);
+ /* Read under the lock so the L/R pair is consistent with the
+ * master/mute/dim writers (they update chip->master[] under the
+ * same mutex).
+ */
+ l = chip->master[out][0];
+ r = chip->master[out][1];
+ /* Balance from the L/R ratio: the louder side is the fixed one. */
+ if (l >= r) {
+ bal = r ? -(100 - (100 * r) / l) : -100;
+ fixed = l;
+ } else {
+ bal = l ? (100 - (100 * l) / r) : 100;
+ fixed = r;
+ }
+ bal = clamp(bal + delta * 2, -100, 100);
+ varied = (u16)((u32)fixed * (100 - abs(bal)) / 100);
+ l = bal >= 0 ? varied : fixed;
+ r = bal >= 0 ? fixed : varied;
+
+ bf_panel_write_master(chip, out, l, r);
+ mutex_unlock(&chip->mutex);
+}
+
+/* SET press (byte3 0x42 flash): toggle 48V phantom on the
+ * SELECT-chosen mic(s) of the IN-selected pair. The hardware only
+ * does this in standalone mode (online, TotalMix ignores SET - no USB
+ * write in the captures), but the driver IS the host: it writes the
+ * preamp state itself and the P48 LEDs follow (the tuxmix-core
+ * emulator, hardware-verified). Restricted to IN mode + Ch1/2 (the
+ * phantom-capable pair); Opt/Ch3/4 and SELECT None = no target.
+ */
+static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
+{
+ u16 bits = 0;
+ int m;
+
+ if (chip->panel_mix || chip->panel_in != 1 ||
+ chip->panel_select == 3)
+ return;
+ if (chip->panel_select != 1)
+ bits |= BF_PREAMP_48V_MIC1;
+ if (chip->panel_select != 0)
+ bits |= BF_PREAMP_48V_MIC2;
+
+ mutex_lock(&chip->mutex);
+ /* One channel selected: toggle it. Both selected: ALIGN both to
+ * the same state, so repeated SET presses cycle all-on <-> all-off
+ * (a mixed phantom state cannot persist with both selected).
+ */
+ if (chip->panel_select == 2) {
+ if ((chip->preamp & bits) == bits)
+ chip->preamp &= ~bits;
+ else
+ chip->preamp |= bits;
+ } else {
+ chip->preamp ^= bits;
+ }
+ bf_preamp_state_write(chip);
+ for (m = 0; m < 4; m++)
+ chip->panel_mix_disp[m] = 0;
+ mutex_unlock(&chip->mutex);
+}
+
+static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl)
+{
+ if (chip->panel_kctl[ctl])
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->panel_kctl[ctl]->id);
+}
+
+/* One 0x17 read + decode. Called from the poll work; no locking needed -
+ * the worker is the only writer and the control get callbacks run under
+ * the ALSA controls lock (chip->panel_button/wheel are consumed there).
+ */
+static void bf_panel_tick(struct snd_usb_babyface *chip)
+{
+ u8 st[4];
+ int delta, in, out;
+ bool dim;
+ u8 cls, pcls;
+ int btn;
+ bool mix_flash, fader_now;
+
+ if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0)
+ return; /* device gone / busy - retry next tick */
+
+ if (!chip->panel_seen) {
+ chip->panel_seen = true;
+ memcpy(chip->panel_prev, st, sizeof(st));
+ /* Seed the state controls from the first snapshot. */
+ in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);
+ if (in)
+ chip->panel_in = in;
+ out = bf_panel_out_decode(st[1] & 0x07);
+ if (out)
+ chip->panel_out = out;
+ chip->panel_mix = !!(st[0] & 0x80);
+ chip->panel_saw_fader = (st[2] >> 4) == 0x0;
+ chip->panel_dim = !!(st[1] & 0x20);
+ return;
+ }
+
+ /* The udev alsactl restore (~100 ms after probe) clobbers the host
+ * SELECT with a stale stored value (the control is VOLATILE but
+ * this alsactl stores/restores it anyway) - re-assert the device's
+ * power-on state (nothing selected, cycle ARMED) for the first
+ * ~3 s so the boot always starts in sync.
+ */
+ if (time_is_after_jiffies(chip->panel_start + 3 * HZ))
+ chip->panel_select = 3;
+
+ /* Button flash (byte3 over the 0x40 idle base). */
+ btn = bf_panel_button_decode(st[3]);
+ if (btn)
+ chip->panel_button = btn;
+
+ /* Wheel: signed 4-bit wrap delta of the byte2 low nibble - only
+ * while the mode class is unchanged. A mode switch (IN 0x4x ->
+ * fader 0x0x on a MIX press, or the OUT counter carrying 0x8F ->
+ * 0x90 - the OUT counter is a full byte, cap_set2.pcap) must not
+ * be read as a wheel jump. Class: 0 = fader (0x0x), 1 = OUT
+ * (0x8x/0x9x), 2 = IN (0x4x/0x5x/0x6x).
+ */
+ cls = (st[2] >> 4) == 0x8 || (st[2] >> 4) == 0x9 ? 1 :
+ (st[2] >> 4) == 0x0 ? 0 : 2;
+ pcls = (chip->panel_prev[2] >> 4) == 0x8 ||
+ (chip->panel_prev[2] >> 4) == 0x9 ? 1 :
+ (chip->panel_prev[2] >> 4) == 0x0 ? 0 : 2;
+ delta = (int)(st[2] & 0x0f) - (int)(chip->panel_prev[2] & 0x0f);
+ if (delta > 8)
+ delta -= 16;
+ else if (delta < -8)
+ delta += 16;
+ if (delta && cls == pcls) {
+ chip->panel_wheel = clamp(chip->panel_wheel + delta,
+ SHRT_MIN, SHRT_MAX);
+ bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL);
+ /* Wheel by mode (LINUX-VALIDATION sec. 12, the TotalMix
+ * emulator): MIX -> monitoring level, OUT (0x8x/0x9x) -> the
+ * selected output master (or its balance while SELECT is
+ * held), IN (0x4x/0x5x/0x6x) -> the SELECT-chosen preamp
+ * gain.
+ */
+ if (chip->panel_mix)
+ bf_panel_mix_wheel(chip, delta);
+ else if (chip->panel_sel_hold >= 10 && cls == 1)
+ bf_panel_balance_wheel(chip, delta);
+ else if (cls == 1)
+ bf_panel_out_wheel(chip, delta);
+ else if (cls == 2)
+ bf_panel_gain_wheel(chip, delta);
+ }
+ bf_panel_out_resync(chip);
+
+ /* Selections - keep the previous when the field is not in range
+ * (the fader-mode readback drops the IN position bits).
+ */
+ in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);
+ if (in && in != chip->panel_in) {
+ chip->panel_in = in;
+ /* The card CLEARS its L/R/both selection on an IN pair
+ * switch (user-verified 2026-08-27): re-sync the host-
+ * tracked SELECT so SET / the wheel / MIX target nothing
+ * until the user picks a channel again. This is the main
+ * anti-desync hook (the physical state is not readable).
+ */
+ if (chip->panel_select != 3) {
+ chip->panel_select = 3;
+ bf_panel_notify(chip, BF_PANEL_KCTL_SELECT);
+ }
+ /* An IN-pair switch disarms the device's SELECT cycle: the
+ * next press only re-arms it (no step), the one after that
+ * cycles (device behavior, user-verified 2026-08-28).
+ */
+ chip->panel_select_armed = false;
+ bf_panel_notify(chip, BF_PANEL_KCTL_IN);
+ }
+ out = bf_panel_out_decode(st[1] & 0x07);
+ if (out && out != chip->panel_out) {
+ chip->panel_out = out;
+ bf_panel_notify(chip, BF_PANEL_KCTL_OUT);
+ }
+
+ /* SELECT press cycles the channel selection L -> R -> both -> none
+ * -> L (manual sec. 5.1). The state is NOT in the readback
+ * (panelprobe 2026-08-24), so it is tracked host-side.
+ */
+ if (st[3] == BF_PANEL_FLASH_SELECT &&
+ chip->panel_prev[3] != BF_PANEL_FLASH_SELECT) {
+ if (!chip->panel_select_armed) {
+ /* Disarmed (IN switch since the last step): the
+ * press only re-arms the cycle - the device steps on
+ * the NEXT press (user-verified 2026-08-28).
+ */
+ chip->panel_select_armed = true;
+ } else {
+ chip->panel_select = (chip->panel_select + 1) & 3;
+ }
+ bf_panel_notify(chip, BF_PANEL_KCTL_SELECT);
+ }
+ /* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output
+ * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz
+ * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and
+ * byte0 does NOT gain the 0x80 engaged bit - so the duration is
+ * the only discriminator: >= 10 ticks (200 ms at 50 Hz) = held.
+ */
+ if (st[3] == BF_PANEL_FLASH_SELECT)
+ chip->panel_sel_hold++;
+ else
+ chip->panel_sel_hold = 0;
+
+ /* SET (A) press: host-side 48V phantom toggle on the
+ * SELECT-chosen mic(s) (see bf_panel_set_phantom).
+ */
+ if (st[3] == BF_PANEL_FLASH_SET &&
+ chip->panel_prev[3] != BF_PANEL_FLASH_SET)
+ bf_panel_set_phantom(chip);
+
+ /* DIM press: toggle the host-side dim, same host-in-the-loop
+ * arrangement as SET above. Decoding the press without acting on
+ * it made the button look dead with the driver alone.
+ */
+ if (st[3] == BF_PANEL_FLASH_DIM &&
+ chip->panel_prev[3] != BF_PANEL_FLASH_DIM)
+ bf_panel_toggle_dim(chip);
+
+ /* MIX (fader mode) - HOST-latched, like TotalMix (cap_mix.pcap,
+ * cap_select2.pcap): the raw press readback is `0D 0D 41 44` -
+ * byte3 flash 0x44, NO engaged bit, byte2 still in the current
+ * mode. The host acks the flash with `0x17 0x8480 0x8C80` -> the
+ * device latches fader mode (byte0/1 gain the 0x80 bit, byte2 =
+ * 0x00+n counter) and STAYS there after the physical release; the
+ * SECOND 0x44 flash exits it (`0x17 0x0400 0x8000` + `0x8080`).
+ * A mode button (IN/OUT/SET) pressed during MIX makes the device
+ * leave fader mode by itself -> same exit writes (the user: IN
+ * must return to gain control). `panel_saw_fader` gates the
+ * device-driven exit so a pre-ack readback (byte2 still 0x4x
+ * while the 0x44 flash shows) never ends MIX before it started.
+ */
+ mix_flash = st[3] == BF_PANEL_FLASH_MIX &&
+ chip->panel_prev[3] != BF_PANEL_FLASH_MIX;
+ fader_now = (st[2] >> 4) == 0x0;
+
+ if (mix_flash) {
+ if (chip->panel_mix) {
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+ chip->panel_mix = false;
+ chip->panel_saw_fader = false;
+ } else {
+ int ref, out;
+ int m;
+
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
+ chip->panel_mix = true;
+ /* Seed the monitoring level at the reference
+ * crosspoint's current value so the first wheel
+ * click doesn't jump from -inf (the reference =
+ * the first SELECT-chosen channel of the IN pair;
+ * Opt = the AS1/2 pair).
+ */
+ out = chip->panel_out == 3 ? 5 :
+ chip->panel_out == 2 ? 1 : 0;
+ ref = chip->panel_in == 3 ? 4 :
+ (chip->panel_in == 2 ? 2 : 0) +
+ (chip->panel_select == 1 ? 1 : 0);
+ chip->panel_mix_raw = chip->xpoint[out][ref][0];
+ /* Seed the VU display shadow at the CURRENT level
+ * (cap_panel.pcap: TotalMix writes the display value of
+ * the current fader on engage - 10 in that session -
+ * not a hard 0; cap_mix's 0 was because the fader sat
+ * at the bottom). Only the channels the wheel can move.
+ */
+ for (m = 0; m < 4; m++)
+ chip->panel_mix_disp[m] = 0;
+ if (ref < 4) {
+ int db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
+ int disp = bf_mix_display(db2);
+
+ bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp,
+ BF_REG_PANEL_GAIN + ref);
+ chip->panel_mix_disp[ref] = disp;
+ }
+ }
+ bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+ }
+ if (fader_now) {
+ chip->panel_saw_fader = true;
+ } else if (chip->panel_mix && chip->panel_saw_fader &&
+ st[3] != BF_PANEL_FLASH_MIX) {
+ /* device left fader mode by itself (IN/OUT/SET press) */
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+ chip->panel_mix = false;
+ chip->panel_saw_fader = false;
+ bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+ }
+
+ dim = !!(st[1] & 0x20);
+ if (dim != chip->panel_dim) {
+ chip->panel_dim = dim;
+ bf_panel_notify(chip, BF_PANEL_KCTL_DIM);
+ }
+
+ memcpy(chip->panel_prev, st, sizeof(st));
+}
+
+void babyface_panel_work(struct work_struct *work)
+{
+ struct snd_usb_babyface *chip = container_of(work,
+ struct snd_usb_babyface, panel_work.work);
+ unsigned int ms;
+
+ if (chip->shutdown)
+ return;
+ bf_panel_tick(chip);
+ chip->panel_poll_t = ktime_get();
+ /* Poll fast for a moment after an OUT wheel click. */
+ ms = time_before(jiffies, chip->panel_fast_until) ?
+ BF_PANEL_FAST_POLL_MS : chip->panel_poll_ms;
+ schedule_delayed_work(&chip->panel_work, msecs_to_jiffies(ms));
+}
+
+void babyface_panel_start(struct snd_usb_babyface *chip)
+{
+ chip->panel_seen = false;
+ /* The device boots with NOTHING selected (the SELECT cycle starts
+ * at none -> AN1 -> AN2 -> both -> none) - the unreadable selection
+ * must start there too, or every later SET is off by one channel
+ * (host at AN1 while the LEDs show nothing -> first SELECT makes
+ * the device blink AN1 but the host believes AN2).
+ */
+ chip->panel_select = 3; /* none */
+ chip->panel_select_armed = true;
+ chip->panel_start = jiffies;
+ chip->panel_fast_until = jiffies;
+ chip->panel_poll_t = 0;
+ /* No master holds 0xffff: the wheel starts from the masters. */
+ memset(chip->panel_master_last, 0xff, sizeof(chip->panel_master_last));
+ schedule_delayed_work(&chip->panel_work, 0);
+}
+
+void babyface_panel_stop(struct snd_usb_babyface *chip)
+{
+ cancel_delayed_work_sync(&chip->panel_work);
+}
+
+/* -- controls -------------------------- */
+
+/* The button/wheel controls hold the LATEST state and are NOT consumed
+ * on read: wireplumber subscribes to every notifying control and reads
+ * it, so a clear-on-get would let another reader eat the event. Each
+ * consumer tracks its own baseline and acts on changes (the button is a
+ * last-press code, the wheel an accumulated signed delta). VOLATILE
+ * keeps alsactl from caching them.
+ */
+static int bf_panel_button_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_PANEL_BTN_DIM;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_panel_button_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->panel_button;
+ return 0;
+}
+
+static int bf_panel_wheel_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 = SHRT_MIN;
+ uinfo->value.integer.max = SHRT_MAX;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_panel_wheel_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->panel_wheel;
+ return 0;
+}
+
+static int bf_panel_in_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_in_texts);
+}
+
+static int bf_panel_in_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->panel_in;
+ return 0;
+}
+
+static int bf_panel_out_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_out_texts);
+}
+
+static int bf_panel_out_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->panel_out;
+ return 0;
+}
+
+static int bf_panel_select_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_select_texts);
+}
+
+static int bf_panel_select_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->panel_select;
+ return 0;
+}
+
+/* Writable so software (or the user, after a driver reload) can
+ * re-sync the host-tracked SELECT state to the physical card - the
+ * L/R/both/none state is NOT in the 0x17 readback, so a reload starts
+ * at "Left" while the card may sit at any position; a desync makes
+ * SET / the wheel / MIX target the wrong channel. Writing the
+ * physical state re-aligns the emulation (TotalMix parity: it also
+ * lets software select channels directly).
+ */
+static int bf_panel_select_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ unsigned int v = ucontrol->value.enumerated.item[0];
+ int ret = 0;
+
+ if (v > 3)
+ return -EINVAL;
+ if (v != chip->panel_select) {
+ chip->panel_select = v;
+ bf_panel_notify(chip, BF_PANEL_KCTL_SELECT);
+ ret = 1;
+ }
+ return ret;
+}
+
+/* Shared boolean get - private_value selects mix (0) / dim (1). */
+static int bf_panel_bool_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] =
+ kctl->private_value ? chip->panel_dim : chip->panel_mix;
+ return 0;
+}
+
+int babyface_create_panel(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int err;
+
+ memset(chip->panel_kctl, 0, sizeof(chip->panel_kctl));
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Button",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = bf_panel_button_info,
+ .get = bf_panel_button_get,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_BUTTON] = kctl;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Wheel",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = bf_panel_wheel_info,
+ .get = bf_panel_wheel_get,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_WHEEL] = kctl;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel In",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = bf_panel_in_info,
+ .get = bf_panel_in_get,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_IN] = kctl;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Out",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = bf_panel_out_info,
+ .get = bf_panel_out_get,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_OUT] = kctl;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Mix",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = snd_ctl_boolean_mono_info,
+ .get = bf_panel_bool_get,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_MIX] = kctl;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Dim",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = snd_ctl_boolean_mono_info,
+ .get = bf_panel_bool_get,
+ .private_value = 1,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_DIM] = kctl;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Select",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_WRITE |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = bf_panel_select_info,
+ .get = bf_panel_select_get,
+ .put = bf_panel_select_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_kctl[BF_PANEL_KCTL_SELECT] = kctl;
+
+ return 0;
+}
+
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index ccf3dbf36..8cf68e404 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -1272,6 +1272,7 @@ static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT;
static int nurbs = BF_NURBS_DEFAULT;
+static int panel_poll_ms = BF_PANEL_POLL_MS_DEFAULT;
module_param_array(index, int, NULL, 0444);
MODULE_PARM_DESC(index, "Index value for the Babyface Pro sound card.");
@@ -1281,6 +1282,8 @@ module_param(frames_per_urb, int, 0644);
MODULE_PARM_DESC(frames_per_urb, "Audio frames per URB, 8..1024 (16 = low-latency floor, 256 = default).");
module_param(nurbs, int, 0644);
MODULE_PARM_DESC(nurbs, "URBs in flight per direction, 1..16 (16 = low-latency).");
+module_param(panel_poll_ms, int, 0644);
+MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz).");
/* -- USB driver ------------------------- */
@@ -1366,10 +1369,12 @@ static int babyface_probe(struct usb_interface *intf,
chip->alt = BF_ALT_1;
chip->frame_bytes = 56;
chip->preamp = BF_PREAMP_BASE;
+ chip->panel_poll_ms = clamp(panel_poll_ms, 10, 1000);
mutex_init(&chip->mutex);
spin_lock_init(&chip->lock);
atomic_set(&chip->urb_err, 0);
INIT_WORK(&chip->stream_work, babyface_stream_work);
+ INIT_DELAYED_WORK(&chip->panel_work, babyface_panel_work);
chip->card->private_free = babyface_private_free;
/* Model-neutral on purpose. The FS and the original (2015)
@@ -1524,12 +1529,23 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ err = babyface_create_panel(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "front-panel 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);
goto error;
}
+ /* The panel poll mirrors the physical buttons/wheel into the
+ * Front Panel controls; it runs for the whole card lifetime.
+ */
+ babyface_panel_start(chip);
+
usb_set_intfdata(intf, chip);
dev_info(&intf->dev,
"Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n",
@@ -1569,6 +1585,7 @@ static void babyface_disconnect(struct usb_interface *intf)
chip->shutdown = true;
cancel_work_sync(&chip->stream_work);
+ babyface_panel_stop(chip);
/* Balance the probe()-time usb_disable_autosuspend(): the usb_device
* outlives this interface claim (a usbfs detach re-probes without
* the physical device ever disconnecting), so leaving autosuspend
@@ -1605,6 +1622,7 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
snd_pcm_suspend_all(sdev->device_data);
}
cancel_work_sync(&chip->stream_work);
+ babyface_panel_stop(chip);
mutex_lock(&chip->mutex);
if (chip->streaming)
babyface_stream_kill(chip);
@@ -1635,6 +1653,8 @@ static int babyface_resume(struct usb_interface *intf)
err = babyface_restore_state(chip);
out:
mutex_unlock(&chip->mutex);
+ if (!err)
+ babyface_panel_start(chip);
return err;
}
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 9822db312..ba1225de6 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -45,6 +45,7 @@
* bf_clock_write / bf_pitch_write.
*/
+#include <linux/ktime.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/unaligned.h>
@@ -182,6 +183,53 @@
*/
#define BF_LOOPBACK_CHANNELS 30
+/* Front-panel poll interval default - Windows polls the 5-register
+ * status set at ~50 cycles/s (20 ms); match that. Tunable via the
+ * panel_poll_ms module param for reviewers/distros who want a slower
+ * (or faster) rate than the Windows-matching default.
+ */
+#define BF_PANEL_POLL_MS_DEFAULT 20
+
+/* Front-panel readback (babyfacepro-ctl.c): 0x17 read at wIdx 0x0000 - the index
+ * the Windows driver polls (cap_buttons2.pcap). byte0 = preamp 48V/PAD,
+ * byte1 = OUT sel + DIM/MIX bits, byte2 = IN sel + wheel counter,
+ * byte3 = button flash (see babyfacepro-ctl.c for the full layout).
+ */
+#define BF_REG_PANEL_READ 0x0000
+#define BF_PANEL_IN_SHIFT 4
+#define BF_PANEL_IN_CH12 0x04
+#define BF_PANEL_IN_CH34 0x05
+#define BF_PANEL_IN_OPT 0x06
+/* OUT selection - the gain-display-mode encoding (cap_dim.pcap);
+ * babyfacepro-ctl.c also accepts the base-mode 0x01/0x02 (cap_buttons.pcap).
+ */
+#define BF_PANEL_OUT_CH12 0x04
+#define BF_PANEL_OUT_PHONES 0x05
+#define BF_PANEL_OUT_OPT 0x06
+#define BF_PANEL_FLASH_IN 0x41
+#define BF_PANEL_FLASH_SET 0x42
+#define BF_PANEL_FLASH_MIX 0x44
+#define BF_PANEL_FLASH_OUT 0x48
+#define BF_PANEL_FLASH_SELECT 0x50
+#define BF_PANEL_FLASH_DIM 0x60
+#define BF_PANEL_BTN_NONE 0
+#define BF_PANEL_BTN_IN 1
+#define BF_PANEL_BTN_SET 2
+#define BF_PANEL_BTN_MIX 3
+#define BF_PANEL_BTN_OUT 4
+#define BF_PANEL_BTN_SELECT 5
+#define BF_PANEL_BTN_DIM 6
+
+/* The front-panel gain/display family (0x1A, wIdx 0x000A + mic 0-3;
+ * cap_panel/cap_mix.pcap): in gain mode the wheel writes the "ADC
+ * gain" here (drives the same preamp as the GUI 0x0000+mic); in MIX
+ * (fader) mode the same registers carry the VU DISPLAY shadow -
+ * TotalMix writes the monitoring level display value (0..~31) and the
+ * card lights the input VU segments accordingly (hardware-verified
+ * 2026-08-26 live: sweeping 0x1A values moved the input VU).
+ */
+#define BF_REG_PANEL_GAIN 0x000a
+
/* The "cross" register block within each output: the L-registers sit at
* odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
* source pairs that can be cross-linked). bf_crosspoint_clear_cross()
@@ -309,6 +357,54 @@ struct snd_usb_babyface {
u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */
bool dim; /* DIM engaged (fixed -20 dB on Phones) */
struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */
+
+ /* front panel (babyfacepro-ctl.c) - 0x17 readback poll */
+ struct delayed_work panel_work;
+ unsigned int panel_poll_ms; /* front-panel poll interval, module param */
+ u8 panel_prev[4]; /* last 0x17 snapshot */
+ bool panel_seen; /* first snapshot taken */
+ bool panel_select_armed; /* device SELECT cycle armed (IN switch disarms) */
+ unsigned long panel_start; /* jiffies at panel_start (boot re-assert) */
+ int panel_button; /* latched button event (consumed on get) */
+ int panel_wheel; /* accumulated wheel delta (consumed on get) */
+ int panel_in; /* enum: 0 unknown, 1 Ch1/2, 2 Ch3/4, 3 Opt */
+ int panel_out; /* enum: 0 unknown, 1 Ch1/2, 2 Phones, 3 Opt */
+ bool panel_mix; /* MIX engaged - HOST-latched (like TotalMix):
+ * set by the 0x44 flash ack, NOT by the readback
+ * 0x80 bit (the raw press has none)
+ */
+ bool panel_dim; /* DIM sticky (byte1 bit 0x20) */
+ bool panel_saw_fader; /* device observed in fader mode (byte2 0x0x)
+ * - gates the device-driven MIX exit
+ */
+ int panel_select; /* SELECT state: 0 L, 1 R, 2 both, 3 none
+ * (host-tracked - not in the readback)
+ */
+ int panel_sel_hold; /* consecutive ticks with byte3 = 0x50
+ * (SELECT held > 200 ms = the OUT-balance
+ * gesture; a tap flashes only ~100-150 ms,
+ * selhold_probe2 - no engaged bit)
+ */
+ /* The OUT wheel's own level per side, in half-dB, valid while the
+ * master still holds the value the wheel last wrote (the 16-bit
+ * value can't hold every half-dB step, nor the device's range
+ * below -64 dB, nor its mute). 0xffff = nothing written yet.
+ */
+ u16 panel_master_last[6][2];
+ s16 panel_out_hdb[6][2];
+ /* OUT wheel acceleration and resync at rest (babyfacepro-ctl.c) */
+ ktime_t panel_out_wheel_t; /* estimated time of the last count */
+ ktime_t panel_poll_t; /* when the previous poll ran */
+ unsigned long panel_fast_until; /* jiffies: poll fast until then */
+ int panel_out_wheel_dir; /* its direction, +1 or -1 */
+ int panel_out_wheel_out; /* the output it moved */
+ bool panel_out_resync; /* 8-bit write due once at rest */
+ u16 panel_mix_raw; /* MIX-mode monitoring level (fader raw) */
+ u8 panel_mix_disp[4]; /* MIX-mode VU display shadow per mic
+ * (0x1A 0x000A+mic - written on change
+ * so the input VU follows the wheel)
+ */
+ struct snd_kcontrol *panel_kctl[7]; /* for snd_ctl_notify */
};
/* The mixer state cached across interface re-probes/resume (see
@@ -394,6 +490,10 @@ u8 bf_gain_raw(int mic, int db);
int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
void bf_panel_toggle_dim(struct snd_usb_babyface *chip);
int babyface_create_flags(struct snd_usb_babyface *chip);
+int babyface_create_panel(struct snd_usb_babyface *chip);
+void babyface_panel_start(struct snd_usb_babyface *chip);
+void babyface_panel_stop(struct snd_usb_babyface *chip);
+void babyface_panel_work(struct work_struct *work);
/* Master gain-law helpers - shared with the front-panel wheels once
* the front panel lands.
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (5 preceding siblings ...)
2026-09-18 11:39 ` [RFC PATCH v5 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
7 siblings, 0 replies; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Adds the 4-strip (AN1-4) 3-band + low-cut parametric EQ: the CORDIC/
fixed-point coefficient math, the 64-byte bulk-upload block format,
and the 15 controls per strip. hw_params() now re-uploads on a rate
change, since the coefficients depend on fs.
The three checkpatch CHECK nits this file gets (BIT() vs (1 << 27),
'ang' x2) are the same known false positives already documented for
this code: BIT() returns unsigned long, which is wrong for this
file's signed Q27 math, and 'ang' is a real CORDIC angle variable.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 575 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 8 +
sound/usb/babyfacepro/babyfacepro.h | 19 +
3 files changed, 602 insertions(+)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index eac97c895..fce1233cc 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -3198,3 +3198,578 @@ int babyface_create_panel(struct snd_usb_babyface *chip)
return 0;
}
+#define BF_EQ_Q27 (1 << 27)
+#define BF_EQ_LC_OFF 0x04000000
+#define BF_EQ_BLOCK_LEN 64
+
+/* atan(2^-i) x 2^27 (CORDIC). */
+static const s64 bf_atan_tab[28] = {
+ 0x6487ED5, 0x3B58CE1, 0x1F5B760, 0xFEADD5,
+ 0x7FD56F, 0x3FFAAB, 0x1FFF55, 0xFFFEB,
+ 0x7FFFD, 0x40000, 0x20000, 0x10000,
+ 0x8000, 0x4000, 0x2000, 0x1000,
+ 0x800, 0x400, 0x200, 0x100,
+ 0x80, 0x40, 0x20, 0x10,
+ 0x8, 0x4, 0x2, 0x1,
+};
+
+/* ---- fixed-point helpers (Q27 in/out, s64 intermediates) ---- */
+
+/* sin/cos of an angle in [0, pi/2] (Q27). Simultaneous CORDIC, 28
+ * iterations (~1e-8 residual). eq_selftest.c verifies the whole
+ * pipeline against the double-precision reference.
+ */
+static void bf_sincos(s64 ang, s64 *sn, s64 *cs)
+{
+ s64 x = 0x4DBA76D; /* 1/1.64676 x 2^27 (CORDIC gain) */
+ s64 y = 0;
+ s64 z = ang;
+ int i;
+
+ for (i = 0; i < 28; i++) {
+ s64 d = z >= 0 ? 1 : -1;
+ s64 nx = x - d * (y >> i);
+ s64 ny = y + d * (x >> i);
+
+ x = nx;
+ y = ny;
+ z -= d * bf_atan_tab[i];
+ }
+ *cs = x;
+ *sn = y;
+}
+
+/* 2^u for u in Q27, u in [-2, 2] (gain-amplitude range). */
+static s64 bf_exp2(s64 u)
+{
+ s64 n = u >> 27;
+ s64 r = u - (n << 27);
+ s64 rl = (r * 0x58B90C0 + (1 << 26)) >> 27; /* r.ln2 */
+ s64 e = BF_EQ_Q27;
+ s64 term = BF_EQ_Q27;
+ int k;
+
+ for (k = 1; k <= 10; k++) {
+ term = div_s64((term * rl + (1 << 26)) >> 27, k);
+ e += term;
+ }
+ return n >= 0 ? e << n : e >> -n;
+}
+
+/* The 5 stored words (c0..c3 + shared c4) for one band.
+ * type: 1 bell, 2 low shelf, 3 high shelf. freq_hz, fs in Hz;
+ * q100 = Q x 100; gain_x10 = dB x 10. fs is the stream rate.
+ */
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+ s32 gain_x10, s32 fs)
+{
+ s64 f = freq_hz;
+ s64 w0, c, s, alpha, A, sq;
+ s64 b0, b1, b2, a0, a1, a2;
+ s64 pi = 0x1921FB54; /* pi, Q27 */
+ s64 hpi = 0xC90FDAA; /* pi/2, Q27 */
+ s64 t;
+ int both = 0, cflip = 0;
+
+ if (gain_x10 == 0 || q100 <= 0) {
+ /* Inactive band: identity words (also guards the alpha
+ * division below against the default Q=0 the controls start
+ * with - a user setting gain before Q used to hit a kernel
+ * divide-by-zero oops).
+ */
+ w[0] = 0;
+ w[1] = 0;
+ w[2] = 0;
+ w[3] = 0;
+ return;
+ }
+
+ /* w0 = 2.pi.f/fs (Q27), reduced to [0, pi/2]. */
+ w0 = div_s64(f * BF_EQ_Q27, fs);
+ w0 = (w0 * 0x3243F6A9) >> 27; /* x 2.pi */
+ t = w0;
+ if (t > pi) {
+ t -= pi;
+ both = 1;
+ }
+ if (t > hpi) {
+ t = pi - t;
+ cflip = 1;
+ }
+ bf_sincos(t, &s, &c);
+ if (both) {
+ s = -s;
+ c = -c;
+ }
+ if (cflip)
+ c = -c;
+
+ alpha = div64_s64(s * 100 + q100, 2 * (s64)q100); /* sin(w0)/(2Q) */
+ /* A = 10^(g/40), sqrt(A): g = gain_x10/10 dB */
+ A = bf_exp2((s64)gain_x10 * 0x11021E);
+ sq = bf_exp2((s64)gain_x10 * 0x8810F);
+
+ if (type == 1) {
+ s64 ta = (alpha * A + (1 << 26)) >> 27;
+
+ b0 = BF_EQ_Q27 + ta;
+ b1 = -2 * c;
+ b2 = BF_EQ_Q27 - ta;
+ a0 = BF_EQ_Q27 + div64_s64(alpha * BF_EQ_Q27 + A / 2, A);
+ a1 = -2 * c;
+ a2 = BF_EQ_Q27 - div64_s64(alpha * BF_EQ_Q27 + A / 2, A);
+ } else {
+ s64 ap1 = A + BF_EQ_Q27;
+ s64 am1 = A - BF_EQ_Q27;
+ s64 cp0 = (am1 * c + (1 << 26)) >> 27; /* (A-1).c */
+ s64 cp1 = (ap1 * c + (1 << 26)) >> 27; /* (A+1).c */
+ s64 ab = (2 * sq * alpha + (1 << 26)) >> 27;
+
+ if (type == 2) { /* low shelf */
+ b0 = (A * (ap1 - cp0 + ab) + (1 << 26)) >> 27;
+ b1 = (2 * A * (am1 - cp1) + (1 << 26)) >> 27;
+ b2 = (A * (ap1 - cp0 - ab) + (1 << 26)) >> 27;
+ a0 = ap1 + cp0 + ab;
+ a1 = -2 * (am1 + cp1);
+ a2 = ap1 + cp0 - ab;
+ } else { /* high shelf */
+ b0 = (A * (ap1 + cp0 + ab) + (1 << 26)) >> 27;
+ b1 = (-2 * A * (am1 + cp1) + (1 << 26)) >> 27;
+ b2 = (A * (ap1 + cp0 - ab) + (1 << 26)) >> 27;
+ a0 = ap1 - cp0 + ab;
+ a1 = -2 * (am1 - cp1);
+ a2 = ap1 - cp0 - ab;
+ }
+ }
+
+ w[0] = (s32)div64_s64(a1 * BF_EQ_Q27 + a0 / 2, a0);
+ w[1] = (s32)div64_s64(a2 * BF_EQ_Q27 + a0 / 2, a0);
+ w[2] = (s32)div64_s64(b1 * BF_EQ_Q27 + b0 / 2, b0);
+ w[3] = (s32)div64_s64(b2 * BF_EQ_Q27 + b0 / 2, b0);
+ w[4] = (s32)div64_s64(b0 * BF_EQ_Q27 + a0 / 2, a0);
+}
+
+/* ---- low cut ---- */
+
+/* Slope byte: 2^n-1 (n poles) -> 6/12/18/24 dB per oct; 0 = off. */
+static u8 bf_eq_lc_slope_byte(s32 slope_db)
+{
+ switch (slope_db) {
+ case 6: return 0x01;
+ case 12: return 0x03;
+ case 18: return 0x07;
+ case 24: return 0x0F;
+ }
+ return 0;
+}
+
+/* The 0x38 low-cut frequency word: round(K.f'.(11656)/(11656+f')) with
+ * K = 11508, f' = f x slope-compensation factor (cap_eq9 fit, 0.003%;
+ * the slope factor keeps the composite -3 dB point constant).
+ */
+static u32 bf_eq_lc_freq_raw(s32 freq_hz, s32 slope_db)
+{
+ s64 f, word;
+
+ if (freq_hz <= 0)
+ return BF_EQ_LC_OFF;
+ f = freq_hz;
+ switch (slope_db) {
+ case 6:
+ f = f * 15267 / 10000;
+ break;
+
+ case 18:
+ f = f * 8061 / 10000;
+ break;
+
+ case 24:
+ f = f * 6977 / 10000;
+ break;
+ }
+ word = (11508 * f * 11656 + (11656 + f) / 2) / (11656 + f);
+ return (u32)word;
+}
+
+/* ---- block build + bulk write ---- */
+
+static void bf_eq_build_block(u8 *b, int ch, u8 slope,
+ const s32 bands[3][4], s32 shared, u32 lc)
+{
+ int slot, k;
+
+ memset(b, 0, BF_EQ_BLOCK_LEN);
+ b[0] = ch;
+ b[1] = slope;
+ b[2] = ch;
+ b[3] = 0x80; /* EQ engine active */
+ for (slot = 0; slot < 3; slot++) {
+ for (k = 0; k < 4; k++) {
+ put_unaligned_le32((u32)bands[slot][k],
+ b + 0x04 + slot * 0x10 + 4 * k);
+ }
+ }
+ put_unaligned_le32((u32)shared, b + 0x34);
+ put_unaligned_le32(lc, b + 0x38);
+}
+
+/* Upload one 64-byte block on bulk OUT ep 0x0A (interface 1). */
+static int bf_eq_upload(struct snd_usb_babyface *chip, const u8 *block)
+{
+ u8 *buf;
+ int ret, len;
+
+ /* usb_bulk_msg DMA-maps the buffer: it must not be on the stack
+ * (usb_hcd_map_urb_for_dma returns -EAGAIN for stack buffers).
+ */
+ buf = kmemdup(block, BF_EQ_BLOCK_LEN, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ ret = usb_bulk_msg(chip->dev, usb_sndbulkpipe(chip->dev, 0x0a),
+ buf, BF_EQ_BLOCK_LEN, &len, 1000);
+ kfree(buf);
+ if (ret < 0)
+ dev_err(&chip->dev->dev, "EQ bulk upload failed: %d\n", ret);
+ return ret;
+}
+
+/* Write the L+R block pair for one strip (channel base = strip x 2). */
+static int bf_eq_write_strip(struct snd_usb_babyface *chip, int strip)
+{
+ struct bf_eq_channel *e = &chip->eq[strip];
+ u8 b[BF_EQ_BLOCK_LEN];
+ s32 identity[3][4] = { { 0 }, { 0 }, { 0 } };
+ s32 shared = e->on ? e->shared : BF_EQ_Q27;
+ u32 lc = e->on ? e->lc_raw : BF_EQ_LC_OFF;
+ /* The header slope byte (b[1]) is only valid while the low cut is
+ * engaged: a stale slope with 0x38 = off made the device apply a
+ * garbage-frequency cut (ear-verified: "low cut off" left only
+ * highs). cap_eq7: byte1 = 0x00 + 0x38 = 0x04000000 when off.
+ */
+ u8 slope = (e->on && e->lc_hz > 0) ? e->slope : 0;
+ int ch, ret;
+
+ for (ch = 0; ch < 2; ch++) {
+ bf_eq_build_block(b, strip * 2 + ch, slope,
+ e->on ? e->words : identity, shared, lc);
+ ret = bf_eq_upload(chip, b);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+/* Recompute one strip's words + low cut from its params, re-upload.
+ * Lock-free by convention: every caller must already hold chip->mutex
+ * (bf_eq_put() and bf_eq_reupload() do) - asserting it here catches a
+ * future caller that forgets, instead of a silent self-deadlock.
+ */
+static void bf_eq_update_strip(struct snd_usb_babyface *chip, int strip)
+{
+ struct bf_eq_channel *e = &chip->eq[strip];
+ s32 fs = chip->rate ? chip->rate : 48000;
+ s32 last_c4 = BF_EQ_Q27;
+ int band, i;
+
+ lockdep_assert_held(&chip->mutex);
+
+ for (band = 0; band < 3; band++) {
+ s32 w[5];
+
+ bf_eq_band_words(w, e->band_type[band], e->band_freq[band],
+ e->band_q[band], e->band_gain[band], fs);
+ for (i = 0; i < 4; i++)
+ e->words[band][i] = w[i];
+ if (e->band_type[band] && e->band_gain[band])
+ last_c4 = w[4]; /* shared scale: the last band */
+ }
+ e->shared = last_c4;
+ e->lc_raw = bf_eq_lc_freq_raw(e->lc_hz, e->slope_db);
+ e->slope = bf_eq_lc_slope_byte(e->slope_db);
+ bf_eq_write_strip(chip, strip);
+}
+
+/* Recompute + re-upload all four strips (rate change). Caller must
+ * hold chip->mutex - bf_eq_update_strip()/bf_eq_write_strip() are
+ * lock-free by convention (see bf_eq_put()) and the only caller,
+ * babyface_pcm_hw_params(), already holds the lock across the rate
+ * change; locking here too self-deadlocked it (hung-task: "blocked
+ * on a mutex likely owned by" itself, hit via regress.sh's rate
+ * sweep).
+ */
+void bf_eq_reupload(struct snd_usb_babyface *chip)
+{
+ int strip;
+
+ for (strip = 0; strip < 4; strip++)
+ bf_eq_update_strip(chip, strip);
+}
+
+/* ---- ALSA controls (4 strips x 19 controls) ---- */
+
+#define EQ_STRIP(pv) ((pv) >> 8)
+#define EQ_PARAM(pv) ((pv) & 0xff)
+/* params: 0 enable, 1-3 type, 4-6 freq, 7-9 q, 10-12 gain, 13 lc freq, 14 lc slope */
+
+static const char *const bf_eq_type_texts[] = {
+ "Off", "Bell", "Low Shelf", "High Shelf", NULL
+};
+
+static const char *const bf_eq_slope_texts[] = {
+ "6 dB/oct", "12 dB/oct", "18 dB/oct", "24 dB/oct", NULL
+};
+
+static int bf_eq_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ int param = EQ_PARAM(kctl->private_value);
+
+ if (param == 0) {
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ return 0;
+ }
+ if (param == 1 || param == 2 || param == 3)
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_type_texts);
+ if (param == 14)
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_slope_texts);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = (param == 10 || param == 11 ||
+ param == 12) ? -240 :
+ (param == 7 || param == 8 ||
+ param == 9) ? 5 : 0;
+ uinfo->value.integer.max = (param == 7 || param == 8 ||
+ param == 9) ? 1000 :
+ (param == 10 || param == 11 ||
+ param == 12) ? 240 : 20000;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_eq_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ struct bf_eq_channel *e = &chip->eq[EQ_STRIP(kctl->private_value)];
+ int param = EQ_PARAM(kctl->private_value);
+ int band = (param - 1) % 3;
+ s32 *v = NULL;
+
+ switch (param) {
+ case 0:
+ break;
+
+ case 1:
+ case 2:
+ case 3:
+ v = &e->band_type[band];
+ break;
+
+ case 4:
+ case 5:
+ case 6:
+ v = &e->band_freq[band];
+ break;
+
+ case 7:
+ case 8:
+ case 9:
+ v = &e->band_q[band];
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ v = &e->band_gain[band];
+ break;
+
+ case 13:
+ v = &e->lc_hz;
+ break;
+
+ case 14:
+ v = &e->slope_db;
+ break;
+ }
+ if (param == 0) {
+ ucontrol->value.integer.value[0] = e->on;
+ } else if (param == 14) {
+ /* Inverse of put's index->dB map: slope_db stores the raw
+ * 6/12/18/24 dB/oct value, but an ENUMERATED control's .get
+ * must return the enum item index (0-3), same as .put
+ * receives - returning the raw dB value here (the bug this
+ * replaces) fed back an out-of-range index to every ALSA
+ * consumer (confirmed via amixer: writing index 1 read back
+ * as value 12, not 1).
+ */
+ s32 slope = v ? *v : 6;
+
+ ucontrol->value.enumerated.item[0] =
+ slope >= 24 ? 3 : slope >= 18 ? 2 : slope >= 12 ? 1 : 0;
+ } else if (param == 1 || param == 2 || param == 3) {
+ /* ENUMERATED band type: use the enumerated union member. */
+ ucontrol->value.enumerated.item[0] = v ? *v : 0;
+ } else {
+ ucontrol->value.integer.value[0] = v ? *v : 0;
+ }
+ return 0;
+}
+
+static int bf_eq_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int strip = EQ_STRIP(kctl->private_value);
+ int param = EQ_PARAM(kctl->private_value);
+ struct bf_eq_channel *e = &chip->eq[strip];
+ int band = (param - 1) % 3;
+ s32 nv;
+ s32 *v = NULL;
+ int ret = 0;
+
+ /* Read from the union member matching the control type: ENUMERATED
+ * params (band type 1-3, slope 14) use .enumerated.item, everything
+ * else (BOOL 0, INTEGER) uses .integer.value.
+ */
+ if (param == 1 || param == 2 || param == 3 || param == 14)
+ nv = (s32)ucontrol->value.enumerated.item[0];
+ else
+ nv = (s32)ucontrol->value.integer.value[0];
+
+ /* Validate against the bounds bf_eq_info() declares. The ALSA core
+ * only checks these when CONFIG_SND_CTL_INPUT_VALIDATION is set, so
+ * an out-of-range value here could otherwise reach the Q27
+ * coefficient math (bf_eq_band_words/bf_exp2) and shift by >= width
+ * (undefined behaviour).
+ */
+ switch (param) {
+ case 0:
+ if (nv < 0 || nv > 1)
+ return -EINVAL;
+ break;
+ case 1:
+ case 2:
+ case 3:
+ case 14:
+ if (nv < 0 || nv > 3)
+ return -EINVAL;
+ break;
+ case 4:
+ case 5:
+ case 6:
+ case 13:
+ if (nv < 0 || nv > 20000)
+ return -EINVAL;
+ break;
+ case 7:
+ case 8:
+ case 9:
+ if (nv < 5 || nv > 1000)
+ return -EINVAL;
+ break;
+ case 10:
+ case 11:
+ case 12:
+ if (nv < -240 || nv > 240)
+ return -EINVAL;
+ break;
+ }
+
+ switch (param) {
+ case 0:
+ v = NULL;
+ break;
+
+ case 1:
+ case 2:
+ case 3:
+ v = &e->band_type[band];
+ break;
+
+ case 4:
+ case 5:
+ case 6:
+ v = &e->band_freq[band];
+ break;
+
+ case 7:
+ case 8:
+ case 9:
+ v = &e->band_q[band];
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ v = &e->band_gain[band];
+ break;
+
+ case 13:
+ v = &e->lc_hz;
+ break;
+
+ case 14:
+ v = &e->slope_db;
+ break;
+ }
+ if (param == 14) /* slope enum items are 6/12/18/24 */
+ nv = nv == 0 ? 6 : nv == 1 ? 12 : nv == 2 ? 18 : 24;
+
+ mutex_lock(&chip->mutex);
+ if (param == 0) {
+ if (e->on != !!nv) {
+ e->on = !!nv;
+ bf_eq_update_strip(chip, strip);
+ ret = 1;
+ }
+ } else if (v && *v != nv) {
+ *v = nv;
+ bf_eq_update_strip(chip, strip);
+ ret = 1;
+ }
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+int babyface_create_eq(struct snd_usb_babyface *chip)
+{
+ static const char *const names[4] = { "AN1", "AN2", "AN3", "AN4" };
+ static const char *const params[] = {
+ "EQ Enable",
+ "EQ Band 1 Type", "EQ Band 2 Type", "EQ Band 3 Type",
+ "EQ Band 1 Freq", "EQ Band 2 Freq", "EQ Band 3 Freq",
+ "EQ Band 1 Q", "EQ Band 2 Q", "EQ Band 3 Q",
+ "EQ Band 1 Gain", "EQ Band 2 Gain", "EQ Band 3 Gain",
+ "EQ Low Cut Freq", "EQ Low Cut Slope",
+ };
+ int strip, i, err;
+
+ for (strip = 0; strip < 4; strip++) {
+ for (i = 0; i < 15; i++) {
+ struct snd_kcontrol *kctl;
+ char name[64];
+
+ snprintf(name, sizeof(name), "%s %s", names[strip],
+ params[i]);
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "EQ",
+ .index = 0,
+ .info = bf_eq_info,
+ .get = bf_eq_get,
+ .put = bf_eq_put,
+ .private_value = (strip << 8) | i,
+ }, chip);
+ if (!kctl)
+ return -ENOMEM;
+ strscpy(kctl->id.name, name, sizeof(kctl->id.name));
+ 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 8cf68e404..2e406fb90 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -1188,6 +1188,8 @@ static int babyface_pcm_hw_params(struct snd_pcm_substream *subs,
chip->rate = r->rate;
chip->alt = r->alt;
chip->frame_bytes = r->frame_bytes;
+ /* The DSP EQ coefficients depend on fs: re-upload. */
+ bf_eq_reupload(chip);
/* Publish the new geometry before restarting either stream. */
schedule_work(&chip->stream_work);
dev_dbg(&chip->dev->dev, "rate %u Hz (alt %u)\n",
@@ -1535,6 +1537,12 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ err = babyface_create_eq(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "EQ 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 ba1225de6..cfb20aa7b 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -405,6 +405,21 @@ struct snd_usb_babyface {
* so the input VU follows the wheel)
*/
struct snd_kcontrol *panel_kctl[7]; /* for snd_ctl_notify */
+
+ /* DSP EQ (babyfacepro-ctl.c) - 4 analog-input strips, params kept in state */
+ struct bf_eq_channel {
+ bool on; /* EQ engaged (else identity blocks) */
+ s32 slope_db; /* low-cut slope 6/12/18/24 (0 = off) */
+ s32 lc_hz; /* low-cut freq, 0 = off */
+ u32 lc_raw; /* cached 0x38 word */
+ u8 slope; /* cached slope byte (2^n - 1) */
+ s32 band_type[3]; /* 0 off, 1 bell, 2 low shelf, 3 high shelf */
+ s32 band_freq[3]; /* Hz */
+ s32 band_q[3]; /* Q x 100 */
+ s32 band_gain[3]; /* dB x 10 */
+ s32 words[3][4]; /* cached c0..c3 */
+ s32 shared; /* cached c4 (shared by the slots) */
+ } eq[4];
};
/* The mixer state cached across interface re-probes/resume (see
@@ -494,6 +509,10 @@ int babyface_create_panel(struct snd_usb_babyface *chip);
void babyface_panel_start(struct snd_usb_babyface *chip);
void babyface_panel_stop(struct snd_usb_babyface *chip);
void babyface_panel_work(struct work_struct *work);
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+ s32 gain_x10, s32 fs);
+void bf_eq_reupload(struct snd_usb_babyface *chip);
+int babyface_create_eq(struct snd_usb_babyface *chip);
/* Master gain-law helpers - shared with the front-panel wheels once
* the front panel lands.
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v5 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (6 preceding siblings ...)
2026-09-18 11:39 ` [RFC PATCH v5 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
@ 2026-09-18 11:39 ` Ismaïl Bahloul
2026-09-18 18:33 ` Randy Dunlap
7 siblings, 1 reply; 10+ messages in thread
From: Ismaïl Bahloul @ 2026-09-18 11:39 UTC (permalink / raw)
To: linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
A big-picture companion to the patch series, for human reviewers:
why the proprietary mode needs a standalone driver instead of a
snd-usb-audio quirk, the vendor protocol's write-only/no-readback
shape and what that forces onto the driver, the asynchronous stream
model, why the mixer state has to be cached and replayed, and why
the driver has to emulate TotalMix's own role for the front panel.
Requested by Takashi Iwai's v4 review: documentation for reviewers,
not restating what the code comments already say inline.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
Documentation/sound/cards/babyface-pro.rst | 163 +++++++++++++++++++++
Documentation/sound/cards/index.rst | 1 +
2 files changed, 164 insertions(+)
create mode 100644 Documentation/sound/cards/babyface-pro.rst
diff --git a/Documentation/sound/cards/babyface-pro.rst b/Documentation/sound/cards/babyface-pro.rst
new file mode 100644
index 000000000..d67f8c6fe
--- /dev/null
+++ b/Documentation/sound/cards/babyface-pro.rst
@@ -0,0 +1,163 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=================================================
+RME Babyface Pro / Pro FS (snd-usb-babyface-pro)
+=================================================
+
+This document describes the design of the ``snd-usb-babyface-pro``
+driver for reviewers who need the big picture before reading the
+patches - what problem the driver solves, why it is a standalone
+driver instead of a snd-usb-audio quirk, and the four design
+decisions (stream model, protocol shape, mixer-state persistence,
+front-panel emulation) that shape most of the code. The patch series
+itself is split by feature (core+PCM, then masters+crosspoint,
+preamp, routing flags, suspend/resume, front panel, DSP EQ, in that
+order) so each patch can be read and built on its own; this document
+does not repeat what each patch's own commit message already covers.
+
+Two USB personalities, one device
+==================================
+
+The RME Babyface Pro and Babyface Pro FS present two different USB
+configurations depending on a physical/firmware switch: a
+class-compliant one, already handled by ``snd-usb-audio``, and a
+proprietary one (USB ID ``2a39:3fc0``) that this driver covers. The
+two hardware models share the same USB ID, ``bcdDevice`` and
+``iProduct`` string shape; nothing in the descriptors tells them
+apart, and the driver runs unmodified on both.
+
+In proprietary mode, interface 5 carries the PCM stream on two
+INTERRUPT endpoints (``0x01`` OUT, ``0x82`` IN) instead of the
+isochronous endpoints the USB Audio Class specifies. Isochronous
+transfers are rejected there with ``-EINVAL``. ``snd-usb-audio`` has
+no interrupt-PCM transport, so this mode cannot be a quirk on top of
+it; the driver is standalone, modelled on ``snd-usb-caiaq`` (another
+interrupt-streaming RME/NI-style device).
+
+Why interrupt endpoints and not isochronous is a hardware/firmware
+choice on RME's side, not something this driver can change - the
+class-compliant mode already exists on the same device for users who
+want a fully standard, quirk-free path with a subset of the
+functionality (no mixer, no front panel). This driver is for users
+who want the full mixer, routing matrix, and hardware DSP EQ that
+only the proprietary mode exposes.
+
+The vendor protocol: writes only, no readback
+==============================================
+
+Every mixer and clock function is one of a handful of USB vendor
+control requests (``bmRequestType 0x40``, i.e. host-to-device,
+vendor, device-recipient), each identified by its request number and
+a 16-bit value/index pair - there is no larger command structure.
+The commonly used ones are:
+
+====== ========================================
+0x10 settings word / stream start trigger
+0x12 16-bit crosspoint and output-master writes
+0x16 cold-init register clear
+0x17 front-panel + preamp state (read and write)
+0x1a 8-bit gain / output-master companion writes
+0x1b clock DDS quads (base rate and varispeed)
+====== ========================================
+
+The full register map, decoded from Windows USB captures and
+cross-checked against hardware, lives in the driver's own development
+repository (not shipped in-tree) - the constants and the comments
+next to each vendor write in the source are the authoritative
+in-tree reference.
+
+The one property that shapes the rest of the driver: **almost nothing
+here is readable back**. The 0x17 request returns the front-panel
+and preamp state, but the crosspoint matrix, the output masters, the
+routing flags and the clock all have to be tracked host-side - the
+device will accept a write blindly and never confirm what it actually
+holds. Two consequences follow directly from this:
+
+* The ``struct snd_usb_babyface`` device state (see
+ ``babyface.h``) is not a cache in the usual sense of "avoid a
+ slow read" - it is the *only* record of what the hardware should
+ currently hold. Every mixer control's ``.get`` callback reads this
+ state directly; none of them ever talks to the device.
+
+* A full reset of the device's registers - which happens on every
+ cold init - has to be followed by replaying the *entire* cached
+ state back, in the right order, or the card comes back silent or at
+ the wrong levels. This is what ``babyface_restore_state()`` and
+ ``bf_state_apply_flags()`` do (see "Mixer-state persistence" below).
+
+The asynchronous stream model
+==============================
+
+The PCM stream is not started or stopped directly by
+``.trigger()``. Instead, ``.trigger()`` only adjusts a shared
+``stream_users`` counter (0..2, one per running substream - playback
+and capture share one physical stream) and schedules
+``stream_work``, a work item that runs in process context because
+starting a session means sleeping USB control transfers
+(cold init, the session-arm sequence) followed by submitting the
+interrupt URBs:
+
+* **users 0 -> 1** (first substream starts): cold-init the device,
+ send the session-start trigger pair, submit the IN/OUT URBs (always
+ as a matched pair - the device does not advance the stream unless
+ both directions have a pending transfer), arm the session, then
+ replay the entire cached mixer state (masters, crosspoints, preamp,
+ flags, pitch) since cold-init just wiped it.
+
+* **users 1 -> 0** (last substream stops): kill the URBs and let the
+ session go idle.
+
+So the device always has exactly one live session regardless of how
+many ALSA substreams are open, and a rate or format change on one
+substream transparently restarts that shared session under the other
+one - the other side sees a brief rate step (PipeWire's resampler
+absorbs it) rather than the ``open()`` failing with ``-EBUSY``.
+
+Mixer-state persistence across re-probes
+==========================================
+
+A userspace client can claim the proprietary interface directly via
+``usbfs`` (``USBDEVFS_DISCONNECT_CLAIM``) - both PipeWire grabbing the
+device for a sink and the project's own TuxMix userspace daemon do
+this via libusb. That detaches the kernel driver and the ALSA card
+disappears for the duration; when the client releases the interface,
+the driver re-probes. The device keeps its register contents across
+this detach, but the driver's own cold-init (required at every
+session start, see above) clears them - so the driver saves the
+in-memory mixer state at ``disconnect()`` and restores it at the next
+``probe()``, keyed by the device's USB serial number (or its sysfs
+path, if it has no serial) so the same physical unit gets its state
+back across the cycle. The same state is also what a system-suspend
+resume replays, since the device loses its registers across a suspend
+the same way.
+
+Front-panel emulation: the driver plays TotalMix's role
+==========================================================
+
+The front panel (IN/OUT/SET/MIX/SELECT/DIM buttons, the rotary
+wheel) has no on-device intelligence of its own for turning a wheel
+click into a mixer change - on Windows/Mac, RME's TotalMix
+application polls the same 0x17 status register this driver polls,
+decodes button/wheel deltas, and performs the resulting mixer writes
+itself. Standalone (no host software) mode exists on the hardware,
+but the proprietary USB mode this driver targets always has a host
+attached, so this driver has to do what TotalMix does: poll 0x17 on
+a delayed work item (``panel_poll_ms`` module parameter, default
+20 ms to match TotalMix's own ~50 Hz), decode the button flash and
+signed wheel delta, and apply the resulting change (an output fader
+step, a preamp gain step, a phantom toggle, DIM) exactly like the
+corresponding ALSA control's ``.put`` would. The front-panel ALSA
+controls this driver exposes are the read side of this: a way for
+userspace (WirePlumber, TuxMix) to observe what the physical panel is
+doing, not a way to drive the hardware.
+
+Some panel state - which channel SELECT currently has chosen, for
+instance - is not part of the 0x17 readback at all and exists only on
+the device's own internal state machine, which the driver cannot
+read. That state is tracked host-side and deliberately re-synced to
+a known value (nothing selected) for the first few seconds after
+probe, because a stale alsactl-restored value would otherwise
+silently desync from the physical LEDs. The relevant code comments
+(``babyface_panel_start()``, the ``panel_select_armed`` handling in
+``bf_panel_tick()``) explain the specific desync scenarios this
+guards against.
diff --git a/Documentation/sound/cards/index.rst b/Documentation/sound/cards/index.rst
index e68bbb13c..31051b7a4 100644
--- a/Documentation/sound/cards/index.rst
+++ b/Documentation/sound/cards/index.rst
@@ -12,6 +12,7 @@ Card-Specific Information
emu10k1-jack
via82xx-mixer
audiophile-usb
+ babyface-pro
mixart
bt87x
maya44
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH v5 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc
2026-09-18 11:39 ` [RFC PATCH v5 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
@ 2026-09-18 18:33 ` Randy Dunlap
0 siblings, 0 replies; 10+ messages in thread
From: Randy Dunlap @ 2026-09-18 18:33 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound
Cc: linux-usb, alsa-devel, perex, tiwai, linux-kernel, corbet, skhan,
linux-doc
Hi,
Just one nit below...
On 9/18/26 4:39 AM, Ismaïl Bahloul wrote:
> A big-picture companion to the patch series, for human reviewers:
> why the proprietary mode needs a standalone driver instead of a
> snd-usb-audio quirk, the vendor protocol's write-only/no-readback
> shape and what that forces onto the driver, the asynchronous stream
> model, why the mixer state has to be cached and replayed, and why
> the driver has to emulate TotalMix's own role for the front panel.
>
> Requested by Takashi Iwai's v4 review: documentation for reviewers,
> not restating what the code comments already say inline.
>
> Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
> ---
> Documentation/sound/cards/babyface-pro.rst | 163 +++++++++++++++++++++
> Documentation/sound/cards/index.rst | 1 +
> 2 files changed, 164 insertions(+)
> create mode 100644 Documentation/sound/cards/babyface-pro.rst
>
> diff --git a/Documentation/sound/cards/babyface-pro.rst b/Documentation/sound/cards/babyface-pro.rst
> new file mode 100644
> index 000000000..d67f8c6fe
> --- /dev/null
> +++ b/Documentation/sound/cards/babyface-pro.rst
> @@ -0,0 +1,163 @@
> +.. SPDX-License-Identifier: GPL-2.0
> +
> +=================================================
> +RME Babyface Pro / Pro FS (snd-usb-babyface-pro)
> +=================================================
> +
> +This document describes the design of the ``snd-usb-babyface-pro``
> +driver for reviewers who need the big picture before reading the
> +patches - what problem the driver solves, why it is a standalone
> +driver instead of a snd-usb-audio quirk, and the four design
> +decisions (stream model, protocol shape, mixer-state persistence,
> +front-panel emulation) that shape most of the code. The patch series
> +itself is split by feature (core+PCM, then masters+crosspoint,
> +preamp, routing flags, suspend/resume, front panel, DSP EQ, in that
> +order) so each patch can be read and built on its own; this document
> +does not repeat what each patch's own commit message already covers.
> +
> +Two USB personalities, one device
> +==================================
> +
> +The RME Babyface Pro and Babyface Pro FS present two different USB
> +configurations depending on a physical/firmware switch: a
> +class-compliant one, already handled by ``snd-usb-audio``, and a
> +proprietary one (USB ID ``2a39:3fc0``) that this driver covers. The
> +two hardware models share the same USB ID, ``bcdDevice`` and
> +``iProduct`` string shape; nothing in the descriptors tells them
> +apart, and the driver runs unmodified on both.
> +
> +In proprietary mode, interface 5 carries the PCM stream on two
> +INTERRUPT endpoints (``0x01`` OUT, ``0x82`` IN) instead of the
> +isochronous endpoints the USB Audio Class specifies. Isochronous
> +transfers are rejected there with ``-EINVAL``. ``snd-usb-audio`` has
> +no interrupt-PCM transport, so this mode cannot be a quirk on top of
> +it; the driver is standalone, modelled on ``snd-usb-caiaq`` (another
> +interrupt-streaming RME/NI-style device).
> +
> +Why interrupt endpoints and not isochronous is a hardware/firmware
> +choice on RME's side, not something this driver can change - the
> +class-compliant mode already exists on the same device for users who
> +want a fully standard, quirk-free path with a subset of the
> +functionality (no mixer, no front panel). This driver is for users
> +who want the full mixer, routing matrix, and hardware DSP EQ that
> +only the proprietary mode exposes.
> +
> +The vendor protocol: writes only, no readback
> +==============================================
> +
> +Every mixer and clock function is one of a handful of USB vendor
> +control requests (``bmRequestType 0x40``, i.e. host-to-device,
> +vendor, device-recipient), each identified by its request number and
> +a 16-bit value/index pair - there is no larger command structure.
> +The commonly used ones are:
> +
> +====== ========================================
> +0x10 settings word / stream start trigger
> +0x12 16-bit crosspoint and output-master writes
> +0x16 cold-init register clear
> +0x17 front-panel + preamp state (read and write)
> +0x1a 8-bit gain / output-master companion writes
> +0x1b clock DDS quads (base rate and varispeed)
> +====== ========================================
> +
> +The full register map, decoded from Windows USB captures and
> +cross-checked against hardware, lives in the driver's own development
> +repository (not shipped in-tree) - the constants and the comments
> +next to each vendor write in the source are the authoritative
> +in-tree reference.
> +
> +The one property that shapes the rest of the driver: **almost nothing
> +here is readable back**. The 0x17 request returns the front-panel
here can be read back**.
> +and preamp state, but the crosspoint matrix, the output masters, the
> +routing flags and the clock all have to be tracked host-side - the
> +device will accept a write blindly and never confirm what it actually
> +holds. Two consequences follow directly from this:
--
~Randy
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-09-18 18:33 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-18 11:39 [RFC PATCH v5 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-09-18 11:39 ` [RFC PATCH v5 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
2026-09-18 18:33 ` Randy Dunlap
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®