* [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode)
@ 2026-11-05 15:15 Ismaïl Bahloul
2026-11-05 15:15 ` [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
` (7 more replies)
0 siblings, 8 replies; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Hi Takashi,
This is v8 of the RME Babyface Pro proprietary-mode driver. The split
is unchanged from v5: a minimal core first, the mixer added piece by
piece, suspend/resume near the end, the front panel and the DSP EQ
last, and a design document. The series is still based on
next-20261026, so this is a fix round, not a rebase. It answers your
reply to v7: your three points are the first item below, and what I
did with the Sashiko findings is the second.
This is not an RFC. Apart from the stereo split one, which a user
reported from USB captures of the vendor software, every point below
is a fix to a defect found in review of v7, or a change you asked for.
None of them changes the protocol or the control layout.
Changes since v7:
- Your comments on v7. The mutexes, the spinlocks, the RCU read
sides and the PCM stream locks use guard(), scoped_guard() and
guard(rcu) in place of paired lock and unlock calls with goto
labels, so the driver has no manual lock handling left. Each
critical section covers the same code as before, and the places that
used to jump to an unlock label now return. The per-channel loops
in babyface_capture_copy() and babyface_playback_copy() are one
memcpy() of the words the frame and the stream have in common, and a
memset() of the rest: the words are S32_LE with 24 msbits on both
sides, so they go over as they are. bf_seen_before() keeps the last
eight units it saw in a fixed table (patch 6) instead of a list that
grew with every unit; a unit pushed out by eight newer ones is taken
for a first probe again.
- Sashiko. I went through its findings on v7 again. The ones that
hold are fixed in the items below. Two I left alone: the control
names, which stay as they are (see patch 3), and the module
parameters it wants in kernel-parameters.txt, which documents the
kernel's own; the driver's are in the Kconfig help and in patch 8.
- Probe and stream (patch 1). probe() returned 0 after freeing the
card when an URB or DMA buffer allocation failed; it now returns
-ENOMEM. Trigger START re-checks the rate and period that prepare()
checks, so a substream prepared before the other direction changed
the session cannot start against stale geometry. in_done and
out_inflight are initialised before the URBs go out, and a URB that
cannot be resubmitted is treated as fatal instead of waiting for a
run of errors that a later good completion would clear.
- Output masters (patch 2). Every snd_ctl_new1() result is checked
before the name is written. The output pair switch is a mono
switch, which is what the hardware does; it used to advertise two
channels and ignore the second.
- Preamp and trim (patch 3). The same NULL checks in the trim
controls, with trim_kctl stored only after snd_ctl_add() succeeds
(it could be left dangling on failure). The Boost 0x0003 commit
value is re-sent on AN 1>2, on the input link and in the state
replay, not only on preamp writes, so Boost no longer falls back to
-10dBV. The ref level is derived from the 0x17 readback at probe,
the phantom bit is sampled under the mutex, and the unused
bf_gain_db() is gone. The preamp and capture control names stay as
they are, with .index selecting the channel: the companion mixer
and the asound.state files already reference them, and the index is
the ALSA convention for a per-channel control. Turning a playback
pair's stereo split off wrote 0x1000 to both sides of its AN1/2
crosspoint, 6 dB below what the vendor software writes for a joined
pair, and lost the level set on that crosspoint; it now restores
the cached crosspoint.
- Routing flags (patch 4). bf_state_apply_flags() used to write
0x1000 to the AN1 and AN2 crosspoints on every probe and resume,
even at width 0, after the default mixer had left those inputs
muted. The cache still said 0, so a later write of 0 returned early
and could not mute them again. The width replay is now guarded and
shares one helper with the width control. DIM, mute, the master
controls and the front-panel wheels all derive the Phones level from
one helper, so they no longer cancel each other. bf_ms_put()
restores each output block's own crosspoint, the loopback control
notifies the pair it clears, and the enum and integer puts check
their bounds before narrowing.
- Suspend/resume (patch 5). No change of its own; it inherits the
replay fixes from patch 4.
- Front panel (patch 6). The MIX level is kept in dB2 instead of the
quantised register value, so clicks below the curve's first point
are no longer lost and a muted input comes up on the first click;
SELECT re-seeds the level while MIX is on. The MIX gate is no
longer re-armed on the tick that leaves the mode, reprobe is set
before the card becomes visible, and the panel master writers
respect DIM.
- EQ (patch 7). The low-cut frequency uses div64_s64() for the
variable divisor, so a 32-bit module does not need __divdi3 (not
exported on ARM). A band of type Off is identity rather than a high
shelf, the default slope matches the value the control reports, and
a failed strip upload is returned to the caller.
- Documentation (patch 8). Wording follows the one-map crosspoint
model; no functional change.
Every patch builds in-tree on its own against next-20261026 with W=1
and no warnings. checkpatch --strict reports no errors; the known
false positives are the same as v7 ('ang' for the CORDIC angle, and the
MAINTAINERS note on the patches that add files).
Validated on the unit with this tree:
- regress.sh --mixer-restore --disconnect-test passes 41/41: the
full-duplex sweep of the nine rates at periods 256 to 2048 with no
xruns, runtime renegotiation, 30 start/stop cycles, the mixer state
restored across an unbind/rebind, and an unbind in the middle of a
capture.
- The DIM/mute/volume matrix was checked on the wire with usbmon:
DIM writes the exact 0xcb/0x0333 pair, a volume change while DIM
is on keeps that pair, mute writes 0x3b/0x0000, unmute while DIM
is on restores 0xcb/0x0333, and releasing DIM restores the new
volume.
- The probe/rebind replay was checked on the wire too: after the
default mixer writes 0x0000 to the AN1/AN2 crosspoints, no 0x1000
write follows, so the width block no longer unmutes the inputs.
- The MIX-wheel arithmetic is covered by fader_selftest: a muted
crosspoint seeds at the curve's first point, one clockwise click
leaves mute, and two counter-clockwise clicks from the old -52 dB
sticking point move.
- The physical MIX-wheel gesture was checked on the unit: from a
muted AN3 crosspoint, one clockwise click moved it to 0x0004 (the
old code left it at 0 for the first five clicks), two clockwise and
two counter-clockwise clicks stepped it to 0x0003, and the level
followed the wheel in both directions.
- The stereo split was checked through the AN1/2 digital loopback:
with PB1 at the default crosspoint, split then join comes back at
the level it started from (-51.5 dB before and after; it used to
come back at -54.5 dB).
Known limitations, unchanged from v7:
- USB autosuspend is not supported; it is disabled at probe. S3
suspend/resume works.
- A few protocol details remain open (documented in the source, not
blocking): the preamp readback index semantics, a width
strip-ownership edge case, whether DIM's scope follows TotalMix's
Main Out setting, and the exact high-frequency warping of the EQ
coefficients against the vendor curve.
- The URB size and count are load-time parameters, though the size
follows the period the application asks for.
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,
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 | 218 ++
Documentation/sound/cards/index.rst | 1 +
MAINTAINERS | 6 +
sound/usb/Kconfig | 17 +
sound/usb/Makefile | 2 +-
sound/usb/babyfacepro/Makefile | 4 +
sound/usb/babyfacepro/babyfacepro-ctl.c | 3946 ++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 1943 ++++++++++
sound/usb/babyfacepro/babyfacepro.h | 568 +++
9 files changed, 6704 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.56.0
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 16:24 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
` (6 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
The RME Babyface Pro and Babyface Pro FS have a proprietary USB mode
(VID 0x2a39, PID 0x3fc0), the one RME's own drivers use. Its PCM
stream runs on the interrupt endpoints of interface 5, which
snd-usb-audio has no path for, so this is a standalone driver in the
style of snd-usb-caiaq.
This first patch covers the card lifecycle and the PCM stream only.
Probe runs the device's cold-start init; the full-duplex stream runs on
interrupt URBs submitted in IN/OUT pairs, since the device only
advances the stream while both directions have a URB pending.
The sample rate is the family register (request 0x10, index 0x0030:
the 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 ratio
applied on top of the family rate. Both directions share one clock,
so while another application has the other direction set up it is
offered that rate alone, and a period no shorter than the running
session's, rather than retuning the device under its stream; the
application that holds both directions may change the rate or the
buffer size itself, as a DAW does from its settings, and the direction
it left running stops with an xrun and is set up again.
A stream session lives from the first prepare to the last hw_free:
hw_params counts a substream, prepare starts the session if it is
down, trigger START/STOP only decides whether the URB handlers move
that substream's audio (the URBs keep running, carrying silence), and
an xrun restart keeps the session. A session start sends what the RME
Windows driver sends - the rate write, the trigger pair, the URBs and
the arm - and first waits until 50 ms have passed since the previous
session stopped: a session triggered within about 15 ms of the last one
comes up with the outputs silent. After three consecutive bad URBs the
handlers stop resubmitting and the substreams get an xrun.
The size of the URBs follows the period the application asks for, so
the latency follows its buffer instead of a fixed queue: hw_params
splits the period into the fewest URBs of at most frames_per_urb frames
(256 by default), each a whole number of the device's IN packets and
not below urb_frames_min (32), and keeps two periods in flight, at most
nurbs (8). The three are load-time limits.
The URB handlers copy to and from the ring the core allocates and take
the application's position from the shared control page, so the ring
is offered for mmap access as well, which JACK needs. runtime->delay
reports the audio queued in the URBs and, per speed, the fixed delay
of the converters and of the device, so that an application that uses
ALSA directly can line up what it records.
The playback handler copies the ring as it is and moves the pointer on
by the whole URB, as a DMA engine would; whether the application has
written every frame is the PCM core's business, whose xrun check and
stop threshold handle an underrun. Clamping the copy to the frames the
application had written kept the pointer still for an application that
runs the stream free, which is how dmix drives the hardware PCM, so
dmix played silence.
Capture exposes the device's twelve input words in the device's own
order, one channel per word: AN1-4, AS1/2, then ADAT3-8. AS1/2 is the
first optical pair, so it carries ADAT 1/2 when the input receives
ADAT and S/PDIF when it receives S/PDIF; the other six are named
ADAT3-8 for that reason. At 96/192 kHz the frame has 10/8 words and
the channels past them read zero.
USB autosuspend is disabled.
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 | 17 +
sound/usb/Makefile | 2 +-
sound/usb/babyfacepro/Makefile | 4 +
sound/usb/babyfacepro/babyfacepro.c | 1523 +++++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.h | 187 ++++
6 files changed, 1738 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 fcff12847..378f528c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24246,6 +24246,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: linux-sound@vger.kernel.org
+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..51772412a 100644
--- a/sound/usb/Kconfig
+++ b/sound/usb/Kconfig
@@ -204,6 +204,23 @@ 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).
+
+ 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 2e842dcc7..5a5ef7d88 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -36,5 +36,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..ac1414adc
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -0,0 +1,1523 @@
+// 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).
+ *
+ * See babyfacepro.h for the shared device state and register map.
+ */
+#include <linux/delay.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.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"
+
+/* 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;
+ }
+}
+
+/* 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). The clock source is
+ * Internal.
+ */
+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 init (cap_coldplug.pcap), verbatim from the user-space
+ * reference (protocol::streaming_init). Run at probe, where the
+ * device state is unknown; a session start does not need it (see
+ * babyface_stream_start()).
+ */
+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;
+}
+
+static bool babyface_capture_copy(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs,
+ const 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 copy = min(chans, words) * 4;
+ unsigned int pos, f;
+ unsigned long new_period;
+ bool crossed = false;
+ u8 *dst;
+
+ guard(spinlock)(&chip->lock);
+ pos = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] % buf_frames;
+ for (f = 0; f < frames; f++) {
+ dst = rt->dma_area + frames_to_bytes(rt, pos);
+ /* The frame carries the device's own order: AN1-4, AS1/2,
+ * then ADAT3-8 (AS1/2 is the first optical pair, so it is
+ * ADAT 1/2 when the input receives ADAT and S/PDIF when it
+ * receives S/PDIF). The PCM follows it one-to-one, so a
+ * channel is word i and the device order is what an
+ * application sees. Confirmed live on 2026-10-17 with the
+ * device's own loopback: a tone on the AS1/2 output bus
+ * appeared on words 4/5. Each word is a 24-bit sample in
+ * bits 31-8, which is S32_LE with 24 msbits, so the words go
+ * over as they are. At 96/192 kHz the frame has fewer words
+ * (10/8); the channels past them read as zero.
+ */
+ memcpy(dst, data + f * chip->frame_bytes, copy);
+ if (chans > words)
+ memset(dst + copy, 0, (chans - words) * 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;
+ }
+
+ 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 copy = min(chans, words) * 4;
+ unsigned int pos, f;
+ unsigned long new_period;
+ bool crossed = false;
+ const u8 *src;
+
+ guard(spinlock)(&chip->lock);
+ /* Copy the ring as it is and move the pointer on by the whole URB,
+ * as a DMA engine reading the buffer would: the device plays every
+ * frame of the URB. Whether the application has written them is
+ * the PCM core's business. It stops the stream on an underrun when
+ * the pointer passes appl_ptr, and an application that lets the
+ * stream run free (a stop threshold at the boundary, as dmix sets
+ * on the hardware PCM it opens) relies on the pointer moving with
+ * the device.
+ */
+ pos = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] % buf_frames;
+ for (f = 0; f < frames; f++) {
+ u8 *frame = 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. The app's 24 valid bits are already
+ * left-justified (bits 31-8), so the words go over as they
+ * are. At 96/192 kHz the frame is shorter - the extra app
+ * channels are dropped.
+ */
+ memcpy(frame, src, copy);
+ memset(frame + copy, 0, words * 4 - copy);
+ 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;
+ }
+
+ return crossed;
+}
+
+/* Count a bad URB. Once BF_URB_ERR_STOP of them are in a row, queue
+ * stream_work to stop the session and return true: the URB must not be
+ * resubmitted. Some host controllers fail a URB on a broken endpoint
+ * at once, without waiting for the bus, and a handler that resubmits
+ * regardless can then spin in interrupt context until the work runs.
+ */
+static bool babyface_urb_error(struct snd_usb_babyface *chip)
+{
+ if (atomic_inc_return(&chip->urb_err) < BF_URB_ERR_STOP)
+ return false;
+ schedule_work(&chip->stream_work);
+ return true;
+}
+
+/* A good URB ends a run of errors - unless the run has already reached
+ * BF_URB_ERR_STOP. The URBs that got it there were not resubmitted, so
+ * the count has to stand until stream_work has seen it; clearing it
+ * here would leave the session running on fewer and fewer URBs.
+ */
+static void babyface_urb_ok(struct snd_usb_babyface *chip)
+{
+ int err = atomic_read(&chip->urb_err);
+
+ while (err && err < BF_URB_ERR_STOP &&
+ !atomic_try_cmpxchg(&chip->urb_err, &err, 0))
+ ;
+}
+
+/* A URB that cannot be resubmitted leaves the queue for good. Later
+ * good completions clear chip->urb_err, so waiting for a run of errors
+ * can leave the session running on fewer and fewer URBs, or on none at
+ * all when urbs_active is 2. Treat it as fatal at once.
+ */
+static void babyface_urb_resubmit_failed(struct snd_usb_babyface *chip)
+{
+ atomic_set(&chip->urb_err, BF_URB_ERR_STOP);
+ schedule_work(&chip->stream_work);
+}
+
+/* An OUT URB that is not resubmitted leaves the queue. */
+static void bf_out_lost(struct snd_usb_babyface *chip)
+{
+ guard(spinlock_irqsave)(&chip->lock);
+ if (chip->out_inflight)
+ chip->out_inflight--;
+}
+
+/* The frames of a completed IN URB go to the capture substream if it
+ * runs; true when that crossed a period boundary.
+ */
+static bool babyface_capture_urb(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs,
+ struct urb *urb)
+{
+ unsigned int frames;
+
+ guard(pcm_stream_lock_irqsave)(subs);
+ if (!snd_pcm_running(subs))
+ return false;
+ frames = urb->actual_length / chip->frame_bytes;
+ if (!frames)
+ return false;
+ return babyface_capture_copy(chip, subs, urb->transfer_buffer, frames);
+}
+
+/* The playback substream, if it runs, fills a completed OUT URB: *fed
+ * says whether it did, and the return value whether that crossed a
+ * period boundary.
+ */
+static bool babyface_playback_urb(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs,
+ struct urb *urb, bool *fed)
+{
+ guard(pcm_stream_lock_irqsave)(subs);
+ if (!snd_pcm_running(subs))
+ return false;
+ *fed = true;
+ return babyface_playback_copy(chip, subs, urb->transfer_buffer,
+ urb->transfer_buffer_length /
+ chip->frame_bytes);
+}
+
+static void babyface_complete_in(struct urb *urb)
+{
+ struct snd_usb_babyface *chip = urb->context;
+ struct snd_pcm_substream *subs;
+ int ret;
+
+ if (urb->status < 0) {
+ if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+ urb->status == -ECONNRESET || urb->status == -ENODEV)
+ return; /* killed, or the device is gone */
+ dev_dbg_ratelimited(&chip->dev->dev, "IN urb status %d\n",
+ urb->status);
+ if (babyface_urb_error(chip))
+ return;
+ goto resubmit;
+ }
+ babyface_urb_ok(chip);
+ scoped_guard(spinlock_irqsave, &chip->lock)
+ chip->in_done = ktime_get();
+
+ /* The session outlives a substream that is closed while the other
+ * direction still runs: close() waits out this RCU section.
+ */
+ scoped_guard(rcu) {
+ subs = rcu_dereference(chip->subs[SNDRV_PCM_STREAM_CAPTURE]);
+ if (subs && babyface_capture_urb(chip, subs, urb))
+ snd_pcm_period_elapsed(subs);
+ }
+resubmit:
+ /* The session is being stopped: babyface_stream_kill() clears
+ * ->streaming before it kills the URBs. A resubmit now would only
+ * fail against the kill and be counted as a stream error.
+ */
+ if (!READ_ONCE(chip->streaming))
+ return;
+ ret = usb_submit_urb(urb, GFP_ATOMIC);
+ if (ret < 0) {
+ dev_err_ratelimited(&chip->dev->dev,
+ "IN resubmit failed: %d\n", ret);
+ babyface_urb_resubmit_failed(chip);
+ }
+}
+
+static void babyface_complete_out(struct urb *urb)
+{
+ struct snd_usb_babyface *chip = urb->context;
+ struct snd_pcm_substream *subs;
+ bool fed = false;
+ int ret;
+
+ if (urb->status < 0) {
+ if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+ urb->status == -ECONNRESET || urb->status == -ENODEV)
+ return; /* killed, or the device is gone */
+ dev_dbg_ratelimited(&chip->dev->dev, "OUT urb status %d\n",
+ urb->status);
+ if (babyface_urb_error(chip)) {
+ bf_out_lost(chip);
+ return;
+ }
+ goto resubmit;
+ }
+ babyface_urb_ok(chip);
+
+ scoped_guard(rcu) {
+ subs = rcu_dereference(chip->subs[SNDRV_PCM_STREAM_PLAYBACK]);
+ if (subs && babyface_playback_urb(chip, subs, urb, &fed))
+ snd_pcm_period_elapsed(subs);
+ }
+ /* Nothing playing (no substream, or one that is set up but stopped,
+ * e.g. between an xrun and the restart): send silence, not the
+ * audio this URB carried last time round.
+ */
+ if (!fed)
+ memset(urb->transfer_buffer, 0, urb->transfer_buffer_length);
+resubmit:
+ /* The session is being stopped: babyface_stream_kill() clears
+ * ->streaming before it kills the URBs. A resubmit now would only
+ * fail against the kill and be counted as a stream error.
+ */
+ if (!READ_ONCE(chip->streaming))
+ return;
+ ret = usb_submit_urb(urb, GFP_ATOMIC);
+ if (ret < 0) {
+ dev_err_ratelimited(&chip->dev->dev,
+ "OUT resubmit failed: %d\n", ret);
+ bf_out_lost(chip);
+ babyface_urb_resubmit_failed(chip);
+ }
+}
+
+void babyface_stream_kill(struct snd_usb_babyface *chip)
+{
+ int i;
+
+ /* Clear the flag before the kills, not after. The completion
+ * handlers test it before resubmitting, so a URB that completes
+ * while this loop runs retires instead of resubmitting into its
+ * own kill, which fails with -EPERM and counts towards
+ * stream_work.
+ */
+ WRITE_ONCE(chip->streaming, false);
+ for (i = 0; i < chip->nurbs; i++) {
+ usb_kill_urb(chip->urbs_in[i]);
+ usb_kill_urb(chip->urbs_out[i]);
+ }
+ chip->stream_stopped = ktime_get();
+}
+
+/* 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.
+ *
+ * Call this after babyface_stream_kill(), not before: trigger START tests
+ * ->streaming under the stream lock taken here, so with the flag already
+ * clear a START either fails with -EPIPE or has finished and is stopped
+ * below. The other way round it can slip in between and leave a substream
+ * running on a session that is gone.
+ */
+void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state)
+{
+ int s;
+
+ /* close() waits out this RCU section, as for the URB handlers. */
+ guard(rcu)();
+ for (s = 0; s < 2; s++) {
+ struct snd_pcm_substream *subs = rcu_dereference(chip->subs[s]);
+
+ if (!subs)
+ continue;
+ guard(pcm_stream_lock_irqsave)(subs);
+ if (snd_pcm_running(subs))
+ snd_pcm_stop(subs, state);
+ }
+}
+
+/* Stream model (big picture):
+ *
+ * The device has one USB session, shared by both directions. Its
+ * lifetime follows the substreams' setup, not their triggers - the
+ * same pattern as the FireWire audio drivers:
+ *
+ * - hw_params counts a substream as a user of the session;
+ * - prepare starts the session if it is not running: the rate write
+ * and the session trigger pair, then the interrupt URBs (nurbs in
+ * each direction) are submitted and the session is armed;
+ * - trigger START/STOP only decides whether the URB handlers move that
+ * substream's audio. The URBs keep running either way, carrying
+ * silence while nothing plays;
+ * - hw_free drops the user, and the last one stops the session.
+ *
+ * A sound server restarts its streams with prepare + START after every
+ * xrun. That must not restart the session: PipeWire does not ride
+ * through it, and an already connected client stays silent afterwards.
+ */
+
+/* Start the session. Caller holds chip->mutex. */
+static int babyface_stream_start(struct snd_usb_babyface *chip)
+{
+ unsigned int urbsize = chip->frame_bytes * chip->urb_frames;
+ s64 since;
+ int i, ret;
+
+ /* A session triggered right after the previous one stopped starts
+ * with the outputs silent: give the device BF_SESSION_GAP_MS.
+ * Only a stop followed at once by a new setup waits (an application
+ * restarting, a probe opening the device repeatedly).
+ */
+ since = ktime_ms_delta(ktime_get(), chip->stream_stopped);
+ if (since < BF_SESSION_GAP_MS)
+ msleep(BF_SESSION_GAP_MS - since);
+
+ /* A session start is what the RME Windows driver sends
+ * (cap_audio): the trigger pair, the URBs, the arm. The device
+ * keeps its mixer state between sessions, so nothing is cleared
+ * or re-applied here; the cold init runs at probe only. The rate
+ * is written first: hw_params only re-points the speed multiplier.
+ */
+ ret = bf_clock_write(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->urbs_active; 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;
+ }
+ /* A count left over from the previous session would stop this one
+ * as soon as a URB errors, or through a stream_work still queued.
+ */
+ atomic_set(&chip->urb_err, 0);
+ /* Mark the session live before the first URB goes out, not once the
+ * state is restored: the URBs complete while the vendor writes below
+ * are still running, and ->streaming is what says the session is
+ * theirs to keep going. The error path clears it again.
+ */
+ scoped_guard(spinlock_irq, &chip->lock) {
+ chip->out_inflight = chip->urbs_active;
+ chip->in_done = ktime_get();
+ }
+ WRITE_ONCE(chip->streaming, true);
+ for (i = 0; i < chip->urbs_active; 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;
+
+ dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
+ chip->urb_frames, chip->urbs_active);
+ return 0;
+
+err:
+ dev_err_ratelimited(&chip->dev->dev, "failed to start stream: %d\n",
+ ret);
+ babyface_stream_kill(chip);
+ return ret;
+}
+
+/* Persistent URB errors (bad link, device wedged): stop the session and
+ * wake the apps with -EPIPE. Their recovery (prepare + START) starts a
+ * new session from a clean slate. Runs in process context because
+ * killing the URBs sleeps.
+ */
+void babyface_stream_work(struct work_struct *work)
+{
+ struct snd_usb_babyface *chip =
+ container_of(work, struct snd_usb_babyface, stream_work);
+
+ guard(mutex)(&chip->mutex);
+ if (!chip->shutdown &&
+ 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);
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+ atomic_set(&chip->urb_err, 0);
+ }
+}
+
+static const struct snd_pcm_hardware babyface_pcm_hw = {
+ /* The URB handlers copy to and from the vmalloc'ed ring the core
+ * allocates (rt->dma_area), and read appl_ptr from the control page,
+ * so the ring can be mapped by the application as well - which JACK
+ * and other mmap-only clients need.
+ */
+ .info = SNDRV_PCM_INFO_MMAP |
+ SNDRV_PCM_INFO_MMAP_VALID |
+ 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,
+};
+
+/* The period of the direction that set the session up, in whole URBs:
+ * half the URBs in flight, rounded up. The other direction's period may
+ * not be shorter. Caller holds chip->mutex.
+ */
+static unsigned int bf_session_period(struct snd_usb_babyface *chip)
+{
+ return chip->urb_frames * DIV_ROUND_UP(chip->urbs_active, 2);
+}
+
+static int urb_split;
+
+/* Does this session split its period into two URBs? Then the queue is
+ * three URBs, one and a half periods, instead of four URBs, two periods:
+ * the round trip is half a period shorter for each of playback and
+ * capture's share of it (64 frames at a 128 period, 128 at 256). Auto
+ * does it for the periods that were soaked for 10 minutes without an
+ * xrun (128 and 256 frames at 48 kHz, 12 channels, mmap, real-time
+ * priority), and for other rates only while a URB lasts at least 1.33 ms,
+ * the length of those. Below that the URB is already near its floor.
+ */
+static bool bf_split_period(const struct bf_rate *r, unsigned int period)
+{
+ if (urb_split == 1)
+ return false;
+ if (urb_split > 1)
+ return true;
+ return period >= 128 && period <= 256 &&
+ (period / 2) * 750 >= r->rate;
+}
+
+/* The URB size for a session, chosen from the period the application
+ * asked for, as snd-usb-audio sizes its playback URBs: split the period
+ * into the fewest URBs of at most frames_per_urb, and make each a whole
+ * number of the device's IN packets (8/16/32 frames at alt 1/2/3; a URB
+ * that ends inside a packet gets -EOVERFLOW), but not below
+ * urb_frames_min. A URB never exceeds the period, so a completion
+ * crosses at most one period boundary. Returns 0 if the rate's packet
+ * does not fit.
+ */
+static unsigned int bf_urb_frames(struct snd_usb_babyface *chip,
+ const struct bf_rate *r,
+ unsigned int period)
+{
+ unsigned int pkt = r->min_fpu;
+ unsigned int hi = rounddown(chip->frames_per_urb, pkt);
+ unsigned int lo = roundup(max(chip->urb_frames_min, pkt), pkt);
+ unsigned int urb;
+
+ if (!hi || lo > hi || period < pkt)
+ return 0;
+ urb = rounddown(period / DIV_ROUND_UP(period, hi), pkt);
+ if (bf_split_period(r, period))
+ urb = rounddown(urb / 2, pkt);
+ urb = clamp(urb, lo, hi);
+ return urb <= period ? urb : 0;
+}
+
+/* Does ANOTHER application hold the clock through the other direction?
+ * The session lives from the first prepare to the last hw_free, so a
+ * substream that is set up but not running - between an xrun and the
+ * sound server's restart, say - still owns the rate it negotiated. Not
+ * "is it open": a client that has the device open but has not called
+ * hw_params must still be able to pick the rate.
+ *
+ * The application that set the other direction up may change the rate
+ * itself, as snd-hdspm allows for the process that holds both
+ * directions: a DAW switches rate from its own settings without closing
+ * the device. Processes, not threads, since an application may open
+ * playback and capture from different threads.
+ *
+ * Caller holds chip->mutex, which is what stream_setup[] and owner[] are
+ * updated under.
+ */
+static bool bf_other_holds_clock(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs)
+{
+ int other = !subs->stream;
+
+ return chip->stream_setup[other] &&
+ chip->owner[other] != chip->owner[subs->stream];
+}
+
+/* While another application holds the clock, offer its rate alone. A
+ * rule, not a constraint set in open(): it is evaluated each time the
+ * application asks, so it follows the other direction being set up and
+ * released, and who set it up.
+ */
+static int bf_hw_rule_rate(struct snd_pcm_hw_params *params,
+ struct snd_pcm_hw_rule *rule)
+{
+ struct snd_pcm_substream *subs = rule->private;
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ struct snd_interval t = { .integer = 1 };
+ bool held;
+
+ scoped_guard(mutex, &chip->mutex) {
+ held = bf_other_holds_clock(chip, subs);
+ t.min = chip->rate;
+ t.max = chip->rate;
+ }
+ if (!held)
+ return 0;
+ return snd_interval_refine(hw_param_interval(params,
+ SNDRV_PCM_HW_PARAM_RATE), &t);
+}
+
+/* While another application holds the session, a period must span its
+ * URBs: at least the session's period, in whole URBs. A rule for the
+ * same reason as the rate.
+ */
+static int bf_hw_rule_period(struct snd_pcm_hw_params *params,
+ struct snd_pcm_hw_rule *rule)
+{
+ struct snd_pcm_substream *subs = rule->private;
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ struct snd_interval t = { .integer = 1, .max = UINT_MAX };
+ bool held;
+
+ scoped_guard(mutex, &chip->mutex) {
+ held = bf_other_holds_clock(chip, subs);
+ t.min = bf_session_period(chip);
+ }
+ if (!held)
+ return 0;
+ return snd_interval_refine(hw_param_interval(params,
+ SNDRV_PCM_HW_PARAM_PERIOD_SIZE), &t);
+}
+
+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;
+ 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;
+ /* A period must span at least one URB, so a completion crosses at
+ * most one period boundary. The URB size follows the period down to
+ * urb_frames_min; while another application holds the session, its
+ * period (in whole URBs) is the floor. Constrain in frames (not
+ * bytes) so the minimum period does not balloon at low channel
+ * counts.
+ */
+ ret = snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+ chip->urb_frames_min, 1 << 18);
+ if (ret < 0)
+ return ret;
+ ret = snd_pcm_hw_rule_add(rt, 0, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+ bf_hw_rule_period, subs,
+ SNDRV_PCM_HW_PARAM_PERIOD_SIZE, -1);
+ if (ret < 0)
+ return ret;
+
+ /* Both directions share one clock, so while another application's
+ * audio is set up the rate belongs to it. Offer that rate alone: a
+ * client arriving later then negotiates down to it, and the sound
+ * server resamples, rather than the device changing rate underneath
+ * a running stream. Advertising the rate 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.
+ */
+ scoped_guard(mutex, &chip->mutex)
+ chip->owner[subs->stream] = task_tgid_nr(current);
+ ret = snd_pcm_hw_rule_add(rt, 0, SNDRV_PCM_HW_PARAM_RATE,
+ bf_hw_rule_rate, subs,
+ SNDRV_PCM_HW_PARAM_RATE, -1);
+ if (ret < 0)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &chip->lock)
+ rcu_assign_pointer(chip->subs[subs->stream], subs);
+ return 0;
+}
+
+static int babyface_pcm_close(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+
+ flush_work(&chip->stream_work);
+ scoped_guard(mutex, &chip->mutex)
+ chip->owner[subs->stream] = 0;
+ scoped_guard(spinlock_irqsave, &chip->lock)
+ RCU_INIT_POINTER(chip->subs[subs->stream], NULL);
+ /* The session keeps running while the other direction is set up,
+ * so a URB handler may still hold this substream: wait for it
+ * before the core frees the runtime.
+ */
+ synchronize_rcu();
+ 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;
+ unsigned int urb, n;
+ bool new_rate = false;
+ 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.
+ */
+ urb = bf_urb_frames(chip, r, params_period_size(params));
+ if (!urb) {
+ dev_err(&chip->dev->dev,
+ "rate %u Hz, period %u: no URB size fits (packet %u frames, URBs %u..%u)\n",
+ r->rate, params_period_size(params), r->min_fpu,
+ chip->urb_frames_min, chip->frames_per_urb);
+ return -EINVAL;
+ }
+
+ guard(mutex)(&chip->mutex);
+ /* disconnect() has let go of the interface: do not re-point it. */
+ if (chip->shutdown)
+ return -ENODEV;
+ if (r->rate != chip->rate) {
+ /* The rule in open() normally means no other application
+ * asks for a rate other than the running one. It can still
+ * happen when the other direction was set up between this
+ * one's refine and its hw_params. 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_holds_clock(chip, subs)) {
+ dev_dbg(&chip->dev->dev,
+ "rate %u Hz refused: %u Hz stream set up\n",
+ r->rate, chip->rate);
+ return -EBUSY;
+ }
+ /* Nothing else holds the clock. Stop the session and
+ * re-point the bandwidth class; prepare starts a new session
+ * at the new rate. If this application's other direction is
+ * still running, stop it with an xrun rather than let it
+ * change pitch; prepare() then refuses it until it is set up
+ * again at the new rate.
+ */
+ if (chip->streaming) {
+ babyface_stream_kill(chip);
+ babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN);
+ }
+ ret = usb_set_interface(chip->dev, BF_IFACE, r->alt);
+ if (ret < 0)
+ return ret;
+ chip->rate = r->rate;
+ new_rate = true;
+ chip->alt = r->alt;
+ chip->frame_bytes = r->frame_bytes;
+ dev_dbg(&chip->dev->dev, "rate %u Hz (alt %u)\n",
+ chip->rate, chip->alt);
+ }
+ /* The first direction to be set up chooses the URB size and count
+ * from its period; the other one then needs a period at least as
+ * long, in whole URBs. Another application must fit the session
+ * (open()'s rule offers no shorter period). The application that
+ * holds it chooses again when it sets up the direction that chose,
+ * as a DAW changing its buffer does, when the other direction no
+ * longer fits, or when the rate changed (the URBs are whole packets
+ * of the rate). A new size needs a new session: stop this one, and
+ * prepare starts the next.
+ */
+ if (bf_other_holds_clock(chip, subs)) {
+ if (params_period_size(params) < bf_session_period(chip))
+ return -EBUSY;
+ } else if (new_rate || !chip->stream_setup[!subs->stream] ||
+ chip->session_dir == subs->stream ||
+ params_period_size(params) < bf_session_period(chip)) {
+ /* URBs in flight for two periods, in whole URBs, and no more:
+ * the queue is latency. Not the whole buffer - the other
+ * direction may be set up later with a smaller one, and it
+ * shares this queue. Every buffer holds at least two periods,
+ * and the other direction's period is at least this one (in
+ * whole URBs, see bf_session_period()), so neither direction
+ * gets more queued than its own buffer.
+ */
+ n = 2 * (params_period_size(params) / urb);
+ if (bf_split_period(r, params_period_size(params)))
+ n--;
+ n = clamp_t(unsigned int, n, 2, chip->nurbs);
+ if (urb != chip->urb_frames || n != chip->urbs_active) {
+ /* This application's other direction may still
+ * run: stop it with an xrun, prepare() refuses it
+ * until it fits the new session.
+ */
+ if (chip->streaming) {
+ babyface_stream_kill(chip);
+ babyface_pcm_stop_both(chip,
+ SNDRV_PCM_STATE_XRUN);
+ }
+ chip->urb_frames = urb;
+ chip->urbs_active = n;
+ dev_dbg(&chip->dev->dev,
+ "URBs: %u x %u frames for period %u, buffer %u\n",
+ n, urb, params_period_size(params),
+ params_buffer_size(params));
+ }
+ chip->session_dir = subs->stream;
+ }
+ /* hw_params can repeat without an hw_free in between: count the
+ * substream once.
+ */
+ if (!chip->stream_setup[subs->stream]) {
+ chip->stream_setup[subs->stream] = true;
+ chip->stream_users++;
+ }
+
+ return ret;
+}
+
+/* The last substream to be released stops the session. */
+static int babyface_pcm_hw_free(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+
+ guard(mutex)(&chip->mutex);
+ if (chip->stream_setup[subs->stream]) {
+ chip->stream_setup[subs->stream] = false;
+ if (--chip->stream_users == 0 && chip->streaming) {
+ babyface_stream_kill(chip);
+ dev_dbg(&chip->dev->dev, "stream stopped\n");
+ }
+ }
+ return 0;
+}
+
+/* Start the session unless it already runs - after an xrun the app
+ * prepares again and this is a no-op for the hardware.
+ */
+static int babyface_pcm_prepare(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ int ret = 0;
+
+ scoped_guard(mutex, &chip->mutex) {
+ if (chip->shutdown)
+ ret = -ENODEV;
+ else if (subs->runtime->rate != chip->rate ||
+ subs->runtime->period_size < bf_session_period(chip))
+ /* The other direction changed the rate or the URBs since
+ * this one was set up: it has to be set up again.
+ */
+ ret = -EINVAL;
+ else if (!chip->streaming)
+ ret = babyface_stream_start(chip);
+ }
+ if (ret < 0)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &chip->lock) {
+ chip->hw_ptr[subs->stream] = 0;
+ chip->prev_period[subs->stream] = 0;
+ }
+ return 0;
+}
+
+static int babyface_pcm_trigger(struct snd_pcm_substream *subs, int cmd)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+
+ switch (cmd) {
+ /* The session runs from prepare to hw_free; START and STOP only
+ * decide whether the URB handlers move this substream's audio.
+ */
+ case SNDRV_PCM_TRIGGER_START:
+ /* A URB error may have stopped the session since prepare. */
+ if (!READ_ONCE(chip->streaming))
+ return -EPIPE;
+ /* The other direction may have changed the rate or the
+ * URB geometry since this substream was prepared, and
+ * START bypasses prepare()'s check. Starting with a
+ * runtime that no longer matches the session would make
+ * an URB completion wrap the ring.
+ */
+ if (subs->runtime->rate != chip->rate ||
+ subs->runtime->period_size < bf_session_period(chip))
+ return -EPIPE;
+ scoped_guard(spinlock_irqsave, &chip->lock) {
+ chip->hw_ptr[subs->stream] = 0;
+ chip->prev_period[subs->stream] = 0;
+ }
+ return 0;
+ case SNDRV_PCM_TRIGGER_STOP:
+ return 0;
+ }
+ return -EINVAL;
+}
+
+/* The audio between hw_ptr and the converters, for runtime->delay.
+ * hw_ptr moves a URB at a time: playback when a URB is refilled, capture
+ * when one completes. A refilled OUT URB goes to the back of the queue,
+ * so the next frame the application writes waits behind the URBs in
+ * flight, whatever they carry.
+ *
+ * Both directions take their time from the last IN completion, the
+ * point where the device has finished a URB's worth of recording. By
+ * then it has also played one of the OUT URBs still counted in flight;
+ * that URB's completion only says its data reached the device. Since
+ * the IN completion, the device has recorded, and played, the time
+ * elapsed, measured with ktime and never more than one URB.
+ * snd-usb-audio estimates it from the USB frame counter, which counts
+ * in 1 ms steps, longer than a 32-frame URB at 48 kHz. With one time
+ * reference the estimate cancels in the sum of the two delays, the
+ * round trip an application compensates a recording with.
+ *
+ * On top of that queue each direction adds a fixed delay inside the
+ * device, per speed, in frames at the stream rate. The values are what
+ * RME's Windows driver reports for its ASIO buffer on a 2015 Babyface
+ * Pro, which RTL Utility measured to within one frame. Playback is
+ * 8 + 32 x the speed: the device's OUT buffer and the DA converter,
+ * the same on the FS. Capture is lowered by 7 frames to the FS, whose
+ * AD converter is 5 samples against 12 on the 2015 model, since the
+ * two share VID:PID and bcdDevice. So the 2015 model is reported a
+ * little short, and neither model too long. Caller holds chip->lock.
+ */
+static const struct {
+ u8 playback;
+ u8 capture;
+} bf_device_delay[] = {
+ [BF_ALT_1] = { 40, 16 },
+ [BF_ALT_2] = { 72, 24 },
+ [BF_ALT_3] = { 136, 38 },
+};
+
+static snd_pcm_uframes_t bf_delay(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs)
+{
+ s64 ns = ktime_to_ns(ktime_sub(ktime_get(), chip->in_done));
+ unsigned int urb = chip->urb_frames;
+ unsigned int moved;
+
+ moved = ns > 0 ? min_t(u64, div_u64((u64)ns * subs->runtime->rate,
+ NSEC_PER_SEC), urb) : 0;
+ if (subs->stream == SNDRV_PCM_STREAM_CAPTURE)
+ return moved + bf_device_delay[chip->alt].capture;
+ if (chip->out_inflight < 2)
+ return 0;
+ return (chip->out_inflight - 1) * urb - moved +
+ bf_device_delay[chip->alt].playback;
+}
+
+static snd_pcm_uframes_t babyface_pcm_pointer(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ snd_pcm_uframes_t pos;
+
+ guard(spinlock_irqsave)(&chip->lock);
+ pos = chip->hw_ptr[subs->stream] % subs->runtime->buffer_size;
+ subs->runtime->delay = bf_delay(chip, subs);
+ 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 urb_frames_min = 32;
+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, 0444);
+MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
+module_param(urb_frames_min, int, 0444);
+MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
+module_param(nurbs, int, 0444);
+MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");
+
+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 && chip->urbs_out[i]) {
+ usb_kill_urb(chip->urbs_out[i]);
+ usb_free_urb(chip->urbs_out[i]);
+ }
+ /* probe() allocates the six arrays separately and can
+ * fail between them, so each may be NULL here - this runs
+ * on the probe error path via snd_card_free().
+ */
+ if (chip->buf_in && chip->dma_in)
+ usb_free_coherent(chip->dev, urbsize,
+ chip->buf_in[i], chip->dma_in[i]);
+ if (chip->buf_out && chip->dma_out)
+ 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, 2, 16);
+ chip->frames_per_urb = clamp(frames_per_urb, 8, 1024) & ~7;
+ chip->urb_frames_min = min_t(unsigned int,
+ clamp(urb_frames_min, 8, 1024) & ~7,
+ chip->frames_per_urb);
+ chip->urb_frames = chip->frames_per_urb;
+ chip->urbs_active = chip->nurbs;
+ 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) {
+ err = -ENOMEM;
+ 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]) {
+ err = -ENOMEM;
+ 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;
+ /* Kill the session before cancelling the work: the URB handlers
+ * queue it, and an unplug is exactly when they see errors. Taking
+ * the mutex also waits out a prepare that was already past its
+ * ->shutdown check.
+ */
+ scoped_guard(mutex, &chip->mutex) {
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ }
+ 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);
+
+ 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..93ef4c3a1
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -0,0 +1,187 @@
+/* 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.
+ *
+ * babyfacepro.c holds the card lifecycle and the PCM stream.
+ *
+ * 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):
+ * - The URB size follows the application's period, and enough URBs
+ * stay in flight to cover two periods (bf_urb_frames(), hw_params):
+ * between urb_frames_min and frames_per_urb, at most nurbs URBs.
+ * A URB must be a whole number of alt packets - the device delivers IN
+ * data in alt-sized packets (448/640/1024 B, 8/16/32 frames for
+ * alt 1/2/3), and smaller URBs get -EOVERFLOW (babble).
+ * - Measured with fixed URBs, sweep 256->128->64->32->16
+ * (<= 128 kHz): with nurbs=8 the period floor is 32 frames
+ * (0.67 ms @ 48 kHz) without glitches;
+ * nurbs=16 drops it to 16 frames (0.33 ms). Soaks (5-15 min,
+ * 2026-08-25) refine this: period 32 is the zero-glitch floor
+ * (0 xruns both directions); period 16 is rock-solid on playback
+ * but the capture side drops ~1 buffer per 7 s (0.67 ms each -
+ * any scheduler hiccup overruns a 0.33 ms ring) - fine for
+ * monitoring, not for clean recording.
+ * - 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 */
+
+/* Upper limits of the stream geometry: the largest URB and the most
+ * URBs in flight. The application's period and buffer choose the
+ * values actually used below these; both are module params.
+ */
+#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
+
+/* Minimum time from the end of one stream session to the next session
+ * trigger. A session triggered within about 15 ms of the previous one's
+ * URBs stopping comes up with the outputs silent (measured on the
+ * hardware); this leaves a margin.
+ */
+#define BF_SESSION_GAP_MS 50
+
+/* Vendor requests (bmRequestType 0x40, value in wValue, no data phase). */
+#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).
+ */
+#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
+
+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; /* URB buffer size: the largest URB */
+ unsigned int urb_frames_min; /* smallest URB a period may choose */
+ unsigned int urb_frames; /* this session's URB size */
+ unsigned int urbs_active; /* this session's URBs in flight */
+ int session_dir; /* the direction that chose them */
+ 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; /* substreams set up (hw_params..hw_free) */
+ bool stream_setup[2]; /* per direction, counted in stream_users */
+ pid_t owner[2]; /* per direction: tgid that opened it */
+ bool streaming; /* URBs actually in flight */
+ ktime_t stream_stopped; /* when the last session's URBs stopped */
+ bool shutdown;
+ atomic_t urb_err; /* consecutive bad URBs (stops the stream) */
+ struct work_struct stream_work;
+
+ /* Set in open() and cleared in close() under ->lock. The URB
+ * handlers and babyface_pcm_stop_both() read it under
+ * rcu_read_lock(); close() waits them out with synchronize_rcu().
+ */
+ struct snd_pcm_substream __rcu *subs[2];
+ unsigned long hw_ptr[2];
+ unsigned long prev_period[2];
+ /* For runtime->delay: when the last IN URB completed, and the OUT
+ * URBs in flight. Under ->lock.
+ */
+ ktime_t in_done;
+ unsigned int out_inflight;
+};
+
+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.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-11-05 15:15 ` [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 15:31 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
` (5 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
Add the first mixer controls: the six output masters (volume + mute)
and the 6x14 crosspoint matrix. The device has no readback for any of
them, so the driver keeps the state host-side and the controls read
that cache.
The probe's cold init clears the mixer registers, so probe writes a
power-on default: every playback channel into every output at 0 dB,
the hardware inputs off until a mixer raises them, and the two analog
outputs (AN1/2, PH3/4) at -20 dB, the level the hardware DIM button
uses. AN1/2 is the first block of the one crosspoint map (0x0000),
not a separate "low map" beside a standard one. A volume change
leaves a muted output muted.
The masters count the 8-bit register's 0.5 dB steps over the front
panel's range: 0 is mute, 1 is -90 dB and BF_MASTER_STEP_MAX is +6 dB.
The control value is that step, so no log table converts it, and the
16-bit companion register is derived from the same step.
A userspace usbfs claim (PipeWire, a libusb mixer daemon) detaches and
re-probes the interface, and the cold init of the re-probe clears the
mixer registers; alsactl restores the user's settings once the card is
registered again.
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 | 656 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 60 ++-
sound/usb/babyfacepro/babyfacepro.h | 124 ++++-
4 files changed, 839 insertions(+), 3 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..942d4732c
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -0,0 +1,656 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
+ *
+ * ALSA control surface: the hardware mixer (output masters and mutes,
+ * the crosspoint matrix, preamps, routing flags, varispeed pitch).
+ *
+ * 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 pair)
+{
+ 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_XPOINT_BASE_L +
+ BF_REG_XPOINT_STRIDE * pair + 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_XPOINT_BASE_R +
+ BF_REG_XPOINT_STRIDE * pair + 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 },
+};
+
+/* The crosspoint blocks are in the canonical output order (AN1/2, PH3/4,
+ * AS1/2, ...), one per output pair at 0x34 * pair, see BF_REG_XPOINT_BASE_L.
+ * They used to be read as starting at 0x34, with the first two swapped,
+ * which put every pair from AS1/2 on the wrong output (see that comment).
+ */
+
+/* Master-register output order - the master map lists AN1/2 first
+ * (0x03E0) and the Phones master SECOND (0x03E2, HARDWARE-VERIFIED
+ * 2026-08-24). 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
+ * - a {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 };
+
+/* dBx2 -> 16-bit master (0x2000*2^(half_db/12), rounded), from 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);
+}
+
+/* A master control step (BF_MASTER_STEP_*) -> the two registers. */
+u8 bf_master_reg8(unsigned int step)
+{
+ if (step == 0)
+ return BF_MASTER_MUTE;
+ return (u8)clamp((int)step - BF_MASTER_STEP_0DB + BF_MASTER_8_0DB,
+ BF_MASTER_8_MIN, 0xff);
+}
+
+u16 bf_master_reg16(unsigned int step)
+{
+ if (step == 0)
+ return 0;
+ return bf_master_16bit((int)step - BF_MASTER_STEP_0DB);
+}
+
+/* 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 on a probe with no saved state it applies a
+ * factory routing so the card makes sound without any user-space mixer:
+ * the six playback channels feed every output at unity, and the
+ * hardware inputs (AN1-4, AS1/2, ADAT) are NOT routed - raising an
+ * input's crosspoint in a mixer is what monitors it.
+ *
+ * The inputs are left out deliberately. Routing all 14 sources into
+ * every output at unity (the earlier default) summed a live mic or line
+ * input straight into the main out and the headphones the moment the
+ * module loaded, before alsa-restore had a chance to put the user's own
+ * levels back - the feedback/level hazard issue #4 raised, and the "mic
+ * audible in the phones" surprise. 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.
+ *
+ * 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, for the same
+ * reason - the six playback channels into an output at unity sum, and
+ * -20 dB is plainly audible but safe. 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;
+ u16 level;
+
+ /* 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] = analog ? BF_MASTER_STEP_MINUS20 :
+ BF_MASTER_STEP_0DB;
+ chip->master[out][1] = chip->master[out][0];
+ chip->muted[out] = false;
+ }
+
+ /* Every playback source into every output pair, L and R, at 0 dB,
+ * and the hardware inputs off. The addresses use the source's
+ * idx_l/idx_r on the pair's 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 cleared.
+ */
+ for (out = 0; out < 6; out++) {
+ for (src = 0; src < 14; src++) {
+ /* Only the playback channels are routed out of the
+ * box; a hardware input stays out of every output
+ * until a mixer raises its crosspoint.
+ */
+ level = src < BF_SRC_PB1 ? 0 : BF_FADER_0DB;
+ ret = bf_xpoint_write(chip, out, src, level, level);
+ if (ret < 0)
+ return ret;
+ }
+ ret = bf_crosspoint_clear_cross(chip, out);
+ 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++) {
+ level = src < BF_SRC_PB1 ? 0 : BF_FADER_0DB;
+ chip->xpoint[out][src][0] = level;
+ chip->xpoint[out][src][1] = level;
+ }
+
+ /* Host settings word (clock source Internal). */
+ return bf_settings_write(chip);
+}
+
+/* The cold init's 0x16 clear leaves the 8-bit masters muted: the
+ * output stays silent until a master write lands (hardware-verified
+ * 2026-08-24 - only a write un-mutes the 8-bit register). Re-apply
+ * the six output masters + mutes from the cache; used by the state
+ * restore that follows the cold init.
+ */
+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 = bf_master_reg8(l);
+ u8 r8 = bf_master_reg8(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, bf_master_reg16(l),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(r),
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+/* dB TLV for the output masters: 0.5 dB per step, step 0 = mute,
+ * step 1 = -90 dB, 181 = 0 dB, and the top step 193 = +6 dB, above
+ * unity as on the OUT wheel (BF_MASTER_STEP_*). WirePlumber needs it
+ * to map the volume 1:1 to the hardware control instead of applying a
+ * software volume on top.
+ */
+static const DECLARE_TLV_DB_SCALE(bf_master_tlv, -9050, 50, 1);
+
+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 = BF_MASTER_STEP_MAX;
+ 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];
+ long nl = ucontrol->value.integer.value[0];
+ long nr = ucontrol->value.integer.value[1];
+ u16 l, r, wire_l, wire_r, flag;
+ int ret = 0;
+
+ /* Reject anything outside the declared range, so the registers and
+ * the cache stay in spec (the ALSA core only enforces this with
+ * CONFIG_SND_CTL_INPUT_VALIDATION). Check before narrowing:
+ * .value.integer.value[] is a long.
+ */
+ if (nl < 0 || nl > BF_MASTER_STEP_MAX ||
+ nr < 0 || nr > BF_MASTER_STEP_MAX)
+ return -EINVAL;
+ l = nl;
+ r = nr;
+
+ guard(mutex)(&chip->mutex);
+ if (l == chip->master[out][0] && r == chip->master[out][1])
+ return 0;
+
+ wire_l = chip->muted[out] ? 0 : l;
+ wire_r = chip->muted[out] ? 0 : r;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+ /* Both registers, as TotalMix writes them: the 8-bit is the analog
+ * level, the 16-bit the digital one.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_l),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_r),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_l),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_r),
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ return 1;
+}
+
+static int bf_mute_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ /* One switch for the pair: bf_mute_put() writes both channels
+ * together, so a count of 2 would silently ignore value[1].
+ */
+ uinfo->count = 1;
+ 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;
+
+ guard(mutex)(&chip->mutex);
+ if (muted == chip->muted[out])
+ return 0;
+
+ 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)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+ } 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_reg8(chip->master[out][0]),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_reg8(chip->master[out][1]),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_master_reg16(chip->master[out][0]),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_master_reg16(chip->master[out][1]),
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+ }
+ chip->muted[out] = muted;
+ return 1;
+}
+
+/* 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; the
+ * whole fader 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 pair's block of the map, see
+ * BF_REG_XPOINT_BASE_L.
+ *
+ * AN1/2 is the block at 0x0000. It used to be written as a "low map"
+ * beside a "standard map" that this function put at 0x0068 (AN1/2 was
+ * thought to be the second block, the Phones the first), and every other
+ * pair was written one block off. Measured on a Pro FS through the six
+ * digital loopbacks (2026-10-26), one source routed to one pair at a time:
+ * the AN1/2 control came out on AN1/2 and AS1/2, AS1/2 on ADAT3/4, ADAT3/4
+ * on ADAT5/6, ADAT5/6 on ADAT7/8, and ADAT7/8 on nothing (0x0138 is the FX
+ * send). That also explains the finding of 2026-09-14 that sweeping the
+ * "standard map" alone changed nothing on AN1/2: it was AS1/2's submix.
+ *
+ * The AN1/2 block has only ever been written without the rotating
+ * transaction bits of wIndex, and keeps being: issue #31 reads them as the
+ * gain's lowest bits, which is a separate question.
+ */
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+ u16 l, u16 r)
+{
+ const struct bf_source *s = &bf_sources[src];
+ u16 flag = 0;
+ int ret;
+
+ if (out) {
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ }
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_XPOINT_BASE_L +
+ BF_REG_XPOINT_STRIDE * out + s->idx_l) | flag);
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_XPOINT_BASE_R +
+ BF_REG_XPOINT_STRIDE * out + 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;
+ long nl = ucontrol->value.integer.value[0];
+ long nr = ucontrol->value.integer.value[1];
+ u16 l, r;
+ int ret = 0;
+
+ /* Check before narrowing: a value of 0x10000 or more would wrap to a
+ * passing u16 (see the note in bf_master_put()).
+ */
+ if (nl < 0 || nl > BF_FADER_TOP || nr < 0 || nr > BF_FADER_TOP)
+ return -EINVAL;
+ l = nl;
+ r = nr;
+
+ guard(mutex)(&chip->mutex);
+ if (l == chip->xpoint[out][src][0] && r == chip->xpoint[out][src][1])
+ return 0;
+
+ ret = bf_xpoint_write(chip, out, src, l, r);
+ if (ret < 0)
+ return ret;
+
+ chip->xpoint[out][src][0] = l;
+ chip->xpoint[out][src][1] = r;
+ return 1;
+}
+
+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);
+ if (!kctl)
+ return -ENOMEM;
+ /* 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);
+ if (!kctl)
+ return -ENOMEM;
+ strlcat(kctl->id.name, " Playback Volume", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->master_kctl[i] = kctl;
+
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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 ac1414adc..0bf94ff5d 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -5,7 +5,8 @@
* Core driver: USB vendor requests + cold init, interrupt-URB PCM
* streaming, and the card lifecycle (probe/disconnect/module entry).
*
- * See babyfacepro.h for the shared device state and register map.
+ * See babyfacepro.h for the shared device state and register map,
+ * and babyfacepro-ctl.c for the ALSA control surface.
*/
#include <linux/delay.h>
#include <linux/math64.h>
@@ -249,6 +250,42 @@ int bf_cold_init(struct snd_usb_babyface *chip)
return 0;
}
+/* Re-apply the whole cached mixer state to the device, whose registers
+ * the cold init cleared. Caller holds chip->mutex.
+ */
+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: one block per output pair, see BF_REG_XPOINT_BASE_L. */
+ for (out = 0; out < 6; 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, out);
+ 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);
+}
+
static bool babyface_capture_copy(struct snd_usb_babyface *chip,
struct snd_pcm_substream *subs,
const u8 *data, unsigned int frames)
@@ -1377,6 +1414,15 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ /* The 0x16 clear zeroed the mixer registers: apply the factory
+ * default routing to keep the outputs live out of the box. alsactl
+ * restores the user's settings once the card is registered.
+ */
+ err = babyface_write_default_mixer(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "default mixer 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);
@@ -1425,6 +1471,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);
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 93ef4c3a1..a3b118bed 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -11,7 +11,10 @@
* standalone (snd-usb-caiaq-style interrupt streaming) instead of an
* snd-usb-audio quirk.
*
- * babyfacepro.c holds the card lifecycle and the PCM stream.
+ * babyfacepro.c holds the card lifecycle, the PCM stream and the
+ * mixer-state persistence across re-probes;
+ * babyfacepro-ctl.c holds the ALSA control surface - the hardware
+ * mixer.
*
* The protocol (vendor requests + 14x32-bit frame layout) was
* reverse-engineered from Windows captures and validated on hardware -
@@ -90,8 +93,10 @@
/* Vendor requests (bmRequestType 0x40, value in wValue, no data phase). */
#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
@@ -114,6 +119,96 @@
*/
#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
+/* 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) */
+/* The crosspoint map, one block per output pair, in the canonical order
+ * (AN1/2, PH3/4, AS1/2, ADAT3/4, ADAT5/6, ADAT7/8), then the FX send bus:
+ *
+ * left side = 0x0000 + 0x34 * pair + idx_l
+ * right side = 0x001a + 0x34 * pair + idx_r (bReq 0x12)
+ *
+ * Measured on a Pro FS through the six digital loopbacks (2026-10-26, and
+ * issue #31 from captures of RME's driver): the block at 0x0034 feeds
+ * PH3/4, 0x0068 AS1/2, 0x009c ADAT3/4, 0x00d0 ADAT5/6, 0x0104 ADAT7/8 and
+ * 0x0138 the FX send. AN1/2 is the block at 0x0000, the one this file
+ * used to call the "low map": it is not a layer beside a standard map but
+ * the first pair of the one map. The map used to start at 0x0034 with
+ * the blocks swapped, which fed every pair from AS1/2 on the wrong output.
+ */
+#define BF_REG_XPOINT_BASE_L 0x0000 /* + 0x34*pair + idx_l */
+#define BF_REG_XPOINT_BASE_R 0x001a /* + 0x34*pair + idx_r */
+#define BF_REG_XPOINT_STRIDE 0x0034
+/* The AN1/2 block, for the features that only reach that output. */
+#define BF_REG_LOWMAP_BASE_L BF_REG_XPOINT_BASE_L
+#define BF_REG_LOWMAP_BASE_R BF_REG_XPOINT_BASE_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
+
+/* Each output has two master registers, and TotalMix writes both. The
+ * 8-bit one is the analog level (hardware-verified 2026-08-24: writing
+ * it changes the level of an analog output, the 16-bit does not), in
+ * 0.5 dB steps: 0xf3 = 0 dB (the scene-load default), bottom 0x73 =
+ * -64 dB (silence), top 0xff = +6 dB, mute 0x3B. The 16-bit one is
+ * linear (BF_MASTER_0DB) and is the level of the digital outputs.
+ */
+#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
+
+/* The output master controls count the 8-bit register's 0.5 dB steps,
+ * over the front-panel OUT wheel's range: 0 is mute, 1 is -90 dB and
+ * BF_MASTER_STEP_MAX is +6 dB. Below -64 dB the 8-bit register stays
+ * at its bottom and only the 16-bit one goes lower.
+ */
+#define BF_MASTER_STEP_0DB 181
+#define BF_MASTER_STEP_MAX 193
+#define BF_MASTER_STEP_MINUS20 (BF_MASTER_STEP_0DB - 40)
+
+/* 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 (babyfacepro-ctl.c). */
+extern const struct bf_source bf_sources[14];
+
+/* The first playback source in bf_sources[]: the four analog inputs, the
+ * optical input and the three ADAT pairs come first, the six playback
+ * channels last. babyface_write_default_mixer() routes only the
+ * playback out of the box; a hardware input is not monitored into any
+ * output until a mixer raises its crosspoint.
+ */
+#define BF_SRC_PB1 8
+
struct snd_usb_babyface {
struct snd_card *card;
struct usb_device *dev;
@@ -160,6 +255,13 @@ struct snd_usb_babyface {
*/
ktime_t in_done;
unsigned int out_inflight;
+
+ /* mixer state (no gain readback exists - host-side mirror) */
+ u8 flag_cnt; /* 0xc000/0x4000/0x8000/0x0000 */
+ struct snd_kcontrol *master_kctl[6];
+ u16 master[6][2]; /* cached masters, in BF_MASTER_STEP_* */
+ bool muted[6];
+ u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
};
struct bf_rate {
@@ -185,3 +287,23 @@ 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. */
+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 pair);
+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 (babyfacepro-ctl.c). */
+int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
+u8 bf_master_reg8(unsigned int step); /* master step -> 8-bit register */
+u16 bf_master_reg16(unsigned int step); /* master step -> 16-bit register */
--
2.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-11-05 15:15 ` [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-11-05 15:15 ` [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 15:32 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
` (4 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
Add the input side of the mixer: phantom power and pad, the instrument
ref level, the four preamp gains, and phase invert, stereo split and
trim. The mic gain byte is packed: bits 0-4 count 3 dB steps and bits
5-7 hold the 0-2 dB remainder. Phase, split and trim have no
registers of their own: the device implements them on the AN1/2
crosspoint registers, and trim writes the trim alone on the master
curve (0x2000 = 0 dB).
bf_state_apply_flags() re-applies phase, split and trim on top of a
restored crosspoint state. A failed preamp write leaves the cached
state as it was.
Co-developed-by: David Fredman <davfre@gmail.com>
Signed-off-by: David Fredman <davfre@gmail.com>
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 624 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 111 ++++-
sound/usb/babyfacepro/babyfacepro.h | 79 +++
3 files changed, 813 insertions(+), 1 deletion(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 942d4732c..fe6266ea1 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -654,3 +654,627 @@ int babyface_create_masters(struct snd_usb_babyface *chip)
return 0;
}
+
+int bf_preamp_state_write(struct snd_usb_babyface *chip)
+{
+ int ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, chip->preamp, BF_REG_PREAMP);
+ if (ret < 0)
+ return ret;
+ /* Boost's 0x21 commit value (0x0003) is NOT a persisted register
+ * bit - PROTOCOL.md's "Ref level" section found it only in the
+ * one-shot 0x21 value alongside the 0x17 state write, so it has
+ * to be re-sent alongside EVERY preamp write (phantom/PAD toggles
+ * included), or Boost would silently degrade to plain -10dBV the
+ * next time anything else touches this shared byte.
+ */
+ return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT,
+ chip->ref_level == BF_REF_LEVEL_BOOST ?
+ 0x0003 : 0x0000, 0x0000);
+}
+
+/* Phase (polarity) invert (AN1-4 only, PROTOCOL.md "Phase toggle",
+ * hardware-verified 2026-08-23): NEGATE (bitwise NOT, not two's
+ * complement) the L crosspoint register on every output pair's
+ * standard map, plus the AN1/2 low-map shadow specifically (the same
+ * single low-map register set CUE/mute/solo already use for that
+ * monitor bus - see this driver's own bf_ms_put for the address
+ * pattern). `chip->xpoint[out][mic][0]` is deliberately left holding
+ * the PLAIN value the user actually set - only the value WRITTEN to
+ * hardware is negated - so the crosspoint control's own readback still
+ * reports the real fader position while phase is engaged.
+ *
+ * KNOWN LIMITATION, same class TuxMix's own USB backend already has
+ * in `usb.rs::set_phase` (not fixed there either, as of this writing):
+ * this negates the CURRENT register value once, at toggle time. A
+ * later `bf_xpoint_put` on the same [out][mic] slot (i.e. the user
+ * drags that fader again while phase is engaged) writes the plain
+ * value, silently un-inverting phase until the user re-toggles it.
+ * Making the crosspoint hot path itself phase-aware would close this
+ * properly, but touches every one of the 84 crosspoint controls'
+ * write path - out of scope for this pass; flagged rather than
+ * silently shipped.
+ */
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert)
+{
+ const struct bf_source *s = &bf_sources[mic];
+ int out, ret;
+ u16 flag;
+
+ for (out = 0; out < 6; out++) {
+ u16 plain = chip->xpoint[out][mic][0];
+ u16 value = invert ? (u16)~plain : plain;
+
+ /* The AN1/2 block is written without the rotating bits, as
+ * bf_xpoint_write() does.
+ */
+ flag = 0;
+ if (out) {
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ }
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+ (BF_REG_XPOINT_BASE_L +
+ BF_REG_XPOINT_STRIDE * out + s->idx_l) |
+ flag);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+static int bf_phase_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_phase_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->phase[mic];
+ return 0;
+}
+
+static int bf_phase_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+ bool invert = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ guard(mutex)(&chip->mutex);
+ if (invert == chip->phase[mic])
+ return 0;
+ ret = bf_phase_apply(chip, mic, invert);
+ if (ret < 0)
+ return ret;
+ chip->phase[mic] = invert;
+ return 1;
+}
+
+/* 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"). Only reaches the AN1/2
+ * destination (its block of the map), the same scope as CUE/mute/solo.
+ * `chip->xpoint[][]` is deliberately left untouched, same reasoning as
+ * Phase.
+ *
+ * Joining the pair again puts back the pair's own AN1/2 crosspoint from
+ * the cache. It used to write L=R=0x1000, read as "-6 dB per side",
+ * which issue #31 found to be 6 dB below what TotalMix FX writes for a
+ * joined pair at 0 dB (0x2000 per side), and which also dropped
+ * whatever level the user had set on that crosspoint.
+ */
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split)
+{
+ const struct bf_source *s = &bf_sources[8 + pb];
+ u16 l = split ? 0x2000 : chip->xpoint[0][8 + pb][0];
+ u16 r = split ? 0x0000 : chip->xpoint[0][8 + pb][1];
+ int ret;
+
+ /* The AN1/2 block. The same values also went to a "standard map"
+ * half at 0x0068, which is AS1/2's submix, not part of AN1/2: it
+ * overwrote AS1/2's own crosspoint for this playback pair.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ BF_REG_XPOINT_BASE_L + s->idx_l);
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ BF_REG_XPOINT_BASE_R + s->idx_r);
+}
+
+static int bf_split_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int pb = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->split[pb];
+ return 0;
+}
+
+static int bf_split_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int pb = kctl->private_value;
+ bool split = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ guard(mutex)(&chip->mutex);
+ if (split == chip->split[pb])
+ return 0;
+ ret = bf_split_apply(chip, pb, split);
+ if (ret < 0)
+ return ret;
+ chip->split[pb] = split;
+ return 1;
+}
+
+/* Input Trim (T button, AN1-4): PROTOCOL.md "Trim (T) write for the
+ * AN1/2 pair" (cap_trim2/3/4.pcap, hardware-verified) - the analog
+ * input's own gain-trim, applied through the crosspoint registers
+ * exactly like a fader (there is no separate trim register). It
+ * writes the trim alone on the MASTER curve (0x2000 = 0 dB,
+ * `bf_master_16bit`) into the AN1/2 block, all 4 registers of the pair
+ * (both AN1+AN2 or both AN3+AN4, matching the vendor software's
+ * linked-strip behaviour); `mic` may be either channel of the pair, and
+ * the base is derived (`mic & ~1`) so the write always lands on the
+ * correct pair's registers regardless of which channel's control
+ * triggered it. Destination is always the AN1/2 monitor bus, the same
+ * scope the MS-processor and CUE writes have, and the same one the
+ * vendor software's Trim reaches.
+ *
+ * It also wrote the fader and the trim summed, on the fader curve, to a
+ * "standard map" at 0x0068. That block is AS1/2's submix (see
+ * bf_xpoint_write), so those writes never reached AN1/2: they only
+ * overwrote AS1/2's own crosspoints for these inputs, and are gone.
+ *
+ * Trim is a genuinely SHARED value per pair on real hardware (one
+ * write always touches both channels' registers) but is exposed as 2
+ * per-channel ALSA controls, one per input strip. `bf_trim_put` keeps
+ * the pair's two cache entries equal and notifies the sibling control,
+ * so the cache never claims a per-channel split the hardware cannot
+ * represent - and `bf_state_apply_flags`, which replays the pair from
+ * its even index, always replays the value that is actually on the
+ * wire.
+ */
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2)
+{
+ int base = mic & ~1;
+ int sib = base + 1;
+ const struct bf_source *sb = &bf_sources[base];
+ const struct bf_source *ss = &bf_sources[sib];
+ u16 trim_raw = bf_master_16bit(trim_db2);
+ int ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_L + sb->idx_l);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_R + sb->idx_r);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_L + ss->idx_l);
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_R + ss->idx_r);
+}
+
+/* Trim's control value is dB as well, -65..+6. */
+static const DECLARE_TLV_DB_SCALE(bf_trim_tlv, -6500, 100, 0);
+
+static int bf_trim_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = -65;
+ uinfo->value.integer.max = 6;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_trim_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->trim[mic];
+ return 0;
+}
+
+static int bf_trim_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+ int sib = mic ^ 1;
+ int db = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (db < -65 || db > 6)
+ return -EINVAL;
+
+ scoped_guard(mutex, &chip->mutex) {
+ if (db == chip->trim[mic])
+ return 0;
+ ret = bf_trim_apply(chip, mic, db * 2);
+ if (ret < 0)
+ return ret;
+ /* 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;
+ }
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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;
+ chip->trim_kctl[src] = kctl;
+ }
+ return 0;
+}
+
+static int bf_bool_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_phantom_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] =
+ !!(chip->preamp & kctl->private_value);
+ return 0;
+}
+
+static int bf_phantom_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ u16 bit = kctl->private_value;
+ bool on = ucontrol->value.integer.value[0];
+ bool cur;
+ u16 old;
+ int ret = 0;
+
+ guard(mutex)(&chip->mutex);
+ cur = !!(chip->preamp & bit);
+ old = chip->preamp;
+ if (on == cur)
+ return 0;
+ chip->preamp = on ? (chip->preamp | bit) : (chip->preamp & ~bit);
+ ret = bf_preamp_state_write(chip);
+ if (ret < 0) {
+ /* Do not leave the cache claiming a state the device
+ * never took.
+ */
+ chip->preamp = old;
+ return ret;
+ }
+ return 1;
+}
+
+/* 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;
+
+ guard(mutex)(&chip->mutex);
+ if ((int)item == chip->ref_level)
+ return 0;
+ 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;
+ return ret;
+ }
+ return 1;
+}
+
+/* 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;
+}
+
+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;
+
+ guard(mutex)(&chip->mutex);
+ if (db == chip->gain[mic])
+ return 0;
+ raw = bf_gain_raw(mic, db);
+
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, (u16)raw,
+ BF_REG_GAIN + mic);
+ if (ret < 0)
+ return ret;
+ chip->gain[mic] = db;
+ return 1;
+}
+
+/* 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;
+
+ /* The control name is the type and .index is the channel (0 =
+ * Mic 1/AN1, 1 = Mic 2/AN2), the convention the companion mixer
+ * and existing asound.state files use.
+ */
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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);
+ if (!kctl)
+ return -ENOMEM;
+ 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 0bf94ff5d..019a46fc9 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -255,7 +255,21 @@ int bf_cold_init(struct snd_usb_babyface *chip)
*/
int babyface_restore_state(struct snd_usb_babyface *chip)
{
- int out, src, ret;
+ int out, src, mic, ret;
+
+ /* Preamp state + commit. */
+ ret = bf_preamp_state_write(chip);
+ if (ret < 0)
+ return ret;
+
+ /* The four input gains. */
+ for (mic = 0; mic < 4; mic++) {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ (u16)bf_gain_raw(mic, chip->gain[mic]),
+ BF_REG_GAIN + mic);
+ if (ret < 0)
+ return ret;
+ }
/* Masters (8-bit = the real volume) + mutes. */
ret = bf_apply_masters(chip);
@@ -286,6 +300,68 @@ int babyface_restore_state(struct snd_usb_babyface *chip)
return bf_pitch_write(chip, chip->pitch);
}
+/* Re-apply the per-input state that sits on top of the crosspoints
+ * (phase invert, stereo split, trim) after a state restore. The write
+ * patterns mirror the corresponding _put() handlers. Caller holds
+ * chip->mutex.
+ */
+int bf_state_apply_flags(struct snd_usb_babyface *chip)
+{
+ int ret;
+
+ /* Re-apply any engaged Phase invert - the crosspoint restore loop
+ * above already re-wrote xpoint[][] as PLAIN values, so a phase
+ * negation needs to be re-asserted on top, the same way the ON
+ * state itself is applied (bf_phase_apply).
+ */
+ {
+ int mic;
+
+ for (mic = 0; mic < 4; mic++) {
+ if (!chip->phase[mic])
+ continue;
+ ret = bf_phase_apply(chip, mic, true);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ /* Re-apply any engaged stereo split (fixed constants, no fader
+ * dependency - see bf_split_apply's own comment).
+ */
+ {
+ int pb;
+
+ for (pb = 0; pb < 6; pb++) {
+ if (!chip->split[pb])
+ continue;
+ ret = bf_split_apply(chip, pb, true);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ /* Re-apply any non-zero Trim (fader+trim combined, same reasoning
+ * as phase - see bf_trim_apply's own comment). Only the pair's
+ * even index needs to fire this (it always writes both channels),
+ * and bf_trim_put keeps both entries of a pair equal, so the even
+ * index holds the value that is really on the wire even when the
+ * odd channel's control was the one the user touched.
+ */
+ {
+ int mic;
+
+ for (mic = 0; mic < 4; mic += 2) {
+ if (!chip->trim[mic])
+ continue;
+ ret = bf_trim_apply(chip, mic, chip->trim[mic] * 2);
+ if (ret < 0)
+ return ret;
+ }
+ }
+ return 0;
+}
+
static bool babyface_capture_copy(struct snd_usb_babyface *chip,
struct snd_pcm_substream *subs,
const u8 *data, unsigned int frames)
@@ -1320,6 +1396,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) {
@@ -1359,6 +1436,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);
@@ -1414,6 +1492,25 @@ 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 and the ref level; it persists across power
+ * cycles). Boost shares -10dBV's bits and only exists in the
+ * one-shot commit value, so it cannot be recovered here.
+ */
+ 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] & (BF_PREAMP_REF_MASK |
+ BF_PREAMP_48V_MIC1 |
+ BF_PREAMP_48V_MIC2 |
+ BF_PREAMP_PAD_MIC1 |
+ BF_PREAMP_PAD_MIC2);
+ chip->ref_level =
+ (chip->preamp & BF_PREAMP_REF_MASK) == BF_PREAMP_REF_4DBU ?
+ BF_REF_LEVEL_4DBU : BF_REF_LEVEL_MINUS10DBV;
+ }
+
/* The 0x16 clear zeroed the mixer registers: apply the factory
* default routing to keep the outputs live out of the box. alsactl
* restores the user's settings once the card is registered.
@@ -1483,6 +1580,18 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ err = babyface_create_trim(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "phase/split/trim control creation failed: %d\n", err);
+ goto error;
+ }
+
+ err = babyface_create_preamp(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "preamp control creation failed: %d\n", err);
+ goto error;
+ }
+
err = snd_card_register(chip->card);
if (err < 0) {
dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index a3b118bed..06af1aa82 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -119,6 +119,45 @@
*/
#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
+/* Preamp state byte (0x17, wIdx 0x003F - full state, verified).
+ * NOTE 2026-08-26 (cap_reflevel3.pcap): the 0x0C "base" is NOT a
+ * constant - it is the Instr 3/4 REF-LEVEL bits (bits 2-3, +4dBu =
+ * 0x0C set; -10dBV/Boost = clear; Boost additionally commits 0x21
+ * wVal 0x0003). Keeping it always set = forcing the default +4dBu,
+ * which is correct for the driver (no ref-level control).
+ */
+#define BF_REG_PREAMP 0x003f
+#define BF_REG_GAIN 0x0000 /* + mic 0-3 (bReq 0x1a) */
+#define BF_PREAMP_REF_4DBU 0x000c
+#define BF_PREAMP_REF_MASK 0x000c
+#define BF_PREAMP_BASE BF_PREAMP_REF_4DBU
+#define BF_PREAMP_48V_MIC1 0x0001
+#define BF_PREAMP_48V_MIC2 0x0002
+#define BF_PREAMP_PAD_MIC1 0x0010
+#define BF_PREAMP_PAD_MIC2 0x0020
+
+/* Ref Level (Instr 3/4) - PROTOCOL.md "Ref level (Instr 3/4) - LABELED"
+ * (cap_reflevel2.pcap, hardware-verified): a single shared 3-state
+ * switch for the Instrument pair. +4dBu/-10dBV are bits 2-3 of the
+ * preamp byte (BF_PREAMP_REF_MASK); Boost shares -10dBV's bits and is
+ * distinguished only by the 0x21 commit value (0x0003, not the usual
+ * 0x0000) - not a persisted register bit, so it must be tracked
+ * host-side (chip->ref_level) and re-asserted on every preamp write,
+ * not just the one that engaged it (see bf_preamp_state_write).
+ */
+#define BF_REF_LEVEL_4DBU 0
+#define BF_REF_LEVEL_MINUS10DBV 1
+#define BF_REF_LEVEL_BOOST 2
+
+/* Preamp gain: 0-65 dB in 1 dB steps, packed coarse/fine (see the
+ * gain-scale comment above bf_gain_max_db).
+ */
+#define BF_GAIN_MAX_DB 65
+/* Mic gain is packed: bits 0-4 coarse (3 dB), bits 5-7 the 0-2 dB rest. */
+#define BF_GAIN_COARSE_MASK 0x1f
+#define BF_GAIN_COARSE_MAX 20
+#define BF_GAIN_FINE_SHIFT 5
+
/* Register addresses (masters + crosspoint matrix). */
#define BF_REG_MASTER_16 0x03e0 /* + 2*out (bReq 0x12) */
#define BF_REG_MASTER_8 0x0004 /* + 2*out (bReq 0x1a) */
@@ -262,6 +301,37 @@ struct snd_usb_babyface {
u16 master[6][2]; /* cached masters, in BF_MASTER_STEP_* */
bool muted[6];
u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
+ u16 preamp; /* 48V/PAD bits, base 0x0c */
+ u8 gain[4]; /* preamp gain in dB 0-65/9 (raw derived
+ * at write: mic packed coarse/fine,
+ * instr 0.5 dB/step)
+ */
+ bool phase[4]; /* polarity invert, AN1-4 (bf_sources 0-3);
+ * xpoint[][0..3][0] stays the PLAIN
+ * value, only the wire write is
+ * negated - see bf_phase_put's own
+ * comment for the known limitation
+ * this implies.
+ */
+ int trim[4]; /* Trim (T), dB (-65..+6), AN1-4;
+ * one shared register per pair, so
+ * both entries of a pair are kept
+ * equal; same "wire-only" caveat as
+ * phase - see bf_trim_apply's own
+ * comment.
+ */
+ bool split[6]; /* stereo split, playback pairs PB1-PB6
+ * (bf_sources idx 8-13); fixed
+ * constants (PROTOCOL.md "Stereo
+ * split"), not derived from the
+ * fader - xpoint[][] is left
+ * untouched, same as phase.
+ */
+ int ref_level; /* Instr 3/4 ref level, one of the
+ * BF_REF_LEVEL_* values
+ * (0 = +4dBu, the default)
+ */
+ struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
};
struct bf_rate {
@@ -290,6 +360,7 @@ extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
/* Mixer-state persistence across interface re-probes. */
int babyface_restore_state(struct snd_usb_babyface *chip);
+int bf_state_apply_flags(struct snd_usb_babyface *chip);
/* babyfacepro-ctl.c */
extern const u16 bf_flag_cycle[4];
@@ -302,6 +373,14 @@ 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);
+u8 bf_gain_raw(int mic, int db);
/* Master gain-law helpers (babyfacepro-ctl.c). */
int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
--
2.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (2 preceding siblings ...)
2026-11-05 15:15 ` [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 15:32 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
` (3 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Add loopback, AN 1>2, the sample clock source, the AN1/2 input link,
the MS processor, DIM, width, FX send and the varispeed pitch. The
clock source is not a register write: it is a bit of the host settings
word sent with the keepalive, so bf_settings_write() now composes that
word from the tracked state. The optical output's format is a second
bit of that word: "Optical Out Format" switches the port between ADAT
(the default) and S/PDIF, and the bit follows every settings write.
bf_state_apply_flags() re-applies this state too, and probe also runs
it after writing the power-on default, since the cold init cleared the
flag registers. A Phones master change made while DIM is engaged
moves the level DIM releases back to.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 823 ++++++++++++++++++++++--
sound/usb/babyfacepro/babyfacepro.c | 117 +++-
sound/usb/babyfacepro/babyfacepro.h | 35 +-
3 files changed, 902 insertions(+), 73 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index fe6266ea1..5482b6f09 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -10,8 +10,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>
@@ -252,7 +254,9 @@ int babyface_write_default_mixer(struct snd_usb_babyface *chip)
chip->xpoint[out][src][1] = level;
}
- /* Host settings word (clock source Internal). */
+ /* Host settings word - composed from tracked state (clock defaults
+ * to Internal, chip->clock_optical is zero-initialized).
+ */
return bf_settings_write(chip);
}
@@ -325,6 +329,33 @@ static int bf_master_get(struct snd_kcontrol *kctl,
return 0;
}
+/* The master step to put on the wire. The cache keeps the real
+ * volume: mute wins, and DIM is the fixed -20 dB step on the Phones
+ * pair, so every writer derives the wire value the same way.
+ */
+/* The two master registers to put on the wire. The cache keeps the
+ * real volume: mute wins, and DIM is the exact -20 dB pair the
+ * hardware's own button writes (0xcb / 0x0333, cap_dim2.pcap), not the
+ * master curve's value at that step.
+ */
+static u8 bf_out_reg8(bool muted, bool dim, u16 step)
+{
+ if (muted)
+ return BF_MASTER_MUTE;
+ if (dim)
+ return BF_MASTER_MINUS20_8;
+ return bf_master_reg8(step);
+}
+
+static u16 bf_out_reg16(bool muted, bool dim, u16 step)
+{
+ if (muted)
+ return 0;
+ if (dim)
+ return BF_MASTER_MINUS20_16;
+ return bf_master_reg16(step);
+}
+
static int bf_master_put(struct snd_kcontrol *kctl,
struct snd_ctl_elem_value *ucontrol)
{
@@ -332,7 +363,8 @@ static int bf_master_put(struct snd_kcontrol *kctl,
int out = bf_master_out[kctl->private_value];
long nl = ucontrol->value.integer.value[0];
long nr = ucontrol->value.integer.value[1];
- u16 l, r, wire_l, wire_r, flag;
+ bool muted, dim;
+ u16 l, r, flag;
int ret = 0;
/* Reject anything outside the declared range, so the registers and
@@ -350,33 +382,40 @@ static int bf_master_put(struct snd_kcontrol *kctl,
if (l == chip->master[out][0] && r == chip->master[out][1])
return 0;
- wire_l = chip->muted[out] ? 0 : l;
- wire_r = chip->muted[out] ? 0 : r;
+ muted = chip->muted[out];
+ dim = out == 1 && chip->dim;
flag = bf_flag_cycle[chip->flag_cnt];
chip->flag_cnt = (chip->flag_cnt + 1) & 3;
/* Both registers, as TotalMix writes them: the 8-bit is the analog
* level, the 16-bit the digital one.
*/
- ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_l),
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, l),
BF_REG_MASTER_8 + 2 * out);
if (ret < 0)
return ret;
- ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_r),
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, r),
BF_REG_MASTER_8 + 2 * out + 1);
if (ret < 0)
return ret;
- ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_l),
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(muted, dim, l),
(BF_REG_MASTER_16 + 2 * out) | flag);
if (ret < 0)
return ret;
- ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_r),
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(muted, dim, r),
(BF_REG_MASTER_16 + 2 * out + 1) | flag);
if (ret < 0)
return ret;
chip->master[out][0] = l;
chip->master[out][1] = r;
+ /* A Phones change while DIM is engaged re-bases the restore point. */
+ if (chip->dim && out == 1) {
+ chip->dim_saved[0] = l;
+ chip->dim_saved[1] = r;
+ }
return 1;
}
@@ -411,6 +450,7 @@ static int bf_mute_put(struct snd_kcontrol *kctl,
struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
int out = bf_master_out[kctl->private_value];
bool muted = !ucontrol->value.integer.value[0];
+ bool dim;
u16 flag;
int ret = 0;
@@ -418,51 +458,33 @@ static int bf_mute_put(struct snd_kcontrol *kctl,
if (muted == chip->muted[out])
return 0;
+ dim = out == 1 && chip->dim;
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)
- return ret;
- ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
- BF_REG_MASTER_8 + 2 * out + 1);
- if (ret < 0)
- return ret;
- ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
- (BF_REG_MASTER_16 + 2 * out) | flag);
- if (ret < 0)
- return ret;
- ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
- (BF_REG_MASTER_16 + 2 * out + 1) | flag);
- if (ret < 0)
- return ret;
- } 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_reg8(chip->master[out][0]),
- BF_REG_MASTER_8 + 2 * out);
- if (ret < 0)
- return ret;
- ret = bf_vendor_write(chip, BF_REQ_GAIN,
- bf_master_reg8(chip->master[out][1]),
- BF_REG_MASTER_8 + 2 * out + 1);
- if (ret < 0)
- return ret;
- ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
- bf_master_reg16(chip->master[out][0]),
- (BF_REG_MASTER_16 + 2 * out) | flag);
- if (ret < 0)
- return ret;
- ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
- bf_master_reg16(chip->master[out][1]),
- (BF_REG_MASTER_16 + 2 * out + 1) | flag);
- if (ret < 0)
- return ret;
- }
+ /* TotalMix keeps the pre-mute fader value host-side, so unmute
+ * restores it; mute is the 8-bit mute code and a zero 16-bit.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_out_reg8(muted, dim, chip->master[out][0]),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_out_reg8(muted, dim, chip->master[out][1]),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(muted, dim, chip->master[out][0]),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(muted, dim, chip->master[out][1]),
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
chip->muted[out] = muted;
return 1;
}
@@ -655,6 +677,20 @@ int babyface_create_masters(struct snd_usb_babyface *chip)
return 0;
}
+/* 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, AN 1>2, link and
+ * the state replay included), or Boost would silently degrade to plain
+ * -10dBV the next time anything else touches this shared byte.
+ */
+int bf_preamp_commit(struct snd_usb_babyface *chip)
+{
+ return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT,
+ chip->ref_level == BF_REF_LEVEL_BOOST ?
+ 0x0003 : 0x0000, 0x0000);
+}
+
int bf_preamp_state_write(struct snd_usb_babyface *chip)
{
int ret;
@@ -662,16 +698,7 @@ int bf_preamp_state_write(struct snd_usb_babyface *chip)
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);
+ return bf_preamp_commit(chip);
}
/* Phase (polarity) invert (AN1-4 only, PROTOCOL.md "Phase toggle",
@@ -1278,3 +1305,679 @@ 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);
+ long np = ucontrol->value.integer.value[0];
+ int p, ret = 0;
+
+ if (np < -50 || np > 50)
+ return -EINVAL;
+ p = np;
+
+ guard(mutex)(&chip->mutex);
+ if (p == chip->pitch)
+ return ret;
+
+ /* 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)
+ return ret;
+ chip->pitch = p;
+ return 1;
+}
+
+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 prev = -1, notify = -1;
+ int i, ret = 0;
+
+ scoped_guard(mutex, &chip->mutex) {
+ if (on == chip->loopback[out])
+ return 0;
+ for (i = 0; i < 6; i++)
+ if (chip->loopback[i])
+ prev = i;
+ ret = bf_loopback_write_map(chip, out, on);
+ if (ret < 0)
+ return ret;
+ /* Single-active model (TotalMix writes one pair at 0x0001,
+ * the rest 0x0000): toggling one output clears the others.
+ * The cleared one did not get its own put(), so tell user
+ * space.
+ */
+ memset(chip->loopback, 0, sizeof(chip->loopback));
+ chip->loopback[out] = on;
+ if (on && prev >= 0 && prev != out)
+ notify = prev;
+ ret = 1;
+ }
+ if (notify >= 0 && chip->loopback_kctl[notify])
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->loopback_kctl[notify]->id);
+ 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;
+
+ guard(mutex)(&chip->mutex);
+ if (an12 == chip->an12)
+ return 0;
+ v = (chip->linked ? 0x0400 : 0x0000) | (an12 ? 0x1000 : 0x0000);
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+ if (ret < 0)
+ return ret;
+ ret = bf_preamp_commit(chip);
+ if (ret < 0)
+ return ret;
+ chip->an12 = an12;
+ return 1;
+}
+
+/* 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);
+ unsigned int item = ucontrol->value.enumerated.item[0];
+ bool optical;
+ int ret = 0;
+
+ if (item > 1)
+ return -EINVAL;
+ optical = item;
+
+ guard(mutex)(&chip->mutex);
+ if (optical == chip->clock_optical)
+ return 0;
+ chip->clock_optical = optical;
+ ret = bf_settings_write(chip);
+ if (ret < 0) {
+ chip->clock_optical = !optical;
+ return ret;
+ }
+ return 1;
+}
+
+/* Optical Out format: bit 10 of the same BF_REG_KEEPALIVE_SETTINGS
+ * word (PROTOCOL.md, LINUX-VALIDATION.md section 7). The single optical
+ * port carries ADAT by default; S/PDIF mode uses only AS1/2. Like the
+ * clock source, this is not a register write of its own.
+ */
+static const char *const bf_optical_texts[] = {
+ "ADAT", "S/PDIF", NULL
+};
+
+static int bf_optical_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 2, bf_optical_texts);
+}
+
+static int bf_optical_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->optical_spdif ? 1 : 0;
+ return 0;
+}
+
+static int bf_optical_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];
+ bool spdif;
+ int ret = 0;
+
+ if (item > 1)
+ return -EINVAL;
+ spdif = item;
+
+ guard(mutex)(&chip->mutex);
+ if (spdif == chip->optical_spdif)
+ return 0;
+ chip->optical_spdif = spdif;
+ ret = bf_settings_write(chip);
+ if (ret < 0) {
+ chip->optical_spdif = !spdif;
+ return ret;
+ }
+ return 1;
+}
+
+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;
+
+ guard(mutex)(&chip->mutex);
+ if (linked == chip->linked)
+ return 0;
+ v = (linked ? 0x0400 : 0x0000) | (chip->an12 ? 0x1000 : 0x0000);
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+ if (ret < 0)
+ return ret;
+ ret = bf_preamp_commit(chip);
+ if (ret < 0)
+ return ret;
+ chip->linked = linked;
+ return 1;
+}
+
+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
+ * the four AN2 (side) crosspoints, AN1/2 0x0001/0x001B and PH3/4
+ * 0x0035/0x004F - the side path is muted (ear-verified 2026-08-26
+ * with the mic on AN2: MS ON = silence); release restores each
+ * block's 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;
+
+ guard(mutex)(&chip->mutex);
+ if (on == chip->ms_proc)
+ return 0;
+ if (on) {
+ 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;
+ } else {
+ /* Restore each block's own AN2 crosspoint: 0x0001/0x001B
+ * is the AN1/2 pair, 0x0035/0x004F is PH3/4.
+ */
+ ret = bf_xpoint_write(chip, 0, 1,
+ chip->xpoint[0][1][0],
+ chip->xpoint[0][1][1]);
+ if (ret < 0)
+ return ret;
+ ret = bf_xpoint_write(chip, 1, 1,
+ chip->xpoint[1][1][0],
+ chip->xpoint[1][1][1]);
+ if (ret < 0)
+ return ret;
+ }
+ chip->ms_proc = on;
+ return 1;
+}
+
+/* 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 by the caller, the
+ * "Dim Switch" put.
+ */
+int bf_dim_apply(struct snd_usb_babyface *chip, bool on)
+{
+ bool old = chip->dim;
+ bool muted = chip->muted[1];
+ u16 step_l, step_r, 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];
+ }
+ chip->dim = on;
+ step_l = on ? chip->master[1][0] : chip->dim_saved[0];
+ step_r = on ? chip->master[1][1] : chip->dim_saved[1];
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_out_reg8(muted, on, step_l),
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_out_reg8(muted, on, step_r),
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(muted, on, step_l),
+ (BF_REG_MASTER_16 + 2 * 1) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(muted, on, step_r),
+ (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP,
+ on ? 0x2000 : 0x0000, 0x2000);
+ if (ret < 0)
+ goto out;
+ return 0;
+out:
+ chip->dim = old;
+ return ret;
+}
+
+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;
+
+ guard(mutex)(&chip->mutex);
+ if (on == chip->dim)
+ return 0;
+ ret = bf_dim_apply(chip, on);
+ if (ret == 0)
+ ret = 1;
+ return ret;
+}
+
+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;
+}
+
+/* Width spread: L = 0x1000*(1+w), R = 0x1000*(1-w), L+R = 0x2000.
+ * TotalMix writes the strip's src pair on the AN1/2 block (0x0000)
+ * and on the block at 0x0034 (cap_width3-7): the stereo pair spreads
+ * L/R in opposition, the mirror src (AN2) gets the swapped values.
+ * Caller holds chip->mutex.
+ */
+int bf_width_apply(struct snd_usb_babyface *chip, int w)
+{
+ u16 l = (u16)(((0x2000 * (100 + w) / 2) + 50) / 100);
+ u16 r = 0x2000 - l;
+ int ret;
+
+ /* AN1/2: AN1 L=0x0000, R=0x001A; AN2 L=0x0001, R=0x001B. */
+ 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;
+ /* The 0x0034 block: AN1 L=0x0034, R=0x004E; AN2 L=0x0035,
+ * R=0x004F. TotalMix writes it in the same drag, so keep the
+ * two halves together here and in the state replay.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0034);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x004e);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0035);
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x004f);
+}
+
+static int bf_width_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ long w = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (w < -100 || w > 100)
+ return -EINVAL;
+
+ guard(mutex)(&chip->mutex);
+ if ((int)w == chip->width)
+ return 0;
+ ret = bf_width_apply(chip, (int)w);
+ if (ret < 0)
+ return ret;
+ chip->width = w;
+ return 1;
+}
+
+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);
+ long nv = ucontrol->value.integer.value[0];
+ u16 v;
+ int ret = 0;
+
+ /* Check before narrowing (a value >= 0x10000 would wrap to a
+ * passing u16); see bf_master_put().
+ */
+ if (nv < 0 || nv > 0x1000)
+ return -EINVAL;
+ v = nv;
+
+ guard(mutex)(&chip->mutex);
+ if (v == chip->fx_send)
+ return 0;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0138);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0153);
+ if (ret < 0)
+ return ret;
+ chip->fx_send = v;
+ return 1;
+}
+
+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;
+ chip->loopback_kctl[i] = kctl;
+ }
+
+ 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 = "Optical Out Format",
+ .info = bf_optical_info,
+ .get = bf_optical_get,
+ .put = bf_optical_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);
+ 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 019a46fc9..12cfd7fea 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -100,14 +100,26 @@ 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). The clock source is
- * Internal.
+/* 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). Clock source and Optical Out format
+ * are tracked today; 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, 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;
+
+ if (chip->optical_spdif)
+ w |= BF_SETTINGS_OPTICAL_SPDIF;
+
+ return bf_vendor_write(chip, BF_REQ_KEEPALIVE, w,
BF_REG_KEEPALIVE_SETTINGS);
}
@@ -193,6 +205,7 @@ int bf_pitch_write(struct snd_usb_babyface *chip, int pitch)
int bf_cold_init(struct snd_usb_babyface *chip)
{
const struct bf_rate *r;
+ u16 w;
int ret, i;
for (i = 0; i <= 0x3d; i++) {
@@ -225,8 +238,12 @@ int bf_cold_init(struct snd_usb_babyface *chip)
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,
+ w = (bf_rate_family(r) << 4) |
+ (chip->clock_optical ? BF_SETTINGS_CLOCK_OPTICAL :
+ BF_SETTINGS_CLOCK_INTERNAL);
+ if (chip->optical_spdif)
+ w |= BF_SETTINGS_OPTICAL_SPDIF;
+ ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE, w,
BF_REG_KEEPALIVE_INIT);
if (ret < 0)
return ret;
@@ -300,14 +317,77 @@ int babyface_restore_state(struct snd_usb_babyface *chip)
return bf_pitch_write(chip, chip->pitch);
}
-/* Re-apply the per-input state that sits on top of the crosspoints
- * (phase invert, stereo split, trim) after a state restore. The write
- * patterns mirror the corresponding _put() handlers. Caller holds
- * chip->mutex.
+/* Re-apply the state that sits on top of the masters and crosspoints
+ * (the routing flags, DIM, phase invert, stereo split, trim) after a
+ * state restore. The write patterns mirror the corresponding _put()
+ * handlers. Caller holds chip->mutex.
*/
int bf_state_apply_flags(struct snd_usb_babyface *chip)
{
- int ret;
+ int out, ret, on_out = -1;
+
+ /* 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_preamp_commit(chip);
+ if (ret < 0)
+ return ret;
+
+ if (chip->width) {
+ ret = bf_width_apply(chip, chip->width);
+ 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 on AN1/2 and PH3/4.
+ */
+ 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. bf_dim_apply() rebuilds dim_saved
+ * from the Phones master, which is still zero on a fresh chip,
+ * and keeps the mute state on top of the -20 dB step.
+ */
+ if (chip->dim) {
+ ret = bf_dim_apply(chip, true);
+ 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
@@ -1520,6 +1600,13 @@ static int babyface_probe(struct usb_interface *intf,
dev_err(&intf->dev, "default mixer restore failed: %d\n", err);
goto error;
}
+ /* The cold init cleared the flag registers as well. */
+ scoped_guard(mutex, &chip->mutex)
+ err = bf_state_apply_flags(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "flag 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);
@@ -1592,6 +1679,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 06af1aa82..1d4c358d9 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -115,9 +115,17 @@
/* 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).
+ * 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); bit 10 = Optical-Out
+ * S/PDIF (bit clear = ADAT, the default). Bit 6 (EQ for Record) is next
+ * in the driver's own upstream follow-up list, not wired to a control
+ * yet - the composer below OR's in every tracked bit, structured so that
+ * one can be added the same way without another flag-stomping rewrite.
*/
#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
+#define BF_SETTINGS_CLOCK_OPTICAL 0x0004
+#define BF_SETTINGS_OPTICAL_SPDIF 0x0400
/* Preamp state byte (0x17, wIdx 0x003F - full state, verified).
* NOTE 2026-08-26 (cap_reflevel3.pcap): the 0x0C "base" is NOT a
@@ -182,6 +190,15 @@
#define BF_REG_LOWMAP_BASE_L BF_REG_XPOINT_BASE_L
#define BF_REG_LOWMAP_BASE_R BF_REG_XPOINT_BASE_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()
@@ -332,6 +349,17 @@ struct snd_usb_babyface {
* (0 = +4dBu, the default)
*/
struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
+ bool loopback[6];
+ struct snd_kcontrol *loopback_kctl[6]; /* for snd_ctl_notify */
+ 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) */
+ bool optical_spdif; /* Optical Out: false = ADAT (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 bf_rate {
@@ -376,11 +404,16 @@ 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_preamp_commit(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);
u8 bf_gain_raw(int mic, int db);
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
+int bf_width_apply(struct snd_usb_babyface *chip, int w);
+int bf_dim_apply(struct snd_usb_babyface *chip, bool on);
+int babyface_create_flags(struct snd_usb_babyface *chip);
/* Master gain-law helpers (babyfacepro-ctl.c). */
int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
--
2.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (3 preceding siblings ...)
2026-11-05 15:15 ` [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 16:45 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
` (2 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
The device loses its registers across a system suspend. On suspend,
let the PCM core suspend the substreams (applications get -ESTRPIPE and
restart), then kill the session's URBs before cancelling the stream
work their handlers queue. On resume, re-run the cold init and replay
the cached mixer state and flags. A resume that follows a device reset
goes through reset_resume instead of a re-probe, so the cached state is
replayed in place.
USB autosuspend stays disabled: nothing in the driver holds a PM
reference while streaming.
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.c | 89 ++++++++++++++++++++++++++---
sound/usb/babyfacepro/babyfacepro.h | 4 +-
2 files changed, 82 insertions(+), 11 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 12cfd7fea..06ef1a97d 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -3,7 +3,8 @@
* 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).
+ * streaming, the mixer-state replay after a resume, and
+ * the card lifecycle (probe/disconnect/PM/module entry).
*
* See babyfacepro.h for the shared device state and register map,
* and babyfacepro-ctl.c for the ALSA control surface.
@@ -198,9 +199,9 @@ int bf_pitch_write(struct snd_usb_babyface *chip, int pitch)
}
/* The cold-start init (cap_coldplug.pcap), verbatim from the user-space
- * reference (protocol::streaming_init). Run at probe, where the
- * device state is unknown; a session start does not need it (see
- * babyface_stream_start()).
+ * reference (protocol::streaming_init). Run at probe and after resume,
+ * where the device state is unknown; a session start does not need it
+ * (see babyface_stream_start()).
*/
int bf_cold_init(struct snd_usb_babyface *chip)
{
@@ -267,8 +268,17 @@ int bf_cold_init(struct snd_usb_babyface *chip)
return 0;
}
-/* Re-apply the whole cached mixer state to the device, whose registers
- * the cold init cleared. Caller holds chip->mutex.
+/* Units probed since the module was loaded.
+ * The SELECT selection is not in the readback, and the values alsactl
+ * restores right after a probe are the ones of the last alsactl store.
+ * After a boot that is the last shutdown, the newest there is. A later
+ * probe of the same unit (a usbfs claim released, an unbind, a replug)
+ * may follow SELECT presses made since, so bf_panel_select_put() needs
+ * to tell the two apart.
+ */
+/* 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.
*/
int babyface_restore_state(struct snd_usb_babyface *chip)
{
@@ -797,8 +807,9 @@ static int babyface_stream_start(struct snd_usb_babyface *chip)
/* A session start is what the RME Windows driver sends
* (cap_audio): the trigger pair, the URBs, the arm. The device
* keeps its mixer state between sessions, so nothing is cleared
- * or re-applied here; the cold init runs at probe only. The rate
- * is written first: hw_params only re-points the speed multiplier.
+ * or re-applied here; the cold init runs at probe and resume
+ * only. The rate is written first: hw_params only re-points the
+ * speed multiplier.
*/
ret = bf_clock_write(chip);
if (ret < 0)
@@ -1497,7 +1508,8 @@ static int babyface_probe(struct usb_interface *intf,
chip->card = card;
chip->dev = usb_get_dev(dev);
- /* USB autosuspend is untested: nothing in this driver holds a PM
+ /* USB autosuspend is untested: babyface_suspend()/_resume() don't
+ * check PMSG_IS_AUTO, and 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
@@ -1752,6 +1764,62 @@ 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);
+ }
+ /* The URB handlers queue the work: kill them first. */
+ scoped_guard(mutex, &chip->mutex) {
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ }
+ cancel_work_sync(&chip->stream_work);
+ 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 (prepare starts a new session).
+ *
+ * This is also the reset_resume handler. Some hosts reset the
+ * device on resume; that needs nothing more than the cold init
+ * already does here, and without the handler the USB core unbinds
+ * and re-probes the driver instead: the card goes away, open PCMs
+ * fail, and udev's alsactl restore then loads the mixer state
+ * stored at the last shutdown over the current one.
+ */
+ guard(mutex)(&chip->mutex);
+ err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
+ if (err < 0)
+ return err;
+ err = bf_cold_init(chip);
+ if (err < 0)
+ return err;
+ err = babyface_restore_state(chip);
+ if (err < 0)
+ return err;
+ err = bf_state_apply_flags(chip);
+
+ return err;
+}
+
static const struct usb_device_id babyface_ids[] = {
{ USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) },
{ }
@@ -1762,6 +1830,9 @@ static struct usb_driver babyface_driver = {
.name = "snd-usb-babyface-pro",
.probe = babyface_probe,
.disconnect = babyface_disconnect,
+ .suspend = babyface_suspend,
+ .resume = babyface_resume,
+ .reset_resume = babyface_resume,
.id_table = babyface_ids,
};
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 1d4c358d9..a32849f83 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -12,7 +12,7 @@
* snd-usb-audio quirk.
*
* babyfacepro.c holds the card lifecycle, the PCM stream and the
- * mixer-state persistence across re-probes;
+ * mixer-state persistence across re-probes and resume;
* babyfacepro-ctl.c holds the ALSA control surface - the hardware
* mixer.
*
@@ -386,7 +386,7 @@ 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. */
+/* Mixer-state persistence across interface re-probes/resume. */
int babyface_restore_state(struct snd_usb_babyface *chip);
int bf_state_apply_flags(struct snd_usb_babyface *chip);
--
2.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (4 preceding siblings ...)
2026-11-05 15:15 ` [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 17:03 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-11-05 15:15 ` [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul, David Fredman
In this mode the front panel is host-driven: the device reports its
buttons, wheel and IN/OUT/SELECT/MIX/DIM state in a 4-byte readback
(request 0x17), and the host software is expected to act on them, as
TotalMix does. Poll it (every 20 ms by default, panel_poll_ms) and
expose the decoded state as read-only controls, notified on change.
The driver acts on SET (phantom power of the selected microphone, the
host standing in for the unit's standalone function), MIX and the wheel:
- With OUT selected, the firmware moves the analog output level
itself on each click, smoothed over about 12 ms, by a step that
depends on the level and on how fast the wheel turns (0.5 to 3 dB,
doubled below a ~62 ms gap between clicks). The driver follows it:
it writes only the 16-bit (digital) master during the gesture,
tracks the level per side in half-dB, polls every 5 ms for 200 ms
after a click to tell fast clicks apart, and writes the 8-bit
master once the wheel has rested for 150 ms, as TotalMix does at
the end of a gesture. A muted output ignores the wheel. The
volume controls are notified.
- With IN selected, the wheel moves the gain of the selected input.
- With SELECT held, the wheel moves the balance of the selected
output.
- In MIX mode, the wheel moves the monitoring level of the selected
input into the selected output, and the input VU display follows.
The controls these change - the output volumes, the preamp gain, the
48V switches and the monitoring crosspoint - are notified, so that a
mixer application follows the unit.
The channel selection of SELECT (left, right, both, none) is not in the
readback. The unit keeps one per IN pair, across IN switches and power
cycles; its LEDs go dark on an IN switch, and the next press only shows
the selection again, later presses step it. The driver follows the
presses and exposes the selection of each pair as a "Front Panel
Selection" control (index 0 is Ch 1/2, 1 Ch 3/4, 2 Opt), which is what
alsactl keeps across boots. A pair it has not been told starts
unknown, and SET and the wheel then do nothing, with one message, rather
than act on a channel that may not be the lit one. After a boot, the
values alsactl restores are what there is. A re-probe of a unit
already probed since the module was loaded ignores them instead, since
SELECT may have been pressed since the last store, and leaves the
pairs unknown.
A DIM press only increments "DIM Button Press Count", for a mixer
application to act on; the driver changes no level on it. The OUT
selector reports 0/1/2 for Ch 1/2, Phones and Opt on both the Pro and
the Pro FS.
The poll is stopped around a system suspend and at disconnect.
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 | 1357 ++++++++++++++++++++++-
sound/usb/babyfacepro/babyfacepro.c | 80 +-
sound/usb/babyfacepro/babyfacepro.h | 126 ++-
3 files changed, 1549 insertions(+), 14 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 5482b6f09..51cf8c7bd 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -3,7 +3,8 @@
* RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
*
* ALSA control surface: the hardware mixer (output masters and mutes,
- * the crosspoint matrix, preamps, routing flags, varispeed pitch).
+ * the crosspoint matrix, preamps, routing flags, varispeed pitch),
+ * and the front-panel poll and controls.
*
* See babyfacepro.h for the shared device state and register map,
* and babyfacepro.c for the core driver (protocol, PCM streaming,
@@ -620,6 +621,7 @@ int babyface_create_xpoints(struct snd_usb_babyface *chip)
err = snd_ctl_add(chip->card, kctl);
if (err < 0)
return err;
+ chip->xpoint_kctl[out][src] = kctl;
}
}
@@ -849,6 +851,83 @@ static int bf_split_put(struct snd_kcontrol *kctl,
return 1;
}
+/* 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.
+ */
+#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;
+}
+
/* 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
@@ -1252,6 +1331,7 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
err = snd_ctl_add(chip->card, kctl);
if (err < 0)
return err;
+ chip->phantom_kctl[i] = kctl;
}
for (i = 0; i < 2; i++) {
@@ -1302,6 +1382,7 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
err = snd_ctl_add(chip->card, kctl);
if (err < 0)
return err;
+ chip->gain_kctl[i] = kctl;
}
return 0;
}
@@ -1981,3 +2062,1277 @@ 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_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", "Unknown", 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);
+}
+
+/* Tell ALSA clients that the front panel changed a mixer control. */
+static void bf_panel_notify_kctl(struct snd_usb_babyface *chip,
+ struct snd_kcontrol *kctl)
+{
+ if (kctl)
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, &kctl->id);
+}
+
+/* The selection of the IN pair now shown, from panel_sel[]. */
+static void bf_panel_sel_load(struct snd_usb_babyface *chip)
+{
+ int pair = READ_ONCE(chip->panel_in) - 1;
+ int sel = pair >= 0 && pair < 3 ? chip->panel_sel[pair] : -1;
+
+ chip->panel_select_known = sel >= 0;
+ WRITE_ONCE(chip->panel_select, sel >= 0 ? sel : 3);
+}
+
+/* SET and the wheel have nothing to act on while the selection is not
+ * known: say so once, since the cause is not visible from the unit.
+ */
+static void bf_panel_sel_hint(struct snd_usb_babyface *chip)
+{
+ if (!chip->panel_select_known)
+ dev_info_once(&chip->dev->dev,
+ "front panel: the SELECT channel selection is not known (the unit keeps it, and the driver cannot read it), so SET and the wheel do nothing; press SELECT to see it, then set the \"Front Panel Selection\" control of the IN pair shown (index 0 = Ch 1/2) to match\n");
+}
+
+/* Canonical output pair of the OUT selection (enum 1 = Ch 1/2,
+ * 2 = Phones, 3 = Opt): AN1/2, PH3/4, AS1/2. "Opt" is the first
+ * optical pair, and the optical format does not move it: with the port
+ * set to ADAT that pair is ADAT 1/2, with it set to S/PDIF it is the
+ * S/PDIF pair (bf_sources), the same output register either way. The
+ * ADAT7/8 this used to point at was an assumption - PROTOCOL.md leaves
+ * the optical outputs unmeasured.
+ */
+static int bf_panel_out_pair(struct snd_usb_babyface *chip)
+{
+ switch (READ_ONCE(chip->panel_out)) {
+ case 2:
+ return 1;
+ case 3:
+ return 2;
+ default:
+ return 0;
+ }
+}
+
+/* Seed the MIX level from the reference crosspoint of the current
+ * IN/SELECT selection, and return that source index. Called when MIX
+ * is engaged and whenever SELECT moves while it is on.
+ */
+static int bf_panel_mix_seed(struct snd_usb_babyface *chip)
+{
+ int out = bf_panel_out_pair(chip);
+ int ref = READ_ONCE(chip->panel_in) == 3 ? 4 :
+ (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) +
+ (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+ int db2 = bf_fader_raw_to_db2(chip->xpoint[out][ref][0]);
+
+ /* A muted crosspoint reads as -inf, six steps below the curve's
+ * first point; seeding there would burn six clockwise clicks
+ * before anything is audible. Start at the first point instead.
+ */
+ if (db2 < bf_fader_curve[0].db2)
+ db2 = bf_fader_curve[0].db2;
+ chip->panel_mix_db2 = db2;
+ return ref;
+}
+
+/* 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)
+{
+ int out = bf_panel_out_pair(chip);
+ 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 (READ_ONCE(chip->panel_in) == 3) {
+ targets[0] = 4; /* Opt: the AS1/2 pair */
+ n = 1;
+ } else if (READ_ONCE(chip->panel_select) != 3) {
+ int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
+
+ targets[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+ n = 1;
+ if (READ_ONCE(chip->panel_select) == 2)
+ targets[n++] = base + 1;
+ } else {
+ bf_panel_sel_hint(chip);
+ }
+
+ guard(mutex)(&chip->mutex);
+ db2 = clamp(chip->panel_mix_db2 + delta, BF_FADER_DB2_INF, 12);
+ chip->panel_mix_db2 = db2;
+ raw = bf_fader_db2_to_raw(db2);
+ 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_XPOINT_BASE_L + BF_REG_XPOINT_STRIDE * out +
+ s->idx_l) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+ (BF_REG_XPOINT_BASE_R + BF_REG_XPOINT_STRIDE * out +
+ s->idx_r) | flag);
+ chip->xpoint[out][targets[i]][0] = raw;
+ chip->xpoint[out][targets[i]][1] = raw;
+ bf_panel_notify_kctl(chip, chip->xpoint_kctl[out][targets[i]]);
+ /* 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;
+ }
+ }
+ }
+}
+
+/* A muted output ignores the wheel, as it does under TotalMix. The
+ * firmware still moves the analog level by itself, up from the mute
+ * code, so the output would become faintly audible while the mute switch
+ * still reads off: put the mute back. Caller holds the mutex.
+ */
+static void bf_panel_remute(struct snd_usb_babyface *chip, int out)
+{
+ bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out + 1);
+}
+
+/* Write an output's L/R masters (8-bit companions + 16-bit with the
+ * transaction flag) and mirror into the cache - used by the balance
+ * wheel on an unmuted output. Caller holds the mutex.
+ */
+static void bf_panel_write_master(struct snd_usb_babyface *chip, int out,
+ u16 l, u16 r)
+{
+ bool muted = chip->muted[out];
+ bool dim = out == 1 && chip->dim;
+ 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_out_reg8(muted, dim, l),
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, r),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, l),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, 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;
+ }
+ if (chip->master_kctl[out])
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->master_kctl[out]->id);
+}
+
+/* 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 ignores the wheel (see bf_panel_remute()).
+ * 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 = canon 2, else AN1/2).
+ */
+static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out,
+ u16 l, u16 r)
+{
+ bool muted = chip->muted[out];
+ bool dim = out == 1 && chip->dim;
+ u16 flag;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, l),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, 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;
+ }
+ if (chip->master_kctl[out])
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->master_kctl[out]->id);
+}
+
+/* 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 step = chip->master[out][ch];
+
+ if (step == chip->panel_master_last[out][ch])
+ return chip->panel_out_hdb[out][ch];
+ if (!step)
+ return BF_OUT_WHEEL_MUTED;
+ return step - BF_MASTER_STEP_0DB;
+}
+
+/* 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 = bf_panel_out_pair(chip);
+ 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));
+
+ guard(mutex)(&chip->mutex);
+ if (chip->muted[out]) {
+ bf_panel_remute(chip, out);
+ return;
+ }
+ 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 :
+ db[ch] + BF_MASTER_STEP_0DB;
+ 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;
+}
+
+/* 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;
+
+ guard(mutex)(&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])
+ return;
+ db[ch] = chip->panel_out_hdb[out][ch];
+ if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
+ return;
+ }
+ if (!chip->muted[out]) {
+ bool dim = out == 1 && chip->dim;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_out_reg8(false, dim, chip->master[out][0]),
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_out_reg8(false, dim, chip->master[out][1]),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(false, dim, chip->master[out][0]),
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ bf_out_reg16(false, dim, chip->master[out][1]),
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ }
+}
+
+/* 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 (READ_ONCE(chip->panel_in) == 3)
+ return;
+ if (READ_ONCE(chip->panel_select) == 3) {
+ bf_panel_sel_hint(chip);
+ return;
+ }
+ {
+ int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
+
+ mics[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+ n = 1;
+ if (READ_ONCE(chip->panel_select) == 2)
+ mics[n++] = base + 1;
+ }
+
+ guard(mutex)(&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);
+ if (db != chip->gain[mic]) {
+ chip->gain[mic] = db;
+ bf_panel_notify_kctl(chip, chip->gain_kctl[mic]);
+ }
+ }
+}
+
+/* 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. The louder side stays where it is; the other
+ * moves by the OUT wheel's step for its distance below the louder one
+ * (bf_out_wheel_step()), so the image moves in 0.5 dB steps near the
+ * centre and faster towards one side, and that side mutes below the
+ * wheel's floor. The balance is read from the L/R masters, so the
+ * gesture needs no state of its own, and the OUT wheel above moves both
+ * sides by the same step, which keeps it.
+ */
+static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ int out = bf_panel_out_pair(chip);
+ int db[2], ch, i, louder, bal, lim;
+
+ guard(mutex)(&chip->mutex);
+ if (chip->muted[out]) {
+ bf_panel_remute(chip, out);
+ return;
+ }
+ /* 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).
+ */
+ for (ch = 0; ch < 2; ch++)
+ db[ch] = chip->master[out][ch] ?
+ chip->master[out][ch] - BF_MASTER_STEP_0DB :
+ BF_OUT_WHEEL_MUTED;
+ louder = max(db[0], db[1]);
+ if (louder < BF_OUT_WHEEL_FLOOR)
+ return;
+ /* bal > 0: the left side is the quieter one, the image is right. */
+ bal = db[1] - db[0];
+ for (i = 0; i < abs(delta); i++)
+ bal += (delta > 0 ? 1 : -1) * bf_out_wheel_step(-abs(bal));
+ lim = louder - BF_OUT_WHEEL_MUTED;
+ bal = clamp(bal, -lim, lim);
+ db[0] = bal > 0 ? louder - bal : louder;
+ db[1] = bal < 0 ? louder + bal : louder;
+ for (ch = 0; ch < 2; ch++)
+ db[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 :
+ db[ch] + BF_MASTER_STEP_0DB;
+
+ bf_panel_write_master(chip, out, db[0], db[1]);
+}
+
+/* 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;
+ u16 old;
+ int m;
+
+ if (READ_ONCE(chip->panel_mix) || READ_ONCE(chip->panel_in) != 1)
+ return;
+ if (READ_ONCE(chip->panel_select) == 3) {
+ bf_panel_sel_hint(chip);
+ return;
+ }
+ if (READ_ONCE(chip->panel_select) != 1)
+ bits |= BF_PREAMP_48V_MIC1;
+ if (READ_ONCE(chip->panel_select) != 0)
+ bits |= BF_PREAMP_48V_MIC2;
+
+ guard(mutex)(&chip->mutex);
+ old = chip->preamp;
+ /* 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 (READ_ONCE(chip->panel_select) == 2) {
+ if ((chip->preamp & bits) == bits)
+ chip->preamp &= ~bits;
+ else
+ chip->preamp |= bits;
+ } else {
+ chip->preamp ^= bits;
+ }
+ if (bf_preamp_state_write(chip) < 0)
+ chip->preamp = old;
+ if ((chip->preamp ^ old) & BF_PREAMP_48V_MIC1)
+ bf_panel_notify_kctl(chip, chip->phantom_kctl[0]);
+ if ((chip->preamp ^ old) & BF_PREAMP_48V_MIC2)
+ bf_panel_notify_kctl(chip, chip->phantom_kctl[1]);
+ for (m = 0; m < 4; m++)
+ chip->panel_mix_disp[m] = 0;
+}
+
+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. The decoded state
+ * (panel_button/wheel/in/out/select/mix/dim) is also read by the control
+ * get callbacks, and panel_select is written by the SELECT control's
+ * put(): those fields are shared between the worker and the ALSA control
+ * layer, so every access to them goes through READ_ONCE()/WRITE_ONCE(),
+ * which keeps the lock-free access defined and tear-free rather than a
+ * plain data race (the worker is the only writer of the other fields).
+ */
+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)
+ WRITE_ONCE(chip->panel_in, in);
+ /* The IN pair is only known from here on: pick up its
+ * selection (kept across a suspend, or set by alsactl).
+ */
+ bf_panel_sel_load(chip);
+ out = bf_panel_out_decode(st[1] & 0x07);
+ if (out)
+ WRITE_ONCE(chip->panel_out, out);
+ WRITE_ONCE(chip->panel_mix, !!(st[0] & 0x80));
+ WRITE_ONCE(chip->panel_saw_fader, (st[2] >> 4) == 0x0);
+ WRITE_ONCE(chip->panel_dim, !!(st[1] & 0x20));
+ return;
+ }
+
+ /* Button flash (byte3 over the 0x40 idle base). Notify on the
+ * edge, as the other flash-driven controls do, so a subscriber sees
+ * each press once rather than on every poll while it is held.
+ */
+ btn = bf_panel_button_decode(st[3]);
+ if (btn && chip->panel_prev[3] != st[3]) {
+ WRITE_ONCE(chip->panel_button, btn);
+ bf_panel_notify(chip, BF_PANEL_KCTL_BUTTON);
+ }
+
+ /* 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) {
+ WRITE_ONCE(chip->panel_wheel,
+ clamp(READ_ONCE(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 (READ_ONCE(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 != READ_ONCE(chip->panel_in)) {
+ WRITE_ONCE(chip->panel_in, in);
+ /* The unit keeps one SELECT selection per IN pair, and an
+ * IN switch does not clear it (hardware-verified
+ * 2026-10-02: back on Ch 1/2 after a trip through Ch 3/4,
+ * the first SELECT press showed the L it had left, while
+ * Ch 3/4 showed nothing). Switch to the new pair's.
+ */
+ bf_panel_sel_load(chip);
+ /* The LEDs go dark on an IN switch: the next SELECT press
+ * only shows the selection again (no step), the one after
+ * that steps it (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 != READ_ONCE(chip->panel_out)) {
+ WRITE_ONCE(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 &&
+ chip->panel_select_known) {
+ if (!chip->panel_select_armed) {
+ /* Disarmed (IN switch since the last step): the press
+ * only shows the selection - the device steps it on
+ * the NEXT press (user-verified 2026-08-28).
+ */
+ chip->panel_select_armed = true;
+ } else {
+ int sel = (READ_ONCE(chip->panel_select) + 1) & 3;
+ struct snd_kcontrol *kctl =
+ chip->panel_select_kctl[READ_ONCE(chip->panel_in) - 1];
+
+ WRITE_ONCE(chip->panel_select, sel);
+ chip->panel_sel[READ_ONCE(chip->panel_in) - 1] = sel;
+ if (kctl)
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &kctl->id);
+ if (READ_ONCE(chip->panel_mix)) {
+ scoped_guard(mutex, &chip->mutex)
+ bf_panel_mix_seed(chip);
+ }
+ }
+ }
+ /* 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 is a software-assignable button. Report presses without
+ * choosing a monitor output or changing any gain in the driver.
+ */
+ if (st[3] == BF_PANEL_FLASH_DIM &&
+ chip->panel_prev[3] != BF_PANEL_FLASH_DIM) {
+ chip->dim_press_count = (chip->dim_press_count + 1) & 0x7fffffff;
+ if (chip->dim_press_kctl)
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->dim_press_kctl->id);
+ }
+
+ /* 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 (READ_ONCE(chip->panel_mix)) {
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+ WRITE_ONCE(chip->panel_mix, false);
+ WRITE_ONCE(chip->panel_saw_fader, false);
+ } else {
+ int ref;
+ int m;
+
+ bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
+ WRITE_ONCE(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).
+ */
+ ref = bf_panel_mix_seed(chip);
+ /* 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 disp = bf_mix_display(chip->panel_mix_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) {
+ /* Do not re-arm the gate on a tick that just left MIX:
+ * the device's byte2 can still show fader mode for a
+ * frame or two after the exit writes.
+ */
+ if (READ_ONCE(chip->panel_mix))
+ WRITE_ONCE(chip->panel_saw_fader, true);
+ } else if (READ_ONCE(chip->panel_mix) && READ_ONCE(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);
+ WRITE_ONCE(chip->panel_mix, false);
+ WRITE_ONCE(chip->panel_saw_fader, false);
+ bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+ }
+
+ dim = !!(st[1] & 0x20);
+ if (dim != READ_ONCE(chip->panel_dim)) {
+ WRITE_ONCE(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 SELECT channel selection is not in the readback, so the
+ * driver follows it from the presses. The unit keeps one per IN
+ * pair across a driver reload and even a power cycle, with the LEDs
+ * dark until the next SELECT press, which shows it again instead of
+ * stepping (hardware-verified 2026-09-29: after an unplug, the
+ * first press lit both channels). What panel_sel[] holds comes
+ * from before a suspend, from alsactl after a boot, or is not
+ * known, and an unknown selection targets nothing: SET, the IN
+ * wheel and the MIX wheel then do nothing rather than act on the
+ * wrong channel, until "Front Panel Select" is set to what the LEDs
+ * show.
+ */
+ bf_panel_sel_load(chip);
+ chip->panel_select_armed = false;
+ 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);
+}
+
+/* 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] = READ_ONCE(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] = READ_ONCE(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] = READ_ONCE(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] = READ_ONCE(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, 5, 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);
+
+ s8 sel = READ_ONCE(chip->panel_sel[kctl->private_value]);
+
+ ucontrol->value.enumerated.item[0] = sel >= 0 ? sel : 4;
+ 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, and the unit keeps
+ * one per IN pair across reloads and power cycles, so the driver cannot
+ * know it after a load. Writing what the LEDs show for the pair now
+ * selected tells it (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 pair = kctl->private_value;
+ /* alsactl restores the values of its last store shortly after
+ * every probe.
+ */
+ bool restore = time_is_after_jiffies(chip->panel_start + 3 * HZ);
+ s8 sel = v < 4 ? v : -1;
+
+ if (v > 4)
+ return -EINVAL;
+ /* After a boot they are what there is. After a re-probe the unit
+ * may have taken SELECT presses since that store, and a wrong
+ * selection makes SET and the wheels act on a channel that is not
+ * the lit one: leave the pair unknown, as for a never-seen pair.
+ */
+ if (restore && chip->reprobe)
+ return 0;
+
+ guard(mutex)(&chip->mutex);
+ if (pair == READ_ONCE(chip->panel_in) - 1 && !restore && sel >= 0)
+ /* Set while the LEDs show the selection: the next press steps
+ * it.
+ */
+ chip->panel_select_armed = true;
+ if (sel == chip->panel_sel[pair])
+ return 0;
+ chip->panel_sel[pair] = sel;
+ if (pair == READ_ONCE(chip->panel_in) - 1) {
+ bf_panel_sel_load(chip);
+ if (READ_ONCE(chip->panel_mix))
+ bf_panel_mix_seed(chip);
+ }
+ return 1;
+}
+
+/* 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 ? READ_ONCE(chip->panel_dim) : READ_ONCE(chip->panel_mix);
+ return 0;
+}
+
+static int bf_dim_press_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 = 0x7fffffff;
+ return 0;
+}
+
+static int bf_dim_press_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] = READ_ONCE(chip->dim_press_count);
+ return 0;
+}
+
+int babyface_create_panel(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int err, pair;
+
+ 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;
+
+ /* One per IN pair (index 0 = Ch 1/2, 1 = Ch 3/4, 2 = Opt): the
+ * unit keeps one selection per pair, and a plain control, not a
+ * volatile one, is what alsactl keeps across boots. Named apart from
+ * the single volatile "Front Panel Select" this replaces, whose stored
+ * value alsactl must not apply.
+ */
+ for (pair = 0; pair < 3; pair++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Front Panel Selection",
+ .index = pair,
+ .info = bf_panel_select_info,
+ .get = bf_panel_select_get,
+ .put = bf_panel_select_put,
+ .private_value = pair,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->panel_select_kctl[pair] = kctl;
+ }
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "DIM Button Press Count",
+ .access = SNDRV_CTL_ELEM_ACCESS_READ |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = bf_dim_press_info,
+ .get = bf_dim_press_get,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ chip->dim_press_kctl = kctl;
+
+ return 0;
+}
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 06ef1a97d..ece2bd0c4 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -268,14 +268,40 @@ int bf_cold_init(struct snd_usb_babyface *chip)
return 0;
}
-/* Units probed since the module was loaded.
+/* The last few units probed since the module was loaded.
* The SELECT selection is not in the readback, and the values alsactl
* restores right after a probe are the ones of the last alsactl store.
* After a boot that is the last shutdown, the newest there is. A later
* probe of the same unit (a usbfs claim released, an unbind, a replug)
* may follow SELECT presses made since, so bf_panel_select_put() needs
- * to tell the two apart.
+ * to tell the two apart. The table is fixed, so it cannot grow with the
+ * number of units seen: one that has been pushed out by BF_SEEN_MAX newer
+ * ones is taken for a first probe again.
*/
+#define BF_SEEN_MAX 8
+#define BF_SEEN_KEYLEN 64
+
+static char bf_seen_keys[BF_SEEN_MAX][BF_SEEN_KEYLEN];
+static unsigned int bf_seen_next;
+static DEFINE_MUTEX(bf_seen_mutex);
+
+/* Record the unit and return whether it was probed before. */
+static bool bf_seen_before(struct usb_device *dev)
+{
+ const char *key = dev->serial && *dev->serial ? dev->serial :
+ dev_name(&dev->dev);
+ unsigned int i;
+
+ guard(mutex)(&bf_seen_mutex);
+ for (i = 0; i < BF_SEEN_MAX; i++) {
+ if (!strncmp(bf_seen_keys[i], key, BF_SEEN_KEYLEN - 1))
+ return true;
+ }
+ strscpy(bf_seen_keys[bf_seen_next], key, BF_SEEN_KEYLEN);
+ bf_seen_next = (bf_seen_next + 1) % BF_SEEN_MAX;
+ return false;
+}
+
/* 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.
@@ -1423,6 +1449,7 @@ static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT;
static int urb_frames_min = 32;
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.");
@@ -1432,8 +1459,12 @@ module_param(frames_per_urb, int, 0444);
MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
module_param(urb_frames_min, int, 0444);
MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
+module_param(urb_split, int, 0444);
+MODULE_PARM_DESC(urb_split, "Split the period into two URBs, queueing 1.5 periods instead of 2: 0 = auto (default), 1 = never, 2 = always.");
module_param(nurbs, int, 0444);
MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");
+module_param(panel_poll_ms, int, 0444);
+MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz).");
static void babyface_private_free(struct snd_card *card)
{
@@ -1510,11 +1541,12 @@ static int babyface_probe(struct usb_interface *intf,
chip->dev = usb_get_dev(dev);
/* USB autosuspend is untested: babyface_suspend()/_resume() don't
* check PMSG_IS_AUTO, and 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.
+ * reference while streaming or while the panel poll/keepalive
+ * timers are running, so an autosuspend request could race a
+ * live stream or panel tick. Disable it explicitly rather than
+ * ship an untested code path - full autosuspend support (correct
+ * autopm_get/put pairing around the stream and the panel/keepalive
+ * work) is a deliberate follow-up, not an oversight.
*/
usb_disable_autosuspend(chip->dev);
chip->iface = intf;
@@ -1529,10 +1561,13 @@ static int babyface_probe(struct usb_interface *intf,
chip->alt = BF_ALT_1;
chip->frame_bytes = 56;
chip->preamp = BF_PREAMP_BASE;
+ memset(chip->panel_sel, -1, sizeof(chip->panel_sel));
+ 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)
@@ -1697,12 +1732,28 @@ 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;
+ }
+
+ /* Set before the card is visible: the "Front Panel Selection"
+ * restore that alsactl issues right after probe must see it.
+ */
+ chip->reprobe = bf_seen_before(dev);
+
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",
@@ -1745,6 +1796,7 @@ static void babyface_disconnect(struct usb_interface *intf)
babyface_stream_kill(chip);
}
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
@@ -1782,6 +1834,7 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
babyface_stream_kill(chip);
}
cancel_work_sync(&chip->stream_work);
+ babyface_panel_stop(chip);
return 0;
}
@@ -1805,18 +1858,21 @@ static int babyface_resume(struct usb_interface *intf)
* fail, and udev's alsactl restore then loads the mixer state
* stored at the last shutdown over the current one.
*/
- guard(mutex)(&chip->mutex);
+ mutex_lock(&chip->mutex);
err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
if (err < 0)
- return err;
+ goto out;
err = bf_cold_init(chip);
if (err < 0)
- return err;
+ goto out;
err = babyface_restore_state(chip);
if (err < 0)
- return err;
+ goto out;
err = bf_state_apply_flags(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 a32849f83..984d1d175 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -14,7 +14,7 @@
* babyfacepro.c holds the card lifecycle, the PCM stream and the
* mixer-state persistence across re-probes and resume;
* babyfacepro-ctl.c holds the ALSA control surface - the hardware
- * mixer.
+ * mixer and the front panel.
*
* The protocol (vendor requests + 14x32-bit frame layout) was
* reverse-engineered from Windows captures and validated on hardware -
@@ -49,6 +49,7 @@
* bf_clock_write / bf_pitch_write.
*/
+#include <linux/ktime.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/unaligned.h>
@@ -199,6 +200,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()
@@ -318,6 +366,12 @@ struct snd_usb_babyface {
u16 master[6][2]; /* cached masters, in BF_MASTER_STEP_* */
bool muted[6];
u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
+ /* The controls the front panel changes on its own (the MIX wheel,
+ * the IN wheel, SET), for snd_ctl_notify.
+ */
+ struct snd_kcontrol *xpoint_kctl[6][14];
+ struct snd_kcontrol *gain_kctl[4];
+ struct snd_kcontrol *phantom_kctl[2];
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,
@@ -360,6 +414,72 @@ struct snd_usb_babyface {
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) */
+
+
+ /* 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; /* the LEDs show the selection: the next SELECT
+ * press steps it (an IN switch clears this)
+ */
+ bool panel_select_known; /* panel_select matches the unit */
+ s8 panel_sel[3]; /* the SELECT selection of each IN pair (Ch 1/2,
+ * Ch 3/4, Opt): -1 not known, else as
+ * panel_select. The unit keeps one per pair,
+ * across IN switches and power cycles; the
+ * "Front Panel Select" controls expose them
+ * so that alsactl can keep them across boots.
+ */
+ unsigned long panel_start; /* jiffies at panel_start (boot re-assert) */
+ bool reprobe; /* the unit was probed before since the
+ * module was loaded
+ */
+ 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 of the IN pair shown: 0 L, 1 R,
+ * 2 both, 3 none (3 too when not known);
+ * 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 (a muted side
+ * below the floor keeps its offset here, which the master can't
+ * hold). 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 */
+ int panel_mix_db2; /* MIX-mode monitoring level (dB x2) */
+ 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[6]; /* for snd_ctl_notify */
+ struct snd_kcontrol *panel_select_kctl[3]; /* per IN pair, likewise */
+ struct snd_kcontrol *dim_press_kctl;
+ u32 dim_press_count; /* front-panel DIM presses, wraps */
};
struct bf_rate {
@@ -414,6 +534,10 @@ int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
int bf_width_apply(struct snd_usb_babyface *chip, int w);
int bf_dim_apply(struct snd_usb_babyface *chip, bool on);
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 (babyfacepro-ctl.c). */
int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
--
2.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (5 preceding siblings ...)
2026-11-05 15:15 ` [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 17:19 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Ismaïl Bahloul
Add the 4-strip (AN1-AN4) 3-band + low-cut parametric EQ: the
fixed-point (Q27, CORDIC) biquad coefficient math, the 64-byte blocks
uploaded on bulk endpoint 0x0A of interface 1, and 15 controls per
strip.
The coefficients depend on the sample rate, so hw_params re-uploads
them on a rate change. The DSP is not part of the register state the
cold init clears, so resume re-uploads them as well; after a re-probe,
alsactl restores the EQ controls and their puts re-upload the
coefficients. The Q controls start at 1.0, and a band with no gain or
no Q uploads the identity.
The three checkpatch CHECK notes on this code (BIT() vs (1 << 27),
'ang' twice) are known false positives: BIT() returns unsigned long,
wrong for this file's signed Q27 math, and 'ang' is the CORDIC angle.
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 610 +++++++++++++++++++++++-
sound/usb/babyfacepro/babyfacepro.c | 59 ++-
sound/usb/babyfacepro/babyfacepro.h | 25 +-
3 files changed, 679 insertions(+), 15 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 51cf8c7bd..a1b8caa28 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -4,7 +4,8 @@
*
* ALSA control surface: the hardware mixer (output masters and mutes,
* the crosspoint matrix, preamps, routing flags, varispeed pitch),
- * and the front-panel poll and controls.
+ * the front-panel poll and controls, and the hardware DSP EQ
+ * (3-band + low cut).
*
* See babyfacepro.h for the shared device state and register map,
* and babyfacepro.c for the core driver (protocol, PCM streaming,
@@ -3336,3 +3337,610 @@ 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,
+};
+
+/* 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, u, su, cu, 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 (type == 0 || gain_x10 == 0 || q100 <= 0) {
+ /* Inactive band (type Off, or no gain/Q yet): 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;
+ /* w[4] is the shared scale bf_eq_update_strip() reads when the
+ * band has a type and a gain but no Q yet (Q defaults to 0).
+ * Leaving it unwritten uploaded a garbage shared scale.
+ */
+ w[4] = BF_EQ_Q27;
+ 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 = sin(w0 / (2Q)), the way TotalMix computes it, not the
+ * cookbook's sin(w0) / (2Q): the two agree where w0 is small and
+ * part ways as it grows, which is the effective Q that shrinks above
+ * a few kHz in the captures (tools/usbdump/eq_biquad.md). Checked
+ * for Q 0.7 to 5 up to 16 kHz. The angle is held to pi/2: past it
+ * sin() falls again, and towards pi it would make a vanishing alpha,
+ * a resonance with its poles on the unit circle. It only matters for
+ * a low Q at a high frequency (below Q 1, w0 > pi.Q), where the band
+ * is as wide as it gets.
+ */
+ u = div64_s64(w0 * 100 + q100, 2 * (s64)q100);
+ if (u > hpi)
+ u = hpi;
+ bf_sincos(u, &su, &cu);
+ alpha = su;
+ /* 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);
+}
+
+/* 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 = div64_s64(11508 * f * 11656 + (11656 + f) / 2, 11656 + f);
+ return (u32)word;
+}
+
+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 int 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);
+ return 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);
+}
+
+#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;
+
+ guard(mutex)(&chip->mutex);
+ if (param == 0) {
+ if (e->on != !!nv) {
+ e->on = !!nv;
+ ret = bf_eq_update_strip(chip, strip);
+ if (ret < 0)
+ return ret;
+ ret = 1;
+ }
+ } else if (v && *v != nv) {
+ *v = nv;
+ ret = bf_eq_update_strip(chip, strip);
+ if (ret < 0)
+ return ret;
+ ret = 1;
+ }
+ return ret;
+}
+
+/* The Q controls declare 5..1000 (Q x 100) and bf_eq_put() rejects
+ * anything below 5, so a zeroed band_q would read back out of range -
+ * alsactl then fails to restore it (EINVAL) - and could reach the
+ * coefficient math. Seed the neutral Q 1.0 on every strip at probe,
+ * before any saved state is applied so a restore still overrides it.
+ */
+void bf_eq_defaults(struct snd_usb_babyface *chip)
+{
+ int strip, band;
+
+ for (strip = 0; strip < 4; strip++) {
+ chip->eq[strip].slope_db = 6;
+ for (band = 0; band < 3; band++)
+ chip->eq[strip].band_q[band] = 100;
+ }
+}
+
+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 ece2bd0c4..ed17600ab 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -1230,6 +1230,8 @@ static int babyface_pcm_hw_params(struct snd_pcm_substream *subs,
new_rate = true;
chip->alt = r->alt;
chip->frame_bytes = r->frame_bytes;
+ /* The DSP EQ coefficients depend on fs: re-upload. */
+ bf_eq_reupload(chip);
dev_dbg(&chip->dev->dev, "rate %u Hz (alt %u)\n",
chip->rate, chip->alt);
}
@@ -1638,6 +1640,8 @@ static int babyface_probe(struct usb_interface *intf,
BF_REF_LEVEL_4DBU : BF_REF_LEVEL_MINUS10DBV;
}
+ bf_eq_defaults(chip);
+
/* The 0x16 clear zeroed the mixer registers: apply the factory
* default routing to keep the outputs live out of the box. alsactl
* restores the user's settings once the card is registered.
@@ -1654,6 +1658,7 @@ static int babyface_probe(struct usb_interface *intf,
dev_err(&intf->dev, "flag 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);
@@ -1743,6 +1748,20 @@ static int babyface_probe(struct usb_interface *intf,
*/
chip->reprobe = bf_seen_before(dev);
+ /* The DSP coefficient stream (EQ, bulk ep 0x0A) lives on interface
+ * 1, which has a single altsetting (alt 0) already active in the
+ * default configuration - the endpoint is scheduled, no
+ * SET_INTERFACE or interface claim is needed (the earlier
+ * -EAGAIN was the on-stack transfer buffer, and SET_INTERFACE on
+ * interface 1 wedged the iface-5 audio stream - playback URBs
+ * never completed).
+ */
+ 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);
@@ -1838,6 +1857,31 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
return 0;
}
+/* The register state of a resumed device. Caller holds chip->mutex. */
+static int bf_resume_locked(struct snd_usb_babyface *chip)
+{
+ int err;
+
+ err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
+ if (err < 0)
+ return err;
+ err = bf_cold_init(chip);
+ if (err < 0)
+ return err;
+ err = babyface_restore_state(chip);
+ if (err < 0)
+ return err;
+ err = bf_state_apply_flags(chip);
+ if (err < 0)
+ return err;
+ /* The device lost its DSP across the suspend; the bulk EQ uploads
+ * are not part of the register state re-applied above, so re-send
+ * them.
+ */
+ bf_eq_reupload(chip);
+ return 0;
+}
+
static int babyface_resume(struct usb_interface *intf)
{
struct snd_usb_babyface *chip = usb_get_intfdata(intf);
@@ -1858,19 +1902,8 @@ static int babyface_resume(struct usb_interface *intf)
* fail, and udev's alsactl restore then loads the mixer state
* stored at the last shutdown over the current one.
*/
- 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);
- if (err < 0)
- goto out;
- err = bf_state_apply_flags(chip);
-out:
- mutex_unlock(&chip->mutex);
+ scoped_guard(mutex, &chip->mutex)
+ err = bf_resume_locked(chip);
if (!err)
babyface_panel_start(chip);
return err;
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 984d1d175..c9ef366f1 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -14,7 +14,7 @@
* babyfacepro.c holds the card lifecycle, the PCM stream and the
* mixer-state persistence across re-probes and resume;
* babyfacepro-ctl.c holds the ALSA control surface - the hardware
- * mixer and the front panel.
+ * mixer, the front panel and the DSP EQ.
*
* The protocol (vendor requests + 14x32-bit frame layout) was
* reverse-engineered from Windows captures and validated on hardware -
@@ -313,6 +313,21 @@ extern const struct bf_source bf_sources[14];
*/
#define BF_SRC_PB1 8
+/* One analog-input strip of the DSP EQ (babyfacepro-ctl.c). */
+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) */
+};
+
struct snd_usb_babyface {
struct snd_card *card;
struct usb_device *dev;
@@ -480,6 +495,9 @@ struct snd_usb_babyface {
struct snd_kcontrol *panel_select_kctl[3]; /* per IN pair, likewise */
struct snd_kcontrol *dim_press_kctl;
u32 dim_press_count; /* front-panel DIM presses, wraps */
+
+ /* DSP EQ (babyfacepro-ctl.c) - 4 analog-input strips, params kept in state */
+ struct bf_eq_channel eq[4];
};
struct bf_rate {
@@ -538,6 +556,11 @@ 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);
+void bf_eq_defaults(struct snd_usb_babyface *chip);
+int babyface_create_eq(struct snd_usb_babyface *chip);
/* Master gain-law helpers (babyfacepro-ctl.c). */
int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
--
2.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (6 preceding siblings ...)
2026-11-05 15:15 ` [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
@ 2026-11-05 15:15 ` Ismaïl Bahloul
2026-11-05 17:21 ` sashiko-bot
7 siblings, 1 reply; 17+ messages in thread
From: Ismaïl Bahloul @ 2026-11-05 15:15 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, linux-usb, alsa-devel, 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 stream session
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 | 218 +++++++++++++++++++++
Documentation/sound/cards/index.rst | 1 +
2 files changed, 219 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..2b79a52d9
--- /dev/null
+++ b/Documentation/sound/cards/babyface-pro.rst
@@ -0,0 +1,218 @@
+.. 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 - what problem the driver solves, why it is a standalone
+driver instead of a snd-usb-audio quirk, and the four design
+decisions (protocol shape, stream model, mixer state across re-probes
+and resume, front-panel emulation) that shape most of the code.
+
+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, rate family, stream trigger
+0x12 16-bit crosspoint and output-master writes
+0x14 stream session arm
+0x16 cold-init register clear
+0x17 front-panel + preamp state (read and write)
+0x1a 8-bit gain / output-master writes
+0x1b varispeed (DDS quad)
+0x1d stream session start
+====== ========================================
+
+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 can be read 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
+ ``babyfacepro.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 the cold init at
+ probe and at resume does - 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 stream model
+================
+
+Playback and capture share one physical stream: the device only
+advances it while both interrupt endpoints have a pending URB, so the
+IN and OUT URBs are always submitted as a pair, and both directions run
+at one sample rate. The driver runs one stream *session* for both
+substreams:
+
+* ``hw_params`` counts a substream as a user of the session;
+* ``prepare`` starts the session if it is not running - the rate write,
+ the session trigger pair, the IN/OUT URBs, then the arm - which is
+ what the RME Windows driver sends at a stream start;
+* trigger START/STOP only decides whether the URB handlers move that
+ substream's audio. The URBs keep running either way, carrying
+ silence while nothing plays, so an xrun restart does not restart the
+ session;
+* ``hw_free`` drops the user, and the last one stops the session.
+
+The device keeps its mixer registers between sessions, so a session
+start writes no mixer state. A session triggered within about 15 ms of
+the previous one stopping comes up with the outputs silent, so a
+session start waits until 50 ms have passed since the last stop.
+
+Both directions share one clock, so while another application has the
+other direction set up, the rate belongs to it: ``open()`` offers that
+rate alone, and the sound server resamples, rather than the device
+changing rate under a running stream - as with RME's own drivers, which
+grey the sample rate out while a stream runs. The application that
+holds both directions may change the rate or the buffer size itself, as
+a DAW does from its settings; the direction it left running then stops
+with an xrun and is set up again.
+
+The size of the URBs follows the period the application asks for, so
+the latency follows the application's buffer instead of a fixed queue:
+``hw_params`` splits the period into the fewest URBs of at most
+``frames_per_urb`` frames, each a whole number of the device's IN
+packets, and keeps two periods' worth in flight (at most ``nurbs``).
+The URB handlers copy to and from the ring the core allocates and take
+the application's position from the shared control page, so the ring can
+also be mapped by the application, which JACK requires.
+``runtime->delay`` reports the audio queued in the URBs plus the fixed
+delay of the converters and of the device, per speed, so that an
+application that uses ALSA directly can line up what it records.
+
+The playback handler copies the ring as it is and moves the pointer on
+by the whole URB, so the pointer follows the device even for an
+application that runs the stream free, as dmix does on the hardware PCM
+it opens; an underrun is the PCM core's to detect and report.
+
+Capture exposes the device's twelve input words in the device's own
+order, one channel per word: AN1-4, AS1/2, then ADAT3-8. AS1/2 is the
+first optical pair, so it carries ADAT 1/2 when the input receives ADAT
+and S/PDIF when it receives S/PDIF. At 96/192 kHz the frame has fewer
+words and the channels past them read zero.
+
+URB completions run in interrupt context; the work that needs to sleep
+(stopping the session after repeated URB errors) runs from
+``stream_work``.
+
+Mixer state across re-probes and resume
+=======================================
+
+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 cold init the probe runs clears the mixer
+registers, and the driver does not carry its cached state across the
+re-probe: alsactl restores the settings stored at the last shutdown
+once the card is registered again, as for any other card. A resume
+that follows a device reset goes through ``reset_resume`` instead of a
+re-probe, so a reset does not lose the state that way.
+
+A system suspend does lose the registers. On resume the driver re-runs
+the cold init and replays the in-memory state (masters, crosspoints,
+routing flags, EQ) through the same paths the controls use. Since
+almost nothing can be read back (see above), that in-memory state is
+still the only record of what the hardware should hold.
+
+The front-panel SELECT selection is the exception: it is not in the
+0x17 readback, so the driver has to be told it once and cannot know
+whether it changed meanwhile. The driver tracks whether a unit has
+been probed since the module was loaded. After a boot it takes the
+values alsactl restores; a re-probe of an already-seen unit ignores
+them and leaves the pairs unknown, because SELECT may have been pressed
+since the last store. The "Front Panel Selection" controls let
+software correct the selection (see below).
+
+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 level
+step, a preamp gain step, a monitoring level step, a phantom toggle)
+exactly like the corresponding ALSA control's ``.put`` would. A DIM
+press is only counted, for a mixer application to act on. 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. The unit keeps one such selection for each IN pair, across IN
+switches and even across a power cycle; its LEDs stay dark until the
+next SELECT press, which only shows the selection again, and later
+presses step it. The driver follows the presses and exposes the
+selection of each pair as a "Front Panel Selection" control (index 0
+is Ch 1/2), so that alsactl keeps it across boots like any other
+mixer setting. It cannot know what the unit holds before it has been
+told once, so a pair starts out unknown, and SET and the wheel then do
+nothing instead of acting on a channel that may not be the lit one;
+setting the control to what the LEDs show for the pair tells it. The
+values alsactl restores right after a probe are the ones of its last
+store: after a boot that is the newest record there is, but a re-probe
+of a unit already seen may follow SELECT presses made since, so the
+driver ignores them then and leaves the pairs unknown. The stored
+values can be wrong only if the selection was changed while the driver
+was not running (the unit used on its own, or with another host). The relevant code
+comments (``babyface_panel_start()``, the ``panel_select_armed``
+handling in ``bf_panel_tick()``) explain the details.
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.56.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* Re: [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing
2026-11-05 15:15 ` [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
@ 2026-11-05 15:31 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 15:31 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: out-of-bounds value[1] write in bf_mute_get()
- [Medium] ALSA: usb: babyfacepro: missing shutdown check in mixer put callbacks
--
commit a0ae4df204731e5d864689d9a4a9ee415579f1a2
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: babyfacepro: add output masters and crosspoint routing
This commit adds ALSA mixer controls for the six output masters (volume and
mute) and the 6x14 crosspoint routing matrix, caching their state on the
host and initializing safe power-on defaults during probe.
> diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
> new file mode 100644
> index 0000000000000..942d4732cbe07
> --- /dev/null
> +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
[ ... ]
> +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];
> + long nl = ucontrol->value.integer.value[0];
> + long nr = ucontrol->value.integer.value[1];
> + u16 l, r, wire_l, wire_r, flag;
> + int ret = 0;
> +
> + /* Reject anything outside the declared range, so the registers and
> + * the cache stay in spec (the ALSA core only enforces this with
> + * CONFIG_SND_CTL_INPUT_VALIDATION). Check before narrowing:
> + * .value.integer.value[] is a long.
> + */
> + if (nl < 0 || nl > BF_MASTER_STEP_MAX ||
> + nr < 0 || nr > BF_MASTER_STEP_MAX)
> + return -EINVAL;
> + l = nl;
> + r = nr;
> +
> + guard(mutex)(&chip->mutex);
> + if (l == chip->master[out][0] && r == chip->master[out][1])
> + return 0;
[Severity: Medium]
As reported by Sashiko on v7 [1], should bf_master_put() (along with
bf_mute_put() and bf_xpoint_put() below) check chip->shutdown and return
-ENODEV after acquiring chip->mutex, matching babyface_pcm_hw_params(),
babyface_pcm_prepare(), and babyface_stream_work()?
In babyface_disconnect(), chip->shutdown is set to true and chip->mutex is
acquired and released to fence in-flight operations before calling
usb_enable_autosuspend(chip->dev) and snd_card_disconnect(chip->card):
sound/usb/babyfacepro/babyfacepro.c:babyface_disconnect() {
...
chip->shutdown = true;
scoped_guard(mutex, &chip->mutex) {
if (chip->streaming)
babyface_stream_kill(chip);
}
cancel_work_sync(&chip->stream_work);
usb_enable_autosuspend(chip->dev);
...
snd_card_disconnect(chip->card);
}
Without checking chip->shutdown under chip->mutex, a concurrent
SNDRV_CTL_IOCTL_ELEM_WRITE ioctl in bf_master_put(), bf_mute_put(), or
bf_xpoint_put() can proceed to issue synchronous USB control transfers via
bf_vendor_write() or bf_xpoint_write() after the disconnect fence has passed
and USB autosuspend has been re-enabled (and on !CONFIG_PM builds, where
snd_power_sync_ref() in snd_card_disconnect() is a no-op, while
babyface_disconnect() is completing).
> +
> + wire_l = chip->muted[out] ? 0 : l;
> + wire_r = chip->muted[out] ? 0 : r;
> + flag = bf_flag_cycle[chip->flag_cnt];
> + chip->flag_cnt = (chip->flag_cnt + 1) & 3;
> +
> + /* Both registers, as TotalMix writes them: the 8-bit is the analog
> + * level, the 16-bit the digital one.
> + */
> + ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_l),
> + BF_REG_MASTER_8 + 2 * out);
> + if (ret < 0)
> + return ret;
> + ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_r),
> + BF_REG_MASTER_8 + 2 * out + 1);
> + if (ret < 0)
> + return ret;
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_l),
> + (BF_REG_MASTER_16 + 2 * out) | flag);
> + if (ret < 0)
> + return ret;
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_r),
> + (BF_REG_MASTER_16 + 2 * out + 1) | flag);
> + if (ret < 0)
> + return ret;
> +
> + chip->master[out][0] = l;
> + chip->master[out][1] = r;
> + return 1;
> +}
> +
> +static int bf_mute_info(struct snd_kcontrol *kctl,
> + struct snd_ctl_elem_info *uinfo)
> +{
> + uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
> + /* One switch for the pair: bf_mute_put() writes both channels
> + * together, so a count of 2 would silently ignore value[1].
> + */
> + uinfo->count = 1;
> + 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;
> +}
[Severity: High]
Since bf_mute_info() declares uinfo->count = 1, doesn't writing to
ucontrol->value.integer.value[1] in bf_mute_get() write past the declared
element count?
When CONFIG_SND_CTL_DEBUG is enabled, snd_ctl_elem_read() poisons bytes in
ucontrol->value past info.count and checks them in sanity_check_elem_value()
after kctl->get(), so overwriting ucontrol->value.integer.value[1] causes
every read of the Playback Switch controls to log an error and fail with
-EINVAL, and causes snd_ctl_put_verify() to report a false mismatch whenever
the output is unmuted. Even without CONFIG_SND_CTL_DEBUG, setting value[1]
breaks userspace comparisons via snd_ctl_elem_value_compare() (such as the
ALSA mixer-test kselftest).
[1] https://lore.kernel.org/r/20261027113140.134257-3-i.bahloul01@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=2
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
2026-11-05 15:15 ` [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
@ 2026-11-05 15:32 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 15:32 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: bf_trim_apply() and bf_phase_apply() clobber xpoints
- [Medium] ALSA: usb: babyfacepro: hardcoded Mic 1 names in babyface_create_preamp()
- [Low] ALSA: usb: babyfacepro: unchecked snd_ctl_new1() in babyface_create_preamp()
- [Low] ALSA: usb: babyfacepro: bf_phase_info() and bf_bool_info() duplicate code
--
commit 82ef805ee77606164dc3814bf736734ca60721d5
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
This commit adds ALSA mixer controls for the input side of the RME Babyface
Pro, including phantom power, pad, instrument reference level, mic/instrument
preamp gains, phase invert, stereo split, and input trim.
> diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
> index 942d4732cbe07..fe6266ea118e6 100644
> --- a/sound/usb/babyfacepro/babyfacepro-ctl.c
> +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
> @@ -654,3 +654,627 @@ int babyface_create_masters(struct snd_usb_babyface *chip)
[ ... ]
> +int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert)
> +{
> + const struct bf_source *s = &bf_sources[mic];
> + int out, ret;
> + u16 flag;
> +
> + for (out = 0; out < 6; out++) {
> + u16 plain = chip->xpoint[out][mic][0];
> + u16 value = invert ? (u16)~plain : plain;
> +
> + /* The AN1/2 block is written without the rotating bits, as
> + * bf_xpoint_write() does.
> + */
> + flag = 0;
> + if (out) {
> + flag = bf_flag_cycle[chip->flag_cnt];
> + chip->flag_cnt = (chip->flag_cnt + 1) & 3;
> + }
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
> + (BF_REG_XPOINT_BASE_L +
> + BF_REG_XPOINT_STRIDE * out + s->idx_l) |
> + flag);
> + if (ret < 0)
> + return ret;
> + }
> + return 0;
> +}
[ ... ]
> +int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split)
> +{
> + const struct bf_source *s = &bf_sources[8 + pb];
> + u16 l = split ? 0x2000 : chip->xpoint[0][8 + pb][0];
> + u16 r = split ? 0x0000 : chip->xpoint[0][8 + pb][1];
> + int ret;
> +
> + /* The AN1/2 block. The same values also went to a "standard map"
> + * half at 0x0068, which is AS1/2's submix, not part of AN1/2: it
> + * overwrote AS1/2's own crosspoint for this playback pair.
> + */
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
> + BF_REG_XPOINT_BASE_L + s->idx_l);
> + if (ret < 0)
> + return ret;
> + return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
> + BF_REG_XPOINT_BASE_R + s->idx_r);
> +}
[ ... ]
> +int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2)
> +{
> + int base = mic & ~1;
> + int sib = base + 1;
> + const struct bf_source *sb = &bf_sources[base];
> + const struct bf_source *ss = &bf_sources[sib];
> + u16 trim_raw = bf_master_16bit(trim_db2);
> + int ret;
> +
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
> + BF_REG_LOWMAP_BASE_L + sb->idx_l);
> + if (ret < 0)
> + return ret;
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
> + BF_REG_LOWMAP_BASE_R + sb->idx_r);
> + if (ret < 0)
> + return ret;
> + ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
> + BF_REG_LOWMAP_BASE_L + ss->idx_l);
> + if (ret < 0)
> + return ret;
> + return bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
> + BF_REG_LOWMAP_BASE_R + ss->idx_r);
> +}
[ ... ]
> +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;
> +
> + scoped_guard(mutex, &chip->mutex) {
> + if (db == chip->trim[mic])
> + return 0;
> + ret = bf_trim_apply(chip, mic, db * 2);
> + if (ret < 0)
> + return ret;
[Severity: High]
As reported by Sashiko on v7 [1], does bf_trim_apply() clobber the AN1/2
crosspoint state and unmute live analog inputs into the main outputs?
In sound/usb/babyfacepro/babyfacepro.h, BF_REG_LOWMAP_BASE_L (0x0000) and
BF_REG_LOWMAP_BASE_R (0x001a) are the exact same register addresses as
BF_REG_XPOINT_BASE_L and BF_REG_XPOINT_BASE_R for output pair 0 (the AN1/2
main output). In babyface_write_default_mixer(), analog inputs AN1-4 have
chip->xpoint[0][0..3][0/1] initialized to 0 so live microphones are not
routed directly into the speakers.
When bf_trim_put() calls bf_trim_apply(), it converts trim_db2 via
bf_master_16bit() (clamped to [1, 0x4000] where 0 dB is 0x2000, compared to
BF_FADER_0DB = 0x16a0 on the crosspoint curve) and writes trim_raw directly
to all four L and R crosspoint registers of the input pair (base and sib) on
AN1/2, ignoring chip->xpoint[0][base/sib][0/1]. Touching Trim Volume at all
(even at -65 dB, or returning it to 0 dB which writes 0x2000 instead of
restoring chip->xpoint[0][base/sib][0/1]) unconditionally routes both inputs
of the pair to both L and R of AN1/2, overriding any muted state, fader
level, or panning.
Because chip->xpoint[0][base/sib][0/1] remains 0 in the cache while the
hardware holds 0x2000, a subsequent write of 0 to the crosspoint control hits
the early return in bf_xpoint_put():
sound/usb/babyfacepro/babyfacepro-ctl.c:bf_xpoint_put() {
guard(mutex)(&chip->mutex);
if (l == chip->xpoint[out][src][0] && r == chip->xpoint[out][src][1])
return 0;
ret = bf_xpoint_write(chip, out, src, l, r);
}
and cannot re-mute the hardware.
Additionally, because bf_phase_apply(), bf_split_apply(), bf_trim_apply(),
and bf_xpoint_put() write to the shared crosspoint registers without
combining their state:
- bf_trim_apply() (both in bf_trim_put() and in bf_state_apply_flags() where
the trim loop runs after the phase loop) overwrites the negated out = 0 L
register written by bf_phase_apply().
- bf_phase_apply() only negates BF_REG_XPOINT_BASE_L (leaving R un-inverted,
and writing ~0 = 0xffff when plain == 0).
- Any subsequent bf_xpoint_put() writes plain l/r values, silently wiping out
active phase, stereo split, or trim settings in hardware while their cached
control states remain active.
[ ... ]
> +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;
> +}
[Severity: Low]
This isn't a bug, but bf_bool_info() is identical to bf_phase_info() added
earlier in sound/usb/babyfacepro/babyfacepro-ctl.c (and to the existing
bf_mute_info()). Could these controls use the standard ALSA helper
snd_ctl_boolean_mono_info() from include/sound/control.h instead of
duplicating the callback?
[ ... ]
> +int babyface_create_preamp(struct snd_usb_babyface *chip)
> +{
> + struct snd_kcontrol *kctl;
> + int i, err;
> +
> + /* The control name is the type and .index is the channel (0 =
> + * Mic 1/AN1, 1 = Mic 2/AN2), the convention the companion mixer
> + * and existing asound.state files use.
> + */
> + 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);
> + if (!kctl)
> + return -ENOMEM;
> + 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);
> + if (!kctl)
> + return -ENOMEM;
> + 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;
[Severity: Low]
If snd_ctl_new1() returns NULL for the "Instrument Ref Level" control in
babyface_create_preamp(), passing NULL to snd_ctl_add() will return -EINVAL
instead of -ENOMEM. Should this check if (!kctl) return -ENOMEM; before
calling snd_ctl_add() like the other control creation calls in this function?
> +
> + 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);
[Severity: Medium]
As reported by Sashiko on v7 [1], why do all three loops in
babyface_create_preamp() hardcode "Mic 1" in .name ("Phantom Power Mic 1",
"Pad Mic 1", and "Mic 1 Capture Volume") across all loop iterations?
When i > 0, this registers Mic 2 phantom power and pad as "Phantom Power
Mic 1",1 and "Pad Mic 1",1, and registers Mic 2, Instrument 3, and Instrument
4 capture gains as "Mic 1 Capture Volume" with indices 1, 2, and 3. Should
these either use a channel-neutral name when indexing by i, or format the
per-channel name as babyface_create_trim() does?
[1] https://lore.kernel.org/r/20261027113140.134257-4-i.bahloul01@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=3
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch
2026-11-05 15:15 ` [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
@ 2026-11-05 15:32 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 15:32 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] ALSA: usb: babyfacepro: out-of-bounds value[1] write in bf_loopback_get()
- [Low] ALSA: usb: babyfacepro: leftover contradictory comment above bf_out_reg8()
--
commit bc91f1e348293fb57b62c700b674ce38c7dc5791
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: babyfacepro: add routing flags and varispeed pitch
This commit adds ALSA mixer controls and state persistence for routing flags
(loopback, AN 1>2, sample clock source, optical out format, AN1/2 link, MS
processor, DIM, width, and FX send) and varispeed pitch on the RME Babyface
Pro.
> diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
> index fe6266ea118e6..5482b6f09b73a 100644
> --- a/sound/usb/babyfacepro/babyfacepro-ctl.c
> +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
[ ... ]
> @@ -325,6 +329,33 @@ static int bf_master_get(struct snd_kcontrol *kctl,
> return 0;
> }
>
> +/* The master step to put on the wire. The cache keeps the real
> + * volume: mute wins, and DIM is the fixed -20 dB step on the Phones
> + * pair, so every writer derives the wire value the same way.
> + */
> +/* The two master registers to put on the wire. The cache keeps the
> + * real volume: mute wins, and DIM is the exact -20 dB pair the
> + * hardware's own button writes (0xcb / 0x0333, cap_dim2.pcap), not the
> + * master curve's value at that step.
> + */
> +static u8 bf_out_reg8(bool muted, bool dim, u16 step)
[Severity: Low]
This isn't a bug, but is the first comment block above bf_out_reg8() in
sound/usb/babyfacepro/babyfacepro-ctl.c a leftover from an earlier draft
that was meant to be replaced by the second comment block?
[ ... ]
> @@ -1278,3 +1305,679 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
[ ... ]
> +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;
> +}
[Severity: Medium]
Does bf_loopback_get() in sound/usb/babyfacepro/babyfacepro-ctl.c write
past the declared element count when populating
ucontrol->value.integer.value[1]?
In babyface_create_flags(), the Loopback Switch controls are registered with
.info = bf_mute_info and .get = bf_loopback_get:
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);
However, bf_mute_info() sets uinfo->count = 1:
uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
/* One switch for the pair: bf_mute_put() writes both channels
* together, so a count of 2 would silently ignore value[1].
*/
uinfo->count = 1;
When userspace reads the control via snd_ctl_elem_read() -> bf_loopback_get()
with CONFIG_SND_CTL_DEBUG enabled, the ALSA control core poisons the buffer
past info.count with fill_remaining_elem_value() and then checks it in
sanity_check_elem_value(). Because bf_loopback_get() (like bf_mute_get())
writes ucontrol->value.integer.value[1] on a count = 1 control,
sanity_check_elem_value() logs an access overflow error and fails the read
with -EINVAL.
Should bf_loopback_get() only write ucontrol->value.integer.value[0] (or use
bf_switch_info and a single-value get, and similarly in bf_mute_get())?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=4
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
2026-11-05 15:15 ` [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
@ 2026-11-05 16:24 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 16:24 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: URB and coherent DMA teardown deferred past disconnect
- [Medium] ALSA: usb: babyfacepro: validate endpoint types in babyface_stream_start()
- [Medium] ALSA: usb: babyfacepro: stop running substreams on babyface_stream_start() error
- [Medium] ALSA: usb: babyfacepro: constrain period_size and rate by packet floor in open()
- [Medium] ALSA: usb: babyfacepro: unconditional usb_enable_autosuspend() on teardown
- [Low] ALSA: usb: babyfacepro: document module parameters in kernel-parameters.txt
- [Low] ALSA: usb: babyfacepro: use per-card slot for index and id in babyface_probe()
--
commit 6f9408f458b3d9ee7e3ba9156ec7fd66f87dee6c
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
This commit adds a standalone ALSA USB audio driver for the RME Babyface
Pro and Babyface Pro FS in proprietary mode, handling cold-start
initialization, card lifecycle, and full-duplex interrupt-URB PCM
streaming.
> diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
> new file mode 100644
> index 0000000000000..ac1414adc73c6
> --- /dev/null
> +++ b/sound/usb/babyfacepro/babyfacepro.c
[ ... ]
> +static int babyface_stream_start(struct snd_usb_babyface *chip)
> +{
> + unsigned int urbsize = chip->frame_bytes * chip->urb_frames;
> + s64 since;
> + int i, ret;
[ ... ]
> + for (i = 0; i < chip->urbs_active; 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);
[ ... ]
> + WRITE_ONCE(chip->streaming, true);
> + for (i = 0; i < chip->urbs_active; 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;
> + }
[Severity: Medium]
As reported by Sashiko on v7 [1], babyface_stream_start() initializes
chip->urbs_in[i] and chip->urbs_out[i] with usb_fill_int_urb() using
hardcoded interrupt pipes for BF_EP_IN and BF_EP_OUT and submits them via
usb_submit_urb() without validating the endpoint descriptors in the active
alternate setting.
If a malformed or emulated USB device with VID 0x2a39 and PID 0x3fc0
exposes non-interrupt endpoints (or omits them) on interface 5 alternate
settings BF_ALT_1, BF_ALT_2, or BF_ALT_3, usb_submit_urb() logs a bogus
URB transfer warning and forwards the interrupt URB with a mismatched
endpoint descriptor to the host controller driver.
Should babyface_stream_start() check usb_urb_ep_type_check() on the
initialized URBs (or validate the endpoints after usb_set_interface())
before submitting them?
> + /* 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;
> +
> + dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
> + chip->urb_frames, chip->urbs_active);
> + return 0;
> +
> +err:
> + dev_err_ratelimited(&chip->dev->dev, "failed to start stream: %d\n",
> + ret);
> + babyface_stream_kill(chip);
> + return ret;
> +}
[Severity: Medium]
As reported by Sashiko on v7 [2], babyface_stream_start() sets
chip->streaming to true before the usb_submit_urb() loop and the blocking
bf_vendor_write(chip, BF_REQ_SESSION_ARM, ...) control transfer.
Because babyface_pcm_trigger() handles SNDRV_PCM_TRIGGER_START under the
PCM stream lock without taking chip->mutex:
case SNDRV_PCM_TRIGGER_START:
if (!READ_ONCE(chip->streaming))
return -EPIPE;
if the other direction's substream is already in SNDRV_PCM_STATE_PREPARED
and triggers start while babyface_stream_start() is in flight, it sees
chip->streaming == true and enters SNDRV_PCM_STATE_RUNNING.
If usb_submit_urb() or bf_vendor_write() then fails, could the err path
leave that other substream stuck in SNDRV_PCM_STATE_RUNNING with no URBs
in flight because it calls babyface_stream_kill(chip) without calling
babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN)?
[ ... ]
> +static unsigned int bf_urb_frames(struct snd_usb_babyface *chip,
> + const struct bf_rate *r,
> + unsigned int period)
> +{
> + unsigned int pkt = r->min_fpu;
> + unsigned int hi = rounddown(chip->frames_per_urb, pkt);
> + unsigned int lo = roundup(max(chip->urb_frames_min, pkt), pkt);
> + unsigned int urb;
> +
> + if (!hi || lo > hi || period < pkt)
> + return 0;
> + urb = rounddown(period / DIV_ROUND_UP(period, hi), pkt);
> + if (bf_split_period(r, period))
> + urb = rounddown(urb / 2, pkt);
> + urb = clamp(urb, lo, hi);
> + return urb <= period ? urb : 0;
> +}
[ ... ]
> +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;
> + 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;
> + /* A period must span at least one URB, so a completion crosses at
> + * most one period boundary. The URB size follows the period down to
> + * urb_frames_min; while another application holds the session, its
> + * period (in whole URBs) is the floor. Constrain in frames (not
> + * bytes) so the minimum period does not balloon at low channel
> + * counts.
> + */
> + ret = snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
> + chip->urb_frames_min, 1 << 18);
> + if (ret < 0)
> + return ret;
[Severity: Medium]
In babyface_probe(), chip->frames_per_urb and chip->urb_frames_min can be
configured down to 8 (such as urb_frames_min=16, which is documented as
the low-latency floor):
chip->frames_per_urb = clamp(frames_per_urb, 8, 1024) & ~7;
chip->urb_frames_min = min_t(unsigned int,
clamp(urb_frames_min, 8, 1024) & ~7,
chip->frames_per_urb);
However, bf_urb_frames() requires the period and URB size to be at least
r->min_fpu frames (16 frames at BF_ALT_2 and 32 frames at BF_ALT_3) and
requires rounddown(chip->frames_per_urb, r->min_fpu) >=
roundup(max(chip->urb_frames_min, r->min_fpu), r->min_fpu), returning 0
otherwise.
Because babyface_pcm_open() only bounds SNDRV_PCM_HW_PARAM_PERIOD_SIZE by
chip->urb_frames_min and offers all rates in bf_rates_constraint, won't
ALSA parameter refinement allow combinations (for example period_size=16
at 192 kHz when urb_frames_min=16) that babyface_pcm_hw_params() then
rejects with -EINVAL?
[ ... ]
> +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 urb_frames_min = 32;
> +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, 0444);
> +MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
> +module_param(urb_frames_min, int, 0444);
> +MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
> +module_param(nurbs, int, 0444);
> +MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");
[Severity: Low]
As reported by Sashiko on v7 [3], this adds five module parameters
(index, id, frames_per_urb, urb_frames_min, and nurbs) that can also be
set on the kernel command line as snd-usb-babyface-pro.<param>=. Should
these parameters be documented in
Documentation/admin-guide/kernel-parameters.txt?
> +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 && chip->urbs_out[i]) {
> + usb_kill_urb(chip->urbs_out[i]);
> + usb_free_urb(chip->urbs_out[i]);
> + }
> + /* probe() allocates the six arrays separately and can
> + * fail between them, so each may be NULL here - this runs
> + * on the probe error path via snd_card_free().
> + */
> + if (chip->buf_in && chip->dma_in)
> + usb_free_coherent(chip->dev, urbsize,
> + chip->buf_in[i], chip->dma_in[i]);
> + if (chip->buf_out && chip->dma_out)
> + usb_free_coherent(chip->dev, urbsize,
> + chip->buf_out[i], chip->dma_out[i]);
> + }
> + }
[Severity: High]
In babyface_disconnect(), the driver stops the stream and calls
snd_card_free_when_closed(chip->card), deferring babyface_private_free()
until user-space closes the last ALSA file descriptor.
If the USB host controller is unbound or hot-unplugged while an audio
application holds the ALSA PCM device open, usb_remove_hcd() runs
babyface_disconnect() and then calls hcd_buffer_destroy(), which destroys
hcd->pool[] and sets the pool pointers to NULL.
When user-space later closes the ALSA file descriptor and
babyface_private_free() runs after disconnect has returned, won't calling
usb_kill_urb() and usb_free_coherent() violate the USB lifecycle contract
and trigger a NULL pointer dereference in dma_pool_free() (when urbsize <=
2048) or free DMA memory on a torn-down HCD device?
> + 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);
[Severity: Low]
As reported by Sashiko on v7 [4], index and id are declared as
SNDRV_CARDS-element module_param_array arrays, but babyface_probe()
always passes index[0] and id[0] to snd_card_new().
If two devices are connected (or a device is re-probed before
snd_card_free_when_closed() finishes releasing the previous card) when a
non-default index or id parameter is specified, won't snd_card_new() fail
trying to claim the same slot or ID, while ignoring elements 1 through
SNDRV_CARDS - 1?
> + 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);
[ ... ]
> +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)
> +{
[ ... ]
> + 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);
[Severity: Medium]
usb_disable_autosuspend() and usb_enable_autosuspend() wrap
pm_runtime_forbid() and pm_runtime_allow() on struct usb_device and are
not reference-counted; USB core initializes non-hub USB devices with
autosuspend disabled (power/control set to on) by default, and because
babyface_driver does not set supports_autosuspend, USB core already keeps
the bound interface active while claimed.
When babyface_probe() fails or babyface_disconnect() unbinds interface 5,
won't calling usb_enable_autosuspend(chip->dev) unconditionally flip the
parent usb_device's power/control setting to auto and allow the composite
device (including its MIDI interface claimed by snd-usb-audio) to
autosuspend unexpectedly?
[1] https://lore.kernel.org/r/20261027113140.134257-2-i.bahloul01@gmail.com
[2] https://lore.kernel.org/r/20261027113140.134257-2-i.bahloul01@gmail.com
[3] https://lore.kernel.org/r/20261027113140.134257-2-i.bahloul01@gmail.com
[4] https://lore.kernel.org/r/20261027113140.134257-2-i.bahloul01@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=1
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume
2026-11-05 15:15 ` [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
@ 2026-11-05 16:45 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 16:45 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] ALSA: usb: babyfacepro: missing snd_power_change_state() in suspend and resume
- [Low] ALSA: usb: babyfacepro: babyface_suspend() walks private card->devices list
--
commit f0c674e7a86798c445b9cc47638dc398955e5a17
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: babyfacepro: add S3 suspend/resume
This commit adds system suspend, resume, and reset_resume callbacks to the
RME Babyface Pro USB audio driver. On suspend, it suspends PCM substreams,
kills active URBs, and cancels stream work; on resume, it re-runs cold
initialization and restores cached mixer state and flags.
> diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
> index 12cfd7fea3315..06ef1a97d27a4 100644
> --- a/sound/usb/babyfacepro/babyfacepro.c
> +++ b/sound/usb/babyfacepro/babyfacepro.c
[ ... ]
> @@ -1752,6 +1764,62 @@ 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);
> + }
[Severity: Low]
This isn't a bug, but why walk the internal chip->card->devices list here?
card->devices and struct snd_device are internal to sound/core/device.c and
are not meant to be traversed directly by drivers. In addition, the commit
message notes:
On suspend, let the PCM core suspend the substreams (applications get
-ESTRPIPE and restart)
With USB autosuspend disabled, the PCM core already registers pcm_dev_pm_ops
(do_pcm_suspend() in sound/core/pcm.c) on the child PCM device, which calls
snd_pcm_suspend_all() before the parent USB interface suspend callback runs.
If the driver ever needs to reference its PCM instance directly, could it
store the struct snd_pcm pointer returned by snd_pcm_new() in struct
snd_usb_babyface instead of iterating over chip->card->devices?
> + /* The URB handlers queue the work: kill them first. */
> + scoped_guard(mutex, &chip->mutex) {
> + if (chip->streaming)
> + babyface_stream_kill(chip);
> + }
> + cancel_work_sync(&chip->stream_work);
> + 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 (prepare starts a new session).
> + *
> + * This is also the reset_resume handler. Some hosts reset the
> + * device on resume; that needs nothing more than the cold init
> + * already does here, and without the handler the USB core unbinds
> + * and re-probes the driver instead: the card goes away, open PCMs
> + * fail, and udev's alsactl restore then loads the mixer state
> + * stored at the last shutdown over the current one.
> + */
> + guard(mutex)(&chip->mutex);
> + err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
> + if (err < 0)
> + return err;
> + err = bf_cold_init(chip);
> + if (err < 0)
> + return err;
> + err = babyface_restore_state(chip);
> + if (err < 0)
> + return err;
> + err = bf_state_apply_flags(chip);
> +
> + return err;
> +}
[Severity: Medium]
Should babyface_suspend() and babyface_resume() update the card power state
via snd_power_change_state()?
In ALSA core (sound/core/pcm_native.c and sound/core/control.c), entry
points such as snd_pcm_common_ioctl(), snd_pcm_release(),
snd_ctl_elem_read_user(), snd_ctl_elem_write_user(),
snd_ctl_elem_info_user(), and snd_ctl_tlv_ioctl() call snd_power_wait() or
snd_power_ref_and_wait(), which only block while snd_power_get_state(card)
is not SNDRV_CTL_POWER_D0.
Because babyface_suspend() never sets SNDRV_CTL_POWER_D3hot and
babyface_resume() never restores SNDRV_CTL_POWER_D0, chip->card->power_state
stays at SNDRV_CTL_POWER_D0 across the entire suspend and resume cycle.
Couldn't a user-space audio server woken by snd_pcm_suspend_all() (receiving
-ESTRPIPE) or issuing SNDRV_PCM_IOCTL_PREPARE or control writes enter
babyface_pcm_prepare() or mixer callbacks while the interface is suspending,
suspended, or before babyface_resume() finishes restoring hardware registers?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=5
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls
2026-11-05 15:15 ` [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
@ 2026-11-05 17:03 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 17:03 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: unlocked selection and MIX state in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: broken panel_start init in babyface_panel_start()
- [Medium] ALSA: usb: babyfacepro: wrong AN1/2 flags in bf_panel_mix_wheel()
- [Medium] ALSA: usb: babyfacepro: broken MIX selection and VU seed in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: stale OUT wheel state in bf_out_wheel_level()
- [Medium] ALSA: usb: babyfacepro: unhandled unknown panel_in in bf_panel_select_put()
- [Medium] ALSA: usb: babyfacepro: unseeded panel_mix_db2 on first tick in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: tick-based SELECT hold detection in bf_panel_tick()
- [Low] ALSA: usb: babyfacepro: undocumented panel_poll_ms and urb_split params
- [Low] ALSA: usb: babyfacepro: panel_wheel saturates at SHRT_MIN/MAX in bf_panel_tick()
--
commit 743b738314668be227407d88e571ec6f84054392
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: babyfacepro: add the front-panel poll and controls
This commit adds periodic polling of the Babyface Pro front-panel readback
register (request 0x17) and exposes the decoded button, wheel, IN/OUT/MIX/DIM,
and per-pair channel selection states as ALSA mixer controls. It also
implements host-driven handling for the front-panel rotary wheel and SET/MIX
buttons.
> diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
> index 5482b6f09b73a..51cf8c7bd4357 100644
> --- a/sound/usb/babyfacepro/babyfacepro-ctl.c
> +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
[ ... ]
> +/* The selection of the IN pair now shown, from panel_sel[]. */
> +static void bf_panel_sel_load(struct snd_usb_babyface *chip)
> +{
> + int pair = READ_ONCE(chip->panel_in) - 1;
> + int sel = pair >= 0 && pair < 3 ? chip->panel_sel[pair] : -1;
> +
> + chip->panel_select_known = sel >= 0;
> + WRITE_ONCE(chip->panel_select, sel >= 0 ? sel : 3);
> +}
[ ... ]
> +static int bf_panel_mix_seed(struct snd_usb_babyface *chip)
> +{
> + int out = bf_panel_out_pair(chip);
> + int ref = READ_ONCE(chip->panel_in) == 3 ? 4 :
> + (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) +
> + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
> + int db2 = bf_fader_raw_to_db2(chip->xpoint[out][ref][0]);
> +
> + /* A muted crosspoint reads as -inf, six steps below the curve's
> + * first point; seeding there would burn six clockwise clicks
> + * before anything is audible. Start at the first point instead.
> + */
> + if (db2 < bf_fader_curve[0].db2)
> + db2 = bf_fader_curve[0].db2;
> + chip->panel_mix_db2 = db2;
> + return ref;
> +}
> +
> +/* 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)
> +{
> + int out = bf_panel_out_pair(chip);
> + 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 (READ_ONCE(chip->panel_in) == 3) {
> + targets[0] = 4; /* Opt: the AS1/2 pair */
> + n = 1;
> + } else if (READ_ONCE(chip->panel_select) != 3) {
> + int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
> +
> + targets[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
> + n = 1;
> + if (READ_ONCE(chip->panel_select) == 2)
> + targets[n++] = base + 1;
> + } else {
> + bf_panel_sel_hint(chip);
> + }
> +
> + guard(mutex)(&chip->mutex);
> + db2 = clamp(chip->panel_mix_db2 + delta, BF_FADER_DB2_INF, 12);
> + chip->panel_mix_db2 = db2;
[Severity: Medium]
As reported by Sashiko on v7 [1], in bf_panel_mix_wheel(), checking
READ_ONCE(chip->panel_in) == 3 before READ_ONCE(chip->panel_select) != 3
causes the wheel to modify AS1/2 (targets[0] = 4, n = 1) when Opt is selected
even if panel_select is 3 (None or Unknown), bypassing panel_sel[2] ("Front
Panel Selection" index 2) and bf_panel_sel_hint().
Also, when panel_in != 3 and panel_select == 3, bf_panel_mix_wheel() calls
bf_panel_sel_hint(chip) with n == 0, but falls through without returning and
still updates chip->panel_mix_db2 on every wheel click.
Should bf_panel_mix_wheel() check for panel_select == 3 and return early
after bf_panel_sel_hint(chip) before handling panel_in == 3 or updating
chip->panel_mix_db2?
> + raw = bf_fader_db2_to_raw(db2);
> + 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_XPOINT_BASE_L + BF_REG_XPOINT_STRIDE * out +
> + s->idx_l) | flag);
> + bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
> + (BF_REG_XPOINT_BASE_R + BF_REG_XPOINT_STRIDE * out +
> + s->idx_r) | flag);
[Severity: Medium]
In bf_xpoint_write() and bf_phase_apply(), the rotating transaction flag bits
in bf_flag_cycle[] are only ORed into wIndex when out != 0, because the AN1/2
crosspoint block at 0x0000 is written with flag == 0:
sound/usb/babyfacepro/babyfacepro-ctl.c:bf_xpoint_write() {
const struct bf_source *s = &bf_sources[src];
u16 flag = 0;
int ret;
if (out) {
flag = bf_flag_cycle[chip->flag_cnt];
chip->flag_cnt = (chip->flag_cnt + 1) & 3;
}
...
}
Here in bf_panel_mix_wheel(), flag is cycled and ORed into wIndex
unconditionally even when out == 0 (Ch 1/2). Can this use bf_xpoint_write()
instead of open-coding the BF_REQ_CROSSPOINT writes?
[ ... ]
> +/* Write an output's L/R masters (8-bit companions + 16-bit with the
> + * transaction flag) and mirror into the cache - used by the balance
> + * wheel on an unmuted output. Caller holds the mutex.
> + */
> +static void bf_panel_write_master(struct snd_usb_babyface *chip, int out,
> + u16 l, u16 r)
> +{
> + bool muted = chip->muted[out];
> + bool dim = out == 1 && chip->dim;
> + 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_out_reg8(muted, dim, l),
> + BF_REG_MASTER_8 + 2 * out);
> + bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, r),
> + BF_REG_MASTER_8 + 2 * out + 1);
> + bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, l),
> + (BF_REG_MASTER_16 + 2 * out) | flag);
> + bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, r),
> + (BF_REG_MASTER_16 + 2 * out + 1) | flag);
> + chip->master[out][0] = l;
> + chip->master[out][1] = r;
[ ... ]
> +/* 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 step = chip->master[out][ch];
> +
> + if (step == chip->panel_master_last[out][ch])
> + return chip->panel_out_hdb[out][ch];
> + if (!step)
> + return BF_OUT_WHEEL_MUTED;
> + return step - BF_MASTER_STEP_0DB;
> +}
[ ... ]
> +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;
> +
> + guard(mutex)(&chip->mutex);
> + chip->panel_out_resync = false;
> + out = chip->panel_out_wheel_out;
> + for (ch = 0; ch < 2; ch++) {\n> + if (chip->master[out][ch] != chip->panel_master_last[out][ch])
> + return;
> + db[ch] = chip->panel_out_hdb[out][ch];
> + if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
> + return;
> + }
[ ... ]
> +static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
> +{
> + int out = bf_panel_out_pair(chip);
> + int db[2], ch, i, louder, bal, lim;
> +
> + guard(mutex)(&chip->mutex);
> + if (chip->muted[out]) {
> + bf_panel_remute(chip, out);
> + return;
> + }
> + /* 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).
> + */
> + for (ch = 0; ch < 2; ch++)
> + db[ch] = chip->master[out][ch] ?
> + chip->master[out][ch] - BF_MASTER_STEP_0DB :
> + BF_OUT_WHEEL_MUTED;
[Severity: Medium]
As reported by Sashiko on v7 [2], how is chip->panel_master_last[out] kept in
sync when chip->master[out] is modified outside bf_panel_out_wheel()?
1. Neither bf_master_put() nor bf_panel_write_master() (called from
bf_panel_balance_wheel()) invalidates chip->panel_master_last[out] to 0xffff.
If the OUT wheel previously left a channel muted at step 0 with a sub-floor
balance offset in chip->panel_out_hdb[out][ch], and the user then centers or
sets the master to 0 via the ALSA control or balance wheel,
chip->master[out][ch] == chip->panel_master_last[out][ch] (0 == 0) remains
true and the next OUT wheel turn in bf_out_wheel_level() restores the stale
sub-floor offset.
2. In bf_panel_balance_wheel(), reading chip->master[out][ch] directly
instead of bf_out_wheel_level(chip, out, ch) discards any sub-floor offset
currently tracked in chip->panel_out_hdb[out][ch].
3. In bf_panel_out_resync(), tracking only a single scalar
chip->panel_out_wheel_out drops the pending 8-bit resync if the user switches
outputs and turns the wheel within 150 ms, and returning early from the loop
whenever either channel has db[ch] < BF_OUT_WHEEL_8BIT_MIN skips the 8-bit
resync write for the louder channel of a panned output as well.
[ ... ]
> +/* 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 (READ_ONCE(chip->panel_in) == 3)
> + return;
> + if (READ_ONCE(chip->panel_select) == 3) {
> + bf_panel_sel_hint(chip);
> + return;
> + }
> + {
> + int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
> +
> + mics[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
> + n = 1;
> + if (READ_ONCE(chip->panel_select) == 2)
> + mics[n++] = base + 1;
> + }
[ ... ]
> +static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
> +{
> + u16 bits = 0;
> + u16 old;
> + int m;
> +
> + if (READ_ONCE(chip->panel_mix) || READ_ONCE(chip->panel_in) != 1)
> + return;
> + if (READ_ONCE(chip->panel_select) == 3) {
> + bf_panel_sel_hint(chip);
> + return;
> + }
> + if (READ_ONCE(chip->panel_select) != 1)
> + bits |= BF_PREAMP_48V_MIC1;
> + if (READ_ONCE(chip->panel_select) != 0)
> + bits |= BF_PREAMP_48V_MIC2;
> +
> + guard(mutex)(&chip->mutex);
> + old = chip->preamp;
> + /* 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 (READ_ONCE(chip->panel_select) == 2) {
[ ... ]
> +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);\n> + if (in)
> + WRITE_ONCE(chip->panel_in, in);
> + /* The IN pair is only known from here on: pick up its
> + * selection (kept across a suspend, or set by alsactl).
> + */
> + bf_panel_sel_load(chip);
> + out = bf_panel_out_decode(st[1] & 0x07);
> + if (out)
> + WRITE_ONCE(chip->panel_out, out);
> + WRITE_ONCE(chip->panel_mix, !!(st[0] & 0x80));
> + WRITE_ONCE(chip->panel_saw_fader, (st[2] >> 4) == 0x0);
> + WRITE_ONCE(chip->panel_dim, !!(st[1] & 0x20));
> + return;
> + }
[Severity: Medium]
When the device is already in MIX mode on the first poll tick
(!chip->panel_seen with st[0] & 0x80 set), bf_panel_tick() sets
chip->panel_mix = true and returns without calling bf_panel_mix_seed(chip).
Because chip->panel_mix_db2 is zero-initialized at probe (0 dB unity gain on
bf_fader_curve[], whereas the default input crosspoints are muted at -inf /
-62 dB), won't the first MIX wheel click jump the monitoring crosspoint from
muted directly to ~0 dB?
Also, since the first tick runs asynchronously after snd_card_register() and
babyface_resume(), shouldn't changes to panel_in, panel_out, panel_mix, and
panel_dim here notify the corresponding ALSA controls?
[ ... ]
> + if (delta && cls == pcls) {
> + WRITE_ONCE(chip->panel_wheel,
> + clamp(READ_ONCE(chip->panel_wheel) + delta,
> + SHRT_MIN, SHRT_MAX));
> + bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL);
[Severity: Low]
Unlike the comment in babyfacepro.h ("accumulated wheel delta (consumed on
get)"), bf_panel_wheel_get() never resets chip->panel_wheel on read. Because
chip->panel_wheel is clamped to SHRT_MIN..SHRT_MAX here in bf_panel_tick(),
won't the control permanently saturate once net rotation in one direction
reaches 32767 or -32768 clicks?
> + /* 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 (READ_ONCE(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);
> + }
[ ... ]
> + /* 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 &&
> + chip->panel_select_known) {
> + if (!chip->panel_select_armed) {
> + /* Disarmed (IN switch since the last step): the press
> + * only shows the selection - the device steps it on
> + * the NEXT press (user-verified 2026-08-28).
> + */
> + chip->panel_select_armed = true;
> + } else {
> + int sel = (READ_ONCE(chip->panel_select) + 1) & 3;
> + struct snd_kcontrol *kctl =
> + chip->panel_select_kctl[READ_ONCE(chip->panel_in) - 1];
> +
> + WRITE_ONCE(chip->panel_select, sel);
> + chip->panel_sel[READ_ONCE(chip->panel_in) - 1] = sel;
> + if (kctl)
> + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
> + &kctl->id);
> + if (READ_ONCE(chip->panel_mix)) {
> + scoped_guard(mutex, &chip->mutex)
> + bf_panel_mix_seed(chip);
> + }
> + }
> + }
> + /* 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;
[Severity: Medium]
As reported by Sashiko on v7 [3], bf_panel_tick() detects a 200 ms SELECT hold
by counting 10 poll ticks (chip->panel_sel_hold >= 10) instead of measuring
elapsed time. During the 5 ms fast-poll window after an OUT wheel click
(chip->panel_fast_until), 10 ticks elapse in 50 ms (shorter than a normal
100-150 ms tap flash), whereas with a larger panel_poll_ms module parameter
(up to 1000 ms), 10 ticks require holding SELECT for up to 10 seconds. In
addition, on the initial rising edge of holding SELECT in OUT mode (cls == 1),
the block above immediately steps chip->panel_select and chip->panel_sel[].
Should SELECT hold detection use elapsed time (ktime_t or jiffies) and avoid
stepping the IN channel selection when holding SELECT in OUT mode?
[ ... ]
> + if (mix_flash) {
> + if (READ_ONCE(chip->panel_mix)) {
> + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
> + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
> + WRITE_ONCE(chip->panel_mix, false);
> + WRITE_ONCE(chip->panel_saw_fader, false);
> + } else {
> + int ref;
> + int m;
> +
> + bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
> + WRITE_ONCE(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).
> + */
> + ref = bf_panel_mix_seed(chip);
> + /* 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 disp = bf_mix_display(chip->panel_mix_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);
> + }
[Severity: High]
As reported by Sashiko on v7 [4], can concurrent execution of bf_panel_tick()
and ALSA control writes in bf_panel_select_put() (or bf_xpoint_put()) race on
the selection and MIX state?
1. In bf_panel_select_put(), updates to chip->panel_sel[],
chip->panel_select_known, chip->panel_select, chip->panel_select_armed, and
bf_panel_mix_seed() are serialized under chip->mutex. However, bf_panel_tick()
calls bf_panel_sel_load(), updates chip->panel_select_armed,
chip->panel_select, and chip->panel_sel[] on IN switches and SELECT presses,
and calls bf_panel_mix_seed() and mutates chip->panel_mix_disp[] when MIX is
engaged without holding chip->mutex.
2. In bf_panel_set_phantom(), READ_ONCE(chip->panel_select) is evaluated four
separate times across the unlocked prefix and after acquiring chip->mutex. If
chip->panel_select is 0 or 1 at the initial check and bf_panel_select_put()
concurrently sets chip->panel_select to 3 (None/Unknown), both
READ_ONCE(chip->panel_select) != 1 and READ_ONCE(chip->panel_select) != 0
evaluate to true (setting bits = BF_PREAMP_48V_MIC1 | BF_PREAMP_48V_MIC2) and
READ_ONCE(chip->panel_select) == 2 evaluates to false, toggling +48V phantom
power on both microphones when no channel is selected.
3. Similarly, bf_panel_gain_wheel() and bf_panel_mix_wheel() evaluate
READ_ONCE(chip->panel_select) multiple times outside chip->mutex; a concurrent
change from 1 to 2 populates both targets[0]/mics[0] and targets[1]/mics[1]
with base + 1, stepping the right channel twice.
Also, when mix_flash engages MIX mode above, if panel_select == 3
(None/Unknown), bf_panel_mix_seed() still returns ref = 0 or 2 and writes
BF_REG_PANEL_GAIN + ref, whereas if panel_select == 2 (Both), it only seeds
BF_REG_PANEL_GAIN + ref and leaves ref + 1 unseeded.
[ ... ]
> +void babyface_panel_start(struct snd_usb_babyface *chip)
> +{
> + chip->panel_seen = false;
> + /* The SELECT channel selection is not in the readback, so the
> + * driver follows it from the presses. The unit keeps one per IN
> + * pair across a driver reload and even a power cycle, with the LEDs
> + * dark until the next SELECT press, which shows it again instead of
> + * stepping (hardware-verified 2026-09-29: after an unplug, the
> + * first press lit both channels). What panel_sel[] holds comes
> + * from before a suspend, from alsactl after a boot, or is not
> + * known, and an unknown selection targets nothing: SET, the IN
> + * wheel and the MIX wheel then do nothing rather than act on the
> + * wrong channel, until "Front Panel Select" is set to what the LEDs
> + * show.
> + */
> + bf_panel_sel_load(chip);
> + chip->panel_select_armed = false;
> + 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);
> +}
[ ... ]
> +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 pair = kctl->private_value;
> + /* alsactl restores the values of its last store shortly after
> + * every probe.
> + */
> + bool restore = time_is_after_jiffies(chip->panel_start + 3 * HZ);
> + s8 sel = v < 4 ? v : -1;
> +
> + if (v > 4)
> + return -EINVAL;
> + /* After a boot they are what there is. After a re-probe the unit
> + * may have taken SELECT presses since that store, and a wrong
> + * selection makes SET and the wheels act on a channel that is not
> + * the lit one: leave the pair unknown, as for a never-seen pair.
> + */
> + if (restore && chip->reprobe)
> + return 0;
> +
> + guard(mutex)(&chip->mutex);
> + if (pair == READ_ONCE(chip->panel_in) - 1 && !restore && sel >= 0)
> + /* Set while the LEDs show the selection: the next press steps
> + * it.
> + */
> + chip->panel_select_armed = true;
> + if (sel == chip->panel_sel[pair])
> + return 0;
> + chip->panel_sel[pair] = sel;
> + if (pair == READ_ONCE(chip->panel_in) - 1) {
> + bf_panel_sel_load(chip);
> + if (READ_ONCE(chip->panel_mix))\n> + bf_panel_mix_seed(chip);
> + }
> + return 1;
> +}
[Severity: Medium]
As reported by Sashiko on v7 [5], setting chip->panel_start = jiffies inside
babyface_panel_start() causes two issues for the restore check in
bf_panel_select_put():
1. In babyface_probe(), snd_card_register(chip->card) is called before
babyface_panel_start(chip). If udev's alsactl restore runs immediately upon
card registration before babyface_panel_start() sets chip->panel_start =
jiffies, chip->panel_start is still 0 (so restore evaluates to false once
uptime exceeds ~303 seconds), bypassing if (restore && chip->reprobe) and
allowing babyface_panel_start() to race unlocked with bf_panel_select_put().
2. In babyface_resume(), calling babyface_panel_start(chip) on every system
resume resets chip->panel_start = jiffies, re-opening a 3-second restore
window after every resume during which bf_panel_select_put() drops writes if
chip->reprobe is true or skips setting chip->panel_select_armed = true if
chip->reprobe is false.
[Severity: Medium]
What happens in bf_panel_select_put() and bf_panel_sel_load() when the unit is
in OUT mode (st[2] = 0x8x/0x9x) or fader/MIX mode (st[2] = 0x0x) at probe or
resume time?
In those modes, bf_panel_in_decode() returns 0, leaving chip->panel_in at 0
(Unknown). While chip->panel_in is 0, READ_ONCE(chip->panel_in) - 1 is -1, so
bf_panel_sel_load() forces panel_select_known = false and panel_select = 3,
and writing to any of the "Front Panel Selection" controls in
bf_panel_select_put() never loads the selection or arms panel_select_armed. If
the user enters MIX mode while panel_in is 0, turning the wheel prints the
bf_panel_sel_hint() message instructing them to set "Front Panel Selection",
but setting the control has no effect while panel_in remains 0.
> diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
> index 06ef1a97d27a4..ece2bd0c4e63a 100644
> --- a/sound/usb/babyfacepro/babyfacepro.c
> +++ b/sound/usb/babyfacepro/babyfacepro.c
[ ... ]
> @@ -1432,8 +1459,12 @@ MODULE_PARM_DESC(id, "ID string for the Babyface Pro sound card.");
> module_param(frames_per_urb, int, 0444);
> MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
> module_param(urb_frames_min, int, 0444);
> MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
> +module_param(urb_split, int, 0444);
> +MODULE_PARM_DESC(urb_split, "Split the period into two URBs, queueing 1.5 periods instead of 2: 0 = auto (default), 1 = never, 2 = always.");
> module_param(nurbs, int, 0444);
> MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");
> +module_param(panel_poll_ms, int, 0444);
> +MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz).");
[Severity: Low]
As reported by Sashiko on v7 [6], should the new panel_poll_ms and urb_split
module parameters be documented in
Documentation/admin-guide/kernel-parameters.txt, and should the PCM urb_split
module parameter be moved to the PCM streaming patch where urb_split is
introduced?
[1] https://lore.kernel.org/r/20261027113140.134257-7-i.bahloul01@gmail.com
[2] https://lore.kernel.org/r/20261027113140.134257-7-i.bahloul01@gmail.com
[3] https://lore.kernel.org/r/20261027113140.134257-7-i.bahloul01@gmail.com
[4] https://lore.kernel.org/r/20261027113140.134257-7-i.bahloul01@gmail.com
[5] https://lore.kernel.org/r/20261027113140.134257-7-i.bahloul01@gmail.com
[6] https://lore.kernel.org/r/20261027113140.134257-7-i.bahloul01@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=6
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ
2026-11-05 15:15 ` [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
@ 2026-11-05 17:19 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 17:19 UTC (permalink / raw)
To: Ismaïl Bahloul, linux-sound; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: shared biquad scale overwritten in bf_eq_update_strip()
- [High] ALSA: usb: babyfacepro: inverted sign of high-shelf a1 in bf_eq_band_words()
- [Medium] ALSA: usb: babyfacepro: unrolled EQ cache in bf_eq_put() and ignored errors in bf_eq_reupload()
- [Medium] ALSA: usb: babyfacepro: missing freq_hz <= 0 guard in bf_eq_band_words()
- [Medium] ALSA: usb: babyfacepro: uninitialized boolean min and max in bf_eq_info()
- [Medium] ALSA: usb: babyfacepro: plain 64-bit signed division in bf_eq_lc_freq_raw()
- [Medium] ALSA: usb: babyfacepro: missing hardware DSP EQ reset in babyface_probe()
- [Medium] ALSA: usb: babyfacepro: ignored error return in bf_eq_reupload()
- [Low] ALSA: usb: babyfacepro: 64-bit value truncated before range check in bf_eq_put()
--
commit 100d534996c41ac8c2801f7eafc48e129593aa13
Author: Ismaïl Bahloul <i.bahloul01@gmail.com>
ALSA: usb: babyfacepro: add the hardware DSP EQ
This commit adds support for the 4-strip (AN1-AN4) 3-band and low-cut
hardware DSP parametric EQ, including fixed-point Q27 biquad coefficient
calculation, 64-byte block uploads over bulk endpoint 0x0A, and 15 ALSA
mixer controls per strip.
> diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
> index 51cf8c7bd4357..a1b8caa281dad 100644
> --- a/sound/usb/babyfacepro/babyfacepro-ctl.c
> +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
[ ... ]
> @@ -3336,3 +3337,610 @@ int babyface_create_panel(struct snd_usb_babyface *chip)
[ ... ]
> +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, u, su, cu, 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 (type == 0 || gain_x10 == 0 || q100 <= 0) {
> + /* Inactive band (type Off, or no gain/Q yet): 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;
> + /* w[4] is the shared scale bf_eq_update_strip() reads when the
> + * band has a type and a gain but no Q yet (Q defaults to 0).
> + * Leaving it unwritten uploaded a garbage shared scale.
> + */
> + w[4] = BF_EQ_Q27;
> + return;
> + }
[Severity: Medium]
Should the inactive-band check in bf_eq_band_words() also guard against
freq_hz <= 0, similar to bf_eq_lc_freq_raw()?
At probe time, bf_eq_defaults() initializes band_q[] to 100 while leaving
band_freq[] at 0 Hz, and bf_eq_info() / bf_eq_put() allow frequency values
down to 0. If a user sets a band's Type and Gain before setting Freq (or
sets Freq to 0), bf_eq_band_words() runs with freq_hz == 0, which gives
w0 = 0 and u = 0. Calling bf_sincos(0, ...) produces su = -1 (alpha < 0)
and c = 0x7fffffe (~1.0 in Q27), uploading a degenerate biquad with poles
on or outside the unit circle at z = 1 instead of the identity filter.
[ ... ]
> + 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);
> +}
[Severity: High]
In the high-shelf branch (type == 3) of bf_eq_band_words(), is the sign of
a1 inverted?
With am1 = A - 1 and cp1 = (A + 1) * cos(w0), the Audio EQ Cookbook
high-shelf denominator coefficient is:
a1 = 2 * ((A - 1) - (A + 1) * cos(w0)) = 2 * (am1 - cp1)
Using a1 = -2 * (am1 - cp1) negates w[0], so as gain approaches 0 dB (A -> 1,
am1 -> 0, cp1 -> 2 * cos(w0)), b1 approaches -4 * cos(w0) while a1
approaches +4 * cos(w0), placing the filter poles at pi - w0 instead of w0.
[ ... ]
> +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 = div64_s64(11508 * f * 11656 + (11656 + f) / 2, 11656 + f);
> + return (u32)word;
> +}
[Severity: Medium]
As reported by Sashiko on v7 [1], in bf_eq_lc_freq_raw(), f is declared as
s64 and divided by 10000 using the plain / operator.
Because bf_eq_reupload() -> bf_eq_update_strip() passes chip->eq[strip].lc_hz
(an s32 field in memory) as freq_hz, the compiler only knows freq_hz is in
[1, INT_MAX], so f * 15267 can exceed 32 bits. On 32-bit architectures,
this emits a libcall to __divdi3 / __aeabi_ldivmod and fails at link time.
Should this use div_s64() (or 32-bit arithmetic since lc_hz <= 20000)?
[ ... ]
> +static int 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);
> + return bf_eq_write_strip(chip, strip);
> +}
[Severity: High]
When two or three bands are active on the same strip in
bf_eq_update_strip(), doesn't overwriting last_c4 = w[4] discard the b0/a0
scale factors of the earlier active bands?
In bf_eq_band_words(), each band's numerator coefficients w[2] = b1 / b0
and w[3] = b2 / b0 are normalized by b0 rather than a0, factoring that
band's b0 / a0 gain scale into w[4]. Since the three biquad slots in
bf_eq_build_block() only receive w[0..3] and share a single scale word at
offset 0x34, shouldn't last_c4 accumulate the Q27 product of w[4] across all
active bands rather than taking only the last active band's w[4]?
> +
> +/* 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);
> +}
[Severity: Medium]
As reported by Sashiko on v7 [1], bf_eq_reupload() ignores the error return
value of bf_eq_update_strip().
If kmemdup() or usb_bulk_msg() fails in bf_eq_upload(), bf_eq_reupload()
continues attempting up to 7 more synchronous bulk transfers (with a 1000 ms
timeout each while holding chip->mutex), and callers such as
bf_resume_locked() and babyface_pcm_hw_params() cannot detect or propagate
the failure. Should bf_eq_reupload() return an int error code and stop on
the first failing strip?
[ ... ]
> +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;
> + }
[Severity: Medium]
When param == 0 in bf_eq_info(), uinfo->value.integer.min and
uinfo->value.integer.max are left uninitialized.
In snd_ctl_elem_info_user(), uinfo is copied from userspace without zeroing
uinfo->value, so callers reusing the struct across controls see stale
min/max values; in 32-bit compat mode (snd_ctl_elem_info_compat()), max is
reported as 0 instead of 1. Could this set min = 0 and max = 1 (or return
snd_ctl_boolean_mono_info(uinfo)) like the other boolean controls in this
file?
[ ... ]
> +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];
[Severity: Low]
On 64-bit architectures, ucontrol->value.integer.value[0] is a 64-bit long.
Casting it to s32 before the switch (param) range checks below truncates the
upper 32 bits when CONFIG_SND_CTL_INPUT_VALIDATION is disabled, allowing
out-of-range 64-bit values whose lower 32 bits fall within bounds to wrap
and be accepted. Could nv be kept as a long (or range-checked before
narrowing, as in bf_master_put() and bf_width_put())?
[ ... ]
> + guard(mutex)(&chip->mutex);
> + if (param == 0) {
> + if (e->on != !!nv) {
> + e->on = !!nv;
> + ret = bf_eq_update_strip(chip, strip);
> + if (ret < 0)
> + return ret;
> + ret = 1;
> + }
> + } else if (v && *v != nv) {
> + *v = nv;
> + ret = bf_eq_update_strip(chip, strip);
> + if (ret < 0)
> + return ret;
> + ret = 1;
> + }
> + return ret;
> +}
[Severity: Medium]
As reported by Sashiko on v7 [1], in bf_eq_put(), e->on or *v is updated
before calling bf_eq_update_strip() and is not restored if
bf_eq_update_strip() fails (for example, if kmemdup() returns -ENOMEM or
usb_bulk_msg() fails in bf_eq_upload()).
Because chip->eq[strip] retains the new value nv, the host cache becomes
desynchronized from the hardware DSP, and a subsequent retry writing the
same value nv sees e->on == !!nv or *v == nv and returns 0 without
re-uploading the coefficients. In addition, while bf_eq_upload() sleeps,
concurrent lockless readers in bf_eq_get() observe the uncommitted value.
Should bf_eq_put() restore the previous value on failure (like
bf_phantom_put() and bf_reflevel_put())?
> diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
> index ece2bd0c4e63a..ed17600aba2f0 100644
> --- a/sound/usb/babyfacepro/babyfacepro.c
> +++ b/sound/usb/babyfacepro/babyfacepro.c
[ ... ]
> @@ -1638,6 +1640,8 @@ static int babyface_probe(struct usb_interface *intf,
> BF_REF_LEVEL_4DBU : BF_REF_LEVEL_MINUS10DBV;
> }
>
> + bf_eq_defaults(chip);
> +
> /* The 0x16 clear zeroed the mixer registers: apply the factory
> * default routing to keep the outputs live out of the box. alsactl
> * restores the user's settings once the card is registered.
[Severity: Medium]
Does babyface_probe() also need to call bf_eq_reupload(chip) after
bf_eq_defaults(chip) to reset the hardware DSP EQ state?
As noted in the commit message and bf_resume_locked(), bf_cold_init() does
not clear the DSP EQ on bulk endpoint 0x0A. If the device already had EQ
filters active before a driver reload, warm reboot, or re-probe,
bf_eq_defaults() sets e->on = false and default parameters in chip->eq[]
without uploading identity blocks to the hardware. When alsactl subsequently
restores a state where EQ is disabled (or at default values), bf_eq_put()
sees e->on == !!nv and *v == nv and skips bf_eq_update_strip(), leaving the
stale EQ filters active in the hardware DSP while the ALSA controls report
EQ Enable as Off.
[1] https://lore.kernel.org/r/20261027113140.134257-8-i.bahloul01@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=7
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc
2026-11-05 15:15 ` [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
@ 2026-11-05 17:21 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-11-05 17:21 UTC (permalink / raw)
To: Ismaïl Bahloul; +Cc: linux-usb
> 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 stream session
> 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105151553.223722-1-i.bahloul01@gmail.com?part=8
^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-11-05 17:21 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-05 15:15 [PATCH v8 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-11-05 15:15 ` [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-11-05 16:24 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
2026-11-05 15:31 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
2026-11-05 15:32 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
2026-11-05 15:32 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
2026-11-05 16:45 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
2026-11-05 17:03 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-11-05 17:19 ` sashiko-bot
2026-11-05 15:15 ` [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
2026-11-05 17:21 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox