* [RFC PATCH v6 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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.
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 | 1535 +++++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.h | 187 ++++
6 files changed, 1750 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 5c57cbb68..66bf8d297 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24049,6 +24049,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..8f8667083
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -0,0 +1,1535 @@
+// 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"
+
+/* -- sample-rate / alt classes -------------------- */
+
+/* rate = family_base << (alt - 1): three rate families (32k / 44.1k /
+ * 48k), three interface speeds. Measured on hardware, all nine rates,
+ * through the family register alone (2026-09-16).
+ *
+ * 64 kHz is the 32 kHz family at x2, not a 64 kHz base at x1. Both clock
+ * 64 kHz at pitch 0, but a 64 kHz base on alt 1 needs 64000 x 56 B =
+ * 3584 kB/s, which is exactly alt 1's 448-byte packet ceiling: any
+ * positive varispeed then runs off the end of the bus, and the device
+ * delivers the mirrored rate instead (asked +5 %, got -5 %). At x2 the
+ * frame is 40 B, so the same 64 kHz costs 2560 kB/s against a 5120 kB/s
+ * ceiling. Same reasoning puts 128 kHz on alt 3 rather than alt 2.
+ */
+static const struct bf_rate bf_rates[] = {
+ { 32000, BF_ALT_1, 56, 8 },
+ { 44100, BF_ALT_1, 56, 8 },
+ { 48000, BF_ALT_1, 56, 8 },
+ { 64000, BF_ALT_2, 40, 16 },
+ { 88200, BF_ALT_2, 40, 16 },
+ { 96000, BF_ALT_2, 40, 16 },
+ { 128000, BF_ALT_3, 32, 32 },
+ { 176400, BF_ALT_3, 32, 32 },
+ { 192000, BF_ALT_3, 32, 32 },
+};
+
+static const unsigned int bf_rate_list[ARRAY_SIZE(bf_rates)] = {
+ 32000, 44100, 48000, 64000, 88200,
+ 96000, 128000, 176400, 192000,
+};
+
+const struct snd_pcm_hw_constraint_list bf_rates_constraint = {
+ .count = ARRAY_SIZE(bf_rate_list),
+ .list = bf_rate_list,
+ .mask = 0,
+};
+
+const struct bf_rate *bf_rate_lookup(unsigned int rate)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(bf_rates); i++)
+ if (bf_rates[i].rate == rate)
+ return &bf_rates[i];
+ return NULL;
+}
+
+unsigned int bf_rate_family(const struct bf_rate *r)
+{
+ switch (r->rate >> (r->alt - 1)) {
+ case 32000:
+ return 0;
+ case 44100:
+ return 1;
+ default:
+ return 2;
+ }
+}
+
+/* -- vendor requests ----------------------- */
+
+/* Timeout for every vendor control transfer (ms). */
+#define BF_CTL_TIMEOUT 1000
+
+int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx)
+{
+ return usb_control_msg_send(chip->dev, 0, req,
+ USB_DIR_OUT | USB_TYPE_VENDOR |
+ USB_RECIP_DEVICE,
+ val, idx, NULL, 0, BF_CTL_TIMEOUT, GFP_KERNEL);
+}
+
+int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf)
+{
+ return usb_control_msg_recv(chip->dev, 0, req,
+ USB_DIR_IN | USB_TYPE_VENDOR |
+ USB_RECIP_DEVICE,
+ 0, idx, buf, 4, BF_CTL_TIMEOUT, GFP_KERNEL);
+}
+
+/* Sends the BF_REG_KEEPALIVE_SETTINGS word (PROTOCOL.md: "keepalive
+ * 0x10 0x05CF wVal = host settings-state register" - a single shared
+ * word, not independent per-setting writes). 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;
+}
+
+/* -- PCM data path ------------------------- */
+
+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 pos, f, i;
+ unsigned long new_period;
+ bool crossed = false;
+ u8 *dst;
+
+ spin_lock(&chip->lock);
+ pos = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] % buf_frames;
+ for (f = 0; f < frames; f++) {
+ const __le32 *w = (const __le32 *)(data + f * chip->frame_bytes);
+
+ dst = rt->dma_area + frames_to_bytes(rt, pos);
+ for (i = 0; i < chans; i++) {
+ /* Channel map: app ch0-3 = device words 0-3 (AN1-4);
+ * app ch4-9 = words 6-11 (ADAT/SPDIF); app ch10/11 =
+ * words 12/13 = a FIXED-GAIN playback tap (observed
+ * 2026-08-25: the playback echoes there at ~-27 dB,
+ * independent of the output masters - NOT the output
+ * bus; the ADAT/SPDIF range is words 6-11 only). The
+ * device words 4/5 are a fixed marker, not audio -
+ * skipped. At 96/192 kHz the frame has fewer words;
+ * missing ones read as zero.
+ */
+ static const u8 map[12] = { 0, 1, 2, 3, 6, 7, 8, 9,
+ 10, 11, 12, 13 };
+ u8 wi = i < 12 ? map[i] : 0xff;
+ s32 s = 0;
+
+ if (wi < words) {
+ /* 24-bit sample in bits 31-8 of the 32-bit word;
+ * S32_LE with 24 msbits, so copy the word as-is.
+ */
+ s = (s32)le32_to_cpu(w[wi]);
+ }
+ put_unaligned_le32((u32)s, dst + i * 4);
+ }
+ pos++;
+ if (pos >= buf_frames)
+ pos = 0;
+ }
+ chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] += frames;
+ new_period = chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] / rt->period_size;
+ if (new_period != chip->prev_period[SNDRV_PCM_STREAM_CAPTURE]) {
+ chip->prev_period[SNDRV_PCM_STREAM_CAPTURE] = new_period;
+ crossed = true;
+ }
+ spin_unlock(&chip->lock);
+
+ return crossed;
+}
+
+static bool babyface_playback_copy(struct snd_usb_babyface *chip,
+ struct snd_pcm_substream *subs,
+ u8 *data, unsigned int frames)
+{
+ struct snd_pcm_runtime *rt = subs->runtime;
+ unsigned int buf_frames = rt->buffer_size;
+ unsigned int words = chip->frame_bytes / 4;
+ unsigned int chans = rt->channels;
+ unsigned int pos, f, i;
+ unsigned int total = frames; /* the URB buffer's full frame count */
+ unsigned long new_period;
+ bool crossed = false;
+ const u8 *src;
+
+ spin_lock(&chip->lock);
+ /* Never copy frames the app has not prepared. With several URBs in
+ * flight the device can transiently be ahead of the app ring at low
+ * periods, so hw_ptr may reach past appl_ptr; reading beyond appl_ptr
+ * would trip a spurious XRUN. The device just repeats the last
+ * frames instead. (Both counters are unbounded, so clamp on the
+ * signed difference.)
+ */
+ {
+ snd_pcm_sframes_t avail =
+ (snd_pcm_sframes_t)(rt->control->appl_ptr -
+ chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK]);
+
+ if (avail < 0)
+ avail = 0;
+ else if (avail > (snd_pcm_sframes_t)buf_frames)
+ avail = buf_frames;
+ if ((snd_pcm_uframes_t)avail < frames)
+ frames = (unsigned int)avail;
+ }
+ pos = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] % buf_frames;
+ for (f = 0; f < frames; f++) {
+ __le32 *w = (__le32 *)(data + f * chip->frame_bytes);
+
+ src = rt->dma_area + frames_to_bytes(rt, pos);
+ /* App ch n feeds the device word n (PB1-6 = words 0-11);
+ * words 12/13 stay zero. At 96/192 kHz the frame is
+ * shorter - the extra app channels are dropped.
+ */
+ for (i = 0; i < chans && i < words; i++) {
+ u32 s = get_unaligned_le32(src + i * 4);
+
+ /* S32_LE msbits=24: the app's 24 valid bits are already
+ * left-justified (bits 31-8) - copy the word as-is.
+ */
+ w[i] = cpu_to_le32(s);
+ }
+ for (; i < words; i++)
+ w[i] = 0;
+ pos++;
+ if (pos >= buf_frames)
+ pos = 0;
+ }
+ /* A clamped copy wrote only [0, frames); clear the tail, or the device
+ * plays whatever the previous completion left in the URB buffer.
+ */
+ if (frames < total)
+ memset(data + frames * chip->frame_bytes, 0,
+ (total - frames) * chip->frame_bytes);
+ chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] += frames;
+ new_period = chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] / rt->period_size;
+ if (new_period != chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK]) {
+ chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK] = new_period;
+ crossed = true;
+ }
+ spin_unlock(&chip->lock);
+
+ return crossed;
+}
+
+/* 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))
+ ;
+}
+
+/* An OUT URB that is not resubmitted leaves the queue. */
+static void bf_out_lost(struct snd_usb_babyface *chip)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&chip->lock, flags);
+ if (chip->out_inflight)
+ chip->out_inflight--;
+ spin_unlock_irqrestore(&chip->lock, flags);
+}
+
+static void babyface_complete_in(struct urb *urb)
+{
+ struct snd_usb_babyface *chip = urb->context;
+ struct snd_pcm_substream *subs;
+ unsigned long flags;
+ unsigned int frames;
+ bool crossed = false;
+ int ret;
+
+ if (urb->status < 0) {
+ if (urb->status == -ESHUTDOWN || urb->status == -ENOENT ||
+ urb->status == -ECONNRESET || 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);
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->in_done = ktime_get();
+ spin_unlock_irqrestore(&chip->lock, flags);
+
+ /* The session outlives a substream that is closed while the other
+ * direction still runs: close() waits out this RCU section.
+ */
+ rcu_read_lock();
+ subs = rcu_dereference(chip->subs[SNDRV_PCM_STREAM_CAPTURE]);
+ if (subs) {
+ snd_pcm_stream_lock_irqsave(subs, flags);
+ if (snd_pcm_running(subs)) {
+ frames = urb->actual_length / chip->frame_bytes;
+ if (frames)
+ crossed = babyface_capture_copy(chip, subs,
+ urb->transfer_buffer,
+ frames);
+ }
+ snd_pcm_stream_unlock_irqrestore(subs, flags);
+ if (crossed)
+ snd_pcm_period_elapsed(subs);
+ }
+ rcu_read_unlock();
+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_error(chip);
+ }
+}
+
+static void babyface_complete_out(struct urb *urb)
+{
+ struct snd_usb_babyface *chip = urb->context;
+ struct snd_pcm_substream *subs;
+ unsigned long flags;
+ unsigned int frames;
+ bool crossed = false;
+ 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);
+
+ rcu_read_lock();
+ subs = rcu_dereference(chip->subs[SNDRV_PCM_STREAM_PLAYBACK]);
+ if (subs) {
+ snd_pcm_stream_lock_irqsave(subs, flags);
+ if (snd_pcm_running(subs)) {
+ frames = urb->transfer_buffer_length / chip->frame_bytes;
+ crossed = babyface_playback_copy(chip, subs,
+ urb->transfer_buffer, frames);
+ fed = true;
+ }
+ snd_pcm_stream_unlock_irqrestore(subs, flags);
+ if (crossed)
+ snd_pcm_period_elapsed(subs);
+ }
+ rcu_read_unlock();
+ /* 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_error(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)
+{
+ unsigned long flags;
+ int s;
+
+ /* close() waits out this RCU section, as for the URB handlers. */
+ rcu_read_lock();
+ for (s = 0; s < 2; s++) {
+ struct snd_pcm_substream *subs = rcu_dereference(chip->subs[s]);
+
+ if (!subs)
+ continue;
+ snd_pcm_stream_lock_irqsave(subs, flags);
+ if (snd_pcm_running(subs))
+ snd_pcm_stop(subs, state);
+ snd_pcm_stream_unlock_irqrestore(subs, flags);
+ }
+ rcu_read_unlock();
+}
+
+/* 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.
+ */
+ 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;
+ }
+ spin_lock_irq(&chip->lock);
+ chip->out_inflight = chip->urbs_active;
+ spin_unlock_irq(&chip->lock);
+ /* 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);
+
+ mutex_lock(&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);
+ }
+ mutex_unlock(&chip->mutex);
+}
+
+/* -- PCM --------------------------- */
+
+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);
+}
+
+/* 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);
+ 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;
+
+ mutex_lock(&chip->mutex);
+ held = bf_other_holds_clock(chip, subs);
+ t.min = chip->rate;
+ t.max = chip->rate;
+ mutex_unlock(&chip->mutex);
+ 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;
+
+ mutex_lock(&chip->mutex);
+ held = bf_other_holds_clock(chip, subs);
+ t.min = bf_session_period(chip);
+ mutex_unlock(&chip->mutex);
+ 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;
+ unsigned long flags;
+ int ret;
+
+ rt->hw = babyface_pcm_hw;
+ ret = snd_pcm_hw_constraint_list(rt, 0, SNDRV_PCM_HW_PARAM_RATE,
+ &bf_rates_constraint);
+ if (ret < 0)
+ return ret;
+ /* The stream is 24-bit audio in a 32-bit container, left-justified
+ * (the device word carries the sample in bits 31-8). Declare the
+ * valid-bit width so user-space knows to ignore the low 8 bits.
+ */
+ ret = snd_pcm_hw_constraint_msbits(rt, 0, 32, 24);
+ if (ret < 0)
+ return ret;
+ /* 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.
+ */
+ mutex_lock(&chip->mutex);
+ chip->owner[subs->stream] = task_tgid_nr(current);
+ mutex_unlock(&chip->mutex);
+ 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;
+
+ spin_lock_irqsave(&chip->lock, flags);
+ rcu_assign_pointer(chip->subs[subs->stream], subs);
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+}
+
+static int babyface_pcm_close(struct snd_pcm_substream *subs)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ unsigned long flags;
+
+ flush_work(&chip->stream_work);
+ mutex_lock(&chip->mutex);
+ chip->owner[subs->stream] = 0;
+ mutex_unlock(&chip->mutex);
+ spin_lock_irqsave(&chip->lock, flags);
+ RCU_INIT_POINTER(chip->subs[subs->stream], NULL);
+ spin_unlock_irqrestore(&chip->lock, flags);
+ /* 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;
+ }
+
+ mutex_lock(&chip->mutex);
+ /* disconnect() has let go of the interface: do not re-point it. */
+ if (chip->shutdown) {
+ ret = -ENODEV;
+ goto out;
+ }
+ 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);
+ ret = -EBUSY;
+ goto out;
+ }
+ /* 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)
+ goto out;
+ 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)) {
+ ret = -EBUSY;
+ goto out;
+ }
+ } 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 = clamp_t(unsigned int,
+ 2 * (params_period_size(params) / urb),
+ 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++;
+ }
+out:
+ mutex_unlock(&chip->mutex);
+ 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);
+
+ mutex_lock(&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");
+ }
+ }
+ mutex_unlock(&chip->mutex);
+ 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);
+ unsigned long flags;
+ int ret = 0;
+
+ mutex_lock(&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);
+ mutex_unlock(&chip->mutex);
+ if (ret < 0)
+ return ret;
+
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->hw_ptr[subs->stream] = 0;
+ chip->prev_period[subs->stream] = 0;
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+}
+
+static int babyface_pcm_trigger(struct snd_pcm_substream *subs, int cmd)
+{
+ struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+ unsigned long flags;
+
+ switch (cmd) {
+ /* 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;
+ spin_lock_irqsave(&chip->lock, flags);
+ chip->hw_ptr[subs->stream] = 0;
+ chip->prev_period[subs->stream] = 0;
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return 0;
+ 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);
+ unsigned long flags;
+ snd_pcm_uframes_t pos;
+
+ spin_lock_irqsave(&chip->lock, flags);
+ pos = chip->hw_ptr[subs->stream] % subs->runtime->buffer_size;
+ subs->runtime->delay = bf_delay(chip, subs);
+ spin_unlock_irqrestore(&chip->lock, flags);
+ return pos;
+}
+
+static const struct snd_pcm_ops babyface_pcm_ops = {
+ .open = babyface_pcm_open,
+ .close = babyface_pcm_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .hw_params = babyface_pcm_hw_params,
+ .hw_free = babyface_pcm_hw_free,
+ .prepare = babyface_pcm_prepare,
+ .trigger = babyface_pcm_trigger,
+ .pointer = babyface_pcm_pointer,
+};
+
+static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
+static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
+static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT;
+static int 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).");
+
+/* -- USB driver ------------------------- */
+
+static void babyface_private_free(struct snd_card *card)
+{
+ struct snd_usb_babyface *chip = card->private_data;
+ unsigned int urbsize;
+ int i;
+
+ if (!chip)
+ return;
+
+ /* The URB arrays are NULL when the probe failed before allocating
+ * them (snd_card_free runs private_free on any probe error).
+ */
+ if (chip->urbs_in) {
+ urbsize = BF_WORDS_PER_FRAME * sizeof(u32) * chip->frames_per_urb;
+ for (i = 0; i < chip->nurbs; i++) {
+ if (chip->urbs_in[i]) {
+ usb_kill_urb(chip->urbs_in[i]);
+ usb_free_urb(chip->urbs_in[i]);
+ }
+ if (chip->urbs_out && 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)
+ goto error;
+
+ for (i = 0; i < chip->nurbs; i++) {
+ chip->urbs_in[i] = usb_alloc_urb(0, GFP_KERNEL);
+ chip->urbs_out[i] = usb_alloc_urb(0, GFP_KERNEL);
+ chip->buf_in[i] = usb_alloc_coherent(dev, urbsize, GFP_KERNEL,
+ &chip->dma_in[i]);
+ chip->buf_out[i] = usb_alloc_coherent(dev, urbsize, GFP_KERNEL,
+ &chip->dma_out[i]);
+ if (!chip->urbs_in[i] || !chip->urbs_out[i] ||
+ !chip->buf_in[i] || !chip->buf_out[i])
+ goto error;
+ }
+
+ err = snd_pcm_new(chip->card, "Babyface Pro", 0, 1, 1, &pcm);
+ if (err < 0) {
+ dev_err(&intf->dev, "snd_pcm_new failed: %d\n", err);
+ goto error;
+ }
+ pcm->private_data = chip;
+ strscpy(pcm->name, "Babyface Pro", sizeof(pcm->name));
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &babyface_pcm_ops);
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &babyface_pcm_ops);
+
+ /* The PCM buffer is host-side (the URB callbacks copy in/out of
+ * it); vmalloc is the standard choice for that.
+ */
+ err = snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC,
+ NULL, 0, 1 << 20);
+ if (err < 0) {
+ dev_err(&intf->dev, "buffer allocation failed: %d\n", err);
+ goto error;
+ }
+
+ err = snd_card_register(chip->card);
+ if (err < 0) {
+ dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
+ goto error;
+ }
+
+ usb_set_intfdata(intf, chip);
+ dev_info(&intf->dev,
+ "Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n",
+ chip->card->number, chip->frames_per_urb, chip->nurbs);
+ return 0;
+
+error:
+ usb_set_intfdata(intf, NULL);
+ /* Balance the probe()-time usb_disable_autosuspend(): disconnect()
+ * is never called for a failed probe, so the disable would leak and
+ * leave autosuspend off on this usb_device until a physical unplug.
+ */
+ usb_enable_autosuspend(chip->dev);
+ snd_card_free(chip->card);
+ return err;
+}
+
+static void babyface_disconnect(struct usb_interface *intf)
+{
+ struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+ if (!chip)
+ return;
+
+ /* Idempotence guard: a disconnect can race a re-probe (usbfs
+ * detach/re-attach) - tear the card down exactly once.
+ */
+ usb_set_intfdata(intf, NULL);
+ if (chip->shutdown)
+ return;
+
+ chip->shutdown = true;
+ /* 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.
+ */
+ mutex_lock(&chip->mutex);
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ mutex_unlock(&chip->mutex);
+ 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..60ab0c387
--- /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 [flat|nested] 9+ messages in thread* [RFC PATCH v6 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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. The AN1/2 output sums from its own "low map" registers, so its
crosspoints are written to both maps. A volume change leaves a muted
output muted.
A userspace usbfs claim (PipeWire, a libusb mixer daemon) detaches and
re-probes the interface, and the cold init of the re-probe would wipe
the settings, so the mixer state is saved at disconnect and restored
at the next probe instead of the default.
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 | 710 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 177 +++++-
sound/usb/babyfacepro/babyfacepro.h | 123 +++-
4 files changed, 1008 insertions(+), 4 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..6ef29aa27
--- /dev/null
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -0,0 +1,710 @@
+// 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 blk)
+{
+ int ret, k;
+
+ for (k = BF_CROSS_L_FIRST; k <= BF_CROSS_L_LAST; k += 2) {
+ ret = bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000,
+ BF_REG_CROSS_BASE_L +
+ BF_REG_CROSS_STRIDE * blk + k);
+ if (ret < 0)
+ return ret;
+ }
+ for (k = BF_CROSS_R_FIRST; k <= BF_CROSS_R_LAST; k += 2) {
+ ret = bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000,
+ BF_REG_CROSS_BASE_R +
+ BF_REG_CROSS_STRIDE * blk + k);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+const struct bf_source bf_sources[14] = {
+ { "AN1", 0, 0 },
+ { "AN2", 1, 1 },
+ { "AN3", 2, 2 },
+ { "AN4", 3, 3 },
+ { "AS1/2", 4, 5 },
+ { "ADAT3/4", 6, 7 },
+ { "ADAT5/6", 8, 9 },
+ { "ADAT7/8", 10, 11 },
+ { "PB1", 12, 13 },
+ { "PB2", 14, 15 },
+ { "PB3", 16, 17 },
+ { "PB4", 18, 19 },
+ { "PB5", 20, 21 },
+ { "PB6", 22, 23 },
+};
+
+/* Crosspoint-map output order vs the master-map order - HARDWARE-
+ * VERIFIED 2026-08-24: the block that feeds the Phones is the FIRST
+ * crosspoint block (0x34), while the Phones master is the SECOND
+ * (0x03E2/0x0006). The crosspoint map lists the Phones first (the
+ * monitor output); the master map lists AN1/2 first. Control index =
+ * the canonical order (AN1/2=0, PH3/4=1, ...) so the crosspoint and
+ * master controls line up; this table maps to the register block.
+ */
+const u8 bf_xpoint_block[6] = { 1, 0, 2, 3, 4, 5 };
+
+/* Master-register output order - the master map lists AN1/2 first
+ * (0x03E0) and the Phones master SECOND (0x03E2, HARDWARE-VERIFIED
+ * 2026-08-24); the crosspoint blocks are in the opposite order
+ * (Phones = block 0x34 first, hence bf_xpoint_block above). Control
+ * index -> canonical output (AN1/2=0, PH3/4=1, ...) = the master
+ * register position directly: the names 'AN1/2 Playback Volume' etc.
+ * must match the register they write (corrected 2026-08-26 - the
+ * previous {1,0,...} swap made 'AN1/2' drive the Phones and 'PH3/4'
+ * drive the AN1/2 analog out).
+ */
+static const u8 bf_master_out[6] = { 0, 1, 2, 3, 4, 5 };
+
+/* The 16-bit master value -> the 8-bit companion code (0.5 dB/step).
+ * Integer-only: half_db = 12*log2(v/0x2000) via ilog2 + an 8-bit
+ * fractional-octave table (12*log2(1 + n/256), ~0.05 dB resolution -
+ * fine enough for the +/-0.5 dB panel wheel to track the round-trip).
+ */
+static const u8 bf_lg2_frac[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+};
+
+/* 16-bit master -> dBx2 (12 half-dB per octave; 0x2000 = 0 dB).
+ * Shared by the 8-bit companion and the front-panel OUT wheel.
+ */
+int bf_master_half_db(u16 vol16)
+{
+ unsigned int k, frac;
+
+ vol16 = clamp(vol16, 1, 0x4000);
+ k = ilog2(vol16);
+ frac = ((vol16 - (1u << k)) << 8) >> k;
+ return 12 * (int)k - 156 + bf_lg2_frac[frac];
+}
+
+/* dBx2 -> 16-bit master (0x2000*2^(half_db/12), rounded). The
+ * inverse of bf_master_half_db - the 12th-root table 2^(n/12).
+ */
+static const u16 bf_twelfth[12] = {
+ 0x1000, 0x10f4, 0x11f6, 0x1307, 0x1429, 0x155c,
+ 0x16a1, 0x17f9, 0x1966, 0x1ae9, 0x1c82, 0x1e34,
+};
+
+int bf_master_16bit(int half_db)
+{
+ int k = half_db / 12;
+ int n = half_db % 12;
+ u32 v;
+
+ if (n < 0) {
+ n += 12;
+ k--;
+ }
+ v = (u32)bf_twelfth[n] << 1; /* 0x2000*2^(n/12) */
+ if (k >= 0) {
+ v <<= k;
+ } else {
+ v += 1u << (-k - 1); /* round-half-up */
+ v >>= -k;
+ }
+ return (u16)clamp(v, 1, 0x4000);
+}
+
+u8 bf_master_8bit(u16 vol16)
+{
+ if (vol16 == 0)
+ return BF_MASTER_MUTE;
+ return (u8)clamp(0xf3 + bf_master_half_db(vol16), BF_MASTER_8_MIN, 0xff);
+}
+
+/* The cold-init register clear zeroes the mixer registers TotalMix
+ * re-uploads afterwards. The kernel driver has no saved scene (no
+ * readback for faders), so 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] = gain16;
+ chip->master[out][1] = gain16;
+ chip->muted[out] = false;
+ }
+
+ /* Every playback source into every output pair, L and R, at 0 dB,
+ * and the hardware inputs off (the standard map, plus the low map
+ * on AN1/2 - see bf_xpoint_write's own comment for why AN1/2 needs
+ * both). The addresses use the source's idx_l/idx_r on the
+ * canonical block - writing the raw index on both bases would put
+ * PB1 R on the L side and PB1 L on the R side (L+R on both = mono).
+ * The "cross" registers (L-reg idx_r / R-reg idx_l) are cleared.
+ */
+ for (out = 0; out < 6; out++) {
+ unsigned int blk = bf_xpoint_block[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, blk);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Mirror the defaults into the control cache (14 controls/output). */
+ for (out = 0; out < 6; out++)
+ for (src = 0; src < 14; src++) {
+ 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 = chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(l);
+ u8 r8 = chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(r);
+
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, l8,
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, r8,
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+/* -- mixer controls ------------------------ */
+
+/* dB TLV for the output masters: 0x2000 = 0 dB, 0x4000 = +6 dB
+ * (CALIBRATION.md) with the hardware 20*log10(v/0x2000) law - the raw
+ * 16-bit value IS the linear amplitude. WirePlumber needs this to map
+ * the volume 1:1 to the hardware control instead of applying a software
+ * volume on top (which left the output ~30 dB down).
+ */
+static const DECLARE_TLV_DB_RANGE(bf_master_tlv,
+ 0, 0x2000, TLV_DB_LINEAR_ITEM(-6500, 0),
+ 0x2000, 0x4000, TLV_DB_LINEAR_ITEM(0, 600)
+);
+
+static int bf_master_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 0x4000; /* +6 dB = 2 x 0dB(0x2000) */
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_master_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+
+ ucontrol->value.integer.value[0] = chip->master[out][0];
+ ucontrol->value.integer.value[1] = chip->master[out][1];
+ return 0;
+}
+
+static int bf_master_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+ 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;
+
+ /* The control is declared 0..0x4000 (+6 dB); reject anything outside
+ * so the 16-bit companion register and the cache stay in spec (the
+ * ALSA core only enforces this with CONFIG_SND_CTL_INPUT_VALIDATION).
+ * Check before narrowing: .value.integer.value[] is a long, so a
+ * value of 0x10000 or more would wrap to a passing u16.
+ */
+ if (nl < 0 || nl > 0x4000 || nr < 0 || nr > 0x4000)
+ return -EINVAL;
+ l = nl;
+ r = nr;
+
+ mutex_lock(&chip->mutex);
+ if (l == chip->master[out][0] && r == chip->master[out][1])
+ goto out;
+
+ 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;
+
+ /* The 8-bit register is the real volume; the 16-bit is its
+ * companion (kept in sync like TotalMix).
+ */
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(wire_l),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(wire_r),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, wire_l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, wire_r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ goto out;
+
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_mute_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_mute_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+
+ /* ALSA convention: 1 = enabled (sound on) = not muted. */
+ ucontrol->value.integer.value[0] = !chip->muted[out];
+ ucontrol->value.integer.value[1] = !chip->muted[out];
+ return 0;
+}
+
+static int bf_mute_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = bf_master_out[kctl->private_value];
+ bool muted = !ucontrol->value.integer.value[0];
+ u16 flag;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (muted == chip->muted[out])
+ goto out;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+ if (muted) {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ goto out;
+ } else {
+ /* Unmute restores the cached volume (TotalMix keeps the
+ * pre-mute fader value host-side), 8-bit + 16-bit.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->master[out][0]),
+ BF_REG_MASTER_8 + 2 * out);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->master[out][1]),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->master[out][0],
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->master[out][1],
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ if (ret < 0)
+ goto out;
+ }
+ chip->muted[out] = muted;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* -- crosspoint matrix (6 outputs x 14 sources) -------------- */
+
+/* The crosspoint fader is linear in amplitude: BF_FADER_0DB (0x16a0) is
+ * unity and BF_FADER_TOP (0x2d41) is exactly twice that, i.e. +6 dB; 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 standard map always, and -
+ * for the AN1/2 output only - the low map as well.
+ *
+ * HARDWARE-VERIFIED 2026-09-14: AN1/2 is not just another output with
+ * a redundant "shadow" register, despite what this file used to say.
+ * Sweeping only the standard map (BF_REG_CROSS_BASE_*) into AN1/2
+ * produced no audible change at all, off through +6 dB, with two
+ * independent sources (a generated tone via PB1, a live mic via AN2);
+ * the exact same code path targeting any other output (verified on
+ * PH3/4) tracked the fader correctly, off to +6 dB within 0.6 dB.
+ * PROTOCOL.md's "Scene load" capture explains why: the vendor software
+ * always writes BOTH the standard map and the low map
+ * (BF_REG_LOWMAP_BASE_*) together for AN1/2's own crosspoints, at the
+ * same value - the low map is what actually feeds that output's sum;
+ * the standard map alone is not enough. Every other output only has a
+ * standard map.
+ */
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+ u16 l, u16 r)
+{
+ unsigned int blk = bf_xpoint_block[out];
+ const struct bf_source *s = &bf_sources[src];
+ u16 flag;
+ int ret;
+
+ if (out == 0) {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ BF_REG_LOWMAP_BASE_L + s->idx_l);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ BF_REG_LOWMAP_BASE_R + s->idx_r);
+ if (ret < 0)
+ return ret;
+ }
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ s->idx_l) | flag);
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ s->idx_r) | flag);
+}
+
+static int bf_xpoint_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = BF_FADER_TOP; /* +6 dB fader top */
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_xpoint_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value >> 8;
+ int src = kctl->private_value & 0xff;
+
+ ucontrol->value.integer.value[0] = chip->xpoint[out][src][0];
+ ucontrol->value.integer.value[1] = chip->xpoint[out][src][1];
+ return 0;
+}
+
+static int bf_xpoint_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value >> 8;
+ int src = kctl->private_value & 0xff;
+ 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;
+
+ mutex_lock(&chip->mutex);
+ if (l == chip->xpoint[out][src][0] && r == chip->xpoint[out][src][1])
+ goto out;
+
+ ret = bf_xpoint_write(chip, out, src, l, r);
+ if (ret < 0)
+ goto out;
+
+ chip->xpoint[out][src][0] = l;
+ chip->xpoint[out][src][1] = r;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+int babyface_create_xpoints(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int out, src, err;
+
+ for (out = 0; out < 6; out++) {
+ for (src = 0; src < 14; src++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Playback Volume",
+ .index = out * 14 + src,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = bf_xpoint_info,
+ .get = bf_xpoint_get,
+ .put = bf_xpoint_put,
+ .tlv.p = bf_xpoint_tlv,
+ .private_value = (out << 8) | src,
+ }, chip);
+ /* Name the control by its source: "AN1 Playback Volume",
+ * "PB1 Playback Volume"... with a unique index.
+ */
+ strscpy(kctl->id.name, bf_sources[src].name,
+ sizeof(kctl->id.name));
+ strlcat(kctl->id.name, " Playback Volume",
+ sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+ }
+
+ return 0;
+}
+
+int babyface_create_masters(struct snd_usb_babyface *chip)
+{
+ static const char * const out_names[6] = {
+ "AN1/2", "PH3/4", "AS1/2", "ADAT3/4", "ADAT5/6", "ADAT7/8"
+ };
+ struct snd_kcontrol *kctl;
+ int i, err;
+
+ for (i = 0; i < 6; i++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = out_names[i],
+ .index = i,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = bf_master_info,
+ .get = bf_master_get,
+ .put = bf_master_put,
+ .tlv.p = bf_master_tlv,
+ .private_value = i,
+ }, chip);
+ strlcat(kctl->id.name, " Playback Volume", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ 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);
+ 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 8f8667083..82ec98346 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -3,9 +3,11 @@
* 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, mixer-state persistence across re-probes, 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>
@@ -253,6 +255,135 @@ int bf_cold_init(struct snd_usb_babyface *chip)
return 0;
}
+/* -- mixer-state persistence across interface re-probes --------
+ * A userspace client can claim the proprietary interface via usbfs
+ * (USBDEVFS_DISCONNECT_CLAIM - seen with PipeWire grabbing the
+ * device when a stream targets the sink, and with the TuxMix
+ * user-space daemon's libusb). That detaches us and the card
+ * disappears for the duration; on release the interface re-probes.
+ * The device keeps its registers across the detach, but our cold
+ * init clears them - so save the mixer state at disconnect and
+ * restore it at the next probe.
+ */
+
+static LIST_HEAD(bf_saved_list);
+static DEFINE_MUTEX(bf_saved_mutex);
+
+/* Re-apply the whole cached mixer state 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 (canonical out -> register block; AN1/2 also needs
+ * the low map, see bf_xpoint_write's own comment).
+ */
+ for (out = 0; out < 6; out++) {
+ unsigned int blk = bf_xpoint_block[out];
+
+ for (src = 0; src < 14; src++) {
+ ret = bf_xpoint_write(chip, out, src,
+ chip->xpoint[out][src][0],
+ chip->xpoint[out][src][1]);
+ if (ret < 0)
+ return ret;
+ }
+ ret = bf_crosspoint_clear_cross(chip, blk);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* The rate (family register), then the varispeed ratio, which is
+ * sticky in the device and so must be re-sent even at pitch 0 in
+ * case something else wrote it while we were detached.
+ */
+ ret = bf_clock_write(chip);
+ if (ret < 0)
+ return ret;
+ return bf_pitch_write(chip, chip->pitch);
+}
+
+void bf_state_save(struct snd_usb_babyface *chip)
+{
+ struct bf_saved *s;
+ const char *key = chip->dev->serial ? chip->dev->serial :
+ dev_name(&chip->dev->dev);
+ bool found = false;
+
+ mutex_lock(&bf_saved_mutex);
+ list_for_each_entry(s, &bf_saved_list, list) {
+ if (strcmp(s->key, key))
+ continue;
+ found = true;
+ break;
+ }
+ if (!found) {
+ s = kzalloc_obj(*s, GFP_KERNEL);
+ if (!s) {
+ mutex_unlock(&bf_saved_mutex);
+ return;
+ }
+ strscpy(s->key, key, sizeof(s->key));
+ list_add_tail(&s->list, &bf_saved_list);
+ }
+
+ s->flag_cnt = chip->flag_cnt;
+ memcpy(s->master, chip->master, sizeof(s->master));
+ memcpy(s->muted, chip->muted, sizeof(s->muted));
+ memcpy(s->xpoint, chip->xpoint, sizeof(s->xpoint));
+ mutex_unlock(&bf_saved_mutex);
+}
+
+/* Copy a saved state (if any) into a freshly probed chip and push it
+ * to the device. Returns 1 when restored, -ENOENT when there is none,
+ * or a negative error from the vendor writes.
+ */
+int bf_state_restore(struct snd_usb_babyface *chip)
+{
+ struct bf_saved *s;
+ const char *key = chip->dev->serial ? chip->dev->serial :
+ dev_name(&chip->dev->dev);
+ int ret = -ENOENT;
+
+ mutex_lock(&bf_saved_mutex);
+ list_for_each_entry(s, &bf_saved_list, list) {
+ if (strcmp(s->key, key))
+ continue;
+ chip->flag_cnt = s->flag_cnt;
+ memcpy(chip->master, s->master, sizeof(chip->master));
+ memcpy(chip->muted, s->muted, sizeof(chip->muted));
+ memcpy(chip->xpoint, s->xpoint, sizeof(chip->xpoint));
+ ret = 1;
+ break;
+ }
+ mutex_unlock(&bf_saved_mutex);
+ if (ret != 1)
+ return ret;
+
+ mutex_lock(&chip->mutex);
+ ret = babyface_restore_state(chip);
+ mutex_unlock(&chip->mutex);
+ return ret ? ret : 1;
+}
+
+void bf_state_purge(void)
+{
+ struct bf_saved *s, *tmp;
+
+ mutex_lock(&bf_saved_mutex);
+ list_for_each_entry_safe(s, tmp, &bf_saved_list, list) {
+ list_del(&s->list);
+ kfree(s);
+ }
+ mutex_unlock(&bf_saved_mutex);
+}
+
/* -- PCM data path ------------------------- */
static bool babyface_capture_copy(struct snd_usb_babyface *chip,
@@ -1393,6 +1524,26 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ /* Restore the mixer state saved at the last disconnect (if any);
+ * the device keeps its registers across a usbfs detach, but the
+ * cold init above cleared them, so push the user's settings back.
+ */
+ err = bf_state_restore(chip);
+ if (err == -ENOENT) {
+ /* No saved state: the 0x16 clear zeroed the mixer registers,
+ * so restore the factory default routing to keep the outputs
+ * live out of the box.
+ */
+ err = babyface_write_default_mixer(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "default mixer restore failed: %d\n", err);
+ goto error;
+ }
+ } else if (err < 0) {
+ dev_err(&intf->dev, "mixer state restore failed: %d\n", err);
+ goto error;
+ }
+
/* Keep the allocation size independent of the active USB mode. */
urbsize = BF_WORDS_PER_FRAME * sizeof(u32) * chip->frames_per_urb;
chip->urbs_in = kcalloc(chip->nurbs, sizeof(*chip->urbs_in), GFP_KERNEL);
@@ -1437,6 +1588,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);
@@ -1485,6 +1648,11 @@ static void babyface_disconnect(struct usb_interface *intf)
babyface_stream_kill(chip);
mutex_unlock(&chip->mutex);
cancel_work_sync(&chip->stream_work);
+ /* Keep the mixer state for the next probe: a userspace usbfs claim
+ * (PipeWire sink grab, TuxMix daemon) detaches us and the cold init
+ * of the re-probe would otherwise wipe the settings.
+ */
+ bf_state_save(chip);
/* 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
@@ -1524,7 +1692,12 @@ static int __init babyface_init(void)
static void __exit babyface_exit(void)
{
+ /* Deregister first: each disconnect() calls bf_state_save(), which
+ * allocates a node for a device it has not seen before. Purging
+ * before that frees the list and then leaks those fresh nodes.
+ */
usb_deregister(&babyface_driver);
+ bf_state_purge();
}
module_init(babyface_init);
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 60ab0c387..9694cb9a7 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,80 @@
*/
#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) */
+#define BF_REG_CROSS_BASE_L 0x0034 /* + 0x34*out + src (bReq 0x12) */
+#define BF_REG_CROSS_BASE_R 0x004e /* + 0x34*out + src */
+#define BF_REG_CROSS_STRIDE 0x0034
+/* Low map (the AN1/2 monitor bus's own per-source registers, one set
+ * shared across every output - not one per output block like the
+ * standard crosspoint map above). NOT a shadow/mirror of the standard
+ * map for AN1/2: it is what that output actually sums from, and the
+ * standard map alone has no audible effect on it (hardware-verified
+ * 2026-09-14, see bf_xpoint_write's own comment and KERNEL-DRIVER.md).
+ */
+#define BF_REG_LOWMAP_BASE_L 0x0000 /* + idx_l */
+#define BF_REG_LOWMAP_BASE_R 0x001a /* + idx_r */
+
+/* The "cross" register block within each output: the L-registers sit at
+ * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
+ * source pairs that can be cross-linked). bf_crosspoint_clear_cross()
+ * zeroes them because the cold-init clear does not cover them.
+ */
+#define BF_CROSS_L_FIRST 5
+#define BF_CROSS_L_LAST 23
+#define BF_CROSS_R_FIRST 4
+#define BF_CROSS_R_LAST 22
+
+/* Calibrated master value: 0 dB = 0x2000 (+6 dB = 0x4000). See
+ * CALIBRATION.md. The crosspoint fader curve is DIFFERENT (0 dB =
+ * 0x16a0, top 0x2d41 - see below).
+ */
+#define BF_MASTER_0DB 0x2000
+
+/* The 8-bit master is the REAL output volume (hardware-verified
+ * 2026-08-24: writing it changes the level, the 16-bit does not).
+ * Scale: 0.5 dB per step, 0xf3 = 0 dB (the scene-load default),
+ * bottom 0x73 = -64 dB (silence), top 0xff = +6 dB. The 16-bit
+ * register is a companion kept in sync (TotalMix writes both).
+ * The mute value is 0x3B.
+ */
+#define BF_MASTER_8_0DB 0xf3
+#define BF_MASTER_8_MIN 0x73
+#define BF_MASTER_MUTE 0x3b
+#define BF_MASTER_UNMUTE 0xf3
+/* -20 dB master, 8-bit and 16-bit: the exact pair the hardware DIM
+ * button writes (cap_dim2.pcap), reused as the power-on default.
+ */
+#define BF_MASTER_MINUS20_8 0xcb
+#define BF_MASTER_MINUS20_16 0x0333
+
+/* Crosspoint fader curve: 0 dB = 0x16a0, +6 dB = 0x2d41 (fader curve,
+ * DIFFERENT from the master 0x4000 top - see CALIBRATION.md).
+ */
+#define BF_FADER_0DB 0x16a0
+#define BF_FADER_TOP 0x2d41
+
+/* The crosspoint matrix sources (14 controls per output). */
+struct bf_source {
+ const char *name;
+ u8 idx_l;
+ u8 idx_r;
+};
+
+/* Crosspoint-source order + register block maps (babyfacepro-ctl.c). */
+extern const struct bf_source bf_sources[14];
+extern const u8 bf_xpoint_block[6];
+
+/* 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 +239,25 @@ 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 16-bit masters */
+ bool muted[6];
+ u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
+};
+
+/* The mixer state cached across interface re-probes (see
+ * babyfacepro.c's own comment on bf_state_save/bf_state_restore).
+ */
+struct bf_saved {
+ struct list_head list;
+ char key[32];
+ u8 flag_cnt;
+ u16 master[6][2];
+ bool muted[6];
+ u16 xpoint[6][14][2];
};
struct bf_rate {
@@ -185,3 +283,26 @@ 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. */
+void bf_state_save(struct snd_usb_babyface *chip);
+int bf_state_restore(struct snd_usb_babyface *chip);
+void bf_state_purge(void);
+int babyface_restore_state(struct snd_usb_babyface *chip);
+
+/* -- babyfacepro-ctl.c ----------------------- */
+extern const u16 bf_flag_cycle[4];
+int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, u16 idx);
+int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip,
+ unsigned int blk);
+int babyface_write_default_mixer(struct snd_usb_babyface *chip);
+int bf_apply_masters(struct snd_usb_babyface *chip);
+int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
+ u16 l, u16 r);
+int babyface_create_masters(struct snd_usb_babyface *chip);
+int babyface_create_xpoints(struct snd_usb_babyface *chip);
+
+/* Master gain-law helpers (babyfacepro-ctl.c). */
+int bf_master_half_db(u16 vol16); /* 16-bit master -> dBx2 */
+int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */
+u8 bf_master_8bit(u16 vol16); /* 16-bit master -> 8-bit companion */
--
2.56.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v6 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream) Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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 crosspoint
registers of the AN1/2 monitor bus - and trim combines with the
current fader value on the crosspoint fader curve, which this patch
adds.
bf_state_apply_flags() re-applies phase, split and trim on top of a
restored crosspoint state. A failed preamp write leaves the cached
state as it was.
Co-developed-by: David Fredman <davfre@gmail.com>
Signed-off-by: David Fredman <davfre@gmail.com>
Signed-off-by: Ismaïl Bahloul <i.bahloul01@gmail.com>
---
sound/usb/babyfacepro/babyfacepro-ctl.c | 742 ++++++++++++++++++++++++
sound/usb/babyfacepro/babyfacepro.c | 115 +++-
sound/usb/babyfacepro/babyfacepro.h | 86 +++
3 files changed, 942 insertions(+), 1 deletion(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 6ef29aa27..988766c59 100644
--- a/sound/usb/babyfacepro/babyfacepro-ctl.c
+++ b/sound/usb/babyfacepro/babyfacepro-ctl.c
@@ -708,3 +708,745 @@ 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). `chip->xpoint[out][mic][0]` is deliberately left holding
+ * the PLAIN value the user actually set - only the value WRITTEN to
+ * hardware is negated - so the crosspoint control's own readback still
+ * reports the real fader position while phase is engaged.
+ *
+ * KNOWN LIMITATION, same class TuxMix's own USB backend already has
+ * in `usb.rs::set_phase` (not fixed there either, as of this writing):
+ * this negates the CURRENT register value once, at toggle time. A
+ * later `bf_xpoint_put` on the same [out][mic] slot (i.e. the user
+ * drags that fader again while phase is engaged) writes the plain
+ * value, silently un-inverting phase until the user re-toggles it.
+ * Making the crosspoint hot path itself phase-aware would close this
+ * properly, but touches every one of the 84 crosspoint controls'
+ * write path - out of scope for this pass; flagged rather than
+ * silently shipped.
+ */
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert)
+{
+ const struct bf_source *s = &bf_sources[mic];
+ int out, ret;
+ u16 flag;
+
+ for (out = 0; out < 6; out++) {
+ unsigned int blk = bf_xpoint_block[out];
+ u16 plain = chip->xpoint[out][mic][0];
+ u16 value = invert ? (u16)~plain : plain;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+ (BF_REG_CROSS_BASE_L +
+ BF_REG_CROSS_STRIDE * blk + s->idx_l) |
+ flag);
+ if (ret < 0)
+ return ret;
+
+ if (out == 0) {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+ s->idx_l);
+ if (ret < 0)
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static int bf_phase_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_phase_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->phase[mic];
+ return 0;
+}
+
+static int bf_phase_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+ bool invert = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (invert == chip->phase[mic])
+ goto out;
+ ret = bf_phase_apply(chip, mic, invert);
+ if (ret < 0)
+ goto out;
+ chip->phase[mic] = invert;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Stereo split (PROTOCOL.md "Stereo split", cap_ctrl3.pcap, hardware-
+ * verified): a playback pair's signal into the AN1/2 monitor bus goes
+ * hard-split (L=0x2000/R=0x0000, "split-mono") instead of the normal
+ * stereo pair (L=R=0x1000, -6 dB each side) - fixed constants, not
+ * derived from the current fader value (unlike Phase, there's nothing
+ * to preserve), matching TuxMix's own `usb.rs::set_stereo_split`
+ * exactly. Only reaches the AN1/2 destination (low map + that output's
+ * standard crosspoint block) - same scope as CUE/mute/solo's own
+ * low-map-only reach. `chip->xpoint[][]` is deliberately left
+ * untouched, same reasoning as Phase.
+ */
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split)
+{
+ const struct bf_source *s = &bf_sources[8 + pb];
+ unsigned int blk = bf_xpoint_block[0]; /* AN1/2 output */
+ u16 l = split ? 0x2000 : 0x1000;
+ u16 r = split ? 0x0000 : 0x1000;
+ int ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ BF_REG_LOWMAP_BASE_L + s->idx_l);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ BF_REG_LOWMAP_BASE_R + s->idx_r);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ s->idx_l));
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ s->idx_r));
+}
+
+static int bf_split_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int pb = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->split[pb];
+ return 0;
+}
+
+static int bf_split_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int pb = kctl->private_value;
+ bool split = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (split == chip->split[pb])
+ goto out;
+ ret = bf_split_apply(chip, pb, split);
+ if (ret < 0)
+ goto out;
+ chip->split[pb] = split;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* -- crosspoint 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
+ * exactly like a fader (there is no separate trim register). Two
+ * different curves combine: the low map holds the trim ALONE on the
+ * MASTER curve (0x2000 = 0 dB, `bf_master_16bit`); the standard map
+ * holds fader+trim SUMMED on the FADER curve (`bf_fader_db2_to_raw`).
+ * Always writes all 8 registers for the pair (both AN1+AN2 or both
+ * AN3+AN4, matching the vendor software's linked-strip behaviour);
+ * `mic` may be either channel of the pair, and the base is derived
+ * (`mic & ~1`) so the write always lands on the correct pair's
+ * registers regardless of which channel's control triggered it.
+ * Destination is always the AN1/2 monitor bus, the same scope the
+ * MS-processor and CUE writes have, and the same one the vendor
+ * software's Trim reaches.
+ *
+ * Trim is a genuinely SHARED value per pair on real hardware (one
+ * write always touches both channels' registers) but is exposed as 2
+ * per-channel ALSA controls, one per input strip. `bf_trim_put` keeps
+ * the pair's two cache entries equal and notifies the sibling control,
+ * so the cache never claims a per-channel split the hardware cannot
+ * represent - and `bf_state_apply_flags`, which replays the pair from
+ * its even index, always replays the value that is actually on the
+ * wire.
+ *
+ * ONE KNOWN LIMITATION, kept rather than silently hidden:
+ * Same class as Phase (see `bf_phase_apply`'s own comment):
+ * `chip->xpoint[0][mic][0]` is read here for the CURRENT fader
+ * value but never written back - the standard-map register ends up
+ * holding fader+trim while the cache still holds the plain fader,
+ * so a later `bf_xpoint_put` on the same slot writes the plain
+ * value, silently dropping trim from the combined register until
+ * this is re-applied. Not fixed for the same reason Phase wasn't:
+ * touches the shared 84-crosspoint write path, out of scope here.
+ */
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2)
+{
+ int base = mic & ~1;
+ int sib = base + 1;
+ const struct bf_source *sb = &bf_sources[base];
+ const struct bf_source *ss = &bf_sources[sib];
+ unsigned int blk = bf_xpoint_block[0]; /* AN1/2 output */
+ u16 trim_raw = bf_master_16bit(trim_db2);
+ int fader_db2 = bf_fader_raw_to_db2(chip->xpoint[0][base][0]);
+ u16 standard_raw = bf_fader_db2_to_raw(fader_db2 + trim_db2);
+ int ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_L + sb->idx_l);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_R + sb->idx_r);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_L + ss->idx_l);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+ BF_REG_LOWMAP_BASE_R + ss->idx_r);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ sb->idx_l));
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ sb->idx_r));
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ ss->idx_l));
+ if (ret < 0)
+ return ret;
+ return bf_vendor_write(chip, BF_REQ_CROSSPOINT, standard_raw,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ ss->idx_r));
+}
+
+/* Trim's control value is dB as well, -65..+6. */
+static const DECLARE_TLV_DB_SCALE(bf_trim_tlv, -6500, 100, 0);
+
+static int bf_trim_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = -65;
+ uinfo->value.integer.max = 6;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_trim_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->trim[mic];
+ return 0;
+}
+
+static int bf_trim_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+ int sib = mic ^ 1;
+ int db = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (db < -65 || db > 6)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (db == chip->trim[mic])
+ goto out;
+ ret = bf_trim_apply(chip, mic, db * 2);
+ if (ret < 0)
+ goto out;
+ /* One register per pair on the wire, so both channels of the pair
+ * really did change: mirror the cache (the state restore replays
+ * the pair from the even index) and tell user space about the
+ * sibling control.
+ */
+ chip->trim[mic] = db;
+ chip->trim[sib] = db;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ if (ret == 1 && chip->trim_kctl[sib])
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->trim_kctl[sib]->id);
+ return ret;
+}
+
+/* Phase, stereo split and input trim - registered together since all
+ * three are per-input-strip controls layered on the same AN1/2 monitor
+ * bus crosspoint registers the matrix in babyface_create_xpoints()
+ * already created controls for.
+ */
+int babyface_create_trim(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int src, err;
+
+ for (src = 0; src < 4; src++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Phase Switch",
+ .index = src,
+ .info = bf_phase_info,
+ .get = bf_phase_get,
+ .put = bf_phase_put,
+ .private_value = src,
+ }, chip);
+ strscpy(kctl->id.name, bf_sources[src].name, sizeof(kctl->id.name));
+ strlcat(kctl->id.name, " Phase Switch", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+
+ for (src = 0; src < 6; src++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Stereo Split Switch",
+ .index = src,
+ .info = bf_phase_info, /* plain boolean, same shape */
+ .get = bf_split_get,
+ .put = bf_split_put,
+ .private_value = src,
+ }, chip);
+ strscpy(kctl->id.name, bf_sources[8 + src].name, sizeof(kctl->id.name));
+ strlcat(kctl->id.name, " Stereo Split Switch", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+
+ for (src = 0; src < 4; src++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Trim Volume",
+ .index = src,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = bf_trim_info,
+ .get = bf_trim_get,
+ .put = bf_trim_put,
+ .tlv.p = bf_trim_tlv,
+ .private_value = src,
+ }, chip);
+ chip->trim_kctl[src] = kctl;
+ strscpy(kctl->id.name, bf_sources[src].name, sizeof(kctl->id.name));
+ strlcat(kctl->id.name, " Trim Volume", sizeof(kctl->id.name));
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
+
+static int bf_bool_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_phantom_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] =
+ !!(chip->preamp & kctl->private_value);
+ return 0;
+}
+
+static int bf_phantom_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ u16 bit = kctl->private_value;
+ bool on = ucontrol->value.integer.value[0];
+ bool cur = !!(chip->preamp & bit);
+ u16 old = chip->preamp;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == cur)
+ goto out;
+ chip->preamp = on ? (chip->preamp | bit) : (chip->preamp & ~bit);
+ ret = bf_preamp_state_write(chip);
+ if (ret < 0) {
+ /* Do not leave the cache claiming a state the device
+ * never took.
+ */
+ chip->preamp = old;
+ goto out;
+ }
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Ref Level (Instr 3/4) - see the constants' own comment in the
+ * header. A single shared 3-state switch, not per-channel (the
+ * protocol has no independent bits for IN3 vs IN4).
+ */
+static const char *const bf_reflevel_texts[] = {
+ "+4dBu", "-10dBV", "Boost", NULL
+};
+
+static int bf_reflevel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 3, bf_reflevel_texts);
+}
+
+static int bf_reflevel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.enumerated.item[0] = chip->ref_level;
+ return 0;
+}
+
+static int bf_reflevel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ unsigned int item = ucontrol->value.enumerated.item[0];
+ u16 old_preamp;
+ int old_ref_level;
+ int ret = 0;
+
+ if (item > BF_REF_LEVEL_BOOST)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if ((int)item == chip->ref_level)
+ goto out;
+ old_preamp = chip->preamp;
+ old_ref_level = chip->ref_level;
+ chip->preamp = (chip->preamp & ~BF_PREAMP_REF_MASK) |
+ (item == BF_REF_LEVEL_4DBU ? BF_PREAMP_REF_4DBU : 0);
+ chip->ref_level = item;
+ ret = bf_preamp_state_write(chip);
+ if (ret < 0) {
+ chip->preamp = old_preamp;
+ chip->ref_level = old_ref_level;
+ goto out;
+ }
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Gain scales.
+ *
+ * The mic preamps (AN1/2) span 0-65 dB in 1 dB steps, carried in a
+ * packed byte rather than a plain count:
+ *
+ * coarse = min(db / 3, 20) bits 0-4, 3 dB per step
+ * fine = db - 3 * coarse bits 5-7, the 0-2 dB remainder
+ * value = (fine << 5) | coarse
+ *
+ * Above 60 dB coarse saturates at 20 and fine continues 3, 4, 5, so
+ * 65 dB is 0xb4. Decoded from USBPcap captures of TotalMix on
+ * Windows (bbf-gain2/3/4.pcap, 48 writes, all matching).
+ *
+ * Bits 5-7 were previously read as a transaction counter and written
+ * with a rotating 0x20/0x00/0x40, which both discarded the fine part
+ * of the setting and applied 0-2 dB of error depending on where the
+ * rotation happened to be.
+ *
+ * The Hi-Z instrument inputs (AN3/4) are not packed: the value is the
+ * gain in 0.5 dB units, 0-9 dB over 0-18.
+ */
+int bf_gain_max_db(int mic)
+{
+ return mic < 2 ? BF_GAIN_MAX_DB : 9;
+}
+
+int bf_gain_db(int mic, u8 raw)
+{
+ if (mic >= 2)
+ return raw / 2;
+ return 3 * (raw & BF_GAIN_COARSE_MASK) + (raw >> BF_GAIN_FINE_SHIFT);
+}
+
+u8 bf_gain_raw(int mic, int db)
+{
+ int coarse, fine;
+
+ if (mic >= 2)
+ return db * 2;
+ coarse = min(db / 3, BF_GAIN_COARSE_MAX);
+ fine = db - 3 * coarse;
+ return (u8)((fine << BF_GAIN_FINE_SHIFT) | coarse);
+}
+
+/* The preamp control's value already IS the gain in dB (0..65 for the mic
+ * inputs, 0..9 for the instrument ones), and the hardware really does
+ * resolve every one of those steps - bf_gain_raw() packs it into the
+ * register's coarse and fine fields.
+ */
+static const DECLARE_TLV_DB_SCALE(bf_gain_tlv, 0, 100, 0);
+
+static int bf_gain_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = bf_gain_max_db(kctl->private_value);
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_gain_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+
+ /* chip->gain[] tracks the dB; the packed register value is derived
+ * at write time (bf_gain_raw).
+ */
+ ucontrol->value.integer.value[0] = chip->gain[mic];
+ return 0;
+}
+
+static int bf_gain_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int mic = kctl->private_value;
+ int db = ucontrol->value.integer.value[0];
+ u8 raw;
+ int ret = 0;
+
+ if (db < 0 || db > bf_gain_max_db(mic))
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (db == chip->gain[mic])
+ goto out;
+ raw = bf_gain_raw(mic, db);
+
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, (u16)raw,
+ BF_REG_GAIN + mic);
+ if (ret < 0)
+ goto out;
+ chip->gain[mic] = db;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Preamp controls: phantom power, pad, instrument ref level and the
+ * four mic/instrument gains. Registered separately from the output
+ * masters (babyface_create_masters()) since they cover a different
+ * part of the signal path (mic input, not output routing).
+ */
+int babyface_create_preamp(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int i, err;
+
+ for (i = 0; i < 2; i++) {
+ u16 bit = i == 0 ? BF_PREAMP_48V_MIC1 : BF_PREAMP_48V_MIC2;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Phantom Power Mic 1",
+ .index = i,
+ .info = bf_bool_info,
+ .get = bf_phantom_get,
+ .put = bf_phantom_put,
+ .private_value = bit,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+
+ for (i = 0; i < 2; i++) {
+ u16 bit = i == 0 ? BF_PREAMP_PAD_MIC1 : BF_PREAMP_PAD_MIC2;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Pad Mic 1",
+ .index = i,
+ .info = bf_bool_info,
+ .get = bf_phantom_get,
+ .put = bf_phantom_put,
+ .private_value = bit,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Instrument Ref Level",
+ .info = bf_reflevel_info,
+ .get = bf_reflevel_get,
+ .put = bf_reflevel_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ for (i = 0; i < 4; i++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Mic 1 Capture Volume",
+ .index = i,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = bf_gain_info,
+ .get = bf_gain_get,
+ .put = bf_gain_put,
+ .tlv.p = bf_gain_tlv,
+ .private_value = i,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
+
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 82ec98346..1d7aa832b 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -274,7 +274,21 @@ static DEFINE_MUTEX(bf_saved_mutex);
*/
int babyface_restore_state(struct snd_usb_babyface *chip)
{
- int out, src, ret;
+ int out, src, mic, ret;
+
+ /* Preamp state + commit. */
+ ret = bf_preamp_state_write(chip);
+ if (ret < 0)
+ return ret;
+
+ /* The four input gains. */
+ for (mic = 0; mic < 4; mic++) {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ (u16)bf_gain_raw(mic, chip->gain[mic]),
+ BF_REG_GAIN + mic);
+ if (ret < 0)
+ return ret;
+ }
/* Masters (8-bit = the real volume) + mutes. */
ret = bf_apply_masters(chip);
@@ -309,6 +323,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;
+}
+
void bf_state_save(struct snd_usb_babyface *chip)
{
struct bf_saved *s;
@@ -333,10 +409,16 @@ void bf_state_save(struct snd_usb_babyface *chip)
list_add_tail(&s->list, &bf_saved_list);
}
+ s->preamp = chip->preamp;
+ memcpy(s->gain, chip->gain, sizeof(s->gain));
s->flag_cnt = chip->flag_cnt;
memcpy(s->master, chip->master, sizeof(s->master));
memcpy(s->muted, chip->muted, sizeof(s->muted));
memcpy(s->xpoint, chip->xpoint, sizeof(s->xpoint));
+ memcpy(s->phase, chip->phase, sizeof(s->phase));
+ memcpy(s->trim, chip->trim, sizeof(s->trim));
+ memcpy(s->split, chip->split, sizeof(s->split));
+ s->ref_level = chip->ref_level;
mutex_unlock(&bf_saved_mutex);
}
@@ -355,10 +437,16 @@ int bf_state_restore(struct snd_usb_babyface *chip)
list_for_each_entry(s, &bf_saved_list, list) {
if (strcmp(s->key, key))
continue;
+ chip->preamp = s->preamp;
+ memcpy(chip->gain, s->gain, sizeof(chip->gain));
chip->flag_cnt = s->flag_cnt;
memcpy(chip->master, s->master, sizeof(chip->master));
memcpy(chip->muted, s->muted, sizeof(chip->muted));
memcpy(chip->xpoint, s->xpoint, sizeof(chip->xpoint));
+ memcpy(chip->phase, s->phase, sizeof(chip->phase));
+ memcpy(chip->trim, s->trim, sizeof(chip->trim));
+ memcpy(chip->split, s->split, sizeof(chip->split));
+ chip->ref_level = s->ref_level;
ret = 1;
break;
}
@@ -368,6 +456,8 @@ int bf_state_restore(struct snd_usb_babyface *chip)
mutex_lock(&chip->mutex);
ret = babyface_restore_state(chip);
+ if (ret == 0)
+ ret = bf_state_apply_flags(chip);
mutex_unlock(&chip->mutex);
return ret ? ret : 1;
}
@@ -1430,6 +1520,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) {
@@ -1469,6 +1560,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);
@@ -1524,6 +1616,15 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ /* Sync the preamp state from the 0x17 readback (byte 0 mirrors
+ * the 48V/PAD bits; it persists across power cycles).
+ */
+ err = bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PREAMP, st);
+ if (err < 0)
+ dev_dbg(&intf->dev, "preamp readback failed: %d\n", err);
+ else
+ chip->preamp = st[0];
+
/* Restore the mixer state saved at the last disconnect (if any);
* the device keeps its registers across a usbfs detach, but the
* cold init above cleared them, so push the user's settings back.
@@ -1600,6 +1701,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 9694cb9a7..80848da06 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) */
@@ -246,6 +285,37 @@ struct snd_usb_babyface {
u16 master[6][2]; /* cached 16-bit masters */
bool muted[6];
u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
+ u16 preamp; /* 48V/PAD bits, base 0x0c */
+ u8 gain[4]; /* preamp gain in dB 0-65/9 (raw derived
+ * at write: mic packed coarse/fine,
+ * instr 0.5 dB/step)
+ */
+ bool phase[4]; /* polarity invert, AN1-4 (bf_sources 0-3);
+ * xpoint[][0..3][0] stays the PLAIN
+ * value, only the wire write is
+ * negated - see bf_phase_put's own
+ * comment for the known limitation
+ * this implies.
+ */
+ int trim[4]; /* Trim (T), dB (-65..+6), AN1-4;
+ * one shared register per pair, so
+ * both entries of a pair are kept
+ * equal; same "wire-only" caveat as
+ * phase - see bf_trim_apply's own
+ * comment.
+ */
+ bool split[6]; /* stereo split, playback pairs PB1-PB6
+ * (bf_sources idx 8-13); fixed
+ * constants (PROTOCOL.md "Stereo
+ * split"), not derived from the
+ * fader - xpoint[][] is left
+ * untouched, same as phase.
+ */
+ int ref_level; /* Instr 3/4 ref level, one of the
+ * BF_REF_LEVEL_* values
+ * (0 = +4dBu, the default)
+ */
+ struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
};
/* The mixer state cached across interface re-probes (see
@@ -254,10 +324,16 @@ struct snd_usb_babyface {
struct bf_saved {
struct list_head list;
char key[32];
+ u16 preamp;
+ u8 gain[4];
u8 flag_cnt;
u16 master[6][2];
bool muted[6];
u16 xpoint[6][14][2];
+ bool phase[4];
+ int trim[4];
+ bool split[6];
+ int ref_level;
};
struct bf_rate {
@@ -289,6 +365,7 @@ void bf_state_save(struct snd_usb_babyface *chip);
int bf_state_restore(struct snd_usb_babyface *chip);
void bf_state_purge(void);
int babyface_restore_state(struct snd_usb_babyface *chip);
+int bf_state_apply_flags(struct snd_usb_babyface *chip);
/* -- babyfacepro-ctl.c ----------------------- */
extern const u16 bf_flag_cycle[4];
@@ -301,6 +378,15 @@ int bf_xpoint_write(struct snd_usb_babyface *chip, int out, int src,
u16 l, u16 r);
int babyface_create_masters(struct snd_usb_babyface *chip);
int babyface_create_xpoints(struct snd_usb_babyface *chip);
+int babyface_create_trim(struct snd_usb_babyface *chip);
+int babyface_create_preamp(struct snd_usb_babyface *chip);
+int bf_preamp_state_write(struct snd_usb_babyface *chip);
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert);
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split);
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2);
+int bf_gain_max_db(int mic);
+int bf_gain_db(int mic, u8 raw);
+u8 bf_gain_raw(int mic, int db);
/* Master gain-law helpers (babyfacepro-ctl.c). */
int bf_master_half_db(u16 vol16); /* 16-bit master -> dBx2 */
--
2.56.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v6 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (2 preceding siblings ...)
2026-10-03 19:41 ` [RFC PATCH v6 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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.
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 | 650 +++++++++++++++++++++++-
sound/usb/babyfacepro/babyfacepro.c | 162 +++++-
sound/usb/babyfacepro/babyfacepro.h | 39 +-
3 files changed, 838 insertions(+), 13 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 988766c59..f72879502 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>
@@ -288,7 +290,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);
}
@@ -418,6 +422,11 @@ static int bf_master_put(struct snd_kcontrol *kctl,
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;
+ }
ret = 1;
out:
mutex_unlock(&chip->mutex);
@@ -1450,3 +1459,642 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
return 0;
}
+static int bf_switch_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ return 0;
+}
+
+static int bf_pitch_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = -50; /* -5.0 % */
+ uinfo->value.integer.max = 50; /* +5.0 % */
+ uinfo->value.integer.step = 1; /* 0.1 % */
+ return 0;
+}
+
+static int bf_pitch_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->pitch;
+ return 0;
+}
+
+static int bf_pitch_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int p = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (p < -50 || p > 50)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (p == chip->pitch)
+ goto out;
+
+ /* The DDS quad is a ratio on top of the family rate, the same at
+ * every sample rate; bf_pitch_write() sends it with the settings
+ * keepalive that commits it.
+ */
+ ret = bf_pitch_write(chip, p);
+ if (ret < 0)
+ goto out;
+ chip->pitch = p;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_loopback_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value;
+
+ ucontrol->value.integer.value[0] = chip->loopback[out];
+ ucontrol->value.integer.value[1] = chip->loopback[out];
+ return 0;
+}
+
+/* Write the full 30-channel loopback map: pair (2*out, 2*out+1) at
+ * `on` (0x0001/0x0000), all other channels cleared - exactly what
+ * TotalMix sends on every loopback toggle (cap_loopback2.pcap). The
+ * full-map write is also the reliable OFF (the old per-pair write
+ * sometimes failed to disengage on the hardware).
+ */
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out,
+ bool on)
+{
+ int ch, ret;
+
+ for (ch = 0; ch < BF_LOOPBACK_CHANNELS; ch++) {
+ u16 val = (on && (ch == out * 2 || ch == out * 2 + 1))
+ ? 0x0001 : 0x0000;
+
+ ret = bf_vendor_write(chip, BF_REQ_LOOPBACK, val, ch);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+static int bf_loopback_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int out = kctl->private_value;
+ bool on = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == chip->loopback[out])
+ goto out;
+ ret = bf_loopback_write_map(chip, out, on);
+ if (ret < 0)
+ goto out;
+ /* Single-active model (TotalMix writes one pair at 0x0001, the
+ * rest 0x0000): toggling one output clears the others.
+ */
+ memset(chip->loopback, 0, sizeof(chip->loopback));
+ chip->loopback[out] = on;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_an12_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->an12;
+ return 0;
+}
+
+static int bf_an12_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool an12 = ucontrol->value.integer.value[0];
+ u16 v;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (an12 == chip->an12)
+ goto out;
+ v = (chip->linked ? 0x0400 : 0x0000) | (an12 ? 0x1000 : 0x0000);
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ goto out;
+ chip->an12 = an12;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* Clock source (PROTOCOL.md "Clock source / no-lock state",
+ * hardware-verified 2026-08-22, clktest.c): NOT a register write at
+ * all - only the BF_REG_KEEPALIVE_SETTINGS word changes (bit 2 =
+ * Optical). Matches the naming TuxMix's ALSA backend already looks
+ * for ("Sample Clock Source", the same name found on the stock
+ * snd-usb-audio Class-Compliant driver) so it picks this control up
+ * with zero changes on that side.
+ */
+static const char *const bf_clock_texts[] = {
+ "Internal", "Optical In", NULL
+};
+
+static int bf_clock_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, 2, bf_clock_texts);
+}
+
+static int bf_clock_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.enumerated.item[0] = chip->clock_optical ? 1 : 0;
+ return 0;
+}
+
+static int bf_clock_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool optical = ucontrol->value.enumerated.item[0] != 0;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (optical == chip->clock_optical)
+ goto out;
+ chip->clock_optical = optical;
+ ret = bf_settings_write(chip);
+ if (ret < 0) {
+ chip->clock_optical = !optical;
+ goto out;
+ }
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_link_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->linked;
+ return 0;
+}
+
+static int bf_link_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool linked = ucontrol->value.integer.value[0];
+ u16 v;
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (linked == chip->linked)
+ goto out;
+ v = (linked ? 0x0400 : 0x0000) | (chip->an12 ? 0x1000 : 0x0000);
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ goto out;
+ chip->linked = linked;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_ms_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->ms_proc;
+ return 0;
+}
+
+/* MS-proc: engage per the cap_ms2.pcap ON pattern - write 0x0000 to
+ * ALL FOUR AN2 (side) crosspoints: standard map 0x0035/0x004F (L/R)
+ * + low map 0x0001/0x001B (L/R) - the side path is muted (ear-
+ * verified 2026-08-26 with the mic on AN2: MS ON = silence); release
+ * restores the cached fader values (host-side, like TotalMix).
+ * (The 0x1000/0x0004 writes are the DISENGAGE restore values seen in
+ * cap_ms2 - the driver had them inverted on the engage path.)
+ */
+static int bf_ms_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool on = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == chip->ms_proc)
+ goto out;
+ if (on) {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+ if (ret < 0)
+ goto out;
+ } else {
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][0], 0x0001);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][0], 0x0035);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][1], 0x001b);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->xpoint[1][1][1], 0x004f);
+ if (ret < 0)
+ goto out;
+ }
+ chip->ms_proc = on;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* DIM - cap_dim2.pcap: an absolute -20 dB on the Phones master
+ * (out 1: 8-bit 0xCB / 16-bit 0x0333) regardless of the current level,
+ * plus the 0x17 wVal=0x2000 wIdx=0x2000 flag; release restores the
+ * pre-DIM master host-side. The master cache keeps the real volume.
+ */
+static int bf_dim_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->dim;
+ return 0;
+}
+
+/* Apply DIM on the wire. chip->mutex must be held by the caller, the
+ * "Dim Switch" put.
+ */
+static int bf_dim_apply(struct snd_usb_babyface *chip, bool on)
+{
+ u16 flag;
+ int ret;
+
+ lockdep_assert_held(&chip->mutex);
+ if (on) {
+ chip->dim_saved[0] = chip->master[1][0];
+ chip->dim_saved[1] = chip->master[1][1];
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MINUS20_8,
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MINUS20_8,
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ BF_MASTER_MINUS20_16,
+ (BF_REG_MASTER_16 + 2 * 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ BF_MASTER_MINUS20_16,
+ (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000);
+ if (ret < 0)
+ return ret;
+ } else {
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->dim_saved[0]),
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN,
+ bf_master_8bit(chip->dim_saved[1]),
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ return ret;
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->dim_saved[0],
+ (BF_REG_MASTER_16 + 2 * 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+ chip->dim_saved[1],
+ (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x0000, 0x2000);
+ if (ret < 0)
+ return ret;
+ }
+ chip->dim = on;
+ return 0;
+}
+
+static int bf_dim_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ bool on = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&chip->mutex);
+ if (on == chip->dim)
+ goto out;
+ ret = bf_dim_apply(chip, on);
+ if (ret == 0)
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_width_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = -100;
+ uinfo->value.integer.max = 100;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_width_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->width;
+ return 0;
+}
+
+static int bf_width_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int w = ucontrol->value.integer.value[0];
+ u16 l, r;
+ int ret = 0;
+
+ if (w < -100 || w > 100)
+ return -EINVAL;
+
+ mutex_lock(&chip->mutex);
+ if (w == chip->width)
+ goto out;
+ /* Width spread: L = 0x1000*(1+w), R = 0x1000*(1-w), L+R = 0x2000.
+ * TotalMix writes the strip's src pair on BOTH maps (cap_width3-7,
+ * PROTOCOL.md "Width strip mapping"): the low map (0x0000+src L /
+ * 0x001A+src R) and the std block-0 map (0x0034+src L /
+ * 0x004E+src R) - the stereo pair spreads L/R in opposition, the
+ * mirror src (AN2) gets the swapped values.
+ */
+ l = (u16)(((0x2000 * (100 + w) / 2) + 50) / 100);
+ r = 0x2000 - l;
+ /* Low map: AN1 L=0x0000, R=0x001A; AN2 L=0x0001, R=0x001B. */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+ if (ret < 0)
+ goto out;
+ /* Std block-0 map (item 0b, the missing half): AN1 L=0x0034,
+ * R=0x004E; AN2 L=0x0035, R=0x004F. (The playback strips PB2-6
+ * target block n-2 - 0x00AE family - reserved for the per-strip
+ * controls.)
+ */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0034);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x004e);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0035);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x004f);
+ if (ret < 0)
+ goto out;
+ chip->width = w;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+static int bf_fx_send_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 0x1000;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_fx_send_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = chip->fx_send;
+ return 0;
+}
+
+static int bf_fx_send_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ 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;
+
+ mutex_lock(&chip->mutex);
+ if (v == chip->fx_send)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0138);
+ if (ret < 0)
+ goto out;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0153);
+ if (ret < 0)
+ goto out;
+ chip->fx_send = v;
+ ret = 1;
+out:
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+int babyface_create_flags(struct snd_usb_babyface *chip)
+{
+ struct snd_kcontrol *kctl;
+ int i, err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Varispeed Pitch",
+ .info = bf_pitch_info,
+ .get = bf_pitch_get,
+ .put = bf_pitch_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ for (i = 0; i < 6; i++) {
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Loopback Switch",
+ .index = i,
+ .info = bf_mute_info,
+ .get = bf_loopback_get,
+ .put = bf_loopback_put,
+ .private_value = i,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+ }
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "AN 1>2 Switch",
+ .info = bf_switch_info,
+ .get = bf_an12_get,
+ .put = bf_an12_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Sample Clock Source",
+ .info = bf_clock_info,
+ .get = bf_clock_get,
+ .put = bf_clock_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "AN1/2 Link Switch",
+ .info = bf_switch_info,
+ .get = bf_link_get,
+ .put = bf_link_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "MS Processor Switch",
+ .info = bf_switch_info,
+ .get = bf_ms_get,
+ .put = bf_ms_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Dim Switch",
+ .info = bf_switch_info,
+ .get = bf_dim_get,
+ .put = bf_dim_put,
+ }, chip);
+ chip->dim_kctl = kctl;
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Width",
+ .info = bf_width_info,
+ .get = bf_width_get,
+ .put = bf_width_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "FX Send Volume",
+ .info = bf_fx_send_info,
+ .get = bf_fx_send_get,
+ .put = bf_fx_send_put,
+ }, chip);
+ err = snd_ctl_add(chip->card, kctl);
+ if (err < 0)
+ return err;
+
+ return 0;
+}
+
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 1d7aa832b..0678bea58 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -105,14 +105,23 @@ int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf)
0, idx, buf, 4, BF_CTL_TIMEOUT, GFP_KERNEL);
}
-/* Sends the BF_REG_KEEPALIVE_SETTINGS word (PROTOCOL.md: "keepalive
- * 0x10 0x05CF wVal = host settings-state register" - a single shared
- * word, not independent per-setting writes). 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). Only clock source is tracked so
+ * far; the single call site this replaces (which used to hardcode
+ * 0x0001, i.e. "always Internal") is why this exists as its own
+ * function rather than an inline write at each call site - the next
+ * flag added to this word (EQ for Record / Optical-Out SPDIF, this
+ * driver's own upstream follow-up list) just OR's in here too, instead
+ * of every caller needing to remember every other bit.
*/
int bf_settings_write(struct snd_usb_babyface *chip)
{
- return bf_vendor_write(chip, BF_REQ_KEEPALIVE, BF_SETTINGS_CLOCK_INTERNAL,
+ u16 w = chip->clock_optical ? BF_SETTINGS_CLOCK_OPTICAL :
+ BF_SETTINGS_CLOCK_INTERNAL;
+
+ return bf_vendor_write(chip, BF_REQ_KEEPALIVE, w,
BF_REG_KEEPALIVE_SETTINGS);
}
@@ -231,7 +240,9 @@ int bf_cold_init(struct snd_usb_babyface *chip)
if (!r)
return -EINVAL;
ret = bf_vendor_write(chip, BF_REQ_KEEPALIVE,
- (bf_rate_family(r) << 4) | BF_SETTINGS_CLOCK_INTERNAL,
+ (bf_rate_family(r) << 4) |
+ (chip->clock_optical ? BF_SETTINGS_CLOCK_OPTICAL :
+ BF_SETTINGS_CLOCK_INTERNAL),
BF_REG_KEEPALIVE_INIT);
if (ret < 0)
return ret;
@@ -323,14 +334,111 @@ 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;
+ u16 l, r;
+
+ /* Loopback: the full 30-channel map from the cached state (the
+ * single-active invariant keeps at most one pair at 0x0001).
+ */
+ for (out = 0; out < 6; out++) {
+ if (chip->loopback[out]) {
+ on_out = out;
+ break;
+ }
+ }
+ ret = bf_loopback_write_map(chip, on_out, on_out >= 0);
+ if (ret < 0)
+ return ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP,
+ (chip->linked ? 0x0400 : 0x0000) |
+ (chip->an12 ? 0x1000 : 0x0000), 0x1000);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000);
+ if (ret < 0)
+ return ret;
+
+ l = (u16)(((0x2000 * (100 + chip->width) / 2) + 50) / 100);
+ r = 0x2000 - l;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+ if (ret < 0)
+ return ret;
+
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0138);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0153);
+ if (ret < 0)
+ return ret;
+
+ if (chip->ms_proc) {
+ /* Same ON pattern as bf_ms_put (cap_ms2.pcap): mute the AN2
+ * (side) crosspoints, both maps.
+ */
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Re-apply an engaged DIM (the fixed -20 dB Phones pair + flag). The
+ * level DIM releases back to is not persisted: it always tracks the
+ * Phones master while DIM is engaged, so rebuild it here - otherwise a
+ * DIM released after a re-probe would drive Phones to
+ * silence (dim_saved still zero on the fresh chip).
+ */
+ if (chip->dim) {
+ chip->dim_saved[0] = chip->master[1][0];
+ chip->dim_saved[1] = chip->master[1][1];
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+ BF_REG_MASTER_8 + 2 * 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_GAIN, 0xcb,
+ BF_REG_MASTER_8 + 2 * 1 + 1);
+ if (ret < 0)
+ return ret;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+ (BF_REG_MASTER_16 + 2 * 1) |
+ bf_flag_cycle[chip->flag_cnt]);
+ if (ret < 0)
+ return ret;
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333,
+ (BF_REG_MASTER_16 + 2 * 1 + 1) |
+ bf_flag_cycle[chip->flag_cnt]);
+ if (ret < 0)
+ return ret;
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ ret = bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000);
+ if (ret < 0)
+ return ret;
+ }
/* Re-apply any engaged Phase invert - the crosspoint restore loop
* above already re-wrote xpoint[][] as PLAIN values, so a phase
@@ -419,6 +527,15 @@ void bf_state_save(struct snd_usb_babyface *chip)
memcpy(s->trim, chip->trim, sizeof(s->trim));
memcpy(s->split, chip->split, sizeof(s->split));
s->ref_level = chip->ref_level;
+ s->pitch = chip->pitch;
+ memcpy(s->loopback, chip->loopback, sizeof(s->loopback));
+ s->an12 = chip->an12;
+ s->linked = chip->linked;
+ s->ms_proc = chip->ms_proc;
+ s->clock_optical = chip->clock_optical;
+ s->width = chip->width;
+ s->fx_send = chip->fx_send;
+ s->dim = chip->dim;
mutex_unlock(&bf_saved_mutex);
}
@@ -447,6 +564,15 @@ int bf_state_restore(struct snd_usb_babyface *chip)
memcpy(chip->trim, s->trim, sizeof(chip->trim));
memcpy(chip->split, s->split, sizeof(chip->split));
chip->ref_level = s->ref_level;
+ chip->pitch = s->pitch;
+ memcpy(chip->loopback, s->loopback, sizeof(chip->loopback));
+ chip->an12 = s->an12;
+ chip->linked = s->linked;
+ chip->ms_proc = s->ms_proc;
+ chip->clock_optical = s->clock_optical;
+ chip->width = s->width;
+ chip->fx_send = s->fx_send;
+ chip->dim = s->dim;
ret = 1;
break;
}
@@ -1640,6 +1766,14 @@ 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. */
+ mutex_lock(&chip->mutex);
+ err = bf_state_apply_flags(chip);
+ mutex_unlock(&chip->mutex);
+ if (err < 0) {
+ dev_err(&intf->dev, "flag restore failed: %d\n", err);
+ goto error;
+ }
} else if (err < 0) {
dev_err(&intf->dev, "mixer state restore failed: %d\n", err);
goto error;
@@ -1713,6 +1847,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 80848da06..783ea03ac 100644
--- a/sound/usb/babyfacepro/babyfacepro.h
+++ b/sound/usb/babyfacepro/babyfacepro.h
@@ -115,9 +115,16 @@
/* 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); bits 6/10 (EQ for
+ * Record / Optical-Out SPDIF) are next in the driver's own upstream
+ * follow-up list, not wired to a control yet - the composer below only
+ * OR's in the clock bit today, structured so those can be added the
+ * same way later without another flag-stomping rewrite.
*/
#define BF_SETTINGS_CLOCK_INTERNAL 0x0001
+#define BF_SETTINGS_CLOCK_OPTICAL 0x0004
/* Preamp state byte (0x17, wIdx 0x003F - full state, verified).
* NOTE 2026-08-26 (cap_reflevel3.pcap): the 0x0C "base" is NOT a
@@ -174,6 +181,15 @@
#define BF_REG_LOWMAP_BASE_L 0x0000 /* + idx_l */
#define BF_REG_LOWMAP_BASE_R 0x001a /* + idx_r */
+#define BF_REQ_LOOPBACK 0x15 /* per-output-channel flag */
+
+/* Loopback map width (captured 2026-08-25, cap_loopback2.pcap):
+ * TotalMix writes the FULL 30-channel 0x15 map on every toggle (ON =
+ * the pair at 0x0001 + the other 28 at 0x0000; OFF = all 0x0000).
+ * wIdx = 2xout_index: AN1/2 = 0/1, PH3/4 = 2/3, AS1/2 = 4/5, ...
+ */
+#define BF_LOOPBACK_CHANNELS 30
+
/* The "cross" register block within each output: the L-registers sit at
* odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
* source pairs that can be cross-linked). bf_crosspoint_clear_cross()
@@ -316,6 +332,16 @@ struct snd_usb_babyface {
* (0 = +4dBu, the default)
*/
struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
+ bool loopback[6];
+ bool an12; /* AN 1>2 copy */
+ bool linked; /* AN1/2 input link */
+ bool ms_proc; /* MS processor engaged */
+ bool clock_optical; /* clock source: false = Internal (default) */
+ int width; /* width knob -100..+100 */
+ u16 fx_send; /* FX send level 0..0x1000 */
+ u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */
+ bool dim; /* DIM engaged (fixed -20 dB on Phones) */
+ struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */
};
/* The mixer state cached across interface re-probes (see
@@ -334,6 +360,15 @@ struct bf_saved {
int trim[4];
bool split[6];
int ref_level;
+ int pitch;
+ bool loopback[6];
+ bool an12;
+ bool linked;
+ bool ms_proc;
+ bool clock_optical;
+ int width;
+ u16 fx_send;
+ bool dim;
};
struct bf_rate {
@@ -387,6 +422,8 @@ int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2);
int bf_gain_max_db(int mic);
int bf_gain_db(int mic, u8 raw);
u8 bf_gain_raw(int mic, int db);
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
+int babyface_create_flags(struct snd_usb_babyface *chip);
/* Master gain-law helpers (babyfacepro-ctl.c). */
int bf_master_half_db(u16 vol16); /* 16-bit master -> dBx2 */
--
2.56.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v6 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (3 preceding siblings ...)
2026-10-03 19:41 ` [RFC PATCH v6 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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.
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 | 77 ++++++++++++++++++++++++-----
sound/usb/babyfacepro/babyfacepro.h | 6 +--
2 files changed, 69 insertions(+), 14 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c
index 0678bea58..aaa74ee68 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -3,8 +3,8 @@
* RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
*
* Core driver: USB vendor requests + cold init, interrupt-URB PCM
- * streaming, mixer-state persistence across re-probes, and the card
- * lifecycle (probe/disconnect/module entry).
+ * streaming, mixer-state persistence across re-probes/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.
@@ -200,9 +200,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)
{
@@ -280,8 +280,9 @@ int bf_cold_init(struct snd_usb_babyface *chip)
static LIST_HEAD(bf_saved_list);
static DEFINE_MUTEX(bf_saved_mutex);
-/* Re-apply the whole cached mixer state to the device, whose registers
- * the cold init cleared. Caller holds chip->mutex.
+/* Re-apply the whole cached mixer state after a resume (the device
+ * lost its registers across a system suspend - TotalMix does the same
+ * re-apply). Caller holds chip->mutex.
*/
int babyface_restore_state(struct snd_usb_babyface *chip)
{
@@ -409,7 +410,7 @@ int bf_state_apply_flags(struct snd_usb_babyface *chip)
/* Re-apply an engaged DIM (the fixed -20 dB Phones pair + flag). The
* level DIM releases back to is not persisted: it always tracks the
* Phones master while DIM is engaged, so rebuild it here - otherwise a
- * DIM released after a re-probe would drive Phones to
+ * DIM released after a re-probe or resume would drive Phones to
* silence (dim_saved still zero on the fresh chip).
*/
if (chip->dim) {
@@ -979,8 +980,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)
@@ -1667,7 +1669,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
@@ -1925,6 +1928,56 @@ 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. */
+ mutex_lock(&chip->mutex);
+ if (chip->streaming)
+ babyface_stream_kill(chip);
+ mutex_unlock(&chip->mutex);
+ 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).
+ */
+ 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);
+ return err;
+}
+
static const struct usb_device_id babyface_ids[] = {
{ USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) },
{ }
@@ -1935,6 +1988,8 @@ static struct usb_driver babyface_driver = {
.name = "snd-usb-babyface-pro",
.probe = babyface_probe,
.disconnect = babyface_disconnect,
+ .suspend = babyface_suspend,
+ .resume = babyface_resume,
.id_table = babyface_ids,
};
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 783ea03ac..e324e8411 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.
*
@@ -344,7 +344,7 @@ struct snd_usb_babyface {
struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */
};
-/* The mixer state cached across interface re-probes (see
+/* The mixer state cached across interface re-probes/resume (see
* babyfacepro.c's own comment on bf_state_save/bf_state_restore).
*/
struct bf_saved {
@@ -395,7 +395,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. */
void bf_state_save(struct snd_usb_babyface *chip);
int bf_state_restore(struct snd_usb_babyface *chip);
void bf_state_purge(void);
--
2.56.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v6 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (4 preceding siblings ...)
2026-10-03 19:41 ` [RFC PATCH v6 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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. A re-probe keeps
what the driver had; the values alsactl restores after a probe are
taken for a pair that is not known and ignored for one that is.
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 before
the mixer state is saved, since the poll worker writes that state.
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 | 1235 ++++++++++++++++++++++-
sound/usb/babyfacepro/babyfacepro.c | 39 +-
sound/usb/babyfacepro/babyfacepro.h | 123 ++-
3 files changed, 1389 insertions(+), 8 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index f72879502..183647e25 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,
@@ -664,6 +665,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;
}
}
@@ -1409,6 +1411,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++) {
@@ -1455,6 +1458,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;
}
@@ -2098,3 +2102,1232 @@ 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");
+}
+
+/* The kernel driver plays the TotalMix role for the MIX button (the
+ * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs
+ * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT-
+ * chosen channel(s) of the IN-selected pair, into the OUT-selected
+ * output's crosspoint block - the STANDARD map only (cap_mix.pcap /
+ * cap_select2.pcap, no low-map mirror). Mirrors the change into the
+ * xpoint cache so the ALSA controls follow the wheel. Takes the mutex
+ * (the 0x12 writes cycle the transaction flag like the mixer puts).
+ */
+static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ /* Canonical output of the OUT selection (enum 1 = Ch1/2,
+ * 2 = Phones, 3 = Opt): AN1/2, PH3/4, ADAT7/8 (the optical
+ * output) respectively.
+ */
+ int out = READ_ONCE(chip->panel_out) == 3 ? 5 :
+ READ_ONCE(chip->panel_out) == 2 ? 1 : 0;
+ unsigned int blk = bf_xpoint_block[out];
+ u8 targets[2];
+ int n = 0;
+ int db2;
+ u16 raw, flag;
+ int i;
+
+ /* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT
+ * steps left/right/both; none = nothing selected = no-op wheel).
+ * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4.
+ */
+ if (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);
+ }
+
+ mutex_lock(&chip->mutex);
+ db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
+ db2 = clamp(db2 + delta, BF_FADER_DB2_INF, 12);
+ raw = bf_fader_db2_to_raw(db2);
+ chip->panel_mix_raw = raw;
+ for (i = 0; i < n; i++) {
+ const struct bf_source *s = &bf_sources[targets[i]];
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+ (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk +
+ s->idx_l) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+ (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk +
+ s->idx_r) | flag);
+ chip->xpoint[out][targets[i]][0] = raw;
+ chip->xpoint[out][targets[i]][1] = raw;
+ 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;
+ }
+ }
+ }
+ mutex_unlock(&chip->mutex);
+}
+
+/* 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)
+{
+ u16 flag;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l),
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r),
+ BF_REG_MASTER_8 + 2 * out + 1);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ /* 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 = ADAT7/8 = canon 5, else AN1/2).
+ */
+static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out,
+ u16 l, u16 r)
+{
+ u16 flag;
+
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ chip->master[out][0] = l;
+ chip->master[out][1] = r;
+ /* A Phones change while DIM is engaged re-bases the restore. */
+ if (chip->dim && out == 1) {
+ chip->dim_saved[0] = l;
+ chip->dim_saved[1] = r;
+ }
+ 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 raw = chip->master[out][ch];
+
+ if (raw == chip->panel_master_last[out][ch])
+ return chip->panel_out_hdb[out][ch];
+ if (!raw)
+ return BF_OUT_WHEEL_MUTED;
+ return bf_master_half_db(raw);
+}
+
+/* One click on both sides. They move together, so a balance is kept.
+ * When the louder side would go below the floor, both stop there and the
+ * louder side mutes; from mute, turning up continues from the floor
+ * (-90 dB up one step is -87). A quieter side below the floor is muted
+ * but keeps its offset, so the balance comes back as the level rises.
+ */
+static void bf_out_wheel_click(int db[2], int step)
+{
+ int louder = max(db[0], db[1]);
+ int shift = step;
+
+ if (louder < BF_OUT_WHEEL_FLOOR) {
+ if (step < 0)
+ return;
+ shift = BF_OUT_WHEEL_FLOOR - louder + step;
+ } else if (louder + step < BF_OUT_WHEEL_FLOOR) {
+ shift = BF_OUT_WHEEL_MUTED - louder;
+ } else if (louder + step > 12) {
+ shift = 12 - louder;
+ }
+ db[0] += shift;
+ db[1] += shift;
+}
+
+static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ int out = READ_ONCE(chip->panel_out) == 3 ? 5 :
+ READ_ONCE(chip->panel_out) == 2 ? 1 : 0;
+ int dir = delta > 0 ? 1 : -1;
+ int clicks = abs(delta);
+ ktime_t now = ktime_get();
+ bool fast;
+ int ch, i, step, louder;
+ int db[2];
+ u16 next[2];
+
+ chip->panel_fast_until = jiffies +
+ msecs_to_jiffies(BF_PANEL_FAST_HOLD_MS);
+ /* The click came some time since the previous poll: take the
+ * middle. Clicks after the first in this poll came within one
+ * interval, so only the first can be slow. A reversal starts a
+ * new turn.
+ */
+ if (chip->panel_poll_t)
+ now = ktime_sub(now,
+ ktime_divns(ktime_sub(now, chip->panel_poll_t), 2));
+
+ mutex_lock(&chip->mutex);
+ if (chip->muted[out]) {
+ bf_panel_remute(chip, out);
+ mutex_unlock(&chip->mutex);
+ 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 :
+ bf_master_16bit(db[ch]);
+ bf_panel_out_wheel_write(chip, out, next[0], next[1]);
+ for (ch = 0; ch < 2; ch++) {
+ chip->panel_master_last[out][ch] = next[ch];
+ chip->panel_out_hdb[out][ch] = db[ch];
+ }
+ chip->panel_out_wheel_t = now;
+ chip->panel_out_wheel_dir = dir;
+ chip->panel_out_wheel_out = out;
+ chip->panel_out_resync = true;
+ mutex_unlock(&chip->mutex);
+}
+
+/* Once the wheel has rested, write the wheel's level to the 8-bit (and
+ * 16-bit) master, as TotalMix does at the end of a gesture. The level
+ * already follows the firmware's step rule, so this normally changes
+ * nothing audible. It fixes the rare click whose speed the driver
+ * guessed wrong: one zone step per such click, smoothed by the
+ * firmware. Nothing is written below -64 dB, where the 8-bit codes the
+ * device takes are unknown, nor after a mixer application has set the
+ * master since (that write already set the device).
+ */
+static void bf_panel_out_resync(struct snd_usb_babyface *chip)
+{
+ int out, ch;
+ int db[2];
+ u16 flag;
+
+ if (!chip->panel_out_resync ||
+ ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) <
+ BF_OUT_WHEEL_RESYNC_MS)
+ return;
+
+ mutex_lock(&chip->mutex);
+ chip->panel_out_resync = false;
+ out = chip->panel_out_wheel_out;
+ for (ch = 0; ch < 2; ch++) {
+ if (chip->master[out][ch] != chip->panel_master_last[out][ch])
+ goto unlock;
+ db[ch] = chip->panel_out_hdb[out][ch];
+ if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
+ goto unlock;
+ }
+ if (!chip->muted[out]) {
+ flag = bf_flag_cycle[chip->flag_cnt];
+ chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+ bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[0],
+ BF_REG_MASTER_8 + 2 * out);
+ bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[1],
+ BF_REG_MASTER_8 + 2 * out + 1);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][0],
+ (BF_REG_MASTER_16 + 2 * out) | flag);
+ bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][1],
+ (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+ }
+unlock:
+ mutex_unlock(&chip->mutex);
+}
+
+/* IN-mode wheel: the gain of the SELECT-chosen channel(s) of the
+ * IN-selected pair, +/-1 dB per click (manual sec. 5.1: SELECT steps
+ * left/right/both, then the wheel changes the gain). Writes the PANEL
+ * gain registers 0x1A 0x000A+mic (cap_select.pcap 2026-08-24 - the
+ * "ADC gain" family, which drives the same preamp as the GUI
+ * 0x0000+mic; the cache tracks the raw either way). Opt has no
+ * preamp and SELECT None = no target.
+ */
+static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ u8 mics[2];
+ int n = 0;
+ int i;
+
+ if (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;
+ }
+
+ mutex_lock(&chip->mutex);
+ for (i = 0; i < n; i++) {
+ int mic = mics[i];
+ int db = clamp((int)chip->gain[mic] + delta,
+ 0, bf_gain_max_db(mic));
+ u8 raw = bf_gain_raw(mic, db);
+
+ bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic);
+ if (db != chip->gain[mic]) {
+ chip->gain[mic] = db;
+ bf_panel_notify_kctl(chip, chip->gain_kctl[mic]);
+ }
+ }
+ mutex_unlock(&chip->mutex);
+}
+
+/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output
+ * Balance"): moves the stereo image of the OUT-selected output by
+ * attenuating ONE side, linear in raw (cap_pan_stereo.pcap: the varied
+ * side = fixed*(1-|pan|), ~0x9C raw step per click at 0 dB - the PAN
+ * of the stereo hardware output in TotalMix). The balance position is
+ * derived from the L/R master ratio (the louder side is the fixed
+ * one), so the gesture needs no extra state - and the OUT wheel below
+ * moves both sides by the same dB to preserve an existing balance.
+ */
+static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
+{
+ int out = READ_ONCE(chip->panel_out) == 3 ? 5 :
+ READ_ONCE(chip->panel_out) == 2 ? 1 : 0;
+ u16 l, r;
+ int bal; /* -100..+100; + = image right (left varies) */
+ u16 fixed, varied;
+
+ mutex_lock(&chip->mutex);
+ if (chip->muted[out]) {
+ bf_panel_remute(chip, out);
+ mutex_unlock(&chip->mutex);
+ 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).
+ */
+ l = chip->master[out][0];
+ r = chip->master[out][1];
+ /* Balance from the L/R ratio: the louder side is the fixed one. */
+ if (l >= r) {
+ bal = r ? -(100 - (100 * r) / l) : -100;
+ fixed = l;
+ } else {
+ bal = l ? (100 - (100 * l) / r) : 100;
+ fixed = r;
+ }
+ bal = clamp(bal + delta * 2, -100, 100);
+ varied = (u16)((u32)fixed * (100 - abs(bal)) / 100);
+ l = bal >= 0 ? varied : fixed;
+ r = bal >= 0 ? fixed : varied;
+
+ bf_panel_write_master(chip, out, l, r);
+ mutex_unlock(&chip->mutex);
+}
+
+/* SET press (byte3 0x42 flash): toggle 48V phantom on the
+ * SELECT-chosen mic(s) of the IN-selected pair. The hardware only
+ * does this in standalone mode (online, TotalMix ignores SET - no USB
+ * write in the captures), but the driver IS the host: it writes the
+ * preamp state itself and the P48 LEDs follow (the tuxmix-core
+ * emulator, hardware-verified). Restricted to IN mode + Ch1/2 (the
+ * phantom-capable pair); Opt/Ch3/4 and SELECT None = no target.
+ */
+static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
+{
+ u16 bits = 0;
+ 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;
+
+ mutex_lock(&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;
+ mutex_unlock(&chip->mutex);
+}
+
+static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl)
+{
+ if (chip->panel_kctl[ctl])
+ snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &chip->panel_kctl[ctl]->id);
+}
+
+/* One 0x17 read + decode. Called from the poll work. 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 re-probe).
+ */
+ 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);
+ }
+ }
+ /* 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, out;
+ 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).
+ */
+ out = READ_ONCE(chip->panel_out) == 3 ? 5 :
+ READ_ONCE(chip->panel_out) == 2 ? 1 : 0;
+ ref = READ_ONCE(chip->panel_in) == 3 ? 4 :
+ (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) +
+ (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+ chip->panel_mix_raw = chip->xpoint[out][ref][0];
+ /* Seed the VU display shadow at the CURRENT level
+ * (cap_panel.pcap: TotalMix writes the display value of
+ * the current fader on engage - 10 in that session -
+ * not a hard 0; cap_mix's 0 was because the fader sat
+ * at the bottom). Only the channels the wheel can move.
+ */
+ for (m = 0; m < 4; m++)
+ chip->panel_mix_disp[m] = 0;
+ if (ref < 4) {
+ int db2 = bf_fader_raw_to_db2(chip->panel_mix_raw);
+ int disp = bf_mix_display(db2);
+
+ bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp,
+ BF_REG_PANEL_GAIN + ref);
+ chip->panel_mix_disp[ref] = disp;
+ }
+ }
+ bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+ }
+ if (fader_now) {
+ 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 re-probe keeps it, see bf_saved) 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);
+}
+
+/* -- controls -------------------------- */
+
+/* The button/wheel controls hold the LATEST state and are NOT consumed
+ * on read: wireplumber subscribes to every notifying control and reads
+ * it, so a clear-on-get would let another reader eat the event. Each
+ * consumer tracks its own baseline and acts on changes (the button is a
+ * last-press code, the wheel an accumulated signed delta). VOLATILE
+ * keeps alsactl from caching them.
+ */
+static int bf_panel_button_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = BF_PANEL_BTN_DIM;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_panel_button_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+ ucontrol->value.integer.value[0] = 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 stored at the last shutdown 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 nothing is known and the stored values are what there
+ * is; after a re-probe the driver kept what it tracked, which is
+ * newer than anything stored.
+ */
+ if (restore && chip->panel_sel[pair] >= 0)
+ return 0;
+ 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);
+ 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 aaa74ee68..18b8ac08d 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -537,6 +537,7 @@ void bf_state_save(struct snd_usb_babyface *chip)
s->width = chip->width;
s->fx_send = chip->fx_send;
s->dim = chip->dim;
+ memcpy(s->panel_sel, chip->panel_sel, sizeof(s->panel_sel));
mutex_unlock(&bf_saved_mutex);
}
@@ -574,6 +575,7 @@ int bf_state_restore(struct snd_usb_babyface *chip)
chip->width = s->width;
chip->fx_send = s->fx_send;
chip->dim = s->dim;
+ memcpy(chip->panel_sel, s->panel_sel, sizeof(chip->panel_sel));
ret = 1;
break;
}
@@ -1582,6 +1584,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.");
@@ -1593,6 +1596,8 @@ 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).");
+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).");
/* -- USB driver ------------------------- */
@@ -1671,11 +1676,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;
@@ -1690,10 +1696,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)
@@ -1856,12 +1865,23 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ err = babyface_create_panel(chip);
+ if (err < 0) {
+ dev_err(&intf->dev, "front-panel control creation failed: %d\n", err);
+ goto error;
+ }
+
err = snd_card_register(chip->card);
if (err < 0) {
dev_err(&intf->dev, "snd_card_register failed: %d\n", err);
goto error;
}
+ /* The panel poll mirrors the physical buttons/wheel into the
+ * Front Panel controls; it runs for the whole card lifetime.
+ */
+ babyface_panel_start(chip);
+
usb_set_intfdata(intf, chip);
dev_info(&intf->dev,
"Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n",
@@ -1904,9 +1924,13 @@ static void babyface_disconnect(struct usb_interface *intf)
babyface_stream_kill(chip);
mutex_unlock(&chip->mutex);
cancel_work_sync(&chip->stream_work);
+ babyface_panel_stop(chip);
/* Keep the mixer state for the next probe: a userspace usbfs claim
* (PipeWire sink grab, TuxMix daemon) detaches us and the cold init
- * of the re-probe would otherwise wipe the settings.
+ * of the re-probe would otherwise wipe the settings. Saved after
+ * the panel poll is stopped: the worker writes master/gain/xpoint
+ * under chip->mutex and this copy does not take it, so an unplug
+ * during a wheel turn could otherwise snapshot a torn state.
*/
bf_state_save(chip);
/* Balance the probe()-time usb_disable_autosuspend(): the usb_device
@@ -1946,6 +1970,7 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
babyface_stream_kill(chip);
mutex_unlock(&chip->mutex);
cancel_work_sync(&chip->stream_work);
+ babyface_panel_stop(chip);
return 0;
}
@@ -1975,6 +2000,8 @@ static int babyface_resume(struct usb_interface *intf)
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 e324e8411..1959bdf2e 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>
@@ -190,6 +191,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()
@@ -301,6 +349,12 @@ struct snd_usb_babyface {
u16 master[6][2]; /* cached 16-bit masters */
bool muted[6];
u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */
+ /* 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,
@@ -342,6 +396,68 @@ struct snd_usb_babyface {
u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */
bool dim; /* DIM engaged (fixed -20 dB on Phones) */
struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */
+
+ /* front panel (babyfacepro-ctl.c) - 0x17 readback poll */
+ struct delayed_work panel_work;
+ unsigned int panel_poll_ms; /* front-panel poll interval, module param */
+ u8 panel_prev[4]; /* last 0x17 snapshot */
+ bool panel_seen; /* first snapshot taken */
+ bool panel_select_armed; /* 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) */
+ 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 (the 16-bit
+ * value can't hold every half-dB step, nor the device's range
+ * below -64 dB, nor its mute). 0xffff = nothing written yet.
+ */
+ u16 panel_master_last[6][2];
+ s16 panel_out_hdb[6][2];
+ /* OUT wheel acceleration and resync at rest (babyfacepro-ctl.c) */
+ ktime_t panel_out_wheel_t; /* estimated time of the last count */
+ ktime_t panel_poll_t; /* when the previous poll ran */
+ unsigned long panel_fast_until; /* jiffies: poll fast until then */
+ int panel_out_wheel_dir; /* its direction, +1 or -1 */
+ int panel_out_wheel_out; /* the output it moved */
+ bool panel_out_resync; /* 8-bit write due once at rest */
+ u16 panel_mix_raw; /* MIX-mode monitoring level (fader raw) */
+ u8 panel_mix_disp[4]; /* MIX-mode VU display shadow per mic
+ * (0x1A 0x000A+mic - written on change
+ * so the input VU follows the wheel)
+ */
+ struct snd_kcontrol *panel_kctl[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 */
};
/* The mixer state cached across interface re-probes/resume (see
@@ -369,6 +485,7 @@ struct bf_saved {
int width;
u16 fx_send;
bool dim;
+ s8 panel_sel[3];
};
struct bf_rate {
@@ -424,6 +541,10 @@ int bf_gain_db(int mic, u8 raw);
u8 bf_gain_raw(int mic, int db);
int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
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_half_db(u16 vol16); /* 16-bit master -> dBx2 */
--
2.56.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v6 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (5 preceding siblings ...)
2026-10-03 19:41 ` [RFC PATCH v6 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
2026-10-03 19:41 ` [RFC PATCH v6 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc Ismaïl Bahloul
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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 and a re-probe re-upload them as well, and
the EQ parameters are saved with the rest of the mixer state. 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 | 598 +++++++++++++++++++++++-
sound/usb/babyfacepro/babyfacepro.c | 33 ++
sound/usb/babyfacepro/babyfacepro.h | 29 +-
3 files changed, 658 insertions(+), 2 deletions(-)
diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c
index 183647e25..0c04972e4 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,
@@ -3331,3 +3332,598 @@ int babyface_create_panel(struct snd_usb_babyface *chip)
return 0;
}
+#define BF_EQ_Q27 (1 << 27)
+#define BF_EQ_LC_OFF 0x04000000
+#define BF_EQ_BLOCK_LEN 64
+
+/* atan(2^-i) x 2^27 (CORDIC). */
+static const s64 bf_atan_tab[28] = {
+ 0x6487ED5, 0x3B58CE1, 0x1F5B760, 0xFEADD5,
+ 0x7FD56F, 0x3FFAAB, 0x1FFF55, 0xFFFEB,
+ 0x7FFFD, 0x40000, 0x20000, 0x10000,
+ 0x8000, 0x4000, 0x2000, 0x1000,
+ 0x800, 0x400, 0x200, 0x100,
+ 0x80, 0x40, 0x20, 0x10,
+ 0x8, 0x4, 0x2, 0x1,
+};
+
+/* ---- fixed-point helpers (Q27 in/out, s64 intermediates) ---- */
+
+/* sin/cos of an angle in [0, pi/2] (Q27). Simultaneous CORDIC, 28
+ * iterations (~1e-8 residual). eq_selftest.c verifies the whole
+ * pipeline against the double-precision reference.
+ */
+static void bf_sincos(s64 ang, s64 *sn, s64 *cs)
+{
+ s64 x = 0x4DBA76D; /* 1/1.64676 x 2^27 (CORDIC gain) */
+ s64 y = 0;
+ s64 z = ang;
+ int i;
+
+ for (i = 0; i < 28; i++) {
+ s64 d = z >= 0 ? 1 : -1;
+ s64 nx = x - d * (y >> i);
+ s64 ny = y + d * (x >> i);
+
+ x = nx;
+ y = ny;
+ z -= d * bf_atan_tab[i];
+ }
+ *cs = x;
+ *sn = y;
+}
+
+/* 2^u for u in Q27, u in [-2, 2] (gain-amplitude range). */
+static s64 bf_exp2(s64 u)
+{
+ s64 n = u >> 27;
+ s64 r = u - (n << 27);
+ s64 rl = (r * 0x58B90C0 + (1 << 26)) >> 27; /* r.ln2 */
+ s64 e = BF_EQ_Q27;
+ s64 term = BF_EQ_Q27;
+ int k;
+
+ for (k = 1; k <= 10; k++) {
+ term = div_s64((term * rl + (1 << 26)) >> 27, k);
+ e += term;
+ }
+ return n >= 0 ? e << n : e >> -n;
+}
+
+/* The 5 stored words (c0..c3 + shared c4) for one band.
+ * type: 1 bell, 2 low shelf, 3 high shelf. freq_hz, fs in Hz;
+ * q100 = Q x 100; gain_x10 = dB x 10. fs is the stream rate.
+ */
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+ s32 gain_x10, s32 fs)
+{
+ s64 f = freq_hz;
+ s64 w0, c, s, alpha, A, sq;
+ s64 b0, b1, b2, a0, a1, a2;
+ s64 pi = 0x1921FB54; /* pi, Q27 */
+ s64 hpi = 0xC90FDAA; /* pi/2, Q27 */
+ s64 t;
+ int both = 0, cflip = 0;
+
+ if (gain_x10 == 0 || q100 <= 0) {
+ /* Inactive band: identity words (also guards the alpha
+ * division below against the default Q=0 the controls start
+ * with - a user setting gain before Q used to hit a kernel
+ * divide-by-zero oops).
+ */
+ w[0] = 0;
+ w[1] = 0;
+ w[2] = 0;
+ w[3] = 0;
+ /* 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 = div64_s64(s * 100 + q100, 2 * (s64)q100); /* sin(w0)/(2Q) */
+ /* A = 10^(g/40), sqrt(A): g = gain_x10/10 dB */
+ A = bf_exp2((s64)gain_x10 * 0x11021E);
+ sq = bf_exp2((s64)gain_x10 * 0x8810F);
+
+ if (type == 1) {
+ s64 ta = (alpha * A + (1 << 26)) >> 27;
+
+ b0 = BF_EQ_Q27 + ta;
+ b1 = -2 * c;
+ b2 = BF_EQ_Q27 - ta;
+ a0 = BF_EQ_Q27 + div64_s64(alpha * BF_EQ_Q27 + A / 2, A);
+ a1 = -2 * c;
+ a2 = BF_EQ_Q27 - div64_s64(alpha * BF_EQ_Q27 + A / 2, A);
+ } else {
+ s64 ap1 = A + BF_EQ_Q27;
+ s64 am1 = A - BF_EQ_Q27;
+ s64 cp0 = (am1 * c + (1 << 26)) >> 27; /* (A-1).c */
+ s64 cp1 = (ap1 * c + (1 << 26)) >> 27; /* (A+1).c */
+ s64 ab = (2 * sq * alpha + (1 << 26)) >> 27;
+
+ if (type == 2) { /* low shelf */
+ b0 = (A * (ap1 - cp0 + ab) + (1 << 26)) >> 27;
+ b1 = (2 * A * (am1 - cp1) + (1 << 26)) >> 27;
+ b2 = (A * (ap1 - cp0 - ab) + (1 << 26)) >> 27;
+ a0 = ap1 + cp0 + ab;
+ a1 = -2 * (am1 + cp1);
+ a2 = ap1 + cp0 - ab;
+ } else { /* high shelf */
+ b0 = (A * (ap1 + cp0 + ab) + (1 << 26)) >> 27;
+ b1 = (-2 * A * (am1 + cp1) + (1 << 26)) >> 27;
+ b2 = (A * (ap1 + cp0 - ab) + (1 << 26)) >> 27;
+ a0 = ap1 - cp0 + ab;
+ a1 = -2 * (am1 - cp1);
+ a2 = ap1 - cp0 - ab;
+ }
+ }
+
+ w[0] = (s32)div64_s64(a1 * BF_EQ_Q27 + a0 / 2, a0);
+ w[1] = (s32)div64_s64(a2 * BF_EQ_Q27 + a0 / 2, a0);
+ w[2] = (s32)div64_s64(b1 * BF_EQ_Q27 + b0 / 2, b0);
+ w[3] = (s32)div64_s64(b2 * BF_EQ_Q27 + b0 / 2, b0);
+ w[4] = (s32)div64_s64(b0 * BF_EQ_Q27 + a0 / 2, a0);
+}
+
+/* ---- low cut ---- */
+
+/* Slope byte: 2^n-1 (n poles) -> 6/12/18/24 dB per oct; 0 = off. */
+static u8 bf_eq_lc_slope_byte(s32 slope_db)
+{
+ switch (slope_db) {
+ case 6: return 0x01;
+ case 12: return 0x03;
+ case 18: return 0x07;
+ case 24: return 0x0F;
+ }
+ return 0;
+}
+
+/* The 0x38 low-cut frequency word: round(K.f'.(11656)/(11656+f')) with
+ * K = 11508, f' = f x slope-compensation factor (cap_eq9 fit, 0.003%;
+ * the slope factor keeps the composite -3 dB point constant).
+ */
+static u32 bf_eq_lc_freq_raw(s32 freq_hz, s32 slope_db)
+{
+ s64 f, word;
+
+ if (freq_hz <= 0)
+ return BF_EQ_LC_OFF;
+ f = freq_hz;
+ switch (slope_db) {
+ case 6:
+ f = f * 15267 / 10000;
+ break;
+
+ case 18:
+ f = f * 8061 / 10000;
+ break;
+
+ case 24:
+ f = f * 6977 / 10000;
+ break;
+ }
+ word = (11508 * f * 11656 + (11656 + f) / 2) / (11656 + f);
+ return (u32)word;
+}
+
+/* ---- block build + bulk write ---- */
+
+static void bf_eq_build_block(u8 *b, int ch, u8 slope,
+ const s32 bands[3][4], s32 shared, u32 lc)
+{
+ int slot, k;
+
+ memset(b, 0, BF_EQ_BLOCK_LEN);
+ b[0] = ch;
+ b[1] = slope;
+ b[2] = ch;
+ b[3] = 0x80; /* EQ engine active */
+ for (slot = 0; slot < 3; slot++) {
+ for (k = 0; k < 4; k++) {
+ put_unaligned_le32((u32)bands[slot][k],
+ b + 0x04 + slot * 0x10 + 4 * k);
+ }
+ }
+ put_unaligned_le32((u32)shared, b + 0x34);
+ put_unaligned_le32(lc, b + 0x38);
+}
+
+/* Upload one 64-byte block on bulk OUT ep 0x0A (interface 1). */
+static int bf_eq_upload(struct snd_usb_babyface *chip, const u8 *block)
+{
+ u8 *buf;
+ int ret, len;
+
+ /* usb_bulk_msg DMA-maps the buffer: it must not be on the stack
+ * (usb_hcd_map_urb_for_dma returns -EAGAIN for stack buffers).
+ */
+ buf = kmemdup(block, BF_EQ_BLOCK_LEN, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ ret = usb_bulk_msg(chip->dev, usb_sndbulkpipe(chip->dev, 0x0a),
+ buf, BF_EQ_BLOCK_LEN, &len, 1000);
+ kfree(buf);
+ if (ret < 0)
+ dev_err(&chip->dev->dev, "EQ bulk upload failed: %d\n", ret);
+ return ret;
+}
+
+/* Write the L+R block pair for one strip (channel base = strip x 2). */
+static int bf_eq_write_strip(struct snd_usb_babyface *chip, int strip)
+{
+ struct bf_eq_channel *e = &chip->eq[strip];
+ u8 b[BF_EQ_BLOCK_LEN];
+ s32 identity[3][4] = { { 0 }, { 0 }, { 0 } };
+ s32 shared = e->on ? e->shared : BF_EQ_Q27;
+ u32 lc = e->on ? e->lc_raw : BF_EQ_LC_OFF;
+ /* The header slope byte (b[1]) is only valid while the low cut is
+ * engaged: a stale slope with 0x38 = off made the device apply a
+ * garbage-frequency cut (ear-verified: "low cut off" left only
+ * highs). cap_eq7: byte1 = 0x00 + 0x38 = 0x04000000 when off.
+ */
+ u8 slope = (e->on && e->lc_hz > 0) ? e->slope : 0;
+ int ch, ret;
+
+ for (ch = 0; ch < 2; ch++) {
+ bf_eq_build_block(b, strip * 2 + ch, slope,
+ e->on ? e->words : identity, shared, lc);
+ ret = bf_eq_upload(chip, b);
+ if (ret < 0)
+ return ret;
+ }
+ return 0;
+}
+
+/* Recompute one strip's words + low cut from its params, re-upload.
+ * Lock-free by convention: every caller must already hold chip->mutex
+ * (bf_eq_put() and bf_eq_reupload() do) - asserting it here catches a
+ * future caller that forgets, instead of a silent self-deadlock.
+ */
+static void bf_eq_update_strip(struct snd_usb_babyface *chip, int strip)
+{
+ struct bf_eq_channel *e = &chip->eq[strip];
+ s32 fs = chip->rate ? chip->rate : 48000;
+ s32 last_c4 = BF_EQ_Q27;
+ int band, i;
+
+ lockdep_assert_held(&chip->mutex);
+
+ for (band = 0; band < 3; band++) {
+ s32 w[5];
+
+ bf_eq_band_words(w, e->band_type[band], e->band_freq[band],
+ e->band_q[band], e->band_gain[band], fs);
+ for (i = 0; i < 4; i++)
+ e->words[band][i] = w[i];
+ if (e->band_type[band] && e->band_gain[band])
+ last_c4 = w[4]; /* shared scale: the last band */
+ }
+ e->shared = last_c4;
+ e->lc_raw = bf_eq_lc_freq_raw(e->lc_hz, e->slope_db);
+ e->slope = bf_eq_lc_slope_byte(e->slope_db);
+ bf_eq_write_strip(chip, strip);
+}
+
+/* Recompute + re-upload all four strips (rate change). Caller must
+ * hold chip->mutex - bf_eq_update_strip()/bf_eq_write_strip() are
+ * lock-free by convention (see bf_eq_put()) and the only caller,
+ * babyface_pcm_hw_params(), already holds the lock across the rate
+ * change; locking here too self-deadlocked it (hung-task: "blocked
+ * on a mutex likely owned by" itself, hit via regress.sh's rate
+ * sweep).
+ */
+void bf_eq_reupload(struct snd_usb_babyface *chip)
+{
+ int strip;
+
+ for (strip = 0; strip < 4; strip++)
+ bf_eq_update_strip(chip, strip);
+}
+
+/* ---- ALSA controls (4 strips x 19 controls) ---- */
+
+#define EQ_STRIP(pv) ((pv) >> 8)
+#define EQ_PARAM(pv) ((pv) & 0xff)
+/* params: 0 enable, 1-3 type, 4-6 freq, 7-9 q, 10-12 gain, 13 lc freq, 14 lc slope */
+
+static const char *const bf_eq_type_texts[] = {
+ "Off", "Bell", "Low Shelf", "High Shelf", NULL
+};
+
+static const char *const bf_eq_slope_texts[] = {
+ "6 dB/oct", "12 dB/oct", "18 dB/oct", "24 dB/oct", NULL
+};
+
+static int bf_eq_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ int param = EQ_PARAM(kctl->private_value);
+
+ if (param == 0) {
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->count = 1;
+ return 0;
+ }
+ if (param == 1 || param == 2 || param == 3)
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_type_texts);
+ if (param == 14)
+ return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_slope_texts);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = (param == 10 || param == 11 ||
+ param == 12) ? -240 :
+ (param == 7 || param == 8 ||
+ param == 9) ? 5 : 0;
+ uinfo->value.integer.max = (param == 7 || param == 8 ||
+ param == 9) ? 1000 :
+ (param == 10 || param == 11 ||
+ param == 12) ? 240 : 20000;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int bf_eq_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ struct bf_eq_channel *e = &chip->eq[EQ_STRIP(kctl->private_value)];
+ int param = EQ_PARAM(kctl->private_value);
+ int band = (param - 1) % 3;
+ s32 *v = NULL;
+
+ switch (param) {
+ case 0:
+ break;
+
+ case 1:
+ case 2:
+ case 3:
+ v = &e->band_type[band];
+ break;
+
+ case 4:
+ case 5:
+ case 6:
+ v = &e->band_freq[band];
+ break;
+
+ case 7:
+ case 8:
+ case 9:
+ v = &e->band_q[band];
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ v = &e->band_gain[band];
+ break;
+
+ case 13:
+ v = &e->lc_hz;
+ break;
+
+ case 14:
+ v = &e->slope_db;
+ break;
+ }
+ if (param == 0) {
+ ucontrol->value.integer.value[0] = e->on;
+ } else if (param == 14) {
+ /* Inverse of put's index->dB map: slope_db stores the raw
+ * 6/12/18/24 dB/oct value, but an ENUMERATED control's .get
+ * must return the enum item index (0-3), same as .put
+ * receives - returning the raw dB value here (the bug this
+ * replaces) fed back an out-of-range index to every ALSA
+ * consumer (confirmed via amixer: writing index 1 read back
+ * as value 12, not 1).
+ */
+ s32 slope = v ? *v : 6;
+
+ ucontrol->value.enumerated.item[0] =
+ slope >= 24 ? 3 : slope >= 18 ? 2 : slope >= 12 ? 1 : 0;
+ } else if (param == 1 || param == 2 || param == 3) {
+ /* ENUMERATED band type: use the enumerated union member. */
+ ucontrol->value.enumerated.item[0] = v ? *v : 0;
+ } else {
+ ucontrol->value.integer.value[0] = v ? *v : 0;
+ }
+ return 0;
+}
+
+static int bf_eq_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+ int strip = EQ_STRIP(kctl->private_value);
+ int param = EQ_PARAM(kctl->private_value);
+ struct bf_eq_channel *e = &chip->eq[strip];
+ int band = (param - 1) % 3;
+ s32 nv;
+ s32 *v = NULL;
+ int ret = 0;
+
+ /* Read from the union member matching the control type: ENUMERATED
+ * params (band type 1-3, slope 14) use .enumerated.item, everything
+ * else (BOOL 0, INTEGER) uses .integer.value.
+ */
+ if (param == 1 || param == 2 || param == 3 || param == 14)
+ nv = (s32)ucontrol->value.enumerated.item[0];
+ else
+ nv = (s32)ucontrol->value.integer.value[0];
+
+ /* Validate against the bounds bf_eq_info() declares. The ALSA core
+ * only checks these when CONFIG_SND_CTL_INPUT_VALIDATION is set, so
+ * an out-of-range value here could otherwise reach the Q27
+ * coefficient math (bf_eq_band_words/bf_exp2) and shift by >= width
+ * (undefined behaviour).
+ */
+ switch (param) {
+ case 0:
+ if (nv < 0 || nv > 1)
+ return -EINVAL;
+ break;
+ case 1:
+ case 2:
+ case 3:
+ case 14:
+ if (nv < 0 || nv > 3)
+ return -EINVAL;
+ break;
+ case 4:
+ case 5:
+ case 6:
+ case 13:
+ if (nv < 0 || nv > 20000)
+ return -EINVAL;
+ break;
+ case 7:
+ case 8:
+ case 9:
+ if (nv < 5 || nv > 1000)
+ return -EINVAL;
+ break;
+ case 10:
+ case 11:
+ case 12:
+ if (nv < -240 || nv > 240)
+ return -EINVAL;
+ break;
+ }
+
+ switch (param) {
+ case 0:
+ v = NULL;
+ break;
+
+ case 1:
+ case 2:
+ case 3:
+ v = &e->band_type[band];
+ break;
+
+ case 4:
+ case 5:
+ case 6:
+ v = &e->band_freq[band];
+ break;
+
+ case 7:
+ case 8:
+ case 9:
+ v = &e->band_q[band];
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ v = &e->band_gain[band];
+ break;
+
+ case 13:
+ v = &e->lc_hz;
+ break;
+
+ case 14:
+ v = &e->slope_db;
+ break;
+ }
+ if (param == 14) /* slope enum items are 6/12/18/24 */
+ nv = nv == 0 ? 6 : nv == 1 ? 12 : nv == 2 ? 18 : 24;
+
+ mutex_lock(&chip->mutex);
+ if (param == 0) {
+ if (e->on != !!nv) {
+ e->on = !!nv;
+ bf_eq_update_strip(chip, strip);
+ ret = 1;
+ }
+ } else if (v && *v != nv) {
+ *v = nv;
+ bf_eq_update_strip(chip, strip);
+ ret = 1;
+ }
+ mutex_unlock(&chip->mutex);
+ return ret;
+}
+
+/* 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++)
+ 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 18b8ac08d..fb2eba392 100644
--- a/sound/usb/babyfacepro/babyfacepro.c
+++ b/sound/usb/babyfacepro/babyfacepro.c
@@ -537,6 +537,7 @@ void bf_state_save(struct snd_usb_babyface *chip)
s->width = chip->width;
s->fx_send = chip->fx_send;
s->dim = chip->dim;
+ memcpy(s->eq, chip->eq, sizeof(s->eq));
memcpy(s->panel_sel, chip->panel_sel, sizeof(s->panel_sel));
mutex_unlock(&bf_saved_mutex);
}
@@ -575,6 +576,7 @@ int bf_state_restore(struct snd_usb_babyface *chip)
chip->width = s->width;
chip->fx_send = s->fx_send;
chip->dim = s->dim;
+ memcpy(chip->eq, s->eq, sizeof(chip->eq));
memcpy(chip->panel_sel, s->panel_sel, sizeof(chip->panel_sel));
ret = 1;
break;
@@ -587,6 +589,12 @@ int bf_state_restore(struct snd_usb_babyface *chip)
ret = babyface_restore_state(chip);
if (ret == 0)
ret = bf_state_apply_flags(chip);
+ if (ret == 0)
+ /* The DSP is not part of the register state the cold init
+ * clears; re-upload the restored coefficients so a usbfs
+ * detach/re-probe keeps the EQ too.
+ */
+ bf_eq_reupload(chip);
mutex_unlock(&chip->mutex);
return ret ? ret : 1;
}
@@ -1367,6 +1375,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);
}
@@ -1763,6 +1773,8 @@ static int babyface_probe(struct usb_interface *intf,
else
chip->preamp = st[0];
+ bf_eq_defaults(chip);
+
/* Restore the mixer state saved at the last disconnect (if any);
* the device keeps its registers across a usbfs detach, but the
* cold init above cleared them, so push the user's settings back.
@@ -1871,6 +1883,20 @@ static int babyface_probe(struct usb_interface *intf,
goto error;
}
+ /* 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);
@@ -1998,6 +2024,13 @@ static int babyface_resume(struct usb_interface *intf)
if (err < 0)
goto out;
err = bf_state_apply_flags(chip);
+ if (err < 0)
+ goto out;
+ /* 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);
out:
mutex_unlock(&chip->mutex);
if (!err)
diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h
index 1959bdf2e..ba036df5e 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 -
@@ -296,6 +296,24 @@ extern const u8 bf_xpoint_block[6];
*/
#define BF_SRC_PB1 8
+/* One analog-input strip of the DSP EQ (babyfacepro-ctl.c). A named type
+ * rather than one nested in the chip, so it can also be saved with the rest
+ * of the mixer state (struct bf_saved).
+ */
+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;
@@ -458,6 +476,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];
};
/* The mixer state cached across interface re-probes/resume (see
@@ -485,6 +506,7 @@ struct bf_saved {
int width;
u16 fx_send;
bool dim;
+ struct bf_eq_channel eq[4];
s8 panel_sel[3];
};
@@ -545,6 +567,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_half_db(u16 vol16); /* 16-bit master -> dBx2 */
--
2.56.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [RFC PATCH v6 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc
2026-10-03 19:41 [RFC PATCH v6 0/8] ALSA: usb: add RME Babyface Pro driver (proprietary mode) Ismaïl Bahloul
` (6 preceding siblings ...)
2026-10-03 19:41 ` [RFC PATCH v6 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ Ismaïl Bahloul
@ 2026-10-03 19:41 ` Ismaïl Bahloul
7 siblings, 0 replies; 9+ messages in thread
From: Ismaïl Bahloul @ 2026-10-03 19:41 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 | 193 +++++++++++++++++++++
Documentation/sound/cards/index.rst | 1 +
2 files changed, 194 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..f75b80886
--- /dev/null
+++ b/Documentation/sound/cards/babyface-pro.rst
@@ -0,0 +1,193 @@
+.. 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 persistence,
+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.
+
+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 persistence across re-probes
+==========================================
+
+A userspace client can claim the proprietary interface directly via
+``usbfs`` (``USBDEVFS_DISCONNECT_CLAIM``) - both PipeWire grabbing the
+device for a sink and the project's own TuxMix userspace daemon do
+this via libusb. That detaches the kernel driver and the ALSA card
+disappears for the duration; when the client releases the interface,
+the driver re-probes. The device keeps its register contents across
+this detach, but the cold init the probe runs clears them - so the
+driver saves the in-memory mixer state at ``disconnect()`` and
+restores it at the next ``probe()``, keyed by the device's USB serial
+number (or its sysfs path, if it has no serial) so the same physical
+unit gets its state back across the cycle. The same state is also
+what a system-suspend resume replays, since the device loses its
+registers across a suspend the same way.
+
+Front-panel emulation: the driver plays TotalMix's role
+==========================================================
+
+The front panel (IN/OUT/SET/MIX/SELECT/DIM buttons, the rotary
+wheel) has no on-device intelligence of its own for turning a wheel
+click into a mixer change - on Windows/Mac, RME's TotalMix
+application polls the same 0x17 status register this driver polls,
+decodes button/wheel deltas, and performs the resulting mixer writes
+itself. Standalone (no host software) mode exists on the hardware,
+but the proprietary USB mode this driver targets always has a host
+attached, so this driver has to do what TotalMix does: poll 0x17 on
+a delayed work item (``panel_poll_ms`` module parameter, default
+20 ms to match TotalMix's own ~50 Hz), decode the button flash and
+signed wheel delta, and apply the resulting change (an output 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. A
+re-probe, which does not change the unit, keeps what the driver had,
+and the older values alsactl restores shortly after probe are ignored
+for a pair the driver already knows. 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 [flat|nested] 9+ messages in thread