Skip to contentSkip to conversation

ALSA: usb: add RME Babyface Pro driver (proprietary mode)

PATCH New Ismaïl Bahloul 2 participants 17 messages Nov 5

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

v8 cover ALSA: usb: add RME Babyface Pro driver (proprietary mode)

Hi Takashi,

This is v8 of the RME Babyface Pro proprietary-mode driver. The split is unchanged from v5: a minimal core first, the mixer added piece by piece, suspend/resume near the end, the front panel and the DSP EQ last, and a design document. The series is still based on next-20261026, so this is a fix round, not a rebase. It answers your reply to v7: your three points are the first item below, and what I did with the Sashiko findings is the second.

This is not an RFC. Apart from the stereo split one, which a user reported from USB captures of the vendor software, every point below is a fix to a defect found in review of v7, or a change you asked for. None of them changes the protocol or the control layout.

Changes since v7:

- Your comments on v7. The mutexes, the spinlocks, the RCU read sides and the PCM stream locks use guard(), scoped_guard() and guard(rcu) in place of paired lock and unlock calls with goto labels, so the driver has no manual lock handling left. Each critical section covers the same code as before, and the places that used to jump to an unlock label now return. The per-channel loops in babyface_capture_copy() and babyface_playback_copy() are one memcpy() of the words the frame and the stream have in common, and a memset() of the rest: the words are S32_LE with 24 msbits on both sides, so they go over as they are. bf_seen_before() keeps the last eight units it saw in a fixed table (patch 6) instead of a list that grew with every unit; a unit pushed out by eight newer ones is taken for a first probe again.

- Sashiko. I went through its findings on v7 again. The ones that hold are fixed in the items below. Two I left alone: the control names, which stay as they are (see patch 3), and the module parameters it wants in kernel-parameters.txt, which documents the kernel's own; the driver's are in the Kconfig help and in patch 8.

- Probe and stream (patch 1). probe() returned 0 after freeing the card when an URB or DMA buffer allocation failed; it now returns -ENOMEM. Trigger START re-checks the rate and period that prepare() checks, so a substream prepared before the other direction changed the session cannot start against stale geometry. in_done and out_inflight are initialised before the URBs go out, and a URB that cannot be resubmitted is treated as fatal instead of waiting for a run of errors that a later good completion would clear.

- Output masters (patch 2). Every snd_ctl_new1() result is checked before the name is written. The output pair switch is a mono switch, which is what the hardware does; it used to advertise two channels and ignore the second.

- Preamp and trim (patch 3). The same NULL checks in the trim controls, with trim_kctl stored only after snd_ctl_add() succeeds (it could be left dangling on failure). The Boost 0x0003 commit value is re-sent on AN 1>2, on the input link and in the state replay, not only on preamp writes, so Boost no longer falls back to -10dBV. The ref level is derived from the 0x17 readback at probe, the phantom bit is sampled under the mutex, and the unused bf_gain_db() is gone. The preamp and capture control names stay as they are, with .index selecting the channel: the companion mixer and the asound.state files already reference them, and the index is the ALSA convention for a per-channel control. Turning a playback pair's stereo split off wrote 0x1000 to both sides of its AN1/2 crosspoint, 6 dB below what the vendor software writes for a joined pair, and lost the level set on that crosspoint; it now restores the cached crosspoint.

- Routing flags (patch 4). bf_state_apply_flags() used to write 0x1000 to the AN1 and AN2 crosspoints on every probe and resume, even at width 0, after the default mixer had left those inputs muted. The cache still said 0, so a later write of 0 returned early and could not mute them again. The width replay is now guarded and shares one helper with the width control. DIM, mute, the master controls and the front-panel wheels all derive the Phones level from one helper, so they no longer cancel each other. bf_ms_put() restores each output block's own crosspoint, the loopback control notifies the pair it clears, and the enum and integer puts check their bounds before narrowing.

- Suspend/resume (patch 5). No change of its own; it inherits the replay fixes from patch 4.

- Front panel (patch 6). The MIX level is kept in dB2 instead of the quantised register value, so clicks below the curve's first point are no longer lost and a muted input comes up on the first click; SELECT re-seeds the level while MIX is on. The MIX gate is no longer re-armed on the tick that leaves the mode, reprobe is set before the card becomes visible, and the panel master writers respect DIM.

- EQ (patch 7). The low-cut frequency uses div64_s64() for the variable divisor, so a 32-bit module does not need __divdi3 (not exported on ARM). A band of type Off is identity rather than a high shelf, the default slope matches the value the control reports, and a failed strip upload is returned to the caller.

- Documentation (patch 8). Wording follows the one-map crosspoint model; no functional change.

Every patch builds in-tree on its own against next-20261026 with W=1 and no warnings. checkpatch --strict reports no errors; the known false positives are the same as v7 ('ang' for the CORDIC angle, and the MAINTAINERS note on the patches that add files).

Validated on the unit with this tree:

- regress.sh --mixer-restore --disconnect-test passes 41/41: the full-duplex sweep of the nine rates at periods 256 to 2048 with no xruns, runtime renegotiation, 30 start/stop cycles, the mixer state restored across an unbind/rebind, and an unbind in the middle of a capture.

- The DIM/mute/volume matrix was checked on the wire with usbmon:
DIM writes the exact 0xcb/0x0333 pair, a volume change while DIM is on keeps that pair, mute writes 0x3b/0x0000, unmute while DIM is on restores 0xcb/0x0333, and releasing DIM restores the new volume.

- The probe/rebind replay was checked on the wire too: after the default mixer writes 0x0000 to the AN1/AN2 crosspoints, no 0x1000 write follows, so the width block no longer unmutes the inputs.

- The MIX-wheel arithmetic is covered by fader_selftest: a muted crosspoint seeds at the curve's first point, one clockwise click leaves mute, and two counter-clockwise clicks from the old -52 dB sticking point move.

- The physical MIX-wheel gesture was checked on the unit: from a muted AN3 crosspoint, one clockwise click moved it to 0x0004 (the old code left it at 0 for the first five clicks), two clockwise and two counter-clockwise clicks stepped it to 0x0003, and the level followed the wheel in both directions.

- The stereo split was checked through the AN1/2 digital loopback:
with PB1 at the default crosspoint, split then join comes back at the level it started from (-51.5 dB before and after; it used to come back at -54.5 dB).

Known limitations, unchanged from v7:

- USB autosuspend is not supported; it is disabled at probe. S3 suspend/resume works.

- A few protocol details remain open (documented in the source, not blocking): the preamp readback index semantics, a width strip-ownership edge case, whether DIM's scope follows TotalMix's Main Out setting, and the exact high-frequency warping of the EQ coefficients against the vendor curve.

- The URB size and count are load-time parameters, though the size follows the period the application asks for.

Disclosure: as with previous versions, the reverse-engineering and substantial portions of the kernel implementation were written with heavy assistance from AI coding assistants. All of it was verified by hand on real hardware; the human authors remain responsible for the result via Signed-off-by.

Thanks,
Ismaïl

Ismaïl Bahloul (8):
ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)
ALSA: usb: babyfacepro: add output masters and crosspoint routing
ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim
ALSA: usb: babyfacepro: add routing flags and varispeed pitch
ALSA: usb: babyfacepro: add S3 suspend/resume
ALSA: usb: babyfacepro: add the front-panel poll and controls
ALSA: usb: babyfacepro: add the hardware DSP EQ
Documentation: sound: add the Babyface Pro proprietary-mode design doc

 Documentation/sound/cards/babyface-pro.rst |  218 ++
 Documentation/sound/cards/index.rst        |    1 +
 MAINTAINERS                                |    6 +
 sound/usb/Kconfig                          |   17 +
 sound/usb/Makefile                         |    2 +-
 sound/usb/babyfacepro/Makefile             |    4 +
 sound/usb/babyfacepro/babyfacepro-ctl.c    | 3946 ++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c        | 1943 ++++++++++
 sound/usb/babyfacepro/babyfacepro.h        |  568 +++
 9 files changed, 6704 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/sound/cards/babyface-pro.rst
 create mode 100644 sound/usb/babyfacepro/Makefile
 create mode 100644 sound/usb/babyfacepro/babyfacepro-ctl.c
 create mode 100644 sound/usb/babyfacepro/babyfacepro.c
 create mode 100644 sound/usb/babyfacepro/babyfacepro.h
Signature · 1 line
2.56.0
#1 lore raw

PATCH v8 1/8 ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)

The RME Babyface Pro and Babyface Pro FS have a proprietary USB mode (VID 0x2a39, PID 0x3fc0), the one RME's own drivers use. Its PCM stream runs on the interrupt endpoints of interface 5, which snd-usb-audio has no path for, so this is a standalone driver in the style of snd-usb-caiaq.

This first patch covers the card lifecycle and the PCM stream only. Probe runs the device's cold-start init; the full-duplex stream runs on interrupt URBs submitted in IN/OUT pairs, since the device only advances the stream while both directions have a URB pending.

The sample rate is the family register (request 0x10, index 0x0030:
the 32k/44.1k/48k family) with SET_INTERFACE(5, alt) selecting the x1/x2/x4 speed on top of it; the 0x1B DDS quad is varispeed, a ratio applied on top of the family rate. Both directions share one clock, so while another application has the other direction set up it is offered that rate alone, and a period no shorter than the running session's, rather than retuning the device under its stream; the application that holds both directions may change the rate or the buffer size itself, as a DAW does from its settings, and the direction it left running stops with an xrun and is set up again.

A stream session lives from the first prepare to the last hw_free:
hw_params counts a substream, prepare starts the session if it is down, trigger START/STOP only decides whether the URB handlers move that substream's audio (the URBs keep running, carrying silence), and an xrun restart keeps the session. A session start sends what the RME Windows driver sends - the rate write, the trigger pair, the URBs and the arm - and first waits until 50 ms have passed since the previous session stopped: a session triggered within about 15 ms of the last one comes up with the outputs silent. After three consecutive bad URBs the handlers stop resubmitting and the substreams get an xrun.

The size of the URBs follows the period the application asks for, so the latency follows its buffer instead of a fixed queue: hw_params splits the period into the fewest URBs of at most frames_per_urb frames (256 by default), each a whole number of the device's IN packets and not below urb_frames_min (32), and keeps two periods in flight, at most nurbs (8). The three are load-time limits.

The URB handlers copy to and from the ring the core allocates and take the application's position from the shared control page, so the ring is offered for mmap access as well, which JACK needs. runtime->delay reports the audio queued in the URBs and, per speed, the fixed delay of the converters and of the device, so that an application that uses ALSA directly can line up what it records.

The playback handler copies the ring as it is and moves the pointer on by the whole URB, as a DMA engine would; whether the application has written every frame is the PCM core's business, whose xrun check and stop threshold handle an underrun. Clamping the copy to the frames the application had written kept the pointer still for an application that runs the stream free, which is how dmix drives the hardware PCM, so dmix played silence.

Capture exposes the device's twelve input words in the device's own order, one channel per word: AN1-4, AS1/2, then ADAT3-8. AS1/2 is the first optical pair, so it carries ADAT 1/2 when the input receives ADAT and S/PDIF when it receives S/PDIF; the other six are named ADAT3-8 for that reason. At 96/192 kHz the frame has 10/8 words and the channels past them read zero.

USB autosuspend is disabled.

---
 MAINTAINERS                         |    6 +
 sound/usb/Kconfig                   |   17 +
 sound/usb/Makefile                  |    2 +-
 sound/usb/babyfacepro/Makefile      |    4 +
 sound/usb/babyfacepro/babyfacepro.c | 1523 +++++++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.h |  187 ++++
 6 files changed, 1738 insertions(+), 1 deletion(-)
 create mode 100644 sound/usb/babyfacepro/Makefile
 create mode 100644 sound/usb/babyfacepro/babyfacepro.c
 create mode 100644 sound/usb/babyfacepro/babyfacepro.h
MAINTAINERS +6 -0
index fcff12847..378f528c6 100644
@@ -24246,6 +24246,12 @@ F:	include/dt-bindings/power/thead,th1520-power.h
 F:	include/dt-bindings/reset/thead,th1520-reset.h
 F:	include/linux/firmware/thead/thead,th1520-aon.h
 
+RME BABYFACE PRO DRIVER (PROPRIETARY MODE)
+M:	Ismaïl Bahloul <[email protected]>
+L:	[email protected]
+S:	Maintained
+F:	sound/usb/babyfacepro/
+
 RNBD BLOCK DRIVERS
 M:	Md. Haris Iqbal <[email protected]>
 M:	Jack Wang <[email protected]>
sound/usb/Kconfig +17 -0
index b4588915e..51772412a 100644
@@ -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
sound/usb/Makefile +1 -1
index 2e842dcc7..5a5ef7d88 100644
@@ -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/
sound/usb/babyfacepro/Makefile new file +4 -0
new file mode 100644
index 000000000..5adc6d474
@@ -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
sound/usb/babyfacepro/babyfacepro.c new file +1523 -0

1,523 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.h new file +187 -0

187 lines not on this page · raw message

Signature · 1 line
2.56.0
#2 lore raw

Re: [PATCH v8 1/8] ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider: - [High] ALSA: usb: babyfacepro: URB and coherent DMA teardown deferred past disconnect - [Medium] ALSA…

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: URB and coherent DMA teardown deferred past disconnect
- [Medium] ALSA: usb: babyfacepro: validate endpoint types in babyface_stream_start()
- [Medium] ALSA: usb: babyfacepro: stop running substreams on babyface_stream_start() error
- [Medium] ALSA: usb: babyfacepro: constrain period_size and rate by packet floor in open()
- [Medium] ALSA: usb: babyfacepro: unconditional usb_enable_autosuspend() on teardown
- [Low] ALSA: usb: babyfacepro: document module parameters in kernel-parameters.txt
- [Low] ALSA: usb: babyfacepro: use per-card slot for index and id in babyface_probe() --

commit 6f9408f458b3d9ee7e3ba9156ec7fd66f87dee6c
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: add RME Babyface Pro driver core (probe, PCM stream)

This commit adds a standalone ALSA USB audio driver for the RME Babyface Pro and Babyface Pro FS in proprietary mode, handling cold-start initialization, card lifecycle, and full-duplex interrupt-URB PCM streaming.

Quoted 5 lines
sound/usb/babyfacepro/babyfacepro.c new file +0 -0
new file mode 100644
index 0000000000000..ac1414adc73c6

[ ... ]

Quoted 5 lines
sound/usb/babyfacepro/babyfacepro.c new file +5 -0
+static int babyface_stream_start(struct snd_usb_babyface *chip)
+{
+	unsigned int urbsize = chip->frame_bytes * chip->urb_frames;
+	s64 since;
+	int i, ret;

[ ... ]

Quoted 11 lines
sound/usb/babyfacepro/babyfacepro.c new file +11 -0
+	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);

[ ... ]

sound/usb/babyfacepro/babyfacepro.c line 670

Quoted 9 lines
sound/usb/babyfacepro/babyfacepro.c new file +9 -0
+	WRITE_ONCE(chip->streaming, true);
+	for (i = 0; i < chip->urbs_active; i++) {
+		ret = usb_submit_urb(chip->urbs_in[i], GFP_KERNEL);
+		if (ret < 0)
+			goto err;
+		ret = usb_submit_urb(chip->urbs_out[i], GFP_KERNEL);
+		if (ret < 0)
+			goto err;
+	}

[Severity: Medium]
As reported by Sashiko on v7 [1], babyface_stream_start() initializes chip->urbs_in[i] and chip->urbs_out[i] with usb_fill_int_urb() using hardcoded interrupt pipes for BF_EP_IN and BF_EP_OUT and submits them via usb_submit_urb() without validating the endpoint descriptors in the active alternate setting.

If a malformed or emulated USB device with VID 0x2a39 and PID 0x3fc0 exposes non-interrupt endpoints (or omits them) on interface 5 alternate settings BF_ALT_1, BF_ALT_2, or BF_ALT_3, usb_submit_urb() logs a bogus URB transfer warning and forwards the interrupt URB with a mismatched endpoint descriptor to the host controller driver.

Should babyface_stream_start() check usb_urb_ep_type_check() on the initialized URBs (or validate the endpoints after usb_set_interface()) before submitting them?

Quoted 5 lines
sound/usb/babyfacepro/babyfacepro.c new file +5 -0
+	/* 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;
+

sound/usb/babyfacepro/babyfacepro.c line 685

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c new file +10 -0
+	dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n",
+		chip->urb_frames, chip->urbs_active);
+	return 0;
+
+err:
+	dev_err_ratelimited(&chip->dev->dev, "failed to start stream: %d\n",
+			    ret);
+	babyface_stream_kill(chip);
+	return ret;
+}

[Severity: Medium]
As reported by Sashiko on v7 [2], babyface_stream_start() sets chip->streaming to true before the usb_submit_urb() loop and the blocking bf_vendor_write(chip, BF_REQ_SESSION_ARM, ...) control transfer.

Because babyface_pcm_trigger() handles SNDRV_PCM_TRIGGER_START under the PCM stream lock without taking chip->mutex:

	case SNDRV_PCM_TRIGGER_START:
		if (!READ_ONCE(chip->streaming))
			return -EPIPE;

if the other direction's substream is already in SNDRV_PCM_STATE_PREPARED and triggers start while babyface_stream_start() is in flight, it sees chip->streaming == true and enters SNDRV_PCM_STATE_RUNNING.

If usb_submit_urb() or bf_vendor_write() then fails, could the err path leave that other substream stuck in SNDRV_PCM_STATE_RUNNING with no URBs in flight because it calls babyface_stream_kill(chip) without calling babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN)?

[ ... ]

Quoted 17 lines
sound/usb/babyfacepro/babyfacepro.c new file +17 -0
+static unsigned int bf_urb_frames(struct snd_usb_babyface *chip,
+				  const struct bf_rate *r,
+				  unsigned int period)
+{
+	unsigned int pkt = r->min_fpu;
+	unsigned int hi = rounddown(chip->frames_per_urb, pkt);
+	unsigned int lo = roundup(max(chip->urb_frames_min, pkt), pkt);
+	unsigned int urb;
+
+	if (!hi || lo > hi || period < pkt)
+		return 0;
+	urb = rounddown(period / DIV_ROUND_UP(period, hi), pkt);
+	if (bf_split_period(r, period))
+		urb = rounddown(urb / 2, pkt);
+	urb = clamp(urb, lo, hi);
+	return urb <= period ? urb : 0;
+}

[ ... ]

Quoted 19 lines
sound/usb/babyfacepro/babyfacepro.c new file +19 -0
+static int babyface_pcm_open(struct snd_pcm_substream *subs)
+{
+	struct snd_usb_babyface *chip = snd_pcm_substream_chip(subs);
+	struct snd_pcm_runtime *rt = subs->runtime;
+	int ret;
+
+	rt->hw = babyface_pcm_hw;
+	ret = snd_pcm_hw_constraint_list(rt, 0, SNDRV_PCM_HW_PARAM_RATE,
+					 &bf_rates_constraint);
+	if (ret < 0)
+		return ret;
+	/* The stream is 24-bit audio in a 32-bit container, left-justified
+	 * (the device word carries the sample in bits 31-8).  Declare the
+	 * valid-bit width so user-space knows to ignore the low 8 bits.
+	 */
+	ret = snd_pcm_hw_constraint_msbits(rt, 0, 32, 24);
+	if (ret < 0)
+		return ret;
+	/* A period must span at least one URB, so a completion crosses at

sound/usb/babyfacepro/babyfacepro.c line 887

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c new file +10 -0
+	 * most one period boundary.  The URB size follows the period down to
+	 * urb_frames_min; while another application holds the session, its
+	 * period (in whole URBs) is the floor.  Constrain in frames (not
+	 * bytes) so the minimum period does not balloon at low channel
+	 * counts.
+	 */
+	ret = snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+					   chip->urb_frames_min, 1 << 18);
+	if (ret < 0)
+		return ret;

[Severity: Medium]
In babyface_probe(), chip->frames_per_urb and chip->urb_frames_min can be configured down to 8 (such as urb_frames_min=16, which is documented as the low-latency floor):

	chip->frames_per_urb = clamp(frames_per_urb, 8, 1024) & ~7;
	chip->urb_frames_min = min_t(unsigned int,
				     clamp(urb_frames_min, 8, 1024) & ~7,
				     chip->frames_per_urb);

However, bf_urb_frames() requires the period and URB size to be at least r->min_fpu frames (16 frames at BF_ALT_2 and 32 frames at BF_ALT_3) and requires rounddown(chip->frames_per_urb, r->min_fpu) >= roundup(max(chip->urb_frames_min, r->min_fpu), r->min_fpu), returning 0 otherwise.

Because babyface_pcm_open() only bounds SNDRV_PCM_HW_PARAM_PERIOD_SIZE by chip->urb_frames_min and offers all rates in bf_rates_constraint, won't ALSA parameter refinement allow combinations (for example period_size=16 at 192 kHz when urb_frames_min=16) that babyface_pcm_hw_params() then rejects with -EINVAL?

[ ... ]

Quoted 6 lines
sound/usb/babyfacepro/babyfacepro.c new file +6 -0
+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;
+

sound/usb/babyfacepro/babyfacepro.c line 1232

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c new file +10 -0
+module_param_array(index, int, NULL, 0444);
+MODULE_PARM_DESC(index, "Index value for the Babyface Pro sound card.");
+module_param_array(id, charp, NULL, 0444);
+MODULE_PARM_DESC(id, "ID string for the Babyface Pro sound card.");
+module_param(frames_per_urb, int, 0444);
+MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
+module_param(urb_frames_min, int, 0444);
+MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
+module_param(nurbs, int, 0444);
+MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");

[Severity: Low]
As reported by Sashiko on v7 [3], this adds five module parameters (index, id, frames_per_urb, urb_frames_min, and nurbs) that can also be set on the kernel command line as snd-usb-babyface-pro.<param>=. Should these parameters be documented in Documentation/admin-guide/kernel-parameters.txt?

Quoted 25 lines
sound/usb/babyfacepro/babyfacepro.c new file +25 -0
+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

sound/usb/babyfacepro/babyfacepro.c line 1268

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c new file +10 -0
+			 * on the probe error path via snd_card_free().
+			 */
+			if (chip->buf_in && chip->dma_in)
+				usb_free_coherent(chip->dev, urbsize,
+						  chip->buf_in[i], chip->dma_in[i]);
+			if (chip->buf_out && chip->dma_out)
+				usb_free_coherent(chip->dev, urbsize,
+						  chip->buf_out[i], chip->dma_out[i]);
+		}
+	}

[Severity: High]
In babyface_disconnect(), the driver stops the stream and calls snd_card_free_when_closed(chip->card), deferring babyface_private_free() until user-space closes the last ALSA file descriptor.

If the USB host controller is unbound or hot-unplugged while an audio application holds the ALSA PCM device open, usb_remove_hcd() runs babyface_disconnect() and then calls hcd_buffer_destroy(), which destroys hcd->pool[] and sets the pool pointers to NULL.

When user-space later closes the ALSA file descriptor and babyface_private_free() runs after disconnect has returned, won't calling usb_kill_urb() and usb_free_coherent() violate the USB lifecycle contract and trigger a NULL pointer dereference in dma_pool_free() (when urbsize <= 2048) or free DMA memory on a torn-down HCD device?

Quoted 19 lines
sound/usb/babyfacepro/babyfacepro.c new file +19 -0
+	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;
+

sound/usb/babyfacepro/babyfacepro.c line 1297

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c new file +10 -0
+	if (intf->cur_altsetting->desc.bInterfaceNumber != BF_IFACE) {
+		/* Only the proprietary audio interface is ours; the MIDI
+		 * (standard class) and bulk interfaces stay unclaimed so
+		 * snd-usb-audio can take the MIDI one.
+		 */
+		return -ENODEV;
+	}
+
+	err = snd_card_new(&intf->dev, index[0], id[0], THIS_MODULE,
+			   sizeof(*chip), &card);

[Severity: Low]
As reported by Sashiko on v7 [4], index and id are declared as SNDRV_CARDS-element module_param_array arrays, but babyface_probe() always passes index[0] and id[0] to snd_card_new().

If two devices are connected (or a device is re-probed before snd_card_free_when_closed() finishes releasing the previous card) when a non-default index or id parameter is specified, won't snd_card_new() fail trying to claim the same slot or ID, while ignoring elements 1 through SNDRV_CARDS - 1?

Quoted 16 lines
sound/usb/babyfacepro/babyfacepro.c new file +16 -0
+	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);

[ ... ]

Quoted 13 lines
sound/usb/babyfacepro/babyfacepro.c new file +13 -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)
+{

[ ... ]

sound/usb/babyfacepro/babyfacepro.c line 1481

Quoted 7 lines
sound/usb/babyfacepro/babyfacepro.c new file +7 -0
+	cancel_work_sync(&chip->stream_work);
+	/* Balance the probe()-time usb_disable_autosuspend(): the usb_device
+	 * outlives this interface claim (a usbfs detach re-probes without
+	 * the physical device ever disconnecting), so leaving autosuspend
+	 * disabled here would wrongly affect whatever claims the device next.
+	 */
+	usb_enable_autosuspend(chip->dev);

[Severity: Medium]
usb_disable_autosuspend() and usb_enable_autosuspend() wrap pm_runtime_forbid() and pm_runtime_allow() on struct usb_device and are not reference-counted; USB core initializes non-hub USB devices with autosuspend disabled (power/control set to on) by default, and because babyface_driver does not set supports_autosuspend, USB core already keeps the bound interface active while claimed.

When babyface_probe() fails or babyface_disconnect() unbinds interface 5, won't calling usb_enable_autosuspend(chip->dev) unconditionally flip the parent usb_device's power/control setting to auto and allow the composite device (including its MIDI interface claimed by snd-usb-audio) to autosuspend unexpectedly?

[1] https://lore.kernel.org/r/[email protected]
[2] https://lore.kernel.org/r/[email protected]
[3] https://lore.kernel.org/r/[email protected]
[4] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#3 lore raw

PATCH v8 2/8 ALSA: usb: babyfacepro: add output masters and crosspoint routing

In reply to Ismaïl Bahloul

Add the first mixer controls: the six output masters (volume + mute) and the 6x14 crosspoint matrix. The device has no readback for any of them, so the driver keeps the state host-side and the controls read that cache.

The probe's cold init clears the mixer registers, so probe writes a power-on default: every playback channel into every output at 0 dB, the hardware inputs off until a mixer raises them, and the two analog outputs (AN1/2, PH3/4) at -20 dB, the level the hardware DIM button uses. AN1/2 is the first block of the one crosspoint map (0x0000), not a separate "low map" beside a standard one. A volume change leaves a muted output muted.

The masters count the 8-bit register's 0.5 dB steps over the front panel's range: 0 is mute, 1 is -90 dB and BF_MASTER_STEP_MAX is +6 dB. The control value is that step, so no log table converts it, and the 16-bit companion register is derived from the same step.

A userspace usbfs claim (PipeWire, a libusb mixer daemon) detaches and re-probes the interface, and the cold init of the re-probe clears the mixer registers; alsactl restores the user's settings once the card is registered again.

---
 sound/usb/babyfacepro/Makefile          |   2 +-
 sound/usb/babyfacepro/babyfacepro-ctl.c | 656 ++++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     |  60 ++-
 sound/usb/babyfacepro/babyfacepro.h     | 124 ++++-
 4 files changed, 839 insertions(+), 3 deletions(-)
 create mode 100644 sound/usb/babyfacepro/babyfacepro-ctl.c
sound/usb/babyfacepro/Makefile +1 -1
index 5adc6d474..a50647a06 100644
@@ -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
sound/usb/babyfacepro/babyfacepro-ctl.c new file +656 -0

656 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.c +59 -1

84 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.h +123 -1

153 lines not on this page · raw message

Signature · 1 line
2.56.0
#4 lore raw

Re: [PATCH v8 2/8] ALSA: usb: babyfacepro: add output masters and crosspoint routing

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] ALSA: usb: babyfacepro: out-of-bounds value[1] write in bf_mute_get() - [Medium] ALSA: usb…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: out-of-bounds value[1] write in bf_mute_get()
- [Medium] ALSA: usb: babyfacepro: missing shutdown check in mixer put callbacks --

commit a0ae4df204731e5d864689d9a4a9ee415579f1a2
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: babyfacepro: add output masters and crosspoint routing

This commit adds ALSA mixer controls for the six output masters (volume and mute) and the 6x14 crosspoint routing matrix, caching their state on the host and initializing safe power-on defaults during probe.

Quoted 5 lines
sound/usb/babyfacepro/babyfacepro-ctl.c new file +0 -0
new file mode 100644
index 0000000000000..942d4732cbe07

[ ... ]

Quoted 14 lines
sound/usb/babyfacepro/babyfacepro-ctl.c new file +14 -0
+static int bf_master_put(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = bf_master_out[kctl->private_value];
+	long nl = ucontrol->value.integer.value[0];
+	long nr = ucontrol->value.integer.value[1];
+	u16 l, r, wire_l, wire_r, flag;
+	int ret = 0;
+
+	/* Reject anything outside the declared range, so the registers and
+	 * the cache stay in spec (the ALSA core only enforces this with
+	 * CONFIG_SND_CTL_INPUT_VALIDATION).  Check before narrowing:
+	 * .value.integer.value[] is a long.

sound/usb/babyfacepro/babyfacepro-ctl.c line 351

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c new file +10 -0
+	 */
+	if (nl < 0 || nl > BF_MASTER_STEP_MAX ||
+	    nr < 0 || nr > BF_MASTER_STEP_MAX)
+		return -EINVAL;
+	l = nl;
+	r = nr;
+
+	guard(mutex)(&chip->mutex);
+	if (l == chip->master[out][0] && r == chip->master[out][1])
+		return 0;

[Severity: Medium]
As reported by Sashiko on v7 [1], should bf_master_put() (along with bf_mute_put() and bf_xpoint_put() below) check chip->shutdown and return -ENODEV after acquiring chip->mutex, matching babyface_pcm_hw_params(), babyface_pcm_prepare(), and babyface_stream_work()?

In babyface_disconnect(), chip->shutdown is set to true and chip->mutex is acquired and released to fence in-flight operations before calling usb_enable_autosuspend(chip->dev) and snd_card_disconnect(chip->card):

sound/usb/babyfacepro/babyfacepro.c:babyface_disconnect() {
    ...
	chip->shutdown = true;
	scoped_guard(mutex, &chip->mutex) {
		if (chip->streaming)
			babyface_stream_kill(chip);
	}
	cancel_work_sync(&chip->stream_work);
	usb_enable_autosuspend(chip->dev);
    ...
	snd_card_disconnect(chip->card);
}

Without checking chip->shutdown under chip->mutex, a concurrent SNDRV_CTL_IOCTL_ELEM_WRITE ioctl in bf_master_put(), bf_mute_put(), or bf_xpoint_put() can proceed to issue synchronous USB control transfers via bf_vendor_write() or bf_xpoint_write() after the disconnect fence has passed and USB autosuspend has been re-enabled (and on !CONFIG_PM builds, where snd_power_sync_ref() in snd_card_disconnect() is a no-op, while babyface_disconnect() is completing).

Quoted 45 lines
sound/usb/babyfacepro/babyfacepro-ctl.c new file +45 -0
+
+	wire_l = chip->muted[out] ? 0 : l;
+	wire_r = chip->muted[out] ? 0 : r;
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+
+	/* Both registers, as TotalMix writes them: the 8-bit is the analog
+	 * level, the 16-bit the digital one.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_l),
+			      BF_REG_MASTER_8 + 2 * out);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_r),
+			      BF_REG_MASTER_8 + 2 * out + 1);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_l),
+			      (BF_REG_MASTER_16 + 2 * out) | flag);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_r),
+			      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	if (ret < 0)
+		return ret;
+
+	chip->master[out][0] = l;
+	chip->master[out][1] = r;
+	return 1;
+}
+
+static int bf_mute_info(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	/* One switch for the pair: bf_mute_put() writes both channels
+	 * together, so a count of 2 would silently ignore value[1].
+	 */
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_mute_get(struct snd_kcontrol *kctl,

sound/usb/babyfacepro/babyfacepro-ctl.c line 406

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c new file +10 -0
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = bf_master_out[kctl->private_value];
+
+	/* ALSA convention: 1 = enabled (sound on) = not muted. */
+	ucontrol->value.integer.value[0] = !chip->muted[out];
+	ucontrol->value.integer.value[1] = !chip->muted[out];
+	return 0;
+}

[Severity: High]
Since bf_mute_info() declares uinfo->count = 1, doesn't writing to ucontrol->value.integer.value[1] in bf_mute_get() write past the declared element count?

When CONFIG_SND_CTL_DEBUG is enabled, snd_ctl_elem_read() poisons bytes in ucontrol->value past info.count and checks them in sanity_check_elem_value() after kctl->get(), so overwriting ucontrol->value.integer.value[1] causes every read of the Playback Switch controls to log an error and fail with -EINVAL, and causes snd_ctl_put_verify() to report a false mismatch whenever the output is unmuted. Even without CONFIG_SND_CTL_DEBUG, setting value[1] breaks userspace comparisons via snd_ctl_elem_value_compare() (such as the ALSA mixer-test kselftest).

[1] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#5 lore raw

PATCH v8 3/8 ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim

In reply to Ismaïl Bahloul

Add the input side of the mixer: phantom power and pad, the instrument ref level, the four preamp gains, and phase invert, stereo split and trim. The mic gain byte is packed: bits 0-4 count 3 dB steps and bits 5-7 hold the 0-2 dB remainder. Phase, split and trim have no registers of their own: the device implements them on the AN1/2 crosspoint registers, and trim writes the trim alone on the master curve (0x2000 = 0 dB).

bf_state_apply_flags() re-applies phase, split and trim on top of a restored crosspoint state. A failed preamp write leaves the cached state as it was.

---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 624 ++++++++++++++++++++++++
 sound/usb/babyfacepro/babyfacepro.c     | 111 ++++-
 sound/usb/babyfacepro/babyfacepro.h     |  79 +++
 3 files changed, 813 insertions(+), 1 deletion(-)
sound/usb/babyfacepro/babyfacepro-ctl.c +624 -0
index 942d4732c..fe6266ea1 100644
@@ -654,3 +654,627 @@ int babyface_create_masters(struct snd_usb_babyface *chip)
 
 	return 0;
 }
+
+int bf_preamp_state_write(struct snd_usb_babyface *chip)
+{
+	int ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, chip->preamp, BF_REG_PREAMP);
+	if (ret < 0)
+		return ret;
+	/* Boost's 0x21 commit value (0x0003) is NOT a persisted register
+	 * bit - PROTOCOL.md's "Ref level" section found it only in the
+	 * one-shot 0x21 value alongside the 0x17 state write, so it has
+	 * to be re-sent alongside EVERY preamp write (phantom/PAD toggles
+	 * included), or Boost would silently degrade to plain -10dBV the
+	 * next time anything else touches this shared byte.
+	 */
+	return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT,
+			       chip->ref_level == BF_REF_LEVEL_BOOST ?
+			       0x0003 : 0x0000, 0x0000);
+}
+
+/* Phase (polarity) invert (AN1-4 only, PROTOCOL.md "Phase toggle",
+ * hardware-verified 2026-08-23): NEGATE (bitwise NOT, not two's
+ * complement) the L crosspoint register on every output pair's
+ * standard map, plus the AN1/2 low-map shadow specifically (the same
+ * single low-map register set CUE/mute/solo already use for that
+ * monitor bus - see this driver's own bf_ms_put for the address
+ * pattern). `chip->xpoint[out][mic][0]` is deliberately left holding
+ * the PLAIN value the user actually set - only the value WRITTEN to
+ * hardware is negated - so the crosspoint control's own readback still
+ * reports the real fader position while phase is engaged.
+ *
+ * KNOWN LIMITATION, same class TuxMix's own USB backend already has
+ * in `usb.rs::set_phase` (not fixed there either, as of this writing):
+ * this negates the CURRENT register value once, at toggle time. A
+ * later `bf_xpoint_put` on the same [out][mic] slot (i.e. the user
+ * drags that fader again while phase is engaged) writes the plain
+ * value, silently un-inverting phase until the user re-toggles it.
+ * Making the crosspoint hot path itself phase-aware would close this
+ * properly, but touches every one of the 84 crosspoint controls'
+ * write path - out of scope for this pass; flagged rather than
+ * silently shipped.
+ */
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert)
+{
+	const struct bf_source *s = &bf_sources[mic];
+	int out, ret;
+	u16 flag;
+
+	for (out = 0; out < 6; out++) {
+		u16 plain = chip->xpoint[out][mic][0];
+		u16 value = invert ? (u16)~plain : plain;
+
+		/* The AN1/2 block is written without the rotating bits, as
+		 * bf_xpoint_write() does.
+		 */
+		flag = 0;
+		if (out) {
+			flag = bf_flag_cycle[chip->flag_cnt];
+			chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		}
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+				      (BF_REG_XPOINT_BASE_L +
+				       BF_REG_XPOINT_STRIDE * out + s->idx_l) |
+				      flag);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+static int bf_phase_info(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_phase_get(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->phase[mic];
+	return 0;
+}
+
+static int bf_phase_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+	bool invert = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	if (invert == chip->phase[mic])
+		return 0;
+	ret = bf_phase_apply(chip, mic, invert);
+	if (ret < 0)
+		return ret;
+	chip->phase[mic] = invert;
+	return 1;
+}
+
+/* Stereo split (PROTOCOL.md "Stereo split", cap_ctrl3.pcap, hardware-
+ * verified): a playback pair's signal into the AN1/2 monitor bus goes
+ * hard-split (L=0x2000/R=0x0000, "split-mono").  Only reaches the AN1/2
+ * destination (its block of the map), the same scope as CUE/mute/solo.
+ * `chip->xpoint[][]` is deliberately left untouched, same reasoning as
+ * Phase.
+ *
+ * Joining the pair again puts back the pair's own AN1/2 crosspoint from
+ * the cache.  It used to write L=R=0x1000, read as "-6 dB per side",
+ * which issue #31 found to be 6 dB below what TotalMix FX writes for a
+ * joined pair at 0 dB (0x2000 per side), and which also dropped
+ * whatever level the user had set on that crosspoint.
+ */
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split)
+{
+	const struct bf_source *s = &bf_sources[8 + pb];
+	u16 l = split ? 0x2000 : chip->xpoint[0][8 + pb][0];
+	u16 r = split ? 0x0000 : chip->xpoint[0][8 + pb][1];
+	int ret;
+
+	/* The AN1/2 block.  The same values also went to a "standard map"
+	 * half at 0x0068, which is AS1/2's submix, not part of AN1/2: it
+	 * overwrote AS1/2's own crosspoint for this playback pair.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			      BF_REG_XPOINT_BASE_L + s->idx_l);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			       BF_REG_XPOINT_BASE_R + s->idx_r);
+}
+
+static int bf_split_get(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int pb = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->split[pb];
+	return 0;
+}
+
+static int bf_split_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int pb = kctl->private_value;
+	bool split = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	if (split == chip->split[pb])
+		return 0;
+	ret = bf_split_apply(chip, pb, split);
+	if (ret < 0)
+		return ret;
+	chip->split[pb] = split;
+	return 1;
+}
+
+/* Input Trim (T button, AN1-4): PROTOCOL.md "Trim (T) write for the
+ * AN1/2 pair" (cap_trim2/3/4.pcap, hardware-verified) - the analog
+ * input's own gain-trim, applied through the crosspoint registers
+ * exactly like a fader (there is no separate trim register).  It
+ * writes the trim alone on the MASTER curve (0x2000 = 0 dB,
+ * `bf_master_16bit`) into the AN1/2 block, all 4 registers of the pair
+ * (both AN1+AN2 or both AN3+AN4, matching the vendor software's
+ * linked-strip behaviour); `mic` may be either channel of the pair, and
+ * the base is derived (`mic & ~1`) so the write always lands on the
+ * correct pair's registers regardless of which channel's control
+ * triggered it.  Destination is always the AN1/2 monitor bus, the same
+ * scope the MS-processor and CUE writes have, and the same one the
+ * vendor software's Trim reaches.
+ *
+ * It also wrote the fader and the trim summed, on the fader curve, to a
+ * "standard map" at 0x0068.  That block is AS1/2's submix (see
+ * bf_xpoint_write), so those writes never reached AN1/2: they only
+ * overwrote AS1/2's own crosspoints for these inputs, and are gone.
+ *
+ * Trim is a genuinely SHARED value per pair on real hardware (one
+ * write always touches both channels' registers) but is exposed as 2
+ * per-channel ALSA controls, one per input strip. `bf_trim_put` keeps
+ * the pair's two cache entries equal and notifies the sibling control,
+ * so the cache never claims a per-channel split the hardware cannot
+ * represent - and `bf_state_apply_flags`, which replays the pair from
+ * its even index, always replays the value that is actually on the
+ * wire.
+ */
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2)
+{
+	int base = mic & ~1;
+	int sib = base + 1;
+	const struct bf_source *sb = &bf_sources[base];
+	const struct bf_source *ss = &bf_sources[sib];
+	u16 trim_raw = bf_master_16bit(trim_db2);
+	int ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_L + sb->idx_l);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_R + sb->idx_r);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_L + ss->idx_l);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			       BF_REG_LOWMAP_BASE_R + ss->idx_r);
+}
+
+/* Trim's control value is dB as well, -65..+6. */
+static const DECLARE_TLV_DB_SCALE(bf_trim_tlv, -6500, 100, 0);
+
+static int bf_trim_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = -65;
+	uinfo->value.integer.max = 6;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_trim_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->trim[mic];
+	return 0;
+}
+
+static int bf_trim_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+	int sib = mic ^ 1;
+	int db = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	if (db < -65 || db > 6)
+		return -EINVAL;
+
+	scoped_guard(mutex, &chip->mutex) {
+		if (db == chip->trim[mic])
+			return 0;
+		ret = bf_trim_apply(chip, mic, db * 2);
+		if (ret < 0)
+			return ret;
+		/* One register per pair on the wire, so both channels of the
+		 * pair really did change: mirror the cache (the state restore
+		 * replays the pair from the even index) and tell user space
+		 * about the sibling control.
+		 */
+		chip->trim[mic] = db;
+		chip->trim[sib] = db;
+		ret = 1;
+	}
+	if (ret == 1 && chip->trim_kctl[sib])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->trim_kctl[sib]->id);
+	return ret;
+}
+
+/* Phase, stereo split and input trim - registered together since all
+ * three are per-input-strip controls layered on the same AN1/2 monitor
+ * bus crosspoint registers the matrix in babyface_create_xpoints()
+ * already created controls for.
+ */
+int babyface_create_trim(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int src, err;
+
+	for (src = 0; src < 4; src++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Phase Switch",
+			.index = src,
+			.info = bf_phase_info,
+			.get = bf_phase_get,
+			.put = bf_phase_put,
+			.private_value = src,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		strscpy(kctl->id.name, bf_sources[src].name, sizeof(kctl->id.name));
+		strlcat(kctl->id.name, " Phase Switch", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (src = 0; src < 6; src++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Stereo Split Switch",
+			.index = src,
+			.info = bf_phase_info, /* plain boolean, same shape */
+			.get = bf_split_get,
+			.put = bf_split_put,
+			.private_value = src,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		strscpy(kctl->id.name, bf_sources[8 + src].name, sizeof(kctl->id.name));
+		strlcat(kctl->id.name, " Stereo Split Switch", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (src = 0; src < 4; src++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Trim Volume",
+			.index = src,
+			.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+				  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+			.info = bf_trim_info,
+			.get = bf_trim_get,
+			.put = bf_trim_put,
+			.tlv.p = bf_trim_tlv,
+			.private_value = src,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		strscpy(kctl->id.name, bf_sources[src].name, sizeof(kctl->id.name));
+		strlcat(kctl->id.name, " Trim Volume", sizeof(kctl->id.name));
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+		chip->trim_kctl[src] = kctl;
+	}
+	return 0;
+}
+
+static int bf_bool_info(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_phantom_get(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] =
+		!!(chip->preamp & kctl->private_value);
+	return 0;
+}
+
+static int bf_phantom_put(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	u16 bit = kctl->private_value;
+	bool on = ucontrol->value.integer.value[0];
+	bool cur;
+	u16 old;
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	cur = !!(chip->preamp & bit);
+	old = chip->preamp;
+	if (on == cur)
+		return 0;
+	chip->preamp = on ? (chip->preamp | bit) : (chip->preamp & ~bit);
+	ret = bf_preamp_state_write(chip);
+	if (ret < 0) {
+		/* Do not leave the cache claiming a state the device
+		 * never took.
+		 */
+		chip->preamp = old;
+		return ret;
+	}
+	return 1;
+}
+
+/* Ref Level (Instr 3/4) - see the constants' own comment in the
+ * header. A single shared 3-state switch, not per-channel (the
+ * protocol has no independent bits for IN3 vs IN4).
+ */
+static const char *const bf_reflevel_texts[] = {
+	"+4dBu", "-10dBV", "Boost", NULL
+};
+
+static int bf_reflevel_info(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 3, bf_reflevel_texts);
+}
+
+static int bf_reflevel_get(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = chip->ref_level;
+	return 0;
+}
+
+static int bf_reflevel_put(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	unsigned int item = ucontrol->value.enumerated.item[0];
+	u16 old_preamp;
+	int old_ref_level;
+	int ret = 0;
+
+	if (item > BF_REF_LEVEL_BOOST)
+		return -EINVAL;
+
+	guard(mutex)(&chip->mutex);
+	if ((int)item == chip->ref_level)
+		return 0;
+	old_preamp = chip->preamp;
+	old_ref_level = chip->ref_level;
+	chip->preamp = (chip->preamp & ~BF_PREAMP_REF_MASK) |
+		       (item == BF_REF_LEVEL_4DBU ? BF_PREAMP_REF_4DBU : 0);
+	chip->ref_level = item;
+	ret = bf_preamp_state_write(chip);
+	if (ret < 0) {
+		chip->preamp = old_preamp;
+		chip->ref_level = old_ref_level;
+		return ret;
+	}
+	return 1;
+}
+
+/* Gain scales.
+ *
+ * The mic preamps (AN1/2) span 0-65 dB in 1 dB steps, carried in a
+ * packed byte rather than a plain count:
+ *
+ *	coarse = min(db / 3, 20)	bits 0-4, 3 dB per step
+ *	fine   = db - 3 * coarse	bits 5-7, the 0-2 dB remainder
+ *	value  = (fine << 5) | coarse
+ *
+ * Above 60 dB coarse saturates at 20 and fine continues 3, 4, 5, so
+ * 65 dB is 0xb4.  Decoded from USBPcap captures of TotalMix on
+ * Windows (bbf-gain2/3/4.pcap, 48 writes, all matching).
+ *
+ * Bits 5-7 were previously read as a transaction counter and written
+ * with a rotating 0x20/0x00/0x40, which both discarded the fine part
+ * of the setting and applied 0-2 dB of error depending on where the
+ * rotation happened to be.
+ *
+ * The Hi-Z instrument inputs (AN3/4) are not packed: the value is the
+ * gain in 0.5 dB units, 0-9 dB over 0-18.
+ */
+int bf_gain_max_db(int mic)
+{
+	return mic < 2 ? BF_GAIN_MAX_DB : 9;
+}
+
+u8 bf_gain_raw(int mic, int db)
+{
+	int coarse, fine;
+
+	if (mic >= 2)
+		return db * 2;
+	coarse = min(db / 3, BF_GAIN_COARSE_MAX);
+	fine = db - 3 * coarse;
+	return (u8)((fine << BF_GAIN_FINE_SHIFT) | coarse);
+}
+
+/* The preamp control's value already IS the gain in dB (0..65 for the mic
+ * inputs, 0..9 for the instrument ones), and the hardware really does
+ * resolve every one of those steps - bf_gain_raw() packs it into the
+ * register's coarse and fine fields.
+ */
+static const DECLARE_TLV_DB_SCALE(bf_gain_tlv, 0, 100, 0);
+
+static int bf_gain_info(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = bf_gain_max_db(kctl->private_value);
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_gain_get(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+
+	/* chip->gain[] tracks the dB; the packed register value is derived
+	 * at write time (bf_gain_raw).
+	 */
+	ucontrol->value.integer.value[0] = chip->gain[mic];
+	return 0;
+}
+
+static int bf_gain_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int mic = kctl->private_value;
+	int db = ucontrol->value.integer.value[0];
+	u8 raw;
+	int ret = 0;
+
+	if (db < 0 || db > bf_gain_max_db(mic))
+		return -EINVAL;
+
+	guard(mutex)(&chip->mutex);
+	if (db == chip->gain[mic])
+		return 0;
+	raw = bf_gain_raw(mic, db);
+
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, (u16)raw,
+			      BF_REG_GAIN + mic);
+	if (ret < 0)
+		return ret;
+	chip->gain[mic] = db;
+	return 1;
+}
+
+/* Preamp controls: phantom power, pad, instrument ref level and the
+ * four mic/instrument gains.  Registered separately from the output
+ * masters (babyface_create_masters()) since they cover a different
+ * part of the signal path (mic input, not output routing).
+ */
+int babyface_create_preamp(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int i, err;
+
+	/* The control name is the type and .index is the channel (0 =
+	 * Mic 1/AN1, 1 = Mic 2/AN2), the convention the companion mixer
+	 * and existing asound.state files use.
+	 */
+	for (i = 0; i < 2; i++) {
+		u16 bit = i == 0 ? BF_PREAMP_48V_MIC1 : BF_PREAMP_48V_MIC2;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Phantom Power Mic 1",
+			.index = i,
+			.info = bf_bool_info,
+			.get = bf_phantom_get,
+			.put = bf_phantom_put,
+			.private_value = bit,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (i = 0; i < 2; i++) {
+		u16 bit = i == 0 ? BF_PREAMP_PAD_MIC1 : BF_PREAMP_PAD_MIC2;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Pad Mic 1",
+			.index = i,
+			.info = bf_bool_info,
+			.get = bf_phantom_get,
+			.put = bf_phantom_put,
+			.private_value = bit,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Instrument Ref Level",
+		.info = bf_reflevel_info,
+		.get = bf_reflevel_get,
+		.put = bf_reflevel_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	for (i = 0; i < 4; i++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Mic 1 Capture Volume",
+			.index = i,
+			.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+				  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+			.info = bf_gain_info,
+			.get = bf_gain_get,
+			.put = bf_gain_put,
+			.tlv.p = bf_gain_tlv,
+			.private_value = i,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+	return 0;
+}
sound/usb/babyfacepro/babyfacepro.c +110 -1

147 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.h +79 -0

103 lines not on this page · raw message

Signature · 1 line
2.56.0
#6 lore raw

Re: [PATCH v8 3/8] ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [High] ALSA: usb: babyfacepro: bf_trim_apply() and bf_phase_apply() clobber xpoints - [Medium] ALSA: usb…

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: bf_trim_apply() and bf_phase_apply() clobber xpoints
- [Medium] ALSA: usb: babyfacepro: hardcoded Mic 1 names in babyface_create_preamp()
- [Low] ALSA: usb: babyfacepro: unchecked snd_ctl_new1() in babyface_create_preamp()
- [Low] ALSA: usb: babyfacepro: bf_phase_info() and bf_bool_info() duplicate code --

commit 82ef805ee77606164dc3814bf736734ca60721d5
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: babyfacepro: add mic preamp, phantom/pad and input trim

This commit adds ALSA mixer controls for the input side of the RME Babyface Pro, including phantom power, pad, instrument reference level, mic/instrument preamp gains, phase invert, stereo split, and input trim.

Quoted 5 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
index 942d4732cbe07..fe6266ea118e6 100644
@@ -654,3 +654,627 @@ int babyface_create_masters(struct snd_usb_babyface *chip)

[ ... ]

Quoted 27 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +27 -0
+int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert)
+{
+	const struct bf_source *s = &bf_sources[mic];
+	int out, ret;
+	u16 flag;
+
+	for (out = 0; out < 6; out++) {
+		u16 plain = chip->xpoint[out][mic][0];
+		u16 value = invert ? (u16)~plain : plain;
+
+		/* The AN1/2 block is written without the rotating bits, as
+		 * bf_xpoint_write() does.
+		 */
+		flag = 0;
+		if (out) {
+			flag = bf_flag_cycle[chip->flag_cnt];
+			chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		}
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, value,
+				      (BF_REG_XPOINT_BASE_L +
+				       BF_REG_XPOINT_STRIDE * out + s->idx_l) |
+				      flag);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}

[ ... ]

Quoted 18 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +18 -0
+int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split)
+{
+	const struct bf_source *s = &bf_sources[8 + pb];
+	u16 l = split ? 0x2000 : chip->xpoint[0][8 + pb][0];
+	u16 r = split ? 0x0000 : chip->xpoint[0][8 + pb][1];
+	int ret;
+
+	/* The AN1/2 block.  The same values also went to a "standard map"
+	 * half at 0x0068, which is AS1/2's submix, not part of AN1/2: it
+	 * overwrote AS1/2's own crosspoint for this playback pair.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l,
+			      BF_REG_XPOINT_BASE_L + s->idx_l);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, r,
+			       BF_REG_XPOINT_BASE_R + s->idx_r);
+}

[ ... ]

Quoted 24 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +24 -0
+int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2)
+{
+	int base = mic & ~1;
+	int sib = base + 1;
+	const struct bf_source *sb = &bf_sources[base];
+	const struct bf_source *ss = &bf_sources[sib];
+	u16 trim_raw = bf_master_16bit(trim_db2);
+	int ret;
+
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_L + sb->idx_l);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_R + sb->idx_r);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			      BF_REG_LOWMAP_BASE_L + ss->idx_l);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, trim_raw,
+			       BF_REG_LOWMAP_BASE_R + ss->idx_r);
+}

[ ... ]

Quoted 7 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +7 -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;

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_masters(struct snd_usb_babyface *chip) · line 916

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+
+	if (db < -65 || db > 6)
+		return -EINVAL;
+
+	scoped_guard(mutex, &chip->mutex) {
+		if (db == chip->trim[mic])
+			return 0;
+		ret = bf_trim_apply(chip, mic, db * 2);
+		if (ret < 0)
+			return ret;

[Severity: High]
As reported by Sashiko on v7 [1], does bf_trim_apply() clobber the AN1/2 crosspoint state and unmute live analog inputs into the main outputs?

In sound/usb/babyfacepro/babyfacepro.h, BF_REG_LOWMAP_BASE_L (0x0000) and BF_REG_LOWMAP_BASE_R (0x001a) are the exact same register addresses as BF_REG_XPOINT_BASE_L and BF_REG_XPOINT_BASE_R for output pair 0 (the AN1/2 main output). In babyface_write_default_mixer(), analog inputs AN1-4 have chip->xpoint[0][0..3][0/1] initialized to 0 so live microphones are not routed directly into the speakers.

When bf_trim_put() calls bf_trim_apply(), it converts trim_db2 via bf_master_16bit() (clamped to [1, 0x4000] where 0 dB is 0x2000, compared to BF_FADER_0DB = 0x16a0 on the crosspoint curve) and writes trim_raw directly to all four L and R crosspoint registers of the input pair (base and sib) on AN1/2, ignoring chip->xpoint[0][base/sib][0/1]. Touching Trim Volume at all (even at -65 dB, or returning it to 0 dB which writes 0x2000 instead of restoring chip->xpoint[0][base/sib][0/1]) unconditionally routes both inputs of the pair to both L and R of AN1/2, overriding any muted state, fader level, or panning.

Because chip->xpoint[0][base/sib][0/1] remains 0 in the cache while the hardware holds 0x2000, a subsequent write of 0 to the crosspoint control hits the early return in bf_xpoint_put():

sound/usb/babyfacepro/babyfacepro-ctl.c:bf_xpoint_put() {
	guard(mutex)(&chip->mutex);
	if (l == chip->xpoint[out][src][0] && r == chip->xpoint[out][src][1])
		return 0;

	ret = bf_xpoint_write(chip, out, src, l, r);
}

and cannot re-mute the hardware.

Additionally, because bf_phase_apply(), bf_split_apply(), bf_trim_apply(), and bf_xpoint_put() write to the shared crosspoint registers without combining their state:
- bf_trim_apply() (both in bf_trim_put() and in bf_state_apply_flags() where the trim loop runs after the phase loop) overwrites the negated out = 0 L register written by bf_phase_apply().
- bf_phase_apply() only negates BF_REG_XPOINT_BASE_L (leaving R un-inverted, and writing ~0 = 0xffff when plain == 0).
- Any subsequent bf_xpoint_put() writes plain l/r values, silently wiping out active phase, stereo split, or trim settings in hardware while their cached control states remain active.

[ ... ]

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_masters(struct snd_usb_babyface *chip) · line 1013

Quoted 9 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +9 -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;
+}

[Severity: Low]
This isn't a bug, but bf_bool_info() is identical to bf_phase_info() added earlier in sound/usb/babyfacepro/babyfacepro-ctl.c (and to the existing bf_mute_info()). Could these controls use the standard ALSA helper snd_ctl_boolean_mono_info() from include/sound/control.h instead of duplicating the callback?

[ ... ]

Quoted 47 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +47 -0
+int babyface_create_preamp(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int i, err;
+
+	/* The control name is the type and .index is the channel (0 =
+	 * Mic 1/AN1, 1 = Mic 2/AN2), the convention the companion mixer
+	 * and existing asound.state files use.
+	 */
+	for (i = 0; i < 2; i++) {
+		u16 bit = i == 0 ? BF_PREAMP_48V_MIC1 : BF_PREAMP_48V_MIC2;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Phantom Power Mic 1",
+			.index = i,
+			.info = bf_bool_info,
+			.get = bf_phantom_get,
+			.put = bf_phantom_put,
+			.private_value = bit,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+
+	for (i = 0; i < 2; i++) {
+		u16 bit = i == 0 ? BF_PREAMP_PAD_MIC1 : BF_PREAMP_PAD_MIC2;
+
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Pad Mic 1",
+			.index = i,
+			.info = bf_bool_info,
+			.get = bf_phantom_get,
+			.put = bf_phantom_put,
+			.private_value = bit,
+		}, chip);
+		if (!kctl)
+			return -ENOMEM;
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+	}
+

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_masters(struct snd_usb_babyface *chip) · line 1258

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Instrument Ref Level",
+		.info = bf_reflevel_info,
+		.get = bf_reflevel_get,
+		.put = bf_reflevel_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;

[Severity: Low]
If snd_ctl_new1() returns NULL for the "Instrument Ref Level" control in babyface_create_preamp(), passing NULL to snd_ctl_add() will return -EINVAL instead of -ENOMEM. Should this check if (!kctl) return -ENOMEM; before calling snd_ctl_add() like the other control creation calls in this function?

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +4 -0
+
+	for (i = 0; i < 4; i++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_masters(struct snd_usb_babyface *chip) · line 1272

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+			.name = "Mic 1 Capture Volume",
+			.index = i,
+			.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+				  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+			.info = bf_gain_info,
+			.get = bf_gain_get,
+			.put = bf_gain_put,
+			.tlv.p = bf_gain_tlv,
+			.private_value = i,
+		}, chip);

[Severity: Medium]
As reported by Sashiko on v7 [1], why do all three loops in babyface_create_preamp() hardcode "Mic 1" in .name ("Phantom Power Mic 1", "Pad Mic 1", and "Mic 1 Capture Volume") across all loop iterations?

When i > 0, this registers Mic 2 phantom power and pad as "Phantom Power Mic 1",1 and "Pad Mic 1",1, and registers Mic 2, Instrument 3, and Instrument 4 capture gains as "Mic 1 Capture Volume" with indices 1, 2, and 3. Should these either use a channel-neutral name when indexing by i, or format the per-channel name as babyface_create_trim() does?

[1] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#7 lore raw

PATCH v8 4/8 ALSA: usb: babyfacepro: add routing flags and varispeed pitch

In reply to Ismaïl Bahloul

Add loopback, AN 1>2, the sample clock source, the AN1/2 input link, the MS processor, DIM, width, FX send and the varispeed pitch. The clock source is not a register write: it is a bit of the host settings word sent with the keepalive, so bf_settings_write() now composes that word from the tracked state. The optical output's format is a second bit of that word: "Optical Out Format" switches the port between ADAT (the default) and S/PDIF, and the bit follows every settings write.

bf_state_apply_flags() re-applies this state too, and probe also runs it after writing the power-on default, since the cold init cleared the flag registers. A Phones master change made while DIM is engaged moves the level DIM releases back to.

---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 823 ++++++++++++++++++++++--
 sound/usb/babyfacepro/babyfacepro.c     | 117 +++-
 sound/usb/babyfacepro/babyfacepro.h     |  35 +-
 3 files changed, 902 insertions(+), 73 deletions(-)
sound/usb/babyfacepro/babyfacepro-ctl.c +763 -60
index fe6266ea1..5482b6f09 100644
@@ -10,8 +10,10 @@
  * state persistence, card lifecycle).
  */
 #include <linux/log2.h>
+#include <linux/math64.h>
 #include <linux/module.h>
 #include <linux/mutex.h>
+#include <linux/string.h>
 #include <linux/unaligned.h>
 #include <linux/usb.h>
 #include <linux/workqueue.h>
@@ -252,7 +254,9 @@ int babyface_write_default_mixer(struct snd_usb_babyface *chip)
 			chip->xpoint[out][src][1] = level;
 		}
 
-	/* Host settings word (clock source Internal). */
+	/* Host settings word - composed from tracked state (clock defaults
+	 * to Internal, chip->clock_optical is zero-initialized).
+	 */
 	return bf_settings_write(chip);
 }
 
@@ -325,6 +329,33 @@ static int bf_master_get(struct snd_kcontrol *kctl,
 	return 0;
 }
 
+/* The master step to put on the wire.  The cache keeps the real
+ * volume: mute wins, and DIM is the fixed -20 dB step on the Phones
+ * pair, so every writer derives the wire value the same way.
+ */
+/* The two master registers to put on the wire.  The cache keeps the
+ * real volume: mute wins, and DIM is the exact -20 dB pair the
+ * hardware's own button writes (0xcb / 0x0333, cap_dim2.pcap), not the
+ * master curve's value at that step.
+ */
+static u8 bf_out_reg8(bool muted, bool dim, u16 step)
+{
+	if (muted)
+		return BF_MASTER_MUTE;
+	if (dim)
+		return BF_MASTER_MINUS20_8;
+	return bf_master_reg8(step);
+}
+
+static u16 bf_out_reg16(bool muted, bool dim, u16 step)
+{
+	if (muted)
+		return 0;
+	if (dim)
+		return BF_MASTER_MINUS20_16;
+	return bf_master_reg16(step);
+}
+
 static int bf_master_put(struct snd_kcontrol *kctl,
 			 struct snd_ctl_elem_value *ucontrol)
 {
@@ -332,7 +363,8 @@ static int bf_master_put(struct snd_kcontrol *kctl,
 	int out = bf_master_out[kctl->private_value];
 	long nl = ucontrol->value.integer.value[0];
 	long nr = ucontrol->value.integer.value[1];
-	u16 l, r, wire_l, wire_r, flag;
+	bool muted, dim;
+	u16 l, r, flag;
 	int ret = 0;
 
 	/* Reject anything outside the declared range, so the registers and
@@ -350,33 +382,40 @@ static int bf_master_put(struct snd_kcontrol *kctl,
 	if (l == chip->master[out][0] && r == chip->master[out][1])
 		return 0;
 
-	wire_l = chip->muted[out] ? 0 : l;
-	wire_r = chip->muted[out] ? 0 : r;
+	muted = chip->muted[out];
+	dim = out == 1 && chip->dim;
 	flag = bf_flag_cycle[chip->flag_cnt];
 	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
 
 	/* Both registers, as TotalMix writes them: the 8-bit is the analog
 	 * level, the 16-bit the digital one.
 	 */
-	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_l),
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, l),
 			      BF_REG_MASTER_8 + 2 * out);
 	if (ret < 0)
 		return ret;
-	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_master_reg8(wire_r),
+	ret = bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, r),
 			      BF_REG_MASTER_8 + 2 * out + 1);
 	if (ret < 0)
 		return ret;
-	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_l),
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+			      bf_out_reg16(muted, dim, l),
 			      (BF_REG_MASTER_16 + 2 * out) | flag);
 	if (ret < 0)
 		return ret;
-	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_master_reg16(wire_r),
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+			      bf_out_reg16(muted, dim, r),
 			      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
 	if (ret < 0)
 		return ret;
 
 	chip->master[out][0] = l;
 	chip->master[out][1] = r;
+	/* A Phones change while DIM is engaged re-bases the restore point. */
+	if (chip->dim && out == 1) {
+		chip->dim_saved[0] = l;
+		chip->dim_saved[1] = r;
+	}
 	return 1;
 }
 
@@ -411,6 +450,7 @@ static int bf_mute_put(struct snd_kcontrol *kctl,
 	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
 	int out = bf_master_out[kctl->private_value];
 	bool muted = !ucontrol->value.integer.value[0];
+	bool dim;
 	u16 flag;
 	int ret = 0;
 
@@ -418,51 +458,33 @@ static int bf_mute_put(struct snd_kcontrol *kctl,
 	if (muted == chip->muted[out])
 		return 0;
 
+	dim = out == 1 && chip->dim;
 	flag = bf_flag_cycle[chip->flag_cnt];
 	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
 
-	if (muted) {
-		ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
-				      BF_REG_MASTER_8 + 2 * out);
-		if (ret < 0)
-			return ret;
-		ret = bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
-				      BF_REG_MASTER_8 + 2 * out + 1);
-		if (ret < 0)
-			return ret;
-		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
-				      (BF_REG_MASTER_16 + 2 * out) | flag);
-		if (ret < 0)
-			return ret;
-		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000,
-				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
-		if (ret < 0)
-			return ret;
-	} else {
-		/* Unmute restores the cached volume (TotalMix keeps the
-		 * pre-mute fader value host-side), 8-bit + 16-bit.
-		 */
-		ret = bf_vendor_write(chip, BF_REQ_GAIN,
-				      bf_master_reg8(chip->master[out][0]),
-				      BF_REG_MASTER_8 + 2 * out);
-		if (ret < 0)
-			return ret;
-		ret = bf_vendor_write(chip, BF_REQ_GAIN,
-				      bf_master_reg8(chip->master[out][1]),
-				      BF_REG_MASTER_8 + 2 * out + 1);
-		if (ret < 0)
-			return ret;
-		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
-				      bf_master_reg16(chip->master[out][0]),
-				      (BF_REG_MASTER_16 + 2 * out) | flag);
-		if (ret < 0)
-			return ret;
-		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
-				      bf_master_reg16(chip->master[out][1]),
-				      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
-		if (ret < 0)
-			return ret;
-	}
+	/* TotalMix keeps the pre-mute fader value host-side, so unmute
+	 * restores it; mute is the 8-bit mute code and a zero 16-bit.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_GAIN,
+			      bf_out_reg8(muted, dim, chip->master[out][0]),
+			      BF_REG_MASTER_8 + 2 * out);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_GAIN,
+			      bf_out_reg8(muted, dim, chip->master[out][1]),
+			      BF_REG_MASTER_8 + 2 * out + 1);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+			      bf_out_reg16(muted, dim, chip->master[out][0]),
+			      (BF_REG_MASTER_16 + 2 * out) | flag);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+			      bf_out_reg16(muted, dim, chip->master[out][1]),
+			      (BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	if (ret < 0)
+		return ret;
 	chip->muted[out] = muted;
 	return 1;
 }
@@ -655,6 +677,20 @@ int babyface_create_masters(struct snd_usb_babyface *chip)
 	return 0;
 }
 
+/* Boost's 0x21 commit value (0x0003) is NOT a persisted register bit -
+ * PROTOCOL.md's "Ref level" section found it only in the one-shot 0x21
+ * value alongside the 0x17 state write, so it has to be re-sent
+ * alongside EVERY preamp write (phantom/PAD toggles, AN 1>2, link and
+ * the state replay included), or Boost would silently degrade to plain
+ * -10dBV the next time anything else touches this shared byte.
+ */
+int bf_preamp_commit(struct snd_usb_babyface *chip)
+{
+	return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT,
+			       chip->ref_level == BF_REF_LEVEL_BOOST ?
+			       0x0003 : 0x0000, 0x0000);
+}
+
 int bf_preamp_state_write(struct snd_usb_babyface *chip)
 {
 	int ret;
@@ -662,16 +698,7 @@ int bf_preamp_state_write(struct snd_usb_babyface *chip)
 	ret = bf_vendor_write(chip, BF_REQ_PREAMP, chip->preamp, BF_REG_PREAMP);
 	if (ret < 0)
 		return ret;
-	/* Boost's 0x21 commit value (0x0003) is NOT a persisted register
-	 * bit - PROTOCOL.md's "Ref level" section found it only in the
-	 * one-shot 0x21 value alongside the 0x17 state write, so it has
-	 * to be re-sent alongside EVERY preamp write (phantom/PAD toggles
-	 * included), or Boost would silently degrade to plain -10dBV the
-	 * next time anything else touches this shared byte.
-	 */
-	return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT,
-			       chip->ref_level == BF_REF_LEVEL_BOOST ?
-			       0x0003 : 0x0000, 0x0000);
+	return bf_preamp_commit(chip);
 }
 
 /* Phase (polarity) invert (AN1-4 only, PROTOCOL.md "Phase toggle",
@@ -1278,3 +1305,679 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
 	}
 	return 0;
 }
+
+static int bf_switch_info(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int bf_pitch_info(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = -50;		/* -5.0 % */
+	uinfo->value.integer.max = 50;		/* +5.0 % */
+	uinfo->value.integer.step = 1;		/* 0.1 % */
+	return 0;
+}
+
+static int bf_pitch_get(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->pitch;
+	return 0;
+}
+
+static int bf_pitch_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	long np = ucontrol->value.integer.value[0];
+	int p, ret = 0;
+
+	if (np < -50 || np > 50)
+		return -EINVAL;
+	p = np;
+
+	guard(mutex)(&chip->mutex);
+	if (p == chip->pitch)
+		return ret;
+
+	/* The DDS quad is a ratio on top of the family rate, the same at
+	 * every sample rate; bf_pitch_write() sends it with the settings
+	 * keepalive that commits it.
+	 */
+	ret = bf_pitch_write(chip, p);
+	if (ret < 0)
+		return ret;
+	chip->pitch = p;
+	return 1;
+}
+
+static int bf_loopback_get(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->loopback[out];
+	ucontrol->value.integer.value[1] = chip->loopback[out];
+	return 0;
+}
+
+/* Write the full 30-channel loopback map: pair (2*out, 2*out+1) at
+ * `on` (0x0001/0x0000), all other channels cleared - exactly what
+ * TotalMix sends on every loopback toggle (cap_loopback2.pcap).  The
+ * full-map write is also the reliable OFF (the old per-pair write
+ * sometimes failed to disengage on the hardware).
+ */
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out,
+			  bool on)
+{
+	int ch, ret;
+
+	for (ch = 0; ch < BF_LOOPBACK_CHANNELS; ch++) {
+		u16 val = (on && (ch == out * 2 || ch == out * 2 + 1))
+			  ? 0x0001 : 0x0000;
+
+		ret = bf_vendor_write(chip, BF_REQ_LOOPBACK, val, ch);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+static int bf_loopback_put(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value;
+	bool on = ucontrol->value.integer.value[0];
+	int prev = -1, notify = -1;
+	int i, ret = 0;
+
+	scoped_guard(mutex, &chip->mutex) {
+		if (on == chip->loopback[out])
+			return 0;
+		for (i = 0; i < 6; i++)
+			if (chip->loopback[i])
+				prev = i;
+		ret = bf_loopback_write_map(chip, out, on);
+		if (ret < 0)
+			return ret;
+		/* Single-active model (TotalMix writes one pair at 0x0001,
+		 * the rest 0x0000): toggling one output clears the others.
+		 * The cleared one did not get its own put(), so tell user
+		 * space.
+		 */
+		memset(chip->loopback, 0, sizeof(chip->loopback));
+		chip->loopback[out] = on;
+		if (on && prev >= 0 && prev != out)
+			notify = prev;
+		ret = 1;
+	}
+	if (notify >= 0 && chip->loopback_kctl[notify])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->loopback_kctl[notify]->id);
+	return ret;
+}
+
+static int bf_an12_get(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->an12;
+	return 0;
+}
+
+static int bf_an12_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool an12 = ucontrol->value.integer.value[0];
+	u16 v;
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	if (an12 == chip->an12)
+		return 0;
+	v = (chip->linked ? 0x0400 : 0x0000) | (an12 ? 0x1000 : 0x0000);
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+	if (ret < 0)
+		return ret;
+	ret = bf_preamp_commit(chip);
+	if (ret < 0)
+		return ret;
+	chip->an12 = an12;
+	return 1;
+}
+
+/* Clock source (PROTOCOL.md "Clock source / no-lock state",
+ * hardware-verified 2026-08-22, clktest.c): NOT a register write at
+ * all - only the BF_REG_KEEPALIVE_SETTINGS word changes (bit 2 =
+ * Optical). Matches the naming TuxMix's ALSA backend already looks
+ * for ("Sample Clock Source", the same name found on the stock
+ * snd-usb-audio Class-Compliant driver) so it picks this control up
+ * with zero changes on that side.
+ */
+static const char *const bf_clock_texts[] = {
+	"Internal", "Optical In", NULL
+};
+
+static int bf_clock_info(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 2, bf_clock_texts);
+}
+
+static int bf_clock_get(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = chip->clock_optical ? 1 : 0;
+	return 0;
+}
+
+static int bf_clock_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	unsigned int item = ucontrol->value.enumerated.item[0];
+	bool optical;
+	int ret = 0;
+
+	if (item > 1)
+		return -EINVAL;
+	optical = item;
+
+	guard(mutex)(&chip->mutex);
+	if (optical == chip->clock_optical)
+		return 0;
+	chip->clock_optical = optical;
+	ret = bf_settings_write(chip);
+	if (ret < 0) {
+		chip->clock_optical = !optical;
+		return ret;
+	}
+	return 1;
+}
+
+/* Optical Out format: bit 10 of the same BF_REG_KEEPALIVE_SETTINGS
+ * word (PROTOCOL.md, LINUX-VALIDATION.md section 7).  The single optical
+ * port carries ADAT by default; S/PDIF mode uses only AS1/2.  Like the
+ * clock source, this is not a register write of its own.
+ */
+static const char *const bf_optical_texts[] = {
+	"ADAT", "S/PDIF", NULL
+};
+
+static int bf_optical_info(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 2, bf_optical_texts);
+}
+
+static int bf_optical_get(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = chip->optical_spdif ? 1 : 0;
+	return 0;
+}
+
+static int bf_optical_put(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	unsigned int item = ucontrol->value.enumerated.item[0];
+	bool spdif;
+	int ret = 0;
+
+	if (item > 1)
+		return -EINVAL;
+	spdif = item;
+
+	guard(mutex)(&chip->mutex);
+	if (spdif == chip->optical_spdif)
+		return 0;
+	chip->optical_spdif = spdif;
+	ret = bf_settings_write(chip);
+	if (ret < 0) {
+		chip->optical_spdif = !spdif;
+		return ret;
+	}
+	return 1;
+}
+
+static int bf_link_get(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->linked;
+	return 0;
+}
+
+static int bf_link_put(struct snd_kcontrol *kctl,
+		       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool linked = ucontrol->value.integer.value[0];
+	u16 v;
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	if (linked == chip->linked)
+		return 0;
+	v = (linked ? 0x0400 : 0x0000) | (chip->an12 ? 0x1000 : 0x0000);
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000);
+	if (ret < 0)
+		return ret;
+	ret = bf_preamp_commit(chip);
+	if (ret < 0)
+		return ret;
+	chip->linked = linked;
+	return 1;
+}
+
+static int bf_ms_get(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->ms_proc;
+	return 0;
+}
+
+/* MS-proc: engage per the cap_ms2.pcap ON pattern - write 0x0000 to
+ * the four AN2 (side) crosspoints, AN1/2 0x0001/0x001B and PH3/4
+ * 0x0035/0x004F - the side path is muted (ear-verified 2026-08-26
+ * with the mic on AN2: MS ON = silence); release restores each
+ * block's cached fader values (host-side, like TotalMix).
+ * (The 0x1000/0x0004 writes are the DISENGAGE restore values seen in
+ * cap_ms2 - the driver had them inverted on the engage path.)
+ */
+static int bf_ms_put(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool on = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	if (on == chip->ms_proc)
+		return 0;
+	if (on) {
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001);
+		if (ret < 0)
+			return ret;
+		ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b);
+		if (ret < 0)
+			return ret;
+	} else {
+		/* Restore each block's own AN2 crosspoint: 0x0001/0x001B
+		 * is the AN1/2 pair, 0x0035/0x004F is PH3/4.
+		 */
+		ret = bf_xpoint_write(chip, 0, 1,
+				      chip->xpoint[0][1][0],
+				      chip->xpoint[0][1][1]);
+		if (ret < 0)
+			return ret;
+		ret = bf_xpoint_write(chip, 1, 1,
+				      chip->xpoint[1][1][0],
+				      chip->xpoint[1][1][1]);
+		if (ret < 0)
+			return ret;
+	}
+	chip->ms_proc = on;
+	return 1;
+}
+
+/* DIM - cap_dim2.pcap: an absolute -20 dB on the Phones master
+ * (out 1: 8-bit 0xCB / 16-bit 0x0333) regardless of the current level,
+ * plus the 0x17 wVal=0x2000 wIdx=0x2000 flag; release restores the
+ * pre-DIM master host-side.  The master cache keeps the real volume.
+ */
+static int bf_dim_get(struct snd_kcontrol *kctl,
+		      struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->dim;
+	return 0;
+}
+
+/* Apply DIM on the wire.  chip->mutex must be held by the caller, the
+ * "Dim Switch" put.
+ */
+int bf_dim_apply(struct snd_usb_babyface *chip, bool on)
+{
+	bool old = chip->dim;
+	bool muted = chip->muted[1];
+	u16 step_l, step_r, flag;
+	int ret;
+
+	lockdep_assert_held(&chip->mutex);
+	if (on) {
+		chip->dim_saved[0] = chip->master[1][0];
+		chip->dim_saved[1] = chip->master[1][1];
+	}
+	chip->dim = on;
+	step_l = on ? chip->master[1][0] : chip->dim_saved[0];
+	step_r = on ? chip->master[1][1] : chip->dim_saved[1];
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+	ret = bf_vendor_write(chip, BF_REQ_GAIN,
+			      bf_out_reg8(muted, on, step_l),
+			      BF_REG_MASTER_8 + 2 * 1);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_GAIN,
+			      bf_out_reg8(muted, on, step_r),
+			      BF_REG_MASTER_8 + 2 * 1 + 1);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+			      bf_out_reg16(muted, on, step_l),
+			      (BF_REG_MASTER_16 + 2 * 1) | flag);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+			      bf_out_reg16(muted, on, step_r),
+			      (BF_REG_MASTER_16 + 2 * 1 + 1) | flag);
+	if (ret < 0)
+		goto out;
+	ret = bf_vendor_write(chip, BF_REQ_PREAMP,
+			      on ? 0x2000 : 0x0000, 0x2000);
+	if (ret < 0)
+		goto out;
+	return 0;
+out:
+	chip->dim = old;
+	return ret;
+}
+
+static int bf_dim_put(struct snd_kcontrol *kctl,
+		      struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	bool on = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	guard(mutex)(&chip->mutex);
+	if (on == chip->dim)
+		return 0;
+	ret = bf_dim_apply(chip, on);
+	if (ret == 0)
+		ret = 1;
+	return ret;
+}
+
+static int bf_width_info(struct snd_kcontrol *kctl,
+			 struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = -100;
+	uinfo->value.integer.max = 100;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_width_get(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->width;
+	return 0;
+}
+
+/* Width spread: L = 0x1000*(1+w), R = 0x1000*(1-w), L+R = 0x2000.
+ * TotalMix writes the strip's src pair on the AN1/2 block (0x0000)
+ * and on the block at 0x0034 (cap_width3-7): the stereo pair spreads
+ * L/R in opposition, the mirror src (AN2) gets the swapped values.
+ * Caller holds chip->mutex.
+ */
+int bf_width_apply(struct snd_usb_babyface *chip, int w)
+{
+	u16 l = (u16)(((0x2000 * (100 + w) / 2) + 50) / 100);
+	u16 r = 0x2000 - l;
+	int ret;
+
+	/* AN1/2: AN1 L=0x0000, R=0x001A; AN2 L=0x0001, R=0x001B. */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b);
+	if (ret < 0)
+		return ret;
+	/* The 0x0034 block: AN1 L=0x0034, R=0x004E; AN2 L=0x0035,
+	 * R=0x004F.  TotalMix writes it in the same drag, so keep the
+	 * two halves together here and in the state replay.
+	 */
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0034);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x004e);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0035);
+	if (ret < 0)
+		return ret;
+	return bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x004f);
+}
+
+static int bf_width_put(struct snd_kcontrol *kctl,
+			struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	long w = ucontrol->value.integer.value[0];
+	int ret = 0;
+
+	if (w < -100 || w > 100)
+		return -EINVAL;
+
+	guard(mutex)(&chip->mutex);
+	if ((int)w == chip->width)
+		return 0;
+	ret = bf_width_apply(chip, (int)w);
+	if (ret < 0)
+		return ret;
+	chip->width = w;
+	return 1;
+}
+
+static int bf_fx_send_info(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 0x1000;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_fx_send_get(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = chip->fx_send;
+	return 0;
+}
+
+static int bf_fx_send_put(struct snd_kcontrol *kctl,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	long nv = ucontrol->value.integer.value[0];
+	u16 v;
+	int ret = 0;
+
+	/* Check before narrowing (a value >= 0x10000 would wrap to a
+	 * passing u16); see bf_master_put().
+	 */
+	if (nv < 0 || nv > 0x1000)
+		return -EINVAL;
+	v = nv;
+
+	guard(mutex)(&chip->mutex);
+	if (v == chip->fx_send)
+		return 0;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0138);
+	if (ret < 0)
+		return ret;
+	ret = bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0153);
+	if (ret < 0)
+		return ret;
+	chip->fx_send = v;
+	return 1;
+}
+
+int babyface_create_flags(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int i, err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Varispeed Pitch",
+		.info = bf_pitch_info,
+		.get = bf_pitch_get,
+		.put = bf_pitch_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	for (i = 0; i < 6; i++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Loopback Switch",
+			.index = i,
+			.info = bf_mute_info,
+			.get = bf_loopback_get,
+			.put = bf_loopback_put,
+			.private_value = i,
+		}, chip);
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+		chip->loopback_kctl[i] = kctl;
+	}
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "AN 1>2 Switch",
+		.info = bf_switch_info,
+		.get = bf_an12_get,
+		.put = bf_an12_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Sample Clock Source",
+		.info = bf_clock_info,
+		.get = bf_clock_get,
+		.put = bf_clock_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Optical Out Format",
+		.info = bf_optical_info,
+		.get = bf_optical_get,
+		.put = bf_optical_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "AN1/2 Link Switch",
+		.info = bf_switch_info,
+		.get = bf_link_get,
+		.put = bf_link_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "MS Processor Switch",
+		.info = bf_switch_info,
+		.get = bf_ms_get,
+		.put = bf_ms_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Dim Switch",
+		.info = bf_switch_info,
+		.get = bf_dim_get,
+		.put = bf_dim_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Width",
+		.info = bf_width_info,
+		.get = bf_width_get,
+		.put = bf_width_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "FX Send Volume",
+		.info = bf_fx_send_info,
+		.get = bf_fx_send_get,
+		.put = bf_fx_send_put,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+
+	return 0;
+}
sound/usb/babyfacepro/babyfacepro.c +105 -12

159 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.h +34 -1
index 06af1aa82..1d4c358d9 100644
@@ -115,9 +115,17 @@
 
 /* Host settings-state word carried by the BF_REG_KEEPALIVE_SETTINGS
  * keepalive (PROTOCOL.md "keepalive 0x10 0x05CF wVal = host settings-
- * state register", hardware-verified 2026-08-22/23).
+ * state register", hardware-verified 2026-08-22/23): clock source is
+ * NOT a register write at all, only this flag word changes.  Bit 2 =
+ * clock Optical (bit clear = Internal, the default); bit 10 = Optical-Out
+ * S/PDIF (bit clear = ADAT, the default).  Bit 6 (EQ for Record) is next
+ * in the driver's own upstream follow-up list, not wired to a control
+ * yet - the composer below OR's in every tracked bit, structured so that
+ * one can be added the same way without another flag-stomping rewrite.
  */
 #define BF_SETTINGS_CLOCK_INTERNAL	0x0001
+#define BF_SETTINGS_CLOCK_OPTICAL	0x0004
+#define BF_SETTINGS_OPTICAL_SPDIF	0x0400
 
 /* Preamp state byte (0x17, wIdx 0x003F - full state, verified).
  * NOTE 2026-08-26 (cap_reflevel3.pcap): the 0x0C "base" is NOT a
@@ -182,6 +190,15 @@
 #define BF_REG_LOWMAP_BASE_L		BF_REG_XPOINT_BASE_L
 #define BF_REG_LOWMAP_BASE_R		BF_REG_XPOINT_BASE_R
 
+#define BF_REQ_LOOPBACK			0x15	/* per-output-channel flag */
+
+/* Loopback map width (captured 2026-08-25, cap_loopback2.pcap):
+ * TotalMix writes the FULL 30-channel 0x15 map on every toggle (ON =
+ * the pair at 0x0001 + the other 28 at 0x0000; OFF = all 0x0000).
+ * wIdx = 2xout_index: AN1/2 = 0/1, PH3/4 = 2/3, AS1/2 = 4/5, ...
+ */
+#define BF_LOOPBACK_CHANNELS		30
+
 /* The "cross" register block within each output: the L-registers sit at
  * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo
  * source pairs that can be cross-linked).  bf_crosspoint_clear_cross()
@@ -332,6 +349,17 @@ struct snd_usb_babyface {
 					 * (0 = +4dBu, the default)
 					 */
 	struct snd_kcontrol *trim_kctl[4]; /* for snd_ctl_notify */
+	bool loopback[6];
+	struct snd_kcontrol *loopback_kctl[6]; /* for snd_ctl_notify */
+	bool an12;			/* AN 1>2 copy */
+	bool linked;			/* AN1/2 input link */
+	bool ms_proc;			/* MS processor engaged */
+	bool clock_optical;		/* clock source: false = Internal (default) */
+	bool optical_spdif;		/* Optical Out: false = ADAT (default) */
+	int width;			/* width knob -100..+100 */
+	u16 fx_send;			/* FX send level 0..0x1000 */
+	u16 dim_saved[2];		/* pre-DIM Phones master (out 1 L/R) */
+	bool dim;			/* DIM engaged (fixed -20 dB on Phones) */
 };
 
 struct bf_rate {
@@ -376,11 +404,16 @@ int babyface_create_xpoints(struct snd_usb_babyface *chip);
 int babyface_create_trim(struct snd_usb_babyface *chip);
 int babyface_create_preamp(struct snd_usb_babyface *chip);
 int bf_preamp_state_write(struct snd_usb_babyface *chip);
+int bf_preamp_commit(struct snd_usb_babyface *chip);
 int bf_phase_apply(struct snd_usb_babyface *chip, int mic, bool invert);
 int bf_split_apply(struct snd_usb_babyface *chip, int pb, bool split);
 int bf_trim_apply(struct snd_usb_babyface *chip, int mic, int trim_db2);
 int bf_gain_max_db(int mic);
 u8 bf_gain_raw(int mic, int db);
+int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on);
+int bf_width_apply(struct snd_usb_babyface *chip, int w);
+int bf_dim_apply(struct snd_usb_babyface *chip, bool on);
+int babyface_create_flags(struct snd_usb_babyface *chip);
 
 /* Master gain-law helpers (babyfacepro-ctl.c). */
 int bf_master_16bit(int half_db);	/* dBx2 -> 16-bit master */
Signature · 1 line
2.56.0
#8 lore raw

Re: [PATCH v8 4/8] ALSA: usb: babyfacepro: add routing flags and varispeed pitch

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] ALSA: usb: babyfacepro: out-of-bounds value[1] write in bf_loopback_get() - [Low] ALSA: usb…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] ALSA: usb: babyfacepro: out-of-bounds value[1] write in bf_loopback_get()
- [Low] ALSA: usb: babyfacepro: leftover contradictory comment above bf_out_reg8() --

commit bc91f1e348293fb57b62c700b674ce38c7dc5791
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: babyfacepro: add routing flags and varispeed pitch

This commit adds ALSA mixer controls and state persistence for routing flags (loopback, AN 1>2, sample clock source, optical out format, AN1/2 link, MS processor, DIM, width, and FX send) and varispeed pitch on the RME Babyface Pro.

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
index fe6266ea118e6..5482b6f09b73a 100644

[ ... ]

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
@@ -325,6 +329,33 @@ static int bf_master_get(struct snd_kcontrol *kctl,
 	return 0;
 }
 

sound/usb/babyfacepro/babyfacepro-ctl.c static int bf_master_get(struct snd_kcontrol *kctl, · line 341

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+/* The master step to put on the wire.  The cache keeps the real
+ * volume: mute wins, and DIM is the fixed -20 dB step on the Phones
+ * pair, so every writer derives the wire value the same way.
+ */
+/* The two master registers to put on the wire.  The cache keeps the
+ * real volume: mute wins, and DIM is the exact -20 dB pair the
+ * hardware's own button writes (0xcb / 0x0333, cap_dim2.pcap), not the
+ * master curve's value at that step.
+ */
+static u8 bf_out_reg8(bool muted, bool dim, u16 step)

[Severity: Low]
This isn't a bug, but is the first comment block above bf_out_reg8() in sound/usb/babyfacepro/babyfacepro-ctl.c a leftover from an earlier draft that was meant to be replaced by the second comment block?

[ ... ]

Quoted 1 line
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
@@ -1278,3 +1305,679 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)

[ ... ]

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_preamp(struct snd_usb_babyface *chip) · line 1374

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+static int bf_loopback_get(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int out = kctl->private_value;
+
+	ucontrol->value.integer.value[0] = chip->loopback[out];
+	ucontrol->value.integer.value[1] = chip->loopback[out];
+	return 0;
+}

[Severity: Medium]
Does bf_loopback_get() in sound/usb/babyfacepro/babyfacepro-ctl.c write past the declared element count when populating ucontrol->value.integer.value[1]?

In babyface_create_flags(), the Loopback Switch controls are registered with .info = bf_mute_info and .get = bf_loopback_get:

for (i = 0; i < 6; i++) {
kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = "Loopback Switch", .index = i,
.info = bf_mute_info,
.get = bf_loopback_get,
.put = bf_loopback_put, .private_value = i,
}, chip);

However, bf_mute_info() sets uinfo->count = 1:

	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
	/* One switch for the pair: bf_mute_put() writes both channels
	 * together, so a count of 2 would silently ignore value[1].
	 */
	uinfo->count = 1;

When userspace reads the control via snd_ctl_elem_read() -> bf_loopback_get() with CONFIG_SND_CTL_DEBUG enabled, the ALSA control core poisons the buffer past info.count with fill_remaining_elem_value() and then checks it in sanity_check_elem_value(). Because bf_loopback_get() (like bf_mute_get()) writes ucontrol->value.integer.value[1] on a count = 1 control, sanity_check_elem_value() logs an access overflow error and fails the read with -EINVAL.

Should bf_loopback_get() only write ucontrol->value.integer.value[0] (or use bf_switch_info and a single-value get, and similarly in bf_mute_get())?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
#9 lore raw

PATCH v8 5/8 ALSA: usb: babyfacepro: add S3 suspend/resume

In reply to Ismaïl Bahloul

The device loses its registers across a system suspend. On suspend, let the PCM core suspend the substreams (applications get -ESTRPIPE and restart), then kill the session's URBs before cancelling the stream work their handlers queue. On resume, re-run the cold init and replay the cached mixer state and flags. A resume that follows a device reset goes through reset_resume instead of a re-probe, so the cached state is replayed in place.

USB autosuspend stays disabled: nothing in the driver holds a PM reference while streaming.

---
 sound/usb/babyfacepro/babyfacepro.c | 89 ++++++++++++++++++++++++++---
 sound/usb/babyfacepro/babyfacepro.h |  4 +-
 2 files changed, 82 insertions(+), 11 deletions(-)
sound/usb/babyfacepro/babyfacepro.c +80 -9
index 12cfd7fea..06ef1a97d 100644
@@ -3,7 +3,8 @@
  * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
  *
  * Core driver: USB vendor requests + cold init, interrupt-URB PCM
- * streaming, and the card lifecycle (probe/disconnect/module entry).
+ * streaming, the mixer-state replay after a resume, and
+ * the card lifecycle (probe/disconnect/PM/module entry).
  *
  * See babyfacepro.h for the shared device state and register map,
  * and babyfacepro-ctl.c for the ALSA control surface.
@@ -198,9 +199,9 @@ int bf_pitch_write(struct snd_usb_babyface *chip, int pitch)
 }
 
 /* The cold-start init (cap_coldplug.pcap), verbatim from the user-space
- * reference (protocol::streaming_init).  Run at probe, where the
- * device state is unknown; a session start does not need it (see
- * babyface_stream_start()).
+ * reference (protocol::streaming_init).  Run at probe and after resume,
+ * where the device state is unknown; a session start does not need it
+ * (see babyface_stream_start()).
  */
 int bf_cold_init(struct snd_usb_babyface *chip)
 {
@@ -267,8 +268,17 @@ int bf_cold_init(struct snd_usb_babyface *chip)
 	return 0;
 }
 
-/* Re-apply the whole cached mixer state to the device, whose registers
- * the cold init cleared.  Caller holds chip->mutex.
+/* Units probed since the module was loaded.
+ * The SELECT selection is not in the readback, and the values alsactl
+ * restores right after a probe are the ones of the last alsactl store.
+ * After a boot that is the last shutdown, the newest there is.  A later
+ * probe of the same unit (a usbfs claim released, an unbind, a replug)
+ * may follow SELECT presses made since, so bf_panel_select_put() needs
+ * to tell the two apart.
+ */
+/* Re-apply the whole cached mixer state after a resume (the device
+ * lost its registers across a system suspend - TotalMix does the same
+ * re-apply).  Caller holds chip->mutex.
  */
 int babyface_restore_state(struct snd_usb_babyface *chip)
 {
@@ -797,8 +807,9 @@ static int babyface_stream_start(struct snd_usb_babyface *chip)
 	/* A session start is what the RME Windows driver sends
 	 * (cap_audio): the trigger pair, the URBs, the arm.  The device
 	 * keeps its mixer state between sessions, so nothing is cleared
-	 * or re-applied here; the cold init runs at probe only.  The rate
-	 * is written first: hw_params only re-points the speed multiplier.
+	 * or re-applied here; the cold init runs at probe and resume
+	 * only.  The rate is written first: hw_params only re-points the
+	 * speed multiplier.
 	 */
 	ret = bf_clock_write(chip);
 	if (ret < 0)
@@ -1497,7 +1508,8 @@ static int babyface_probe(struct usb_interface *intf,
 	chip->card = card;
 
 	chip->dev = usb_get_dev(dev);
-	/* USB autosuspend is untested: nothing in this driver holds a PM
+	/* USB autosuspend is untested: babyface_suspend()/_resume() don't
+	 * check PMSG_IS_AUTO, and nothing in this driver holds a PM
 	 * reference while streaming, so an autosuspend request could race
 	 * a live stream.  Disable it explicitly rather than ship an
 	 * untested code path - full autosuspend support (correct
@@ -1752,6 +1764,62 @@ static void babyface_disconnect(struct usb_interface *intf)
 	snd_card_free_when_closed(chip->card);
 }
 
+static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+	struct snd_device *sdev;
+
+	if (!chip)
+		return 0;
+	list_for_each_entry(sdev, &chip->card->devices, list) {
+		if (sdev->type == SNDRV_DEV_PCM)
+			snd_pcm_suspend_all(sdev->device_data);
+	}
+	/* The URB handlers queue the work: kill them first. */
+	scoped_guard(mutex, &chip->mutex) {
+		if (chip->streaming)
+			babyface_stream_kill(chip);
+	}
+	cancel_work_sync(&chip->stream_work);
+	return 0;
+}
+
+static int babyface_resume(struct usb_interface *intf)
+{
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+	int err;
+
+	if (!chip)
+		return 0;
+
+	/* The device lost its state across the suspend; re-run the cold
+	 * init and re-apply the cached mixer state.  Suspended PCM
+	 * substreams are woken by the core - apps get -ESTRPIPE and
+	 * restart (prepare starts a new session).
+	 *
+	 * This is also the reset_resume handler.  Some hosts reset the
+	 * device on resume; that needs nothing more than the cold init
+	 * already does here, and without the handler the USB core unbinds
+	 * and re-probes the driver instead: the card goes away, open PCMs
+	 * fail, and udev's alsactl restore then loads the mixer state
+	 * stored at the last shutdown over the current one.
+	 */
+	guard(mutex)(&chip->mutex);
+	err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
+	if (err < 0)
+		return err;
+	err = bf_cold_init(chip);
+	if (err < 0)
+		return err;
+	err = babyface_restore_state(chip);
+	if (err < 0)
+		return err;
+	err = bf_state_apply_flags(chip);
+
+	return err;
+}
+
 static const struct usb_device_id babyface_ids[] = {
 	{ USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) },
 	{ }
@@ -1762,6 +1830,9 @@ static struct usb_driver babyface_driver = {
 	.name = "snd-usb-babyface-pro",
 	.probe = babyface_probe,
 	.disconnect = babyface_disconnect,
+	.suspend = babyface_suspend,
+	.resume = babyface_resume,
+	.reset_resume = babyface_resume,
 	.id_table = babyface_ids,
 };
 
sound/usb/babyfacepro/babyfacepro.h +2 -2
index 1d4c358d9..a32849f83 100644
@@ -12,7 +12,7 @@
  * snd-usb-audio quirk.
  *
  * babyfacepro.c holds the card lifecycle, the PCM stream and the
- * mixer-state persistence across re-probes;
+ * mixer-state persistence across re-probes and resume;
  * babyfacepro-ctl.c holds the ALSA control surface - the hardware
  * mixer.
  *
@@ -386,7 +386,7 @@ void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t state
 void babyface_stream_work(struct work_struct *work);
 extern const struct snd_pcm_hw_constraint_list bf_rates_constraint;
 
-/* Mixer-state persistence across interface re-probes. */
+/* Mixer-state persistence across interface re-probes/resume. */
 int babyface_restore_state(struct snd_usb_babyface *chip);
 int bf_state_apply_flags(struct snd_usb_babyface *chip);
 
Signature · 1 line
2.56.0
#10 lore raw

Re: [PATCH v8 5/8] ALSA: usb: babyfacepro: add S3 suspend/resume

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] ALSA: usb: babyfacepro: missing snd_power_change_state() in suspend and resume - [Low] ALSA…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] ALSA: usb: babyfacepro: missing snd_power_change_state() in suspend and resume
- [Low] ALSA: usb: babyfacepro: babyface_suspend() walks private card->devices list --

commit f0c674e7a86798c445b9cc47638dc398955e5a17
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: babyfacepro: add S3 suspend/resume

This commit adds system suspend, resume, and reset_resume callbacks to the RME Babyface Pro USB audio driver. On suspend, it suspends PCM substreams, kills active URBs, and cancels stream work; on resume, it re-runs cold initialization and restores cached mixer state and flags.

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro.c +0 -0
index 12cfd7fea3315..06ef1a97d27a4 100644

[ ... ]

Quoted 6 lines
sound/usb/babyfacepro/babyfacepro.c +2 -0
@@ -1752,6 +1764,62 @@ static void babyface_disconnect(struct usb_interface *intf)
 	snd_card_free_when_closed(chip->card);
 }
 
+static int babyface_suspend(struct usb_interface *intf, pm_message_t message)
+{

sound/usb/babyfacepro/babyfacepro.c static void babyface_disconnect(struct usb_interface *intf) · line 1778

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c +10 -0
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+
+	struct snd_device *sdev;
+
+	if (!chip)
+		return 0;
+	list_for_each_entry(sdev, &chip->card->devices, list) {
+		if (sdev->type == SNDRV_DEV_PCM)
+			snd_pcm_suspend_all(sdev->device_data);
+	}

[Severity: Low]
This isn't a bug, but why walk the internal chip->card->devices list here?

card->devices and struct snd_device are internal to sound/core/device.c and are not meant to be traversed directly by drivers. In addition, the commit message notes:

On suspend, let the PCM core suspend the substreams (applications get -ESTRPIPE and restart)

With USB autosuspend disabled, the PCM core already registers pcm_dev_pm_ops (do_pcm_suspend() in sound/core/pcm.c) on the child PCM device, which calls snd_pcm_suspend_all() before the parent USB interface suspend callback runs. If the driver ever needs to reference its PCM instance directly, could it store the struct snd_pcm pointer returned by snd_pcm_new() in struct snd_usb_babyface instead of iterating over chip->card->devices?

Quoted 33 lines
sound/usb/babyfacepro/babyfacepro.c +33 -0
+	/* The URB handlers queue the work: kill them first. */
+	scoped_guard(mutex, &chip->mutex) {
+		if (chip->streaming)
+			babyface_stream_kill(chip);
+	}
+	cancel_work_sync(&chip->stream_work);
+	return 0;
+}
+
+static int babyface_resume(struct usb_interface *intf)
+{
+	struct snd_usb_babyface *chip = usb_get_intfdata(intf);
+	int err;
+
+	if (!chip)
+		return 0;
+
+	/* The device lost its state across the suspend; re-run the cold
+	 * init and re-apply the cached mixer state.  Suspended PCM
+	 * substreams are woken by the core - apps get -ESTRPIPE and
+	 * restart (prepare starts a new session).
+	 *
+	 * This is also the reset_resume handler.  Some hosts reset the
+	 * device on resume; that needs nothing more than the cold init
+	 * already does here, and without the handler the USB core unbinds
+	 * and re-probes the driver instead: the card goes away, open PCMs
+	 * fail, and udev's alsactl restore then loads the mixer state
+	 * stored at the last shutdown over the current one.
+	 */
+	guard(mutex)(&chip->mutex);
+	err = usb_set_interface(chip->dev, BF_IFACE, chip->alt);
+	if (err < 0)
+		return err;

sound/usb/babyfacepro/babyfacepro.c static void babyface_disconnect(struct usb_interface *intf) · line 1821

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro.c +10 -0
+	err = bf_cold_init(chip);
+	if (err < 0)
+		return err;
+	err = babyface_restore_state(chip);
+	if (err < 0)
+		return err;
+	err = bf_state_apply_flags(chip);
+
+	return err;
+}

[Severity: Medium]
Should babyface_suspend() and babyface_resume() update the card power state via snd_power_change_state()?

In ALSA core (sound/core/pcm_native.c and sound/core/control.c), entry points such as snd_pcm_common_ioctl(), snd_pcm_release(), snd_ctl_elem_read_user(), snd_ctl_elem_write_user(), snd_ctl_elem_info_user(), and snd_ctl_tlv_ioctl() call snd_power_wait() or snd_power_ref_and_wait(), which only block while snd_power_get_state(card) is not SNDRV_CTL_POWER_D0.

Because babyface_suspend() never sets SNDRV_CTL_POWER_D3hot and babyface_resume() never restores SNDRV_CTL_POWER_D0, chip->card->power_state stays at SNDRV_CTL_POWER_D0 across the entire suspend and resume cycle. Couldn't a user-space audio server woken by snd_pcm_suspend_all() (receiving -ESTRPIPE) or issuing SNDRV_PCM_IOCTL_PREPARE or control writes enter babyface_pcm_prepare() or mixer callbacks while the interface is suspending, suspended, or before babyface_resume() finishes restoring hardware registers?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=5
#11 lore raw

PATCH v8 6/8 ALSA: usb: babyfacepro: add the front-panel poll and controls

In reply to Ismaïl Bahloul

In this mode the front panel is host-driven: the device reports its buttons, wheel and IN/OUT/SELECT/MIX/DIM state in a 4-byte readback (request 0x17), and the host software is expected to act on them, as TotalMix does. Poll it (every 20 ms by default, panel_poll_ms) and expose the decoded state as read-only controls, notified on change.

The driver acts on SET (phantom power of the selected microphone, the host standing in for the unit's standalone function), MIX and the wheel:

- With OUT selected, the firmware moves the analog output level itself on each click, smoothed over about 12 ms, by a step that depends on the level and on how fast the wheel turns (0.5 to 3 dB, doubled below a ~62 ms gap between clicks). The driver follows it:
it writes only the 16-bit (digital) master during the gesture, tracks the level per side in half-dB, polls every 5 ms for 200 ms after a click to tell fast clicks apart, and writes the 8-bit master once the wheel has rested for 150 ms, as TotalMix does at the end of a gesture. A muted output ignores the wheel. The volume controls are notified.
- With IN selected, the wheel moves the gain of the selected input.
- With SELECT held, the wheel moves the balance of the selected output.
- In MIX mode, the wheel moves the monitoring level of the selected input into the selected output, and the input VU display follows.

The controls these change - the output volumes, the preamp gain, the 48V switches and the monitoring crosspoint - are notified, so that a mixer application follows the unit.

The channel selection of SELECT (left, right, both, none) is not in the readback. The unit keeps one per IN pair, across IN switches and power cycles; its LEDs go dark on an IN switch, and the next press only shows the selection again, later presses step it. The driver follows the presses and exposes the selection of each pair as a "Front Panel Selection" control (index 0 is Ch 1/2, 1 Ch 3/4, 2 Opt), which is what alsactl keeps across boots. A pair it has not been told starts unknown, and SET and the wheel then do nothing, with one message, rather than act on a channel that may not be the lit one. After a boot, the values alsactl restores are what there is. A re-probe of a unit already probed since the module was loaded ignores them instead, since SELECT may have been pressed since the last store, and leaves the pairs unknown.

A DIM press only increments "DIM Button Press Count", for a mixer application to act on; the driver changes no level on it. The OUT selector reports 0/1/2 for Ch 1/2, Phones and Opt on both the Pro and the Pro FS.

The poll is stopped around a system suspend and at disconnect.

---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 1357 ++++++++++++++++++++++-
 sound/usb/babyfacepro/babyfacepro.c     |   80 +-
 sound/usb/babyfacepro/babyfacepro.h     |  126 ++-
 3 files changed, 1549 insertions(+), 14 deletions(-)
sound/usb/babyfacepro/babyfacepro-ctl.c +1356 -1
index 5482b6f09..51cf8c7bd 100644
@@ -3,7 +3,8 @@
  * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver
  *
  * ALSA control surface: the hardware mixer (output masters and mutes,
- * the crosspoint matrix, preamps, routing flags, varispeed pitch).
+ * the crosspoint matrix, preamps, routing flags, varispeed pitch),
+ * and the front-panel poll and controls.
  *
  * See babyfacepro.h for the shared device state and register map,
  * and babyfacepro.c for the core driver (protocol, PCM streaming,
@@ -620,6 +621,7 @@ int babyface_create_xpoints(struct snd_usb_babyface *chip)
 			err = snd_ctl_add(chip->card, kctl);
 			if (err < 0)
 				return err;
+			chip->xpoint_kctl[out][src] = kctl;
 		}
 	}
 
@@ -849,6 +851,83 @@ static int bf_split_put(struct snd_kcontrol *kctl,
 	return 1;
 }
 
+/* MIX-mode monitoring level (fader curve).
+ * Calibrated crosspoint-fader curve (AN1->AN1/2, cap_calib.pcap
+ * 2026-08-22; the same table as tuxmix-core/src/usb.rs FADER_CURVE).
+ * dB stored x2 (half-dB grid): the MIX wheel steps +/-0.5 dB per click
+ * on this curve (cap_mix.pcap).  0x0000 = -inf (digital mute),
+ * 0x0003 = -62 dB, ... 0x2D41 = +6 dB.  Raw values interpolate linearly
+ * between the 1-dB points.
+ */
+#define BF_FADER_DB2_INF	(-130)	/* -65 dB = the wheel's -inf floor */
+
+static const struct bf_fader_pt {
+	s16 db2;	/* dB x 2 */
+	u16 raw;
+} bf_fader_curve[] = {
+	{ -124, 0x0003 }, { -122, 0x0004 }, { -120, 0x0005 },
+	{ -118, 0x0006 }, { -116, 0x0007 }, { -114, 0x0008 },
+	{ -112, 0x0009 }, { -110, 0x000a }, { -108, 0x000b },
+	{ -106, 0x000d }, { -104, 0x000e }, { -102, 0x0010 },
+	{ -100, 0x0012 }, {  -98, 0x0014 }, {  -96, 0x0017 },
+	{  -94, 0x0019 }, {  -92, 0x001d }, {  -90, 0x0020 },
+	{  -88, 0x0024 }, {  -86, 0x0029 }, {  -84, 0x002e },
+	{  -82, 0x0033 }, {  -80, 0x003a }, {  -78, 0x0041 },
+	{  -76, 0x0049 }, {  -74, 0x0051 }, {  -72, 0x005b },
+	{  -70, 0x0067 }, {  -68, 0x0073 }, {  -66, 0x0081 },
+	{  -64, 0x0091 }, {  -62, 0x00a3 }, {  -60, 0x00b7 },
+	{  -58, 0x00cd }, {  -56, 0x00e6 }, {  -54, 0x0102 },
+	{  -52, 0x0122 }, {  -50, 0x0145 }, {  -48, 0x016d },
+	{  -46, 0x019a }, {  -44, 0x01cc }, {  -42, 0x0204 },
+	{  -40, 0x0243 }, {  -38, 0x028a }, {  -36, 0x02d9 },
+	{  -34, 0x0332 }, {  -32, 0x0396 }, {  -30, 0x0406 },
+	{  -28, 0x0483 }, {  -26, 0x0510 }, {  -24, 0x05af },
+	{  -22, 0x0660 }, {  -20, 0x0727 }, {  -18, 0x0807 },
+	{  -16, 0x0902 }, {  -14, 0x0a1b }, {  -12, 0x0b57 },
+	{  -10, 0x0cb9 }, {   -8, 0x0e47 }, {   -6, 0x1004 },
+	{   -4, 0x11f9 }, {   -2, 0x142a }, {    0, 0x16a0 },
+	{    2, 0x1963 }, {    4, 0x1c7c }, {    6, 0x1ff6 },
+	{    8, 0x23dc }, {   10, 0x283d }, {   12, 0x2d41 },
+};
+
+/* Fader raw -> dBx2 (linear interpolation; raw 0 = -inf). */
+static int bf_fader_raw_to_db2(u16 raw)
+{
+	int i;
+
+	if (raw == 0 || raw < bf_fader_curve[0].raw)
+		return BF_FADER_DB2_INF;
+	for (i = 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) {
+		if (raw <= bf_fader_curve[i + 1].raw) {
+			u32 num = (u32)(raw - bf_fader_curve[i].raw) *
+				  (u32)(bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2);
+			u32 den = bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw;
+
+			return bf_fader_curve[i].db2 + (int)((num + den / 2) / den);
+		}
+	}
+	return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].db2;
+}
+
+/* dBx2 -> fader raw (linear interpolation; below -62 dB = mute 0). */
+static u16 bf_fader_db2_to_raw(int db2)
+{
+	int i;
+
+	if (db2 <= bf_fader_curve[0].db2)
+		return db2 < bf_fader_curve[0].db2 ? 0 : bf_fader_curve[0].raw;
+	for (i = 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) {
+		if (db2 <= bf_fader_curve[i + 1].db2) {
+			u32 num = (u32)(db2 - bf_fader_curve[i].db2) *
+				  (u32)(bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw);
+			u32 den = bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2;
+
+			return bf_fader_curve[i].raw + (u16)((num + den / 2) / den);
+		}
+	}
+	return bf_fader_curve[ARRAY_SIZE(bf_fader_curve) - 1].raw;
+}
+
 /* Input Trim (T button, AN1-4): PROTOCOL.md "Trim (T) write for the
  * AN1/2 pair" (cap_trim2/3/4.pcap, hardware-verified) - the analog
  * input's own gain-trim, applied through the crosspoint registers
@@ -1252,6 +1331,7 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
 		err = snd_ctl_add(chip->card, kctl);
 		if (err < 0)
 			return err;
+		chip->phantom_kctl[i] = kctl;
 	}
 
 	for (i = 0; i < 2; i++) {
@@ -1302,6 +1382,7 @@ int babyface_create_preamp(struct snd_usb_babyface *chip)
 		err = snd_ctl_add(chip->card, kctl);
 		if (err < 0)
 			return err;
+		chip->gain_kctl[i] = kctl;
 	}
 	return 0;
 }
@@ -1981,3 +2062,1277 @@ int babyface_create_flags(struct snd_usb_babyface *chip)
 
 	return 0;
 }
+
+/* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */
+enum {
+	BF_PANEL_KCTL_BUTTON,
+	BF_PANEL_KCTL_WHEEL,
+	BF_PANEL_KCTL_IN,
+	BF_PANEL_KCTL_OUT,
+	BF_PANEL_KCTL_MIX,
+	BF_PANEL_KCTL_DIM,
+	BF_PANEL_KCTL_NUM,
+};
+
+static const char *const bf_panel_in_texts[] = {
+	"Unknown", "Ch 1/2", "Ch 3/4", "Opt", NULL
+};
+
+static const char *const bf_panel_out_texts[] = {
+	"Unknown", "Ch 1/2", "Phones", "Opt", NULL
+};
+
+static const char *const bf_panel_select_texts[] = {
+	"Left", "Right", "Both", "None", "Unknown", NULL
+};
+
+/* byte3 button flash -> event code (0 = none).  The idle byte3 is 0x40;
+ * a press flashes the value below the base for one or two poll frames.
+ */
+static int bf_panel_button_decode(u8 flash)
+{
+	switch (flash) {
+	case BF_PANEL_FLASH_IN:		return BF_PANEL_BTN_IN;
+	case BF_PANEL_FLASH_SET:	return BF_PANEL_BTN_SET;
+	case BF_PANEL_FLASH_MIX:	return BF_PANEL_BTN_MIX;
+	case BF_PANEL_FLASH_OUT:	return BF_PANEL_BTN_OUT;
+	case BF_PANEL_FLASH_SELECT:	return BF_PANEL_BTN_SELECT;
+	case BF_PANEL_FLASH_DIM:	return BF_PANEL_BTN_DIM;
+	default:			return BF_PANEL_BTN_NONE;
+	}
+}
+
+/* (byte2 >> 4) & 7 = IN position 4/5/6 -> enum index (0 = not in range). */
+static int bf_panel_in_decode(u8 nib)
+{
+	switch (nib) {
+	case BF_PANEL_IN_CH12:		return 1;
+	case BF_PANEL_IN_CH34:		return 2;
+	case BF_PANEL_IN_OPT:		return 3;
+	default:			return 0;
+	}
+}
+
+/* byte1 & 7 = OUT position.  0/1/2 for physical Ch 1/2 / Phones / Opt,
+ * LED-correlated capture, hardware-verified on both an original
+ * Babyface Pro and the Pro FS.
+ */
+static int bf_panel_out_decode(u8 v)
+{
+	switch (v) {
+	case 0x00:
+	case BF_PANEL_OUT_CH12:		return 1;
+	case 0x01:
+	case BF_PANEL_OUT_PHONES:	return 2;
+	case 0x02:
+	case BF_PANEL_OUT_OPT:		return 3;
+	default:			return 0;
+	}
+}
+
+/* MIX-mode VU display law - monitoring dBx2 -> the 0x1A 0x000A display
+ * value.  Piecewise-linear through the captured (dB, display) points
+ * (cap_mix.pcap 2026-08-23: (-62,0) (-54,1) (-48,2) (-42.5,3)
+ * (-35,4) (-28.4,5); cap_panel.pcap: (-7.4,10) (-6.7,11)
+ * (-4.6,12)) - a log-ish VU scale (coarse at the bottom, ~1.4 dB/step
+ * near 0).  The -28..-8 dB middle is interpolated; the exact law is
+ * pending the cap_mixdisp.pcap full-range sweep (TODO 0g).
+ */
+static int bf_mix_display(int db2)
+{
+	static const struct {
+		s16 db2;
+		u8 disp;
+	} pts[] = {
+		{ -124, 0 }, { -108, 1 }, {  -96, 2 }, {  -85, 3 },
+		{  -70, 4 }, {  -57, 5 }, {  -15, 10 }, {  -13, 11 },
+		{   -9, 12 },
+	};
+	int i;
+
+	if (db2 <= pts[0].db2)
+		return 0;
+	for (i = 0; i < ARRAY_SIZE(pts) - 1; i++) {
+		if (db2 <= pts[i + 1].db2) {
+			u32 num = (u32)(db2 - pts[i].db2) *
+				  (u32)(pts[i + 1].disp - pts[i].disp);
+			u32 den = pts[i + 1].db2 - pts[i].db2;
+
+			return pts[i].disp + (int)((num + den / 2) / den);
+		}
+	}
+	/* Above -4.6 dB: keep the last slope (2 dB/step) up to +6 dB. */
+	return pts[ARRAY_SIZE(pts) - 1].disp +
+	       clamp((db2 - pts[ARRAY_SIZE(pts) - 1].db2) / 4, 0, 12);
+}
+
+/* Tell ALSA clients that the front panel changed a mixer control. */
+static void bf_panel_notify_kctl(struct snd_usb_babyface *chip,
+				 struct snd_kcontrol *kctl)
+{
+	if (kctl)
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, &kctl->id);
+}
+
+/* The selection of the IN pair now shown, from panel_sel[]. */
+static void bf_panel_sel_load(struct snd_usb_babyface *chip)
+{
+	int pair = READ_ONCE(chip->panel_in) - 1;
+	int sel = pair >= 0 && pair < 3 ? chip->panel_sel[pair] : -1;
+
+	chip->panel_select_known = sel >= 0;
+	WRITE_ONCE(chip->panel_select, sel >= 0 ? sel : 3);
+}
+
+/* SET and the wheel have nothing to act on while the selection is not
+ * known: say so once, since the cause is not visible from the unit.
+ */
+static void bf_panel_sel_hint(struct snd_usb_babyface *chip)
+{
+	if (!chip->panel_select_known)
+		dev_info_once(&chip->dev->dev,
+			      "front panel: the SELECT channel selection is not known (the unit keeps it, and the driver cannot read it), so SET and the wheel do nothing; press SELECT to see it, then set the \"Front Panel Selection\" control of the IN pair shown (index 0 = Ch 1/2) to match\n");
+}
+
+/* Canonical output pair of the OUT selection (enum 1 = Ch 1/2,
+ * 2 = Phones, 3 = Opt): AN1/2, PH3/4, AS1/2.  "Opt" is the first
+ * optical pair, and the optical format does not move it: with the port
+ * set to ADAT that pair is ADAT 1/2, with it set to S/PDIF it is the
+ * S/PDIF pair (bf_sources), the same output register either way.  The
+ * ADAT7/8 this used to point at was an assumption - PROTOCOL.md leaves
+ * the optical outputs unmeasured.
+ */
+static int bf_panel_out_pair(struct snd_usb_babyface *chip)
+{
+	switch (READ_ONCE(chip->panel_out)) {
+	case 2:
+		return 1;
+	case 3:
+		return 2;
+	default:
+		return 0;
+	}
+}
+
+/* Seed the MIX level from the reference crosspoint of the current
+ * IN/SELECT selection, and return that source index.  Called when MIX
+ * is engaged and whenever SELECT moves while it is on.
+ */
+static int bf_panel_mix_seed(struct snd_usb_babyface *chip)
+{
+	int out = bf_panel_out_pair(chip);
+	int ref = READ_ONCE(chip->panel_in) == 3 ? 4 :
+		  (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) +
+		  (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+	int db2 = bf_fader_raw_to_db2(chip->xpoint[out][ref][0]);
+
+	/* A muted crosspoint reads as -inf, six steps below the curve's
+	 * first point; seeding there would burn six clockwise clicks
+	 * before anything is audible.  Start at the first point instead.
+	 */
+	if (db2 < bf_fader_curve[0].db2)
+		db2 = bf_fader_curve[0].db2;
+	chip->panel_mix_db2 = db2;
+	return ref;
+}
+
+/* The kernel driver plays the TotalMix role for the MIX button (the
+ * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs
+ * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT-
+ * chosen channel(s) of the IN-selected pair, into the OUT-selected
+ * output's crosspoint block - the STANDARD map only (cap_mix.pcap /
+ * cap_select2.pcap, no low-map mirror).  Mirrors the change into the
+ * xpoint cache so the ALSA controls follow the wheel.  Takes the mutex
+ * (the 0x12 writes cycle the transaction flag like the mixer puts).
+ */
+static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = bf_panel_out_pair(chip);
+	u8 targets[2];
+	int n = 0;
+	int db2;
+	u16 raw, flag;
+	int i;
+
+	/* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT
+	 * steps left/right/both; none = nothing selected = no-op wheel).
+	 * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4.
+	 */
+	if (READ_ONCE(chip->panel_in) == 3) {
+		targets[0] = 4;		/* Opt: the AS1/2 pair */
+		n = 1;
+	} else if (READ_ONCE(chip->panel_select) != 3) {
+		int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
+
+		targets[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+		n = 1;
+		if (READ_ONCE(chip->panel_select) == 2)
+			targets[n++] = base + 1;
+	} else {
+		bf_panel_sel_hint(chip);
+	}
+
+	guard(mutex)(&chip->mutex);
+	db2 = clamp(chip->panel_mix_db2 + delta, BF_FADER_DB2_INF, 12);
+	chip->panel_mix_db2 = db2;
+	raw = bf_fader_db2_to_raw(db2);
+	for (i = 0; i < n; i++) {
+		const struct bf_source *s = &bf_sources[targets[i]];
+
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+				(BF_REG_XPOINT_BASE_L + BF_REG_XPOINT_STRIDE * out +
+				 s->idx_l) | flag);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+				(BF_REG_XPOINT_BASE_R + BF_REG_XPOINT_STRIDE * out +
+				 s->idx_r) | flag);
+		chip->xpoint[out][targets[i]][0] = raw;
+		chip->xpoint[out][targets[i]][1] = raw;
+		bf_panel_notify_kctl(chip, chip->xpoint_kctl[out][targets[i]]);
+		/* MIX-mode VU display shadow (0x1A 0x000A+mic): TotalMix
+		 * mirrors the monitoring level into the panel display family
+		 * (cap_mix/cap_panel.pcap) - the input VU segments follow it.
+		 * Written only on change (the captures show TotalMix updating
+		 * it on segment crossings).  Law = bf_mix_display (TODO 0g
+		 * pending the exact full-range capture).
+		 */
+		if (targets[i] < 4) {
+			int disp = bf_mix_display(db2);
+
+			if (disp != chip->panel_mix_disp[targets[i]]) {
+				bf_vendor_write(chip, BF_REQ_GAIN,
+						(u16)disp,
+						BF_REG_PANEL_GAIN + targets[i]);
+				chip->panel_mix_disp[targets[i]] = disp;
+			}
+		}
+	}
+}
+
+/* A muted output ignores the wheel, as it does under TotalMix.  The
+ * firmware still moves the analog level by itself, up from the mute
+ * code, so the output would become faintly audible while the mute switch
+ * still reads off: put the mute back.  Caller holds the mutex.
+ */
+static void bf_panel_remute(struct snd_usb_babyface *chip, int out)
+{
+	bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+			BF_REG_MASTER_8 + 2 * out);
+	bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE,
+			BF_REG_MASTER_8 + 2 * out + 1);
+}
+
+/* Write an output's L/R masters (8-bit companions + 16-bit with the
+ * transaction flag) and mirror into the cache - used by the balance
+ * wheel on an unmuted output.  Caller holds the mutex.
+ */
+static void bf_panel_write_master(struct snd_usb_babyface *chip, int out,
+				  u16 l, u16 r)
+{
+	bool muted = chip->muted[out];
+	bool dim = out == 1 && chip->dim;
+	u16 flag;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+	bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, l),
+			BF_REG_MASTER_8 + 2 * out);
+	bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, r),
+			BF_REG_MASTER_8 + 2 * out + 1);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, l),
+			(BF_REG_MASTER_16 + 2 * out) | flag);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, r),
+			(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	chip->master[out][0] = l;
+	chip->master[out][1] = r;
+	/* A Phones change while DIM is engaged re-bases the restore. */
+	if (chip->dim && out == 1) {
+		chip->dim_saved[0] = l;
+		chip->dim_saved[1] = r;
+	}
+	if (chip->master_kctl[out])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->master_kctl[out]->id);
+}
+
+/* Front-panel OUT wheel, measured on hardware 2026-09-17 with a tone
+ * looped from the headphone output into IN3/IN4:
+ *
+ *  - The firmware moves an analog output's level by itself when the
+ *    wheel turns, with its own ~12 ms smoothing, even when the host
+ *    writes nothing.  See bf_out_wheel_step() for the step size.
+ *  - A host write to the 8-bit master (the analog gain) overrides that.
+ *    Writing it on every poll from the host's own count made the level
+ *    saw-tooth by 1-3 dB while turning, heard as heavy zipper noise.
+ *  - The 16-bit master does not affect the analog level, but it is the
+ *    digital outputs' level.  TotalMix writes only the 16-bit during a
+ *    wheel gesture, and the 8-bit once the wheel is at rest.
+ *
+ * So while the wheel turns, only the 16-bit is written, and the cache
+ * follows the firmware's own count so the ALSA controls read the real
+ * level.  A muted output ignores the wheel (see bf_panel_remute()).
+ * Both sides move by the louder side's step, so a balance (hold-SELECT)
+ * is kept.  Same output mapping as the MIX wheel (Phones = canon 1,
+ * Opt = canon 2, else AN1/2).
+ */
+static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out,
+				     u16 l, u16 r)
+{
+	bool muted = chip->muted[out];
+	bool dim = out == 1 && chip->dim;
+	u16 flag;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, l),
+			(BF_REG_MASTER_16 + 2 * out) | flag);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, r),
+			(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	chip->master[out][0] = l;
+	chip->master[out][1] = r;
+	/* A Phones change while DIM is engaged re-bases the restore. */
+	if (chip->dim && out == 1) {
+		chip->dim_saved[0] = l;
+		chip->dim_saved[1] = r;
+	}
+	if (chip->master_kctl[out])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->master_kctl[out]->id);
+}
+
+/* A click that follows the previous one within this gap moves twice as
+ * far.  In the recordings the firmware doubled clicks up to 58 ms apart
+ * and not from 67 ms.  A normal poll every ~21 ms can't tell those
+ * apart, so after an OUT wheel click the panel is polled every
+ * BF_PANEL_FAST_POLL_MS for BF_PANEL_FAST_HOLD_MS, and each click is
+ * dated at the middle of the interval it was seen in.  A wrong guess is
+ * fixed by the resync at rest below.
+ */
+#define BF_OUT_WHEEL_ACCEL_MS	62
+#define BF_PANEL_FAST_POLL_MS	5
+#define BF_PANEL_FAST_HOLD_MS	200
+
+/* Quiet time after the last click before the resync write. */
+#define BF_OUT_WHEEL_RESYNC_MS	150
+
+/* The device's own OUT wheel range: below -90 dB a side is muted.
+ * BF_OUT_WHEEL_MUTED is where a muted louder side is kept.
+ */
+#define BF_OUT_WHEEL_FLOOR	(-180)		/* half-dB */
+#define BF_OUT_WHEEL_MUTED	(BF_OUT_WHEEL_FLOOR - 1)
+/* Lowest level the 8-bit master is known to take (BF_MASTER_8_MIN). */
+#define BF_OUT_WHEEL_8BIT_MIN	(BF_MASTER_8_MIN - BF_MASTER_8_0DB)
+
+/**
+ * bf_out_wheel_step - the firmware's OUT wheel step, in half-dB
+ * @half_db: the louder side's level before the click, in half-dB
+ *
+ * Measured on the headphone output (a tone looped into IN3/IN4, the
+ * driver writing nothing):
+ *
+ *	-9.5 dB and up		0.5 dB per click
+ *	-10 to -25.5 dB		1 dB
+ *	-26 to -41.5 dB		1.5 dB
+ *	-42 to -57.5 dB		2 dB
+ *	-58 dB and down		3 dB
+ *
+ * A fast click moves twice as far.  Both sides move by the step of the
+ * louder one, so a balance is kept.
+ */
+static int bf_out_wheel_step(int half_db)
+{
+	if (half_db >= -19)
+		return 1;
+	if (half_db >= -51)
+		return 2;
+	if (half_db >= -83)
+		return 3;
+	if (half_db >= -115)
+		return 4;
+	return 6;
+}
+
+/* The wheel's view of one side: its own tracked level while the master
+ * is still what the wheel wrote, else the master's level.
+ */
+static int bf_out_wheel_level(struct snd_usb_babyface *chip, int out, int ch)
+{
+	u16 step = chip->master[out][ch];
+
+	if (step == chip->panel_master_last[out][ch])
+		return chip->panel_out_hdb[out][ch];
+	if (!step)
+		return BF_OUT_WHEEL_MUTED;
+	return step - BF_MASTER_STEP_0DB;
+}
+
+/* One click on both sides.  They move together, so a balance is kept.
+ * When the louder side would go below the floor, both stop there and the
+ * louder side mutes; from mute, turning up continues from the floor
+ * (-90 dB up one step is -87).  A quieter side below the floor is muted
+ * but keeps its offset, so the balance comes back as the level rises.
+ */
+static void bf_out_wheel_click(int db[2], int step)
+{
+	int louder = max(db[0], db[1]);
+	int shift = step;
+
+	if (louder < BF_OUT_WHEEL_FLOOR) {
+		if (step < 0)
+			return;
+		shift = BF_OUT_WHEEL_FLOOR - louder + step;
+	} else if (louder + step < BF_OUT_WHEEL_FLOOR) {
+		shift = BF_OUT_WHEEL_MUTED - louder;
+	} else if (louder + step > 12) {
+		shift = 12 - louder;
+	}
+	db[0] += shift;
+	db[1] += shift;
+}
+
+static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = bf_panel_out_pair(chip);
+	int dir = delta > 0 ? 1 : -1;
+	int clicks = abs(delta);
+	ktime_t now = ktime_get();
+	bool fast;
+	int ch, i, step, louder;
+	int db[2];
+	u16 next[2];
+
+	chip->panel_fast_until = jiffies +
+				 msecs_to_jiffies(BF_PANEL_FAST_HOLD_MS);
+	/* The click came some time since the previous poll: take the
+	 * middle.  Clicks after the first in this poll came within one
+	 * interval, so only the first can be slow.  A reversal starts a
+	 * new turn.
+	 */
+	if (chip->panel_poll_t)
+		now = ktime_sub(now,
+				ktime_divns(ktime_sub(now, chip->panel_poll_t), 2));
+
+	guard(mutex)(&chip->mutex);
+	if (chip->muted[out]) {
+		bf_panel_remute(chip, out);
+		return;
+	}
+	fast = chip->panel_out_wheel_t &&
+	       dir == chip->panel_out_wheel_dir &&
+	       out == chip->panel_out_wheel_out &&
+	       ktime_ms_delta(now, chip->panel_out_wheel_t) <
+	       BF_OUT_WHEEL_ACCEL_MS;
+	for (ch = 0; ch < 2; ch++)
+		db[ch] = bf_out_wheel_level(chip, out, ch);
+	for (i = 0; i < clicks; i++) {
+		/* Muted, the louder side counts as the floor. */
+		louder = max3(db[0], db[1], BF_OUT_WHEEL_FLOOR);
+		step = dir * bf_out_wheel_step(louder);
+		if (fast || i)
+			step *= 2;
+		bf_out_wheel_click(db, step);
+	}
+	for (ch = 0; ch < 2; ch++)
+		next[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 :
+			   db[ch] + BF_MASTER_STEP_0DB;
+	bf_panel_out_wheel_write(chip, out, next[0], next[1]);
+	for (ch = 0; ch < 2; ch++) {
+		chip->panel_master_last[out][ch] = next[ch];
+		chip->panel_out_hdb[out][ch] = db[ch];
+	}
+	chip->panel_out_wheel_t = now;
+	chip->panel_out_wheel_dir = dir;
+	chip->panel_out_wheel_out = out;
+	chip->panel_out_resync = true;
+}
+
+/* Once the wheel has rested, write the wheel's level to the 8-bit (and
+ * 16-bit) master, as TotalMix does at the end of a gesture.  The level
+ * already follows the firmware's step rule, so this normally changes
+ * nothing audible.  It fixes the rare click whose speed the driver
+ * guessed wrong: one zone step per such click, smoothed by the
+ * firmware.  Nothing is written below -64 dB, where the 8-bit codes the
+ * device takes are unknown, nor after a mixer application has set the
+ * master since (that write already set the device).
+ */
+static void bf_panel_out_resync(struct snd_usb_babyface *chip)
+{
+	int out, ch;
+	int db[2];
+	u16 flag;
+
+	if (!chip->panel_out_resync ||
+	    ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) <
+	    BF_OUT_WHEEL_RESYNC_MS)
+		return;
+
+	guard(mutex)(&chip->mutex);
+	chip->panel_out_resync = false;
+	out = chip->panel_out_wheel_out;
+	for (ch = 0; ch < 2; ch++) {
+		if (chip->master[out][ch] != chip->panel_master_last[out][ch])
+			return;
+		db[ch] = chip->panel_out_hdb[out][ch];
+		if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
+			return;
+	}
+	if (!chip->muted[out]) {
+		bool dim = out == 1 && chip->dim;
+
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		bf_vendor_write(chip, BF_REQ_GAIN,
+				bf_out_reg8(false, dim, chip->master[out][0]),
+				BF_REG_MASTER_8 + 2 * out);
+		bf_vendor_write(chip, BF_REQ_GAIN,
+				bf_out_reg8(false, dim, chip->master[out][1]),
+				BF_REG_MASTER_8 + 2 * out + 1);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				bf_out_reg16(false, dim, chip->master[out][0]),
+				(BF_REG_MASTER_16 + 2 * out) | flag);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT,
+				bf_out_reg16(false, dim, chip->master[out][1]),
+				(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	}
+}
+
+/* IN-mode wheel: the gain of the SELECT-chosen channel(s) of the
+ * IN-selected pair, +/-1 dB per click (manual sec. 5.1: SELECT steps
+ * left/right/both, then the wheel changes the gain).  Writes the PANEL
+ * gain registers 0x1A 0x000A+mic (cap_select.pcap 2026-08-24 - the
+ * "ADC gain" family, which drives the same preamp as the GUI
+ * 0x0000+mic; the cache tracks the raw either way).  Opt has no
+ * preamp and SELECT None = no target.
+ */
+static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	u8 mics[2];
+	int n = 0;
+	int i;
+
+	if (READ_ONCE(chip->panel_in) == 3)
+		return;
+	if (READ_ONCE(chip->panel_select) == 3) {
+		bf_panel_sel_hint(chip);
+		return;
+	}
+	{
+		int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
+
+		mics[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+		n = 1;
+		if (READ_ONCE(chip->panel_select) == 2)
+			mics[n++] = base + 1;
+	}
+
+	guard(mutex)(&chip->mutex);
+	for (i = 0; i < n; i++) {
+		int mic = mics[i];
+		int db = clamp((int)chip->gain[mic] + delta,
+				0, bf_gain_max_db(mic));
+		u8 raw = bf_gain_raw(mic, db);
+
+		bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic);
+		if (db != chip->gain[mic]) {
+			chip->gain[mic] = db;
+			bf_panel_notify_kctl(chip, chip->gain_kctl[mic]);
+		}
+	}
+}
+
+/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output
+ * Balance"): moves the stereo image of the OUT-selected output by
+ * attenuating one side.  The louder side stays where it is; the other
+ * moves by the OUT wheel's step for its distance below the louder one
+ * (bf_out_wheel_step()), so the image moves in 0.5 dB steps near the
+ * centre and faster towards one side, and that side mutes below the
+ * wheel's floor.  The balance is read from the L/R masters, so the
+ * gesture needs no state of its own, and the OUT wheel above moves both
+ * sides by the same step, which keeps it.
+ */
+static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = bf_panel_out_pair(chip);
+	int db[2], ch, i, louder, bal, lim;
+
+	guard(mutex)(&chip->mutex);
+	if (chip->muted[out]) {
+		bf_panel_remute(chip, out);
+		return;
+	}
+	/* Read under the lock so the L/R pair is consistent with the
+	 * master/mute/dim writers (they update chip->master[] under the
+	 * same mutex).
+	 */
+	for (ch = 0; ch < 2; ch++)
+		db[ch] = chip->master[out][ch] ?
+			 chip->master[out][ch] - BF_MASTER_STEP_0DB :
+			 BF_OUT_WHEEL_MUTED;
+	louder = max(db[0], db[1]);
+	if (louder < BF_OUT_WHEEL_FLOOR)
+		return;
+	/* bal > 0: the left side is the quieter one, the image is right. */
+	bal = db[1] - db[0];
+	for (i = 0; i < abs(delta); i++)
+		bal += (delta > 0 ? 1 : -1) * bf_out_wheel_step(-abs(bal));
+	lim = louder - BF_OUT_WHEEL_MUTED;
+	bal = clamp(bal, -lim, lim);
+	db[0] = bal > 0 ? louder - bal : louder;
+	db[1] = bal < 0 ? louder + bal : louder;
+	for (ch = 0; ch < 2; ch++)
+		db[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 :
+			 db[ch] + BF_MASTER_STEP_0DB;
+
+	bf_panel_write_master(chip, out, db[0], db[1]);
+}
+
+/* SET press (byte3 0x42 flash): toggle 48V phantom on the
+ * SELECT-chosen mic(s) of the IN-selected pair.  The hardware only
+ * does this in standalone mode (online, TotalMix ignores SET - no USB
+ * write in the captures), but the driver IS the host: it writes the
+ * preamp state itself and the P48 LEDs follow (the tuxmix-core
+ * emulator, hardware-verified).  Restricted to IN mode + Ch1/2 (the
+ * phantom-capable pair); Opt/Ch3/4 and SELECT None = no target.
+ */
+static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
+{
+	u16 bits = 0;
+	u16 old;
+	int m;
+
+	if (READ_ONCE(chip->panel_mix) || READ_ONCE(chip->panel_in) != 1)
+		return;
+	if (READ_ONCE(chip->panel_select) == 3) {
+		bf_panel_sel_hint(chip);
+		return;
+	}
+	if (READ_ONCE(chip->panel_select) != 1)
+		bits |= BF_PREAMP_48V_MIC1;
+	if (READ_ONCE(chip->panel_select) != 0)
+		bits |= BF_PREAMP_48V_MIC2;
+
+	guard(mutex)(&chip->mutex);
+	old = chip->preamp;
+	/* One channel selected: toggle it.  Both selected: ALIGN both to
+	 * the same state, so repeated SET presses cycle all-on <-> all-off
+	 * (a mixed phantom state cannot persist with both selected).
+	 */
+	if (READ_ONCE(chip->panel_select) == 2) {
+		if ((chip->preamp & bits) == bits)
+			chip->preamp &= ~bits;
+		else
+			chip->preamp |= bits;
+	} else {
+		chip->preamp ^= bits;
+	}
+	if (bf_preamp_state_write(chip) < 0)
+		chip->preamp = old;
+	if ((chip->preamp ^ old) & BF_PREAMP_48V_MIC1)
+		bf_panel_notify_kctl(chip, chip->phantom_kctl[0]);
+	if ((chip->preamp ^ old) & BF_PREAMP_48V_MIC2)
+		bf_panel_notify_kctl(chip, chip->phantom_kctl[1]);
+	for (m = 0; m < 4; m++)
+		chip->panel_mix_disp[m] = 0;
+}
+
+static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl)
+{
+	if (chip->panel_kctl[ctl])
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->panel_kctl[ctl]->id);
+}
+
+/* One 0x17 read + decode.  Called from the poll work.  The decoded state
+ * (panel_button/wheel/in/out/select/mix/dim) is also read by the control
+ * get callbacks, and panel_select is written by the SELECT control's
+ * put(): those fields are shared between the worker and the ALSA control
+ * layer, so every access to them goes through READ_ONCE()/WRITE_ONCE(),
+ * which keeps the lock-free access defined and tear-free rather than a
+ * plain data race (the worker is the only writer of the other fields).
+ */
+static void bf_panel_tick(struct snd_usb_babyface *chip)
+{
+	u8 st[4];
+	int delta, in, out;
+	bool dim;
+	u8 cls, pcls;
+	int btn;
+	bool mix_flash, fader_now;
+
+	if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0)
+		return;	/* device gone / busy - retry next tick */
+
+	if (!chip->panel_seen) {
+		chip->panel_seen = true;
+		memcpy(chip->panel_prev, st, sizeof(st));
+		/* Seed the state controls from the first snapshot. */
+		in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);
+		if (in)
+			WRITE_ONCE(chip->panel_in, in);
+		/* The IN pair is only known from here on: pick up its
+		 * selection (kept across a suspend, or set by alsactl).
+		 */
+		bf_panel_sel_load(chip);
+		out = bf_panel_out_decode(st[1] & 0x07);
+		if (out)
+			WRITE_ONCE(chip->panel_out, out);
+		WRITE_ONCE(chip->panel_mix, !!(st[0] & 0x80));
+		WRITE_ONCE(chip->panel_saw_fader, (st[2] >> 4) == 0x0);
+		WRITE_ONCE(chip->panel_dim, !!(st[1] & 0x20));
+		return;
+	}
+
+	/* Button flash (byte3 over the 0x40 idle base).  Notify on the
+	 * edge, as the other flash-driven controls do, so a subscriber sees
+	 * each press once rather than on every poll while it is held.
+	 */
+	btn = bf_panel_button_decode(st[3]);
+	if (btn && chip->panel_prev[3] != st[3]) {
+		WRITE_ONCE(chip->panel_button, btn);
+		bf_panel_notify(chip, BF_PANEL_KCTL_BUTTON);
+	}
+
+	/* Wheel: signed 4-bit wrap delta of the byte2 low nibble - only
+	 * while the mode class is unchanged.  A mode switch (IN 0x4x ->
+	 * fader 0x0x on a MIX press, or the OUT counter carrying 0x8F ->
+	 * 0x90 - the OUT counter is a full byte, cap_set2.pcap) must not
+	 * be read as a wheel jump.  Class: 0 = fader (0x0x), 1 = OUT
+	 * (0x8x/0x9x), 2 = IN (0x4x/0x5x/0x6x).
+	 */
+	cls = (st[2] >> 4) == 0x8 || (st[2] >> 4) == 0x9 ? 1 :
+	      (st[2] >> 4) == 0x0 ? 0 : 2;
+	pcls = (chip->panel_prev[2] >> 4) == 0x8 ||
+	       (chip->panel_prev[2] >> 4) == 0x9 ? 1 :
+	       (chip->panel_prev[2] >> 4) == 0x0 ? 0 : 2;
+	delta = (int)(st[2] & 0x0f) - (int)(chip->panel_prev[2] & 0x0f);
+	if (delta > 8)
+		delta -= 16;
+	else if (delta < -8)
+		delta += 16;
+	if (delta && cls == pcls) {
+		WRITE_ONCE(chip->panel_wheel,
+			   clamp(READ_ONCE(chip->panel_wheel) + delta,
+				 SHRT_MIN, SHRT_MAX));
+		bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL);
+		/* Wheel by mode (LINUX-VALIDATION sec. 12, the TotalMix
+		 * emulator): MIX -> monitoring level, OUT (0x8x/0x9x) -> the
+		 * selected output master (or its balance while SELECT is
+		 * held), IN (0x4x/0x5x/0x6x) -> the SELECT-chosen preamp
+		 * gain.
+		 */
+		if (READ_ONCE(chip->panel_mix))
+			bf_panel_mix_wheel(chip, delta);
+		else if (chip->panel_sel_hold >= 10 && cls == 1)
+			bf_panel_balance_wheel(chip, delta);
+		else if (cls == 1)
+			bf_panel_out_wheel(chip, delta);
+		else if (cls == 2)
+			bf_panel_gain_wheel(chip, delta);
+	}
+	bf_panel_out_resync(chip);
+
+	/* Selections - keep the previous when the field is not in range
+	 * (the fader-mode readback drops the IN position bits).
+	 */
+	in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);
+	if (in && in != READ_ONCE(chip->panel_in)) {
+		WRITE_ONCE(chip->panel_in, in);
+		/* The unit keeps one SELECT selection per IN pair, and an
+		 * IN switch does not clear it (hardware-verified
+		 * 2026-10-02: back on Ch 1/2 after a trip through Ch 3/4,
+		 * the first SELECT press showed the L it had left, while
+		 * Ch 3/4 showed nothing).  Switch to the new pair's.
+		 */
+		bf_panel_sel_load(chip);
+		/* The LEDs go dark on an IN switch: the next SELECT press
+		 * only shows the selection again (no step), the one after
+		 * that steps it (user-verified 2026-08-28).
+		 */
+		chip->panel_select_armed = false;
+		bf_panel_notify(chip, BF_PANEL_KCTL_IN);
+	}
+	out = bf_panel_out_decode(st[1] & 0x07);
+	if (out && out != READ_ONCE(chip->panel_out)) {
+		WRITE_ONCE(chip->panel_out, out);
+		bf_panel_notify(chip, BF_PANEL_KCTL_OUT);
+	}
+
+	/* SELECT press cycles the channel selection L -> R -> both -> none
+	 * -> L (manual sec. 5.1).  The state is NOT in the readback
+	 * (panelprobe 2026-08-24), so it is tracked host-side.
+	 */
+	if (st[3] == BF_PANEL_FLASH_SELECT &&
+	    chip->panel_prev[3] != BF_PANEL_FLASH_SELECT &&
+	    chip->panel_select_known) {
+		if (!chip->panel_select_armed) {
+			/* Disarmed (IN switch since the last step): the press
+			 * only shows the selection - the device steps it on
+			 * the NEXT press (user-verified 2026-08-28).
+			 */
+			chip->panel_select_armed = true;
+		} else {
+			int sel = (READ_ONCE(chip->panel_select) + 1) & 3;
+			struct snd_kcontrol *kctl =
+				chip->panel_select_kctl[READ_ONCE(chip->panel_in) - 1];
+
+			WRITE_ONCE(chip->panel_select, sel);
+			chip->panel_sel[READ_ONCE(chip->panel_in) - 1] = sel;
+			if (kctl)
+				snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+					       &kctl->id);
+			if (READ_ONCE(chip->panel_mix)) {
+				scoped_guard(mutex, &chip->mutex)
+					bf_panel_mix_seed(chip);
+			}
+		}
+	}
+	/* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output
+	 * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz
+	 * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and
+	 * byte0 does NOT gain the 0x80 engaged bit - so the duration is
+	 * the only discriminator: >= 10 ticks (200 ms at 50 Hz) = held.
+	 */
+	if (st[3] == BF_PANEL_FLASH_SELECT)
+		chip->panel_sel_hold++;
+	else
+		chip->panel_sel_hold = 0;
+
+	/* SET (A) press: host-side 48V phantom toggle on the
+	 * SELECT-chosen mic(s) (see bf_panel_set_phantom).
+	 */
+	if (st[3] == BF_PANEL_FLASH_SET &&
+	    chip->panel_prev[3] != BF_PANEL_FLASH_SET)
+		bf_panel_set_phantom(chip);
+
+	/* DIM is a software-assignable button. Report presses without
+	 * choosing a monitor output or changing any gain in the driver.
+	 */
+	if (st[3] == BF_PANEL_FLASH_DIM &&
+	    chip->panel_prev[3] != BF_PANEL_FLASH_DIM) {
+		chip->dim_press_count = (chip->dim_press_count + 1) & 0x7fffffff;
+		if (chip->dim_press_kctl)
+			snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+				       &chip->dim_press_kctl->id);
+	}
+
+	/* MIX (fader mode) - HOST-latched, like TotalMix (cap_mix.pcap,
+	 * cap_select2.pcap): the raw press readback is `0D 0D 41 44` -
+	 * byte3 flash 0x44, NO engaged bit, byte2 still in the current
+	 * mode.  The host acks the flash with `0x17 0x8480 0x8C80` -> the
+	 * device latches fader mode (byte0/1 gain the 0x80 bit, byte2 =
+	 * 0x00+n counter) and STAYS there after the physical release; the
+	 * SECOND 0x44 flash exits it (`0x17 0x0400 0x8000` + `0x8080`).
+	 * A mode button (IN/OUT/SET) pressed during MIX makes the device
+	 * leave fader mode by itself -> same exit writes (the user: IN
+	 * must return to gain control).  `panel_saw_fader` gates the
+	 * device-driven exit so a pre-ack readback (byte2 still 0x4x
+	 * while the 0x44 flash shows) never ends MIX before it started.
+	 */
+	mix_flash = st[3] == BF_PANEL_FLASH_MIX &&
+		    chip->panel_prev[3] != BF_PANEL_FLASH_MIX;
+	fader_now = (st[2] >> 4) == 0x0;
+
+	if (mix_flash) {
+		if (READ_ONCE(chip->panel_mix)) {
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+			WRITE_ONCE(chip->panel_mix, false);
+			WRITE_ONCE(chip->panel_saw_fader, false);
+		} else {
+			int ref;
+			int m;
+
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
+			WRITE_ONCE(chip->panel_mix, true);
+			/* Seed the monitoring level at the reference
+			 * crosspoint's current value so the first wheel
+			 * click doesn't jump from -inf (the reference =
+			 * the first SELECT-chosen channel of the IN pair;
+			 * Opt = the AS1/2 pair).
+			 */
+			ref = bf_panel_mix_seed(chip);
+			/* Seed the VU display shadow at the CURRENT level
+			 * (cap_panel.pcap: TotalMix writes the display value of
+			 * the current fader on engage - 10 in that session -
+			 * not a hard 0; cap_mix's 0 was because the fader sat
+			 * at the bottom).  Only the channels the wheel can move.
+			 */
+			for (m = 0; m < 4; m++)
+				chip->panel_mix_disp[m] = 0;
+			if (ref < 4) {
+				int disp = bf_mix_display(chip->panel_mix_db2);
+
+				bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp,
+						BF_REG_PANEL_GAIN + ref);
+				chip->panel_mix_disp[ref] = disp;
+			}
+		}
+		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+	}
+	if (fader_now) {
+		/* Do not re-arm the gate on a tick that just left MIX:
+		 * the device's byte2 can still show fader mode for a
+		 * frame or two after the exit writes.
+		 */
+		if (READ_ONCE(chip->panel_mix))
+			WRITE_ONCE(chip->panel_saw_fader, true);
+	} else if (READ_ONCE(chip->panel_mix) && READ_ONCE(chip->panel_saw_fader) &&
+		   st[3] != BF_PANEL_FLASH_MIX) {
+		/* device left fader mode by itself (IN/OUT/SET press) */
+		bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+		bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+		WRITE_ONCE(chip->panel_mix, false);
+		WRITE_ONCE(chip->panel_saw_fader, false);
+		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+	}
+
+	dim = !!(st[1] & 0x20);
+	if (dim != READ_ONCE(chip->panel_dim)) {
+		WRITE_ONCE(chip->panel_dim, dim);
+		bf_panel_notify(chip, BF_PANEL_KCTL_DIM);
+	}
+
+	memcpy(chip->panel_prev, st, sizeof(st));
+}
+
+void babyface_panel_work(struct work_struct *work)
+{
+	struct snd_usb_babyface *chip = container_of(work,
+			struct snd_usb_babyface, panel_work.work);
+	unsigned int ms;
+
+	if (chip->shutdown)
+		return;
+	bf_panel_tick(chip);
+	chip->panel_poll_t = ktime_get();
+	/* Poll fast for a moment after an OUT wheel click. */
+	ms = time_before(jiffies, chip->panel_fast_until) ?
+	     BF_PANEL_FAST_POLL_MS : chip->panel_poll_ms;
+	schedule_delayed_work(&chip->panel_work, msecs_to_jiffies(ms));
+}
+
+void babyface_panel_start(struct snd_usb_babyface *chip)
+{
+	chip->panel_seen = false;
+	/* The SELECT channel selection is not in the readback, so the
+	 * driver follows it from the presses.  The unit keeps one per IN
+	 * pair across a driver reload and even a power cycle, with the LEDs
+	 * dark until the next SELECT press, which shows it again instead of
+	 * stepping (hardware-verified 2026-09-29: after an unplug, the
+	 * first press lit both channels).  What panel_sel[] holds comes
+	 * from before a suspend, from alsactl after a boot, or is not
+	 * known, and an unknown selection targets nothing: SET, the IN
+	 * wheel and the MIX wheel then do nothing rather than act on the
+	 * wrong channel, until "Front Panel Select" is set to what the LEDs
+	 * show.
+	 */
+	bf_panel_sel_load(chip);
+	chip->panel_select_armed = false;
+	chip->panel_start = jiffies;
+	chip->panel_fast_until = jiffies;
+	chip->panel_poll_t = 0;
+	/* No master holds 0xffff: the wheel starts from the masters. */
+	memset(chip->panel_master_last, 0xff, sizeof(chip->panel_master_last));
+	schedule_delayed_work(&chip->panel_work, 0);
+}
+
+void babyface_panel_stop(struct snd_usb_babyface *chip)
+{
+	cancel_delayed_work_sync(&chip->panel_work);
+}
+
+/* The button/wheel controls hold the LATEST state and are NOT consumed
+ * on read: wireplumber subscribes to every notifying control and reads
+ * it, so a clear-on-get would let another reader eat the event.  Each
+ * consumer tracks its own baseline and acts on changes (the button is a
+ * last-press code, the wheel an accumulated signed delta).  VOLATILE
+ * keeps alsactl from caching them.
+ */
+static int bf_panel_button_info(struct snd_kcontrol *kctl,
+				struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = BF_PANEL_BTN_DIM;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_panel_button_get(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = READ_ONCE(chip->panel_button);
+	return 0;
+}
+
+static int bf_panel_wheel_info(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = SHRT_MIN;
+	uinfo->value.integer.max = SHRT_MAX;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_panel_wheel_get(struct snd_kcontrol *kctl,
+			      struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = READ_ONCE(chip->panel_wheel);
+	return 0;
+}
+
+static int bf_panel_in_info(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_in_texts);
+}
+
+static int bf_panel_in_get(struct snd_kcontrol *kctl,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = READ_ONCE(chip->panel_in);
+	return 0;
+}
+
+static int bf_panel_out_info(struct snd_kcontrol *kctl,
+			     struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_out_texts);
+}
+
+static int bf_panel_out_get(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.enumerated.item[0] = READ_ONCE(chip->panel_out);
+	return 0;
+}
+
+static int bf_panel_select_info(struct snd_kcontrol *kctl,
+				struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, 5, bf_panel_select_texts);
+}
+
+static int bf_panel_select_get(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	s8 sel = READ_ONCE(chip->panel_sel[kctl->private_value]);
+
+	ucontrol->value.enumerated.item[0] = sel >= 0 ? sel : 4;
+	return 0;
+}
+
+/* Writable so software (or the user, after a driver reload) can
+ * re-sync the host-tracked SELECT state to the physical card - the
+ * L/R/both/none state is NOT in the 0x17 readback, and the unit keeps
+ * one per IN pair across reloads and power cycles, so the driver cannot
+ * know it after a load.  Writing what the LEDs show for the pair now
+ * selected tells it (TotalMix parity: it also lets software select
+ * channels directly).
+ */
+static int bf_panel_select_put(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	unsigned int v = ucontrol->value.enumerated.item[0];
+	int pair = kctl->private_value;
+	/* alsactl restores the values of its last store shortly after
+	 * every probe.
+	 */
+	bool restore = time_is_after_jiffies(chip->panel_start + 3 * HZ);
+	s8 sel = v < 4 ? v : -1;
+
+	if (v > 4)
+		return -EINVAL;
+	/* After a boot they are what there is.  After a re-probe the unit
+	 * may have taken SELECT presses since that store, and a wrong
+	 * selection makes SET and the wheels act on a channel that is not
+	 * the lit one: leave the pair unknown, as for a never-seen pair.
+	 */
+	if (restore && chip->reprobe)
+		return 0;
+
+	guard(mutex)(&chip->mutex);
+	if (pair == READ_ONCE(chip->panel_in) - 1 && !restore && sel >= 0)
+		/* Set while the LEDs show the selection: the next press steps
+		 * it.
+		 */
+		chip->panel_select_armed = true;
+	if (sel == chip->panel_sel[pair])
+		return 0;
+	chip->panel_sel[pair] = sel;
+	if (pair == READ_ONCE(chip->panel_in) - 1) {
+		bf_panel_sel_load(chip);
+		if (READ_ONCE(chip->panel_mix))
+			bf_panel_mix_seed(chip);
+	}
+	return 1;
+}
+
+/* Shared boolean get - private_value selects mix (0) / dim (1). */
+static int bf_panel_bool_get(struct snd_kcontrol *kctl,
+			     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] =
+		kctl->private_value ? READ_ONCE(chip->panel_dim) : READ_ONCE(chip->panel_mix);
+	return 0;
+}
+
+static int bf_dim_press_info(struct snd_kcontrol *kctl,
+			     struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 0x7fffffff;
+	return 0;
+}
+
+static int bf_dim_press_get(struct snd_kcontrol *kctl,
+			    struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+
+	ucontrol->value.integer.value[0] = READ_ONCE(chip->dim_press_count);
+	return 0;
+}
+
+int babyface_create_panel(struct snd_usb_babyface *chip)
+{
+	struct snd_kcontrol *kctl;
+	int err, pair;
+
+	memset(chip->panel_kctl, 0, sizeof(chip->panel_kctl));
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Button",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_button_info,
+		.get = bf_panel_button_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_BUTTON] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Wheel",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_wheel_info,
+		.get = bf_panel_wheel_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_WHEEL] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel In",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_in_info,
+		.get = bf_panel_in_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_IN] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Out",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_panel_out_info,
+		.get = bf_panel_out_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_OUT] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Mix",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = snd_ctl_boolean_mono_info,
+		.get = bf_panel_bool_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_MIX] = kctl;
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Front Panel Dim",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = snd_ctl_boolean_mono_info,
+		.get = bf_panel_bool_get,
+		.private_value = 1,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->panel_kctl[BF_PANEL_KCTL_DIM] = kctl;
+
+	/* One per IN pair (index 0 = Ch 1/2, 1 = Ch 3/4, 2 = Opt): the
+	 * unit keeps one selection per pair, and a plain control, not a
+	 * volatile one, is what alsactl keeps across boots.  Named apart from
+	 * the single volatile "Front Panel Select" this replaces, whose stored
+	 * value alsactl must not apply.
+	 */
+	for (pair = 0; pair < 3; pair++) {
+		kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+			.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+			.name = "Front Panel Selection",
+			.index = pair,
+			.info = bf_panel_select_info,
+			.get = bf_panel_select_get,
+			.put = bf_panel_select_put,
+			.private_value = pair,
+		}, chip);
+		err = snd_ctl_add(chip->card, kctl);
+		if (err < 0)
+			return err;
+		chip->panel_select_kctl[pair] = kctl;
+	}
+
+	kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "DIM Button Press Count",
+		.access = SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info = bf_dim_press_info,
+		.get = bf_dim_press_get,
+	}, chip);
+	err = snd_ctl_add(chip->card, kctl);
+	if (err < 0)
+		return err;
+	chip->dim_press_kctl = kctl;
+
+	return 0;
+}
sound/usb/babyfacepro/babyfacepro.c +68 -12

159 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.h +125 -1

162 lines not on this page · raw message

Signature · 1 line
2.56.0
#12 lore raw

Re: [PATCH v8 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls

Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider: - [High] ALSA: usb: babyfacepro: unlocked selection and MIX state in bf_panel_tick() - [Medium] ALSA: usb…

Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: unlocked selection and MIX state in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: broken panel_start init in babyface_panel_start()
- [Medium] ALSA: usb: babyfacepro: wrong AN1/2 flags in bf_panel_mix_wheel()
- [Medium] ALSA: usb: babyfacepro: broken MIX selection and VU seed in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: stale OUT wheel state in bf_out_wheel_level()
- [Medium] ALSA: usb: babyfacepro: unhandled unknown panel_in in bf_panel_select_put()
- [Medium] ALSA: usb: babyfacepro: unseeded panel_mix_db2 on first tick in bf_panel_tick()
- [Medium] ALSA: usb: babyfacepro: tick-based SELECT hold detection in bf_panel_tick()
- [Low] ALSA: usb: babyfacepro: undocumented panel_poll_ms and urb_split params
- [Low] ALSA: usb: babyfacepro: panel_wheel saturates at SHRT_MIN/MAX in bf_panel_tick() --

commit 743b738314668be227407d88e571ec6f84054392
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: babyfacepro: add the front-panel poll and controls

This commit adds periodic polling of the Babyface Pro front-panel readback register (request 0x17) and exposes the decoded button, wheel, IN/OUT/MIX/DIM, and per-pair channel selection states as ALSA mixer controls. It also implements host-driven handling for the front-panel rotary wheel and SET/MIX buttons.

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
index 5482b6f09b73a..51cf8c7bd4357 100644

[ ... ]

Quoted 9 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +9 -0
+/* 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);
+}

[ ... ]

Quoted 47 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +47 -0
+static int bf_panel_mix_seed(struct snd_usb_babyface *chip)
+{
+	int out = bf_panel_out_pair(chip);
+	int ref = READ_ONCE(chip->panel_in) == 3 ? 4 :
+		  (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) +
+		  (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);
+	int db2 = bf_fader_raw_to_db2(chip->xpoint[out][ref][0]);
+
+	/* A muted crosspoint reads as -inf, six steps below the curve's
+	 * first point; seeding there would burn six clockwise clicks
+	 * before anything is audible.  Start at the first point instead.
+	 */
+	if (db2 < bf_fader_curve[0].db2)
+		db2 = bf_fader_curve[0].db2;
+	chip->panel_mix_db2 = db2;
+	return ref;
+}
+
+/* The kernel driver plays the TotalMix role for the MIX button (the
+ * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs
+ * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT-
+ * chosen channel(s) of the IN-selected pair, into the OUT-selected
+ * output's crosspoint block - the STANDARD map only (cap_mix.pcap /
+ * cap_select2.pcap, no low-map mirror).  Mirrors the change into the
+ * xpoint cache so the ALSA controls follow the wheel.  Takes the mutex
+ * (the 0x12 writes cycle the transaction flag like the mixer puts).
+ */
+static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = bf_panel_out_pair(chip);
+	u8 targets[2];
+	int n = 0;
+	int db2;
+	u16 raw, flag;
+	int i;
+
+	/* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT
+	 * steps left/right/both; none = nothing selected = no-op wheel).
+	 * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4.
+	 */
+	if (READ_ONCE(chip->panel_in) == 3) {
+		targets[0] = 4;		/* Opt: the AS1/2 pair */
+		n = 1;
+	} else if (READ_ONCE(chip->panel_select) != 3) {
+		int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0;
+
+		targets[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0);

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2277

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		n = 1;
+		if (READ_ONCE(chip->panel_select) == 2)
+			targets[n++] = base + 1;
+	} else {
+		bf_panel_sel_hint(chip);
+	}
+
+	guard(mutex)(&chip->mutex);
+	db2 = clamp(chip->panel_mix_db2 + delta, BF_FADER_DB2_INF, 12);
+	chip->panel_mix_db2 = db2;

[Severity: Medium]
As reported by Sashiko on v7 [1], in bf_panel_mix_wheel(), checking READ_ONCE(chip->panel_in) == 3 before READ_ONCE(chip->panel_select) != 3 causes the wheel to modify AS1/2 (targets[0] = 4, n = 1) when Opt is selected even if panel_select is 3 (None or Unknown), bypassing panel_sel[2] ("Front Panel Selection" index 2) and bf_panel_sel_hint().

Also, when panel_in != 3 and panel_select == 3, bf_panel_mix_wheel() calls bf_panel_sel_hint(chip) with n == 0, but falls through without returning and still updates chip->panel_mix_db2 on every wheel click.

Should bf_panel_mix_wheel() check for panel_select == 3 and return early after bf_panel_sel_hint(chip) before handling panel_in == 3 or updating chip->panel_mix_db2?

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2289

Quoted 12 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +12 -0
+	raw = bf_fader_db2_to_raw(db2);
+	for (i = 0; i < n; i++) {
+		const struct bf_source *s = &bf_sources[targets[i]];
+
+		flag = bf_flag_cycle[chip->flag_cnt];
+		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+				(BF_REG_XPOINT_BASE_L + BF_REG_XPOINT_STRIDE * out +
+				 s->idx_l) | flag);
+		bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw,
+				(BF_REG_XPOINT_BASE_R + BF_REG_XPOINT_STRIDE * out +
+				 s->idx_r) | flag);

[Severity: Medium]
In bf_xpoint_write() and bf_phase_apply(), the rotating transaction flag bits in bf_flag_cycle[] are only ORed into wIndex when out != 0, because the AN1/2 crosspoint block at 0x0000 is written with flag == 0:

sound/usb/babyfacepro/babyfacepro-ctl.c:bf_xpoint_write() {
	const struct bf_source *s = &bf_sources[src];
	u16 flag = 0;
	int ret;

	if (out) {
		flag = bf_flag_cycle[chip->flag_cnt];
		chip->flag_cnt = (chip->flag_cnt + 1) & 3;
	}
	...
}

Here in bf_panel_mix_wheel(), flag is cycled and ORed into wIndex unconditionally even when out == 0 (Ch 1/2). Can this use bf_xpoint_write() instead of open-coding the BF_REQ_CROSSPOINT writes?

[ ... ]

Quoted 23 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +23 -0
+/* Write an output's L/R masters (8-bit companions + 16-bit with the
+ * transaction flag) and mirror into the cache - used by the balance
+ * wheel on an unmuted output.  Caller holds the mutex.
+ */
+static void bf_panel_write_master(struct snd_usb_babyface *chip, int out,
+				  u16 l, u16 r)
+{
+	bool muted = chip->muted[out];
+	bool dim = out == 1 && chip->dim;
+	u16 flag;
+
+	flag = bf_flag_cycle[chip->flag_cnt];
+	chip->flag_cnt = (chip->flag_cnt + 1) & 3;
+	bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, l),
+			BF_REG_MASTER_8 + 2 * out);
+	bf_vendor_write(chip, BF_REQ_GAIN, bf_out_reg8(muted, dim, r),
+			BF_REG_MASTER_8 + 2 * out + 1);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, l),
+			(BF_REG_MASTER_16 + 2 * out) | flag);
+	bf_vendor_write(chip, BF_REQ_CROSSPOINT, bf_out_reg16(muted, dim, r),
+			(BF_REG_MASTER_16 + 2 * out + 1) | flag);
+	chip->master[out][0] = l;
+	chip->master[out][1] = r;

[ ... ]

Quoted 13 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +13 -0
+/* The wheel's view of one side: its own tracked level while the master
+ * is still what the wheel wrote, else the master's level.
+ */
+static int bf_out_wheel_level(struct snd_usb_babyface *chip, int out, int ch)
+{
+	u16 step = chip->master[out][ch];
+
+	if (step == chip->panel_master_last[out][ch])
+		return chip->panel_out_hdb[out][ch];
+	if (!step)
+		return BF_OUT_WHEEL_MUTED;
+	return step - BF_MASTER_STEP_0DB;
+}

[ ... ]

Quoted 20 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +20 -0
+static void bf_panel_out_resync(struct snd_usb_babyface *chip)
+{
+	int out, ch;
+	int db[2];
+	u16 flag;
+
+	if (!chip->panel_out_resync ||
+	    ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) <
+	    BF_OUT_WHEEL_RESYNC_MS)
+		return;
+
+	guard(mutex)(&chip->mutex);
+	chip->panel_out_resync = false;
+	out = chip->panel_out_wheel_out;
+	for (ch = 0; ch < 2; ch++) {\n> +		if (chip->master[out][ch] != chip->panel_master_last[out][ch])
+			return;
+		db[ch] = chip->panel_out_hdb[out][ch];
+		if (db[ch] < BF_OUT_WHEEL_8BIT_MIN)
+			return;
+	}

[ ... ]

Quoted 8 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +8 -0
+static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta)
+{
+	int out = bf_panel_out_pair(chip);
+	int db[2], ch, i, louder, bal, lim;
+
+	guard(mutex)(&chip->mutex);
+	if (chip->muted[out]) {
+		bf_panel_remute(chip, out);

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2671

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		return;
+	}
+	/* Read under the lock so the L/R pair is consistent with the
+	 * master/mute/dim writers (they update chip->master[] under the
+	 * same mutex).
+	 */
+	for (ch = 0; ch < 2; ch++)
+		db[ch] = chip->master[out][ch] ?
+			 chip->master[out][ch] - BF_MASTER_STEP_0DB :
+			 BF_OUT_WHEEL_MUTED;

[Severity: Medium]
As reported by Sashiko on v7 [2], how is chip->panel_master_last[out] kept in sync when chip->master[out] is modified outside bf_panel_out_wheel()?

1. Neither bf_master_put() nor bf_panel_write_master() (called from bf_panel_balance_wheel()) invalidates chip->panel_master_last[out] to 0xffff. If the OUT wheel previously left a channel muted at step 0 with a sub-floor balance offset in chip->panel_out_hdb[out][ch], and the user then centers or sets the master to 0 via the ALSA control or balance wheel, chip->master[out][ch] == chip->panel_master_last[out][ch] (0 == 0) remains true and the next OUT wheel turn in bf_out_wheel_level() restores the stale sub-floor offset.

2. In bf_panel_balance_wheel(), reading chip->master[out][ch] directly instead of bf_out_wheel_level(chip, out, ch) discards any sub-floor offset currently tracked in chip->panel_out_hdb[out][ch].

3. In bf_panel_out_resync(), tracking only a single scalar chip->panel_out_wheel_out drops the pending 8-bit resync if the user switches outputs and turns the wheel within 150 ms, and returning early from the loop whenever either channel has db[ch] < BF_OUT_WHEEL_8BIT_MIN skips the 8-bit resync write for the louder channel of a panned output as well.

[ ... ]

Quoted 28 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +28 -0
+/* 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;
+	}

[ ... ]

Quoted 24 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +24 -0
+static void bf_panel_set_phantom(struct snd_usb_babyface *chip)
+{
+	u16 bits = 0;
+	u16 old;
+	int m;
+
+	if (READ_ONCE(chip->panel_mix) || READ_ONCE(chip->panel_in) != 1)
+		return;
+	if (READ_ONCE(chip->panel_select) == 3) {
+		bf_panel_sel_hint(chip);
+		return;
+	}
+	if (READ_ONCE(chip->panel_select) != 1)
+		bits |= BF_PREAMP_48V_MIC1;
+	if (READ_ONCE(chip->panel_select) != 0)
+		bits |= BF_PREAMP_48V_MIC2;
+
+	guard(mutex)(&chip->mutex);
+	old = chip->preamp;
+	/* One channel selected: toggle it.  Both selected: ALIGN both to
+	 * the same state, so repeated SET presses cycle all-on <-> all-off
+	 * (a mixed phantom state cannot persist with both selected).
+	 */
+	if (READ_ONCE(chip->panel_select) == 2) {

[ ... ]

Quoted 20 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +20 -0
+static void bf_panel_tick(struct snd_usb_babyface *chip)
+{
+	u8 st[4];
+	int delta, in, out;
+	bool dim;
+	u8 cls, pcls;
+	int btn;
+	bool mix_flash, fader_now;
+
+	if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0)
+		return;	/* device gone / busy - retry next tick */
+
+	if (!chip->panel_seen) {
+		chip->panel_seen = true;
+		memcpy(chip->panel_prev, st, sizeof(st));
+		/* Seed the state controls from the first snapshot. */
+		in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7);\n> +		if (in)
+			WRITE_ONCE(chip->panel_in, in);
+		/* The IN pair is only known from here on: pick up its
+		 * selection (kept across a suspend, or set by alsactl).

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2784

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		 */
+		bf_panel_sel_load(chip);
+		out = bf_panel_out_decode(st[1] & 0x07);
+		if (out)
+			WRITE_ONCE(chip->panel_out, out);
+		WRITE_ONCE(chip->panel_mix, !!(st[0] & 0x80));
+		WRITE_ONCE(chip->panel_saw_fader, (st[2] >> 4) == 0x0);
+		WRITE_ONCE(chip->panel_dim, !!(st[1] & 0x20));
+		return;
+	}

[Severity: Medium]
When the device is already in MIX mode on the first poll tick (!chip->panel_seen with st[0] & 0x80 set), bf_panel_tick() sets chip->panel_mix = true and returns without calling bf_panel_mix_seed(chip). Because chip->panel_mix_db2 is zero-initialized at probe (0 dB unity gain on bf_fader_curve[], whereas the default input crosspoints are muted at -inf / -62 dB), won't the first MIX wheel click jump the monitoring crosspoint from muted directly to ~0 dB?

Also, since the first tick runs asynchronously after snd_card_register() and babyface_resume(), shouldn't changes to panel_in, panel_out, panel_mix, and panel_dim here notify the corresponding ALSA controls?

[ ... ]

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2817

Quoted 5 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +5 -0
+	if (delta && cls == pcls) {
+		WRITE_ONCE(chip->panel_wheel,
+			   clamp(READ_ONCE(chip->panel_wheel) + delta,
+				 SHRT_MIN, SHRT_MAX));
+		bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL);

[Severity: Low]
Unlike the comment in babyfacepro.h ("accumulated wheel delta (consumed on get)"), bf_panel_wheel_get() never resets chip->panel_wheel on read. Because chip->panel_wheel is clamped to SHRT_MIN..SHRT_MAX here in bf_panel_tick(), won't the control permanently saturate once net rotation in one direction reaches 32767 or -32768 clicks?

Quoted 15 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +15 -0
+		/* 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);
+	}

[ ... ]

Quoted 29 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +29 -0
+	/* SELECT press cycles the channel selection L -> R -> both -> none
+	 * -> L (manual sec. 5.1).  The state is NOT in the readback
+	 * (panelprobe 2026-08-24), so it is tracked host-side.
+	 */
+	if (st[3] == BF_PANEL_FLASH_SELECT &&
+	    chip->panel_prev[3] != BF_PANEL_FLASH_SELECT &&
+	    chip->panel_select_known) {
+		if (!chip->panel_select_armed) {
+			/* Disarmed (IN switch since the last step): the press
+			 * only shows the selection - the device steps it on
+			 * the NEXT press (user-verified 2026-08-28).
+			 */
+			chip->panel_select_armed = true;
+		} else {
+			int sel = (READ_ONCE(chip->panel_select) + 1) & 3;
+			struct snd_kcontrol *kctl =
+				chip->panel_select_kctl[READ_ONCE(chip->panel_in) - 1];
+
+			WRITE_ONCE(chip->panel_select, sel);
+			chip->panel_sel[READ_ONCE(chip->panel_in) - 1] = sel;
+			if (kctl)
+				snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+					       &kctl->id);
+			if (READ_ONCE(chip->panel_mix)) {
+				scoped_guard(mutex, &chip->mutex)
+					bf_panel_mix_seed(chip);
+			}
+		}
+	}

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2899

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+	/* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output
+	 * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz
+	 * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and
+	 * byte0 does NOT gain the 0x80 engaged bit - so the duration is
+	 * the only discriminator: >= 10 ticks (200 ms at 50 Hz) = held.
+	 */
+	if (st[3] == BF_PANEL_FLASH_SELECT)
+		chip->panel_sel_hold++;
+	else
+		chip->panel_sel_hold = 0;

[Severity: Medium]
As reported by Sashiko on v7 [3], bf_panel_tick() detects a 200 ms SELECT hold by counting 10 poll ticks (chip->panel_sel_hold >= 10) instead of measuring elapsed time. During the 5 ms fast-poll window after an OUT wheel click (chip->panel_fast_until), 10 ticks elapse in 50 ms (shorter than a normal 100-150 ms tap flash), whereas with a larger panel_poll_ms module parameter (up to 1000 ms), 10 ticks require holding SELECT for up to 10 seconds. In addition, on the initial rising edge of holding SELECT in OUT mode (cls == 1), the block above immediately steps chip->panel_select and chip->panel_sel[]. Should SELECT hold detection use elapsed time (ktime_t or jiffies) and avoid stepping the IN channel selection when holding SELECT in OUT mode?

[ ... ]

Quoted 27 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +27 -0
+	if (mix_flash) {
+		if (READ_ONCE(chip->panel_mix)) {
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000);
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080);
+			WRITE_ONCE(chip->panel_mix, false);
+			WRITE_ONCE(chip->panel_saw_fader, false);
+		} else {
+			int ref;
+			int m;
+
+			bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80);
+			WRITE_ONCE(chip->panel_mix, true);
+			/* Seed the monitoring level at the reference
+			 * crosspoint's current value so the first wheel
+			 * click doesn't jump from -inf (the reference =
+			 * the first SELECT-chosen channel of the IN pair;
+			 * Opt = the AS1/2 pair).
+			 */
+			ref = bf_panel_mix_seed(chip);
+			/* Seed the VU display shadow at the CURRENT level
+			 * (cap_panel.pcap: TotalMix writes the display value of
+			 * the current fader on engage - 10 in that session -
+			 * not a hard 0; cap_mix's 0 was because the fader sat
+			 * at the bottom).  Only the channels the wheel can move.
+			 */
+			for (m = 0; m < 4; m++)
+				chip->panel_mix_disp[m] = 0;

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 2972

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+			if (ref < 4) {
+				int disp = bf_mix_display(chip->panel_mix_db2);
+
+				bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp,
+						BF_REG_PANEL_GAIN + ref);
+				chip->panel_mix_disp[ref] = disp;
+			}
+		}
+		bf_panel_notify(chip, BF_PANEL_KCTL_MIX);
+	}

[Severity: High]
As reported by Sashiko on v7 [4], can concurrent execution of bf_panel_tick() and ALSA control writes in bf_panel_select_put() (or bf_xpoint_put()) race on the selection and MIX state?

1. In bf_panel_select_put(), updates to chip->panel_sel[], chip->panel_select_known, chip->panel_select, chip->panel_select_armed, and bf_panel_mix_seed() are serialized under chip->mutex. However, bf_panel_tick() calls bf_panel_sel_load(), updates chip->panel_select_armed, chip->panel_select, and chip->panel_sel[] on IN switches and SELECT presses, and calls bf_panel_mix_seed() and mutates chip->panel_mix_disp[] when MIX is engaged without holding chip->mutex.

2. In bf_panel_set_phantom(), READ_ONCE(chip->panel_select) is evaluated four separate times across the unlocked prefix and after acquiring chip->mutex. If chip->panel_select is 0 or 1 at the initial check and bf_panel_select_put() concurrently sets chip->panel_select to 3 (None/Unknown), both READ_ONCE(chip->panel_select) != 1 and READ_ONCE(chip->panel_select) != 0 evaluate to true (setting bits = BF_PREAMP_48V_MIC1 | BF_PREAMP_48V_MIC2) and READ_ONCE(chip->panel_select) == 2 evaluates to false, toggling +48V phantom power on both microphones when no channel is selected.

3. Similarly, bf_panel_gain_wheel() and bf_panel_mix_wheel() evaluate READ_ONCE(chip->panel_select) multiple times outside chip->mutex; a concurrent change from 1 to 2 populates both targets[0]/mics[0] and targets[1]/mics[1] with base + 1, stepping the right channel twice.

Also, when mix_flash engages MIX mode above, if panel_select == 3 (None/Unknown), bf_panel_mix_seed() still returns ref = 0 or 2 and writes BF_REG_PANEL_GAIN + ref, whereas if panel_select == 2 (Both), it only seeds BF_REG_PANEL_GAIN + ref and leaves ref + 1 unseeded.

[ ... ]

Quoted 24 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +24 -0
+void babyface_panel_start(struct snd_usb_babyface *chip)
+{
+	chip->panel_seen = false;
+	/* The SELECT channel selection is not in the readback, so the
+	 * driver follows it from the presses.  The unit keeps one per IN
+	 * pair across a driver reload and even a power cycle, with the LEDs
+	 * dark until the next SELECT press, which shows it again instead of
+	 * stepping (hardware-verified 2026-09-29: after an unplug, the
+	 * first press lit both channels).  What panel_sel[] holds comes
+	 * from before a suspend, from alsactl after a boot, or is not
+	 * known, and an unknown selection targets nothing: SET, the IN
+	 * wheel and the MIX wheel then do nothing rather than act on the
+	 * wrong channel, until "Front Panel Select" is set to what the LEDs
+	 * show.
+	 */
+	bf_panel_sel_load(chip);
+	chip->panel_select_armed = false;
+	chip->panel_start = jiffies;
+	chip->panel_fast_until = jiffies;
+	chip->panel_poll_t = 0;
+	/* No master holds 0xffff: the wheel starts from the masters. */
+	memset(chip->panel_master_last, 0xff, sizeof(chip->panel_master_last));
+	schedule_delayed_work(&chip->panel_work, 0);
+}

[ ... ]

Quoted 27 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +27 -0
+static int bf_panel_select_put(struct snd_kcontrol *kctl,
+			       struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	unsigned int v = ucontrol->value.enumerated.item[0];
+	int pair = kctl->private_value;
+	/* alsactl restores the values of its last store shortly after
+	 * every probe.
+	 */
+	bool restore = time_is_after_jiffies(chip->panel_start + 3 * HZ);
+	s8 sel = v < 4 ? v : -1;
+
+	if (v > 4)
+		return -EINVAL;
+	/* After a boot they are what there is.  After a re-probe the unit
+	 * may have taken SELECT presses since that store, and a wrong
+	 * selection makes SET and the wheels act on a channel that is not
+	 * the lit one: leave the pair unknown, as for a never-seen pair.
+	 */
+	if (restore && chip->reprobe)
+		return 0;
+
+	guard(mutex)(&chip->mutex);
+	if (pair == READ_ONCE(chip->panel_in) - 1 && !restore && sel >= 0)
+		/* Set while the LEDs show the selection: the next press steps
+		 * it.
+		 */

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_flags(struct snd_usb_babyface *chip) · line 3184

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		chip->panel_select_armed = true;
+	if (sel == chip->panel_sel[pair])
+		return 0;
+	chip->panel_sel[pair] = sel;
+	if (pair == READ_ONCE(chip->panel_in) - 1) {
+		bf_panel_sel_load(chip);
+		if (READ_ONCE(chip->panel_mix))\n> +			bf_panel_mix_seed(chip);
+	}
+	return 1;
+}

[Severity: Medium]
As reported by Sashiko on v7 [5], setting chip->panel_start = jiffies inside babyface_panel_start() causes two issues for the restore check in bf_panel_select_put():

1. In babyface_probe(), snd_card_register(chip->card) is called before babyface_panel_start(chip). If udev's alsactl restore runs immediately upon card registration before babyface_panel_start() sets chip->panel_start = jiffies, chip->panel_start is still 0 (so restore evaluates to false once uptime exceeds ~303 seconds), bypassing if (restore && chip->reprobe) and allowing babyface_panel_start() to race unlocked with bf_panel_select_put().

2. In babyface_resume(), calling babyface_panel_start(chip) on every system resume resets chip->panel_start = jiffies, re-opening a 3-second restore window after every resume during which bf_panel_select_put() drops writes if chip->reprobe is true or skips setting chip->panel_select_armed = true if chip->reprobe is false.

[Severity: Medium]
What happens in bf_panel_select_put() and bf_panel_sel_load() when the unit is in OUT mode (st[2] = 0x8x/0x9x) or fader/MIX mode (st[2] = 0x0x) at probe or resume time?

In those modes, bf_panel_in_decode() returns 0, leaving chip->panel_in at 0 (Unknown). While chip->panel_in is 0, READ_ONCE(chip->panel_in) - 1 is -1, so bf_panel_sel_load() forces panel_select_known = false and panel_select = 3, and writing to any of the "Front Panel Selection" controls in bf_panel_select_put() never loads the selection or arms panel_select_armed. If the user enters MIX mode while panel_in is 0, turning the wheel prints the bf_panel_sel_hint() message instructing them to set "Front Panel Selection", but setting the control has no effect while panel_in remains 0.

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro.c +0 -0
index 06ef1a97d27a4..ece2bd0c4e63a 100644

[ ... ]

sound/usb/babyfacepro/babyfacepro.c MODULE_PARM_DESC(id, "ID string for the Babyface Pro sound card."); · line 1468

Quoted 11 lines
sound/usb/babyfacepro/babyfacepro.c +4 -0
@@ -1432,8 +1459,12 @@ MODULE_PARM_DESC(id, "ID string for the Babyface Pro sound card.");
 module_param(frames_per_urb, int, 0444);
 MODULE_PARM_DESC(frames_per_urb, "Largest URB in audio frames, 8..1024 (256 = default); the period chooses the size below it.");
 module_param(urb_frames_min, int, 0444);
 MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor).");
+module_param(urb_split, int, 0444);
+MODULE_PARM_DESC(urb_split, "Split the period into two URBs, queueing 1.5 periods instead of 2: 0 = auto (default), 1 = never, 2 = always.");
 module_param(nurbs, int, 0444);
 MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default).");
+module_param(panel_poll_ms, int, 0444);
+MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz).");

[Severity: Low]
As reported by Sashiko on v7 [6], should the new panel_poll_ms and urb_split module parameters be documented in Documentation/admin-guide/kernel-parameters.txt, and should the PCM urb_split module parameter be moved to the PCM streaming patch where urb_split is introduced?

[1] https://lore.kernel.org/r/[email protected]
[2] https://lore.kernel.org/r/[email protected]
[3] https://lore.kernel.org/r/[email protected]
[4] https://lore.kernel.org/r/[email protected]
[5] https://lore.kernel.org/r/[email protected]
[6] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
#13 lore raw

PATCH v8 7/8 ALSA: usb: babyfacepro: add the hardware DSP EQ

In reply to Ismaïl Bahloul

Add the 4-strip (AN1-AN4) 3-band + low-cut parametric EQ: the fixed-point (Q27, CORDIC) biquad coefficient math, the 64-byte blocks uploaded on bulk endpoint 0x0A of interface 1, and 15 controls per strip.

The coefficients depend on the sample rate, so hw_params re-uploads them on a rate change. The DSP is not part of the register state the cold init clears, so resume re-uploads them as well; after a re-probe, alsactl restores the EQ controls and their puts re-upload the coefficients. The Q controls start at 1.0, and a band with no gain or no Q uploads the identity.

The three checkpatch CHECK notes on this code (BIT() vs (1 << 27), 'ang' twice) are known false positives: BIT() returns unsigned long, wrong for this file's signed Q27 math, and 'ang' is the CORDIC angle.

---
 sound/usb/babyfacepro/babyfacepro-ctl.c | 610 +++++++++++++++++++++++-
 sound/usb/babyfacepro/babyfacepro.c     |  59 ++-
 sound/usb/babyfacepro/babyfacepro.h     |  25 +-
 3 files changed, 679 insertions(+), 15 deletions(-)
sound/usb/babyfacepro/babyfacepro-ctl.c +609 -1
index 51cf8c7bd..a1b8caa28 100644
@@ -4,7 +4,8 @@
  *
  * ALSA control surface: the hardware mixer (output masters and mutes,
  * the crosspoint matrix, preamps, routing flags, varispeed pitch),
- * and the front-panel poll and controls.
+ * the front-panel poll and controls, and the hardware DSP EQ
+ * (3-band + low cut).
  *
  * See babyfacepro.h for the shared device state and register map,
  * and babyfacepro.c for the core driver (protocol, PCM streaming,
@@ -3336,3 +3337,610 @@ int babyface_create_panel(struct snd_usb_babyface *chip)
 
 	return 0;
 }
+
+#define BF_EQ_Q27		(1 << 27)
+#define BF_EQ_LC_OFF		0x04000000
+#define BF_EQ_BLOCK_LEN		64
+
+/* atan(2^-i) x 2^27 (CORDIC). */
+static const s64 bf_atan_tab[28] = {
+	0x6487ED5, 0x3B58CE1, 0x1F5B760, 0xFEADD5,
+	0x7FD56F, 0x3FFAAB, 0x1FFF55, 0xFFFEB,
+	0x7FFFD, 0x40000, 0x20000, 0x10000,
+	0x8000, 0x4000, 0x2000, 0x1000,
+	0x800, 0x400, 0x200, 0x100,
+	0x80, 0x40, 0x20, 0x10,
+	0x8, 0x4, 0x2, 0x1,
+};
+
+/* sin/cos of an angle in [0, pi/2] (Q27).  Simultaneous CORDIC, 28
+ * iterations (~1e-8 residual).  eq_selftest.c verifies the whole
+ * pipeline against the double-precision reference.
+ */
+static void bf_sincos(s64 ang, s64 *sn, s64 *cs)
+{
+	s64 x = 0x4DBA76D;	/* 1/1.64676 x 2^27 (CORDIC gain) */
+	s64 y = 0;
+	s64 z = ang;
+	int i;
+
+	for (i = 0; i < 28; i++) {
+		s64 d = z >= 0 ? 1 : -1;
+		s64 nx = x - d * (y >> i);
+		s64 ny = y + d * (x >> i);
+
+		x = nx;
+		y = ny;
+		z -= d * bf_atan_tab[i];
+	}
+	*cs = x;
+	*sn = y;
+}
+
+/* 2^u for u in Q27, u in [-2, 2] (gain-amplitude range). */
+static s64 bf_exp2(s64 u)
+{
+	s64 n = u >> 27;
+	s64 r = u - (n << 27);
+	s64 rl = (r * 0x58B90C0 + (1 << 26)) >> 27;	/* r.ln2 */
+	s64 e = BF_EQ_Q27;
+	s64 term = BF_EQ_Q27;
+	int k;
+
+	for (k = 1; k <= 10; k++) {
+		term = div_s64((term * rl + (1 << 26)) >> 27, k);
+		e += term;
+	}
+	return n >= 0 ? e << n : e >> -n;
+}
+
+/* The 5 stored words (c0..c3 + shared c4) for one band.
+ * type: 1 bell, 2 low shelf, 3 high shelf.  freq_hz, fs in Hz;
+ * q100 = Q x 100; gain_x10 = dB x 10.  fs is the stream rate.
+ */
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+		      s32 gain_x10, s32 fs)
+{
+	s64 f = freq_hz;
+	s64 w0, c, s, u, su, cu, alpha, A, sq;
+	s64 b0, b1, b2, a0, a1, a2;
+	s64 pi = 0x1921FB54;	/* pi, Q27 */
+	s64 hpi = 0xC90FDAA;	/* pi/2, Q27 */
+	s64 t;
+	int both = 0, cflip = 0;
+
+	if (type == 0 || gain_x10 == 0 || q100 <= 0) {
+		/* Inactive band (type Off, or no gain/Q yet): identity
+		 * words.  Also guards the alpha division below against
+		 * the default Q=0 the controls start with - a user
+		 * setting gain before Q used to hit a kernel
+		 * divide-by-zero oops.
+		 */
+		w[0] = 0;
+		w[1] = 0;
+		w[2] = 0;
+		w[3] = 0;
+		/* w[4] is the shared scale bf_eq_update_strip() reads when the
+		 * band has a type and a gain but no Q yet (Q defaults to 0).
+		 * Leaving it unwritten uploaded a garbage shared scale.
+		 */
+		w[4] = BF_EQ_Q27;
+		return;
+	}
+
+	/* w0 = 2.pi.f/fs (Q27), reduced to [0, pi/2]. */
+	w0 = div_s64(f * BF_EQ_Q27, fs);
+	w0 = (w0 * 0x3243F6A9) >> 27;	/* x 2.pi */
+	t = w0;
+	if (t > pi) {
+		t -= pi;
+		both = 1;
+	}
+	if (t > hpi) {
+		t = pi - t;
+		cflip = 1;
+	}
+	bf_sincos(t, &s, &c);
+	if (both) {
+		s = -s;
+		c = -c;
+	}
+	if (cflip)
+		c = -c;
+
+	/* alpha = sin(w0 / (2Q)), the way TotalMix computes it, not the
+	 * cookbook's sin(w0) / (2Q): the two agree where w0 is small and
+	 * part ways as it grows, which is the effective Q that shrinks above
+	 * a few kHz in the captures (tools/usbdump/eq_biquad.md).  Checked
+	 * for Q 0.7 to 5 up to 16 kHz.  The angle is held to pi/2: past it
+	 * sin() falls again, and towards pi it would make a vanishing alpha,
+	 * a resonance with its poles on the unit circle.  It only matters for
+	 * a low Q at a high frequency (below Q 1, w0 > pi.Q), where the band
+	 * is as wide as it gets.
+	 */
+	u = div64_s64(w0 * 100 + q100, 2 * (s64)q100);
+	if (u > hpi)
+		u = hpi;
+	bf_sincos(u, &su, &cu);
+	alpha = su;
+	/* A = 10^(g/40), sqrt(A): g = gain_x10/10 dB */
+	A = bf_exp2((s64)gain_x10 * 0x11021E);
+	sq = bf_exp2((s64)gain_x10 * 0x8810F);
+
+	if (type == 1) {
+		s64 ta = (alpha * A + (1 << 26)) >> 27;
+
+		b0 = BF_EQ_Q27 + ta;
+		b1 = -2 * c;
+		b2 = BF_EQ_Q27 - ta;
+		a0 = BF_EQ_Q27 + div64_s64(alpha * BF_EQ_Q27 + A / 2, A);
+		a1 = -2 * c;
+		a2 = BF_EQ_Q27 - div64_s64(alpha * BF_EQ_Q27 + A / 2, A);
+	} else {
+		s64 ap1 = A + BF_EQ_Q27;
+		s64 am1 = A - BF_EQ_Q27;
+		s64 cp0 = (am1 * c + (1 << 26)) >> 27;	/* (A-1).c */
+		s64 cp1 = (ap1 * c + (1 << 26)) >> 27;	/* (A+1).c */
+		s64 ab = (2 * sq * alpha + (1 << 26)) >> 27;
+
+		if (type == 2) {	/* low shelf */
+			b0 = (A * (ap1 - cp0 + ab) + (1 << 26)) >> 27;
+			b1 = (2 * A * (am1 - cp1) + (1 << 26)) >> 27;
+			b2 = (A * (ap1 - cp0 - ab) + (1 << 26)) >> 27;
+			a0 = ap1 + cp0 + ab;
+			a1 = -2 * (am1 + cp1);
+			a2 = ap1 + cp0 - ab;
+		} else {		/* high shelf */
+			b0 = (A * (ap1 + cp0 + ab) + (1 << 26)) >> 27;
+			b1 = (-2 * A * (am1 + cp1) + (1 << 26)) >> 27;
+			b2 = (A * (ap1 + cp0 - ab) + (1 << 26)) >> 27;
+			a0 = ap1 - cp0 + ab;
+			a1 = -2 * (am1 - cp1);
+			a2 = ap1 - cp0 - ab;
+		}
+	}
+
+	w[0] = (s32)div64_s64(a1 * BF_EQ_Q27 + a0 / 2, a0);
+	w[1] = (s32)div64_s64(a2 * BF_EQ_Q27 + a0 / 2, a0);
+	w[2] = (s32)div64_s64(b1 * BF_EQ_Q27 + b0 / 2, b0);
+	w[3] = (s32)div64_s64(b2 * BF_EQ_Q27 + b0 / 2, b0);
+	w[4] = (s32)div64_s64(b0 * BF_EQ_Q27 + a0 / 2, a0);
+}
+
+/* Slope byte: 2^n-1 (n poles) -> 6/12/18/24 dB per oct; 0 = off. */
+static u8 bf_eq_lc_slope_byte(s32 slope_db)
+{
+	switch (slope_db) {
+	case 6:  return 0x01;
+	case 12: return 0x03;
+	case 18: return 0x07;
+	case 24: return 0x0F;
+	}
+	return 0;
+}
+
+/* The 0x38 low-cut frequency word: round(K.f'.(11656)/(11656+f')) with
+ * K = 11508, f' = f x slope-compensation factor (cap_eq9 fit, 0.003%;
+ * the slope factor keeps the composite -3 dB point constant).
+ */
+static u32 bf_eq_lc_freq_raw(s32 freq_hz, s32 slope_db)
+{
+	s64 f, word;
+
+	if (freq_hz <= 0)
+		return BF_EQ_LC_OFF;
+	f = freq_hz;
+	switch (slope_db) {
+	case 6:
+		f = f * 15267 / 10000;
+		break;
+
+	case 18:
+		f = f * 8061 / 10000;
+		break;
+
+	case 24:
+		f = f * 6977 / 10000;
+		break;
+	}
+	word = div64_s64(11508 * f * 11656 + (11656 + f) / 2, 11656 + f);
+	return (u32)word;
+}
+
+static void bf_eq_build_block(u8 *b, int ch, u8 slope,
+			      const s32 bands[3][4], s32 shared, u32 lc)
+{
+	int slot, k;
+
+	memset(b, 0, BF_EQ_BLOCK_LEN);
+	b[0] = ch;
+	b[1] = slope;
+	b[2] = ch;
+	b[3] = 0x80;	/* EQ engine active */
+	for (slot = 0; slot < 3; slot++) {
+		for (k = 0; k < 4; k++) {
+			put_unaligned_le32((u32)bands[slot][k],
+					   b + 0x04 + slot * 0x10 + 4 * k);
+		}
+	}
+	put_unaligned_le32((u32)shared, b + 0x34);
+	put_unaligned_le32(lc, b + 0x38);
+}
+
+/* Upload one 64-byte block on bulk OUT ep 0x0A (interface 1). */
+static int bf_eq_upload(struct snd_usb_babyface *chip, const u8 *block)
+{
+	u8 *buf;
+	int ret, len;
+
+	/* usb_bulk_msg DMA-maps the buffer: it must not be on the stack
+	 * (usb_hcd_map_urb_for_dma returns -EAGAIN for stack buffers).
+	 */
+	buf = kmemdup(block, BF_EQ_BLOCK_LEN, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+	ret = usb_bulk_msg(chip->dev, usb_sndbulkpipe(chip->dev, 0x0a),
+			   buf, BF_EQ_BLOCK_LEN, &len, 1000);
+	kfree(buf);
+	if (ret < 0)
+		dev_err(&chip->dev->dev, "EQ bulk upload failed: %d\n", ret);
+	return ret;
+}
+
+/* Write the L+R block pair for one strip (channel base = strip x 2). */
+static int bf_eq_write_strip(struct snd_usb_babyface *chip, int strip)
+{
+	struct bf_eq_channel *e = &chip->eq[strip];
+	u8 b[BF_EQ_BLOCK_LEN];
+	s32 identity[3][4] = { { 0 }, { 0 }, { 0 } };
+	s32 shared = e->on ? e->shared : BF_EQ_Q27;
+	u32 lc = e->on ? e->lc_raw : BF_EQ_LC_OFF;
+	/* The header slope byte (b[1]) is only valid while the low cut is
+	 * engaged: a stale slope with 0x38 = off made the device apply a
+	 * garbage-frequency cut (ear-verified: "low cut off" left only
+	 * highs).  cap_eq7: byte1 = 0x00 + 0x38 = 0x04000000 when off.
+	 */
+	u8 slope = (e->on && e->lc_hz > 0) ? e->slope : 0;
+	int ch, ret;
+
+	for (ch = 0; ch < 2; ch++) {
+		bf_eq_build_block(b, strip * 2 + ch, slope,
+				  e->on ? e->words : identity, shared, lc);
+		ret = bf_eq_upload(chip, b);
+		if (ret < 0)
+			return ret;
+	}
+	return 0;
+}
+
+/* Recompute one strip's words + low cut from its params, re-upload.
+ * Lock-free by convention: every caller must already hold chip->mutex
+ * (bf_eq_put() and bf_eq_reupload() do) - asserting it here catches a
+ * future caller that forgets, instead of a silent self-deadlock.
+ */
+static int bf_eq_update_strip(struct snd_usb_babyface *chip, int strip)
+{
+	struct bf_eq_channel *e = &chip->eq[strip];
+	s32 fs = chip->rate ? chip->rate : 48000;
+	s32 last_c4 = BF_EQ_Q27;
+	int band, i;
+
+	lockdep_assert_held(&chip->mutex);
+
+	for (band = 0; band < 3; band++) {
+		s32 w[5];
+
+		bf_eq_band_words(w, e->band_type[band], e->band_freq[band],
+				 e->band_q[band], e->band_gain[band], fs);
+		for (i = 0; i < 4; i++)
+			e->words[band][i] = w[i];
+		if (e->band_type[band] && e->band_gain[band])
+			last_c4 = w[4];	/* shared scale: the last band */
+	}
+	e->shared = last_c4;
+	e->lc_raw = bf_eq_lc_freq_raw(e->lc_hz, e->slope_db);
+	e->slope = bf_eq_lc_slope_byte(e->slope_db);
+	return bf_eq_write_strip(chip, strip);
+}
+
+/* Recompute + re-upload all four strips (rate change). Caller must
+ * hold chip->mutex - bf_eq_update_strip()/bf_eq_write_strip() are
+ * lock-free by convention (see bf_eq_put()) and the only caller,
+ * babyface_pcm_hw_params(), already holds the lock across the rate
+ * change; locking here too self-deadlocked it (hung-task: "blocked
+ * on a mutex likely owned by" itself, hit via regress.sh's rate
+ * sweep).
+ */
+void bf_eq_reupload(struct snd_usb_babyface *chip)
+{
+	int strip;
+
+	for (strip = 0; strip < 4; strip++)
+		bf_eq_update_strip(chip, strip);
+}
+
+#define EQ_STRIP(pv)	((pv) >> 8)
+#define EQ_PARAM(pv)	((pv) & 0xff)
+/* params: 0 enable, 1-3 type, 4-6 freq, 7-9 q, 10-12 gain, 13 lc freq, 14 lc slope */
+
+static const char *const bf_eq_type_texts[] = {
+	"Off", "Bell", "Low Shelf", "High Shelf", NULL
+};
+
+static const char *const bf_eq_slope_texts[] = {
+	"6 dB/oct", "12 dB/oct", "18 dB/oct", "24 dB/oct", NULL
+};
+
+static int bf_eq_info(struct snd_kcontrol *kctl,
+		      struct snd_ctl_elem_info *uinfo)
+{
+	int param = EQ_PARAM(kctl->private_value);
+
+	if (param == 0) {
+		uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+		uinfo->count = 1;
+		return 0;
+	}
+	if (param == 1 || param == 2 || param == 3)
+		return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_type_texts);
+	if (param == 14)
+		return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_slope_texts);
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = (param == 10 || param == 11 ||
+				    param == 12) ? -240 :
+				   (param == 7 || param == 8 ||
+				    param == 9) ? 5 : 0;
+	uinfo->value.integer.max = (param == 7 || param == 8 ||
+				    param == 9) ? 1000 :
+				   (param == 10 || param == 11 ||
+				    param == 12) ? 240 : 20000;
+	uinfo->value.integer.step = 1;
+	return 0;
+}
+
+static int bf_eq_get(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	struct bf_eq_channel *e = &chip->eq[EQ_STRIP(kctl->private_value)];
+	int param = EQ_PARAM(kctl->private_value);
+	int band = (param - 1) % 3;
+	s32 *v = NULL;
+
+	switch (param) {
+	case 0:
+		break;
+
+	case 1:
+	case 2:
+	case 3:
+		v = &e->band_type[band];
+		break;
+
+	case 4:
+	case 5:
+	case 6:
+		v = &e->band_freq[band];
+		break;
+
+	case 7:
+	case 8:
+	case 9:
+		v = &e->band_q[band];
+		break;
+
+	case 10:
+	case 11:
+	case 12:
+		v = &e->band_gain[band];
+		break;
+
+	case 13:
+		v = &e->lc_hz;
+		break;
+
+	case 14:
+		v = &e->slope_db;
+		break;
+	}
+	if (param == 0) {
+		ucontrol->value.integer.value[0] = e->on;
+	} else if (param == 14) {
+		/* Inverse of put's index->dB map: slope_db stores the raw
+		 * 6/12/18/24 dB/oct value, but an ENUMERATED control's .get
+		 * must return the enum item index (0-3), same as .put
+		 * receives - returning the raw dB value here (the bug this
+		 * replaces) fed back an out-of-range index to every ALSA
+		 * consumer (confirmed via amixer: writing index 1 read back
+		 * as value 12, not 1).
+		 */
+		s32 slope = v ? *v : 6;
+
+		ucontrol->value.enumerated.item[0] =
+			slope >= 24 ? 3 : slope >= 18 ? 2 : slope >= 12 ? 1 : 0;
+	} else if (param == 1 || param == 2 || param == 3) {
+		/* ENUMERATED band type: use the enumerated union member. */
+		ucontrol->value.enumerated.item[0] = v ? *v : 0;
+	} else {
+		ucontrol->value.integer.value[0] = v ? *v : 0;
+	}
+	return 0;
+}
+
+static int bf_eq_put(struct snd_kcontrol *kctl,
+		     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl);
+	int strip = EQ_STRIP(kctl->private_value);
+	int param = EQ_PARAM(kctl->private_value);
+	struct bf_eq_channel *e = &chip->eq[strip];
+	int band = (param - 1) % 3;
+	s32 nv;
+	s32 *v = NULL;
+	int ret = 0;
+
+	/* Read from the union member matching the control type: ENUMERATED
+	 * params (band type 1-3, slope 14) use .enumerated.item, everything
+	 * else (BOOL 0, INTEGER) uses .integer.value.
+	 */
+	if (param == 1 || param == 2 || param == 3 || param == 14)
+		nv = (s32)ucontrol->value.enumerated.item[0];
+	else
+		nv = (s32)ucontrol->value.integer.value[0];
+
+	/* Validate against the bounds bf_eq_info() declares.  The ALSA core
+	 * only checks these when CONFIG_SND_CTL_INPUT_VALIDATION is set, so
+	 * an out-of-range value here could otherwise reach the Q27
+	 * coefficient math (bf_eq_band_words/bf_exp2) and shift by >= width
+	 * (undefined behaviour).
+	 */
+	switch (param) {
+	case 0:
+		if (nv < 0 || nv > 1)
+			return -EINVAL;
+		break;
+	case 1:
+	case 2:
+	case 3:
+	case 14:
+		if (nv < 0 || nv > 3)
+			return -EINVAL;
+		break;
+	case 4:
+	case 5:
+	case 6:
+	case 13:
+		if (nv < 0 || nv > 20000)
+			return -EINVAL;
+		break;
+	case 7:
+	case 8:
+	case 9:
+		if (nv < 5 || nv > 1000)
+			return -EINVAL;
+		break;
+	case 10:
+	case 11:
+	case 12:
+		if (nv < -240 || nv > 240)
+			return -EINVAL;
+		break;
+	}
+
+	switch (param) {
+	case 0:
+		v = NULL;
+		break;
+
+	case 1:
+	case 2:
+	case 3:
+		v = &e->band_type[band];
+		break;
+
+	case 4:
+	case 5:
+	case 6:
+		v = &e->band_freq[band];
+		break;
+
+	case 7:
+	case 8:
+	case 9:
+		v = &e->band_q[band];
+		break;
+
+	case 10:
+	case 11:
+	case 12:
+		v = &e->band_gain[band];
+		break;
+
+	case 13:
+		v = &e->lc_hz;
+		break;
+
+	case 14:
+		v = &e->slope_db;
+		break;
+	}
+	if (param == 14)	/* slope enum items are 6/12/18/24 */
+		nv = nv == 0 ? 6 : nv == 1 ? 12 : nv == 2 ? 18 : 24;
+
+	guard(mutex)(&chip->mutex);
+	if (param == 0) {
+		if (e->on != !!nv) {
+			e->on = !!nv;
+			ret = bf_eq_update_strip(chip, strip);
+			if (ret < 0)
+				return ret;
+			ret = 1;
+		}
+	} else if (v && *v != nv) {
+		*v = nv;
+		ret = bf_eq_update_strip(chip, strip);
+		if (ret < 0)
+			return ret;
+		ret = 1;
+	}
+	return ret;
+}
+
+/* The Q controls declare 5..1000 (Q x 100) and bf_eq_put() rejects
+ * anything below 5, so a zeroed band_q would read back out of range -
+ * alsactl then fails to restore it (EINVAL) - and could reach the
+ * coefficient math.  Seed the neutral Q 1.0 on every strip at probe,
+ * before any saved state is applied so a restore still overrides it.
+ */
+void bf_eq_defaults(struct snd_usb_babyface *chip)
+{
+	int strip, band;
+
+	for (strip = 0; strip < 4; strip++) {
+		chip->eq[strip].slope_db = 6;
+		for (band = 0; band < 3; band++)
+			chip->eq[strip].band_q[band] = 100;
+	}
+}
+
+int babyface_create_eq(struct snd_usb_babyface *chip)
+{
+	static const char *const names[4] = { "AN1", "AN2", "AN3", "AN4" };
+	static const char *const params[] = {
+		"EQ Enable",
+		"EQ Band 1 Type", "EQ Band 2 Type", "EQ Band 3 Type",
+		"EQ Band 1 Freq", "EQ Band 2 Freq", "EQ Band 3 Freq",
+		"EQ Band 1 Q", "EQ Band 2 Q", "EQ Band 3 Q",
+		"EQ Band 1 Gain", "EQ Band 2 Gain", "EQ Band 3 Gain",
+		"EQ Low Cut Freq", "EQ Low Cut Slope",
+	};
+	int strip, i, err;
+
+	for (strip = 0; strip < 4; strip++) {
+		for (i = 0; i < 15; i++) {
+			struct snd_kcontrol *kctl;
+			char name[64];
+
+			snprintf(name, sizeof(name), "%s %s", names[strip],
+				 params[i]);
+			kctl = snd_ctl_new1(&(struct snd_kcontrol_new){
+				.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+				.name = "EQ",
+				.index = 0,
+				.info = bf_eq_info,
+				.get = bf_eq_get,
+				.put = bf_eq_put,
+				.private_value = (strip << 8) | i,
+			}, chip);
+			if (!kctl)
+				return -ENOMEM;
+			strscpy(kctl->id.name, name, sizeof(kctl->id.name));
+			err = snd_ctl_add(chip->card, kctl);
+			if (err < 0)
+				return err;
+		}
+	}
+	return 0;
+}
sound/usb/babyfacepro/babyfacepro.c +46 -13

95 lines not on this page · raw message

sound/usb/babyfacepro/babyfacepro.h +24 -1
index 984d1d175..c9ef366f1 100644
@@ -14,7 +14,7 @@
  * babyfacepro.c holds the card lifecycle, the PCM stream and the
  * mixer-state persistence across re-probes and resume;
  * babyfacepro-ctl.c holds the ALSA control surface - the hardware
- * mixer and the front panel.
+ * mixer, the front panel and the DSP EQ.
  *
  * The protocol (vendor requests + 14x32-bit frame layout) was
  * reverse-engineered from Windows captures and validated on hardware -
@@ -313,6 +313,21 @@ extern const struct bf_source bf_sources[14];
  */
 #define BF_SRC_PB1			8
 
+/* One analog-input strip of the DSP EQ (babyfacepro-ctl.c). */
+struct bf_eq_channel {
+	bool on;		/* EQ engaged (else identity blocks) */
+	s32 slope_db;		/* low-cut slope 6/12/18/24 (0 = off) */
+	s32 lc_hz;		/* low-cut freq, 0 = off */
+	u32 lc_raw;		/* cached 0x38 word */
+	u8 slope;		/* cached slope byte (2^n - 1) */
+	s32 band_type[3];	/* 0 off, 1 bell, 2 low shelf, 3 high shelf */
+	s32 band_freq[3];	/* Hz */
+	s32 band_q[3];		/* Q x 100 */
+	s32 band_gain[3];	/* dB x 10 */
+	s32 words[3][4];	/* cached c0..c3 */
+	s32 shared;		/* cached c4 (shared by the slots) */
+};
+
 struct snd_usb_babyface {
 	struct snd_card *card;
 	struct usb_device *dev;
@@ -480,6 +495,9 @@ struct snd_usb_babyface {
 	struct snd_kcontrol *panel_select_kctl[3]; /* per IN pair, likewise */
 	struct snd_kcontrol *dim_press_kctl;
 	u32 dim_press_count;		/* front-panel DIM presses, wraps */
+
+	/* DSP EQ (babyfacepro-ctl.c) - 4 analog-input strips, params kept in state */
+	struct bf_eq_channel eq[4];
 };
 
 struct bf_rate {
@@ -538,6 +556,11 @@ int babyface_create_panel(struct snd_usb_babyface *chip);
 void babyface_panel_start(struct snd_usb_babyface *chip);
 void babyface_panel_stop(struct snd_usb_babyface *chip);
 void babyface_panel_work(struct work_struct *work);
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+		      s32 gain_x10, s32 fs);
+void bf_eq_reupload(struct snd_usb_babyface *chip);
+void bf_eq_defaults(struct snd_usb_babyface *chip);
+int babyface_create_eq(struct snd_usb_babyface *chip);
 
 /* Master gain-law helpers (babyfacepro-ctl.c). */
 int bf_master_16bit(int half_db);	/* dBx2 -> 16-bit master */
Signature · 1 line
2.56.0
#14 lore raw

Re: [PATCH v8 7/8] ALSA: usb: babyfacepro: add the hardware DSP EQ

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider: - [High] ALSA: usb: babyfacepro: shared biquad scale overwritten in bf_eq_update_strip() - [High] ALSA…

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] ALSA: usb: babyfacepro: shared biquad scale overwritten in bf_eq_update_strip()
- [High] ALSA: usb: babyfacepro: inverted sign of high-shelf a1 in bf_eq_band_words()
- [Medium] ALSA: usb: babyfacepro: unrolled EQ cache in bf_eq_put() and ignored errors in bf_eq_reupload()
- [Medium] ALSA: usb: babyfacepro: missing freq_hz <= 0 guard in bf_eq_band_words()
- [Medium] ALSA: usb: babyfacepro: uninitialized boolean min and max in bf_eq_info()
- [Medium] ALSA: usb: babyfacepro: plain 64-bit signed division in bf_eq_lc_freq_raw()
- [Medium] ALSA: usb: babyfacepro: missing hardware DSP EQ reset in babyface_probe()
- [Medium] ALSA: usb: babyfacepro: ignored error return in bf_eq_reupload()
- [Low] ALSA: usb: babyfacepro: 64-bit value truncated before range check in bf_eq_put() --

commit 100d534996c41ac8c2801f7eafc48e129593aa13
Author: Ismaïl Bahloul <[email protected]>

ALSA: usb: babyfacepro: add the hardware DSP EQ

This commit adds support for the 4-strip (AN1-AN4) 3-band and low-cut hardware DSP parametric EQ, including fixed-point Q27 biquad coefficient calculation, 64-byte block uploads over bulk endpoint 0x0A, and 15 ALSA mixer controls per strip.

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
index 51cf8c7bd4357..a1b8caa281dad 100644

[ ... ]

Quoted 1 line
sound/usb/babyfacepro/babyfacepro-ctl.c +0 -0
@@ -3336,3 +3337,610 @@ int babyface_create_panel(struct snd_usb_babyface *chip)

[ ... ]

Quoted 19 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +19 -0
+void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100,
+		      s32 gain_x10, s32 fs)
+{
+	s64 f = freq_hz;
+	s64 w0, c, s, u, su, cu, alpha, A, sq;
+	s64 b0, b1, b2, a0, a1, a2;
+	s64 pi = 0x1921FB54;	/* pi, Q27 */
+	s64 hpi = 0xC90FDAA;	/* pi/2, Q27 */
+	s64 t;
+	int both = 0, cflip = 0;
+
+	if (type == 0 || gain_x10 == 0 || q100 <= 0) {
+		/* Inactive band (type Off, or no gain/Q yet): identity
+		 * words.  Also guards the alpha division below against
+		 * the default Q=0 the controls start with - a user
+		 * setting gain before Q used to hit a kernel
+		 * divide-by-zero oops.
+		 */
+		w[0] = 0;

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3429

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		w[1] = 0;
+		w[2] = 0;
+		w[3] = 0;
+		/* w[4] is the shared scale bf_eq_update_strip() reads when the
+		 * band has a type and a gain but no Q yet (Q defaults to 0).
+		 * Leaving it unwritten uploaded a garbage shared scale.
+		 */
+		w[4] = BF_EQ_Q27;
+		return;
+	}

[Severity: Medium]
Should the inactive-band check in bf_eq_band_words() also guard against freq_hz <= 0, similar to bf_eq_lc_freq_raw()?

At probe time, bf_eq_defaults() initializes band_q[] to 100 while leaving band_freq[] at 0 Hz, and bf_eq_info() / bf_eq_put() allow frequency values down to 0. If a user sets a band's Type and Gain before setting Freq (or sets Freq to 0), bf_eq_band_words() runs with freq_hz == 0, which gives w0 = 0 and u = 0. Calling bf_sincos(0, ...) produces su = -1 (alpha < 0) and c = 0x7fffffe (~1.0 in Q27), uploading a degenerate biquad with poles on or outside the unit circle at z = 1 instead of the identity filter.

[ ... ]

Quoted 13 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +13 -0
+		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);

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3508

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+			a2 = ap1 - cp0 - ab;
+		}
+	}
+
+	w[0] = (s32)div64_s64(a1 * BF_EQ_Q27 + a0 / 2, a0);
+	w[1] = (s32)div64_s64(a2 * BF_EQ_Q27 + a0 / 2, a0);
+	w[2] = (s32)div64_s64(b1 * BF_EQ_Q27 + b0 / 2, b0);
+	w[3] = (s32)div64_s64(b2 * BF_EQ_Q27 + b0 / 2, b0);
+	w[4] = (s32)div64_s64(b0 * BF_EQ_Q27 + a0 / 2, a0);
+}

[Severity: High]
In the high-shelf branch (type == 3) of bf_eq_band_words(), is the sign of a1 inverted?

With am1 = A - 1 and cp1 = (A + 1) * cos(w0), the Audio EQ Cookbook high-shelf denominator coefficient is:

a1 = 2 * ((A - 1) - (A + 1) * cos(w0)) = 2 * (am1 - cp1)

Using a1 = -2 * (am1 - cp1) negates w[0], so as gain approaches 0 dB (A -> 1, am1 -> 0, cp1 -> 2 * cos(w0)), b1 approaches -4 * cos(w0) while a1 approaches +4 * cos(w0), placing the filter poles at pi - w0 instead of w0.

[ ... ]

Quoted 13 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +13 -0
+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:

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3548

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		f = f * 8061 / 10000;
+		break;
+
+	case 24:
+		f = f * 6977 / 10000;
+		break;
+	}
+	word = div64_s64(11508 * f * 11656 + (11656 + f) / 2, 11656 + f);
+	return (u32)word;
+}

[Severity: Medium]
As reported by Sashiko on v7 [1], in bf_eq_lc_freq_raw(), f is declared as s64 and divided by 10000 using the plain / operator.

Because bf_eq_reupload() -> bf_eq_update_strip() passes chip->eq[strip].lc_hz (an s32 field in memory) as freq_hz, the compiler only knows freq_hz is in [1, INT_MAX], so f * 15267 can exceed 32 bits. On 32-bit architectures, this emits a libcall to __divdi3 / __aeabi_ldivmod and fails at link time. Should this use div_s64() (or 32-bit arithmetic since lc_hz <= 20000)?

[ ... ]

Quoted 14 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +14 -0
+static int bf_eq_update_strip(struct snd_usb_babyface *chip, int strip)
+{
+	struct bf_eq_channel *e = &chip->eq[strip];
+	s32 fs = chip->rate ? chip->rate : 48000;
+	s32 last_c4 = BF_EQ_Q27;
+	int band, i;
+
+	lockdep_assert_held(&chip->mutex);
+
+	for (band = 0; band < 3; band++) {
+		s32 w[5];
+
+		bf_eq_band_words(w, e->band_type[band], e->band_freq[band],
+				 e->band_q[band], e->band_gain[band], fs);

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3644

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		for (i = 0; i < 4; i++)
+			e->words[band][i] = w[i];
+		if (e->band_type[band] && e->band_gain[band])
+			last_c4 = w[4];	/* shared scale: the last band */
+	}
+	e->shared = last_c4;
+	e->lc_raw = bf_eq_lc_freq_raw(e->lc_hz, e->slope_db);
+	e->slope = bf_eq_lc_slope_byte(e->slope_db);
+	return bf_eq_write_strip(chip, strip);
+}

[Severity: High]
When two or three bands are active on the same strip in bf_eq_update_strip(), doesn't overwriting last_c4 = w[4] discard the b0/a0 scale factors of the earlier active bands?

In bf_eq_band_words(), each band's numerator coefficients w[2] = b1 / b0 and w[3] = b2 / b0 are normalized by b0 rather than a0, factoring that band's b0 / a0 gain scale into w[4]. Since the three biquad slots in bf_eq_build_block() only receive w[0..3] and share a single scale word at offset 0x34, shouldn't last_c4 accumulate the Q27 product of w[4] across all active bands rather than taking only the last active band's w[4]?

Quoted 6 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +6 -0
+
+/* 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

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3660

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+ * on a mutex likely owned by" itself, hit via regress.sh's rate
+ * sweep).
+ */
+void bf_eq_reupload(struct snd_usb_babyface *chip)
+{
+	int strip;
+
+	for (strip = 0; strip < 4; strip++)
+		bf_eq_update_strip(chip, strip);
+}

[Severity: Medium]
As reported by Sashiko on v7 [1], bf_eq_reupload() ignores the error return value of bf_eq_update_strip().

If kmemdup() or usb_bulk_msg() fails in bf_eq_upload(), bf_eq_reupload() continues attempting up to 7 more synchronous bulk transfers (with a 1000 ms timeout each while holding chip->mutex), and callers such as bf_resume_locked() and babyface_pcm_hw_params() cannot detect or propagate the failure. Should bf_eq_reupload() return an int error code and stop on the first failing strip?

[ ... ]

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3683

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+static int bf_eq_info(struct snd_kcontrol *kctl,
+		      struct snd_ctl_elem_info *uinfo)
+{
+	int param = EQ_PARAM(kctl->private_value);
+
+	if (param == 0) {
+		uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+		uinfo->count = 1;
+		return 0;
+	}

[Severity: Medium]
When param == 0 in bf_eq_info(), uinfo->value.integer.min and uinfo->value.integer.max are left uninitialized.

In snd_ctl_elem_info_user(), uinfo is copied from userspace without zeroing uinfo->value, so callers reusing the struct across controls see stale min/max values; in 32-bit compat mode (snd_ctl_elem_info_compat()), max is reported as 0 instead of 1. Could this set min = 0 and max = 1 (or return snd_ctl_boolean_mono_info(uinfo)) like the other boolean controls in this file?

[ ... ]

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -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;

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3791

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+	int ret = 0;
+
+	/* Read from the union member matching the control type: ENUMERATED
+	 * params (band type 1-3, slope 14) use .enumerated.item, everything
+	 * else (BOOL 0, INTEGER) uses .integer.value.
+	 */
+	if (param == 1 || param == 2 || param == 3 || param == 14)
+		nv = (s32)ucontrol->value.enumerated.item[0];
+	else
+		nv = (s32)ucontrol->value.integer.value[0];

[Severity: Low]
On 64-bit architectures, ucontrol->value.integer.value[0] is a 64-bit long. Casting it to s32 before the switch (param) range checks below truncates the upper 32 bits when CONFIG_SND_CTL_INPUT_VALIDATION is disabled, allowing out-of-range 64-bit values whose lower 32 bits fall within bounds to wrap and be accepted. Could nv be kept as a long (or range-checked before narrowing, as in bf_master_put() and bf_width_put())?

[ ... ]

Quoted 8 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +8 -0
+	guard(mutex)(&chip->mutex);
+	if (param == 0) {
+		if (e->on != !!nv) {
+			e->on = !!nv;
+			ret = bf_eq_update_strip(chip, strip);
+			if (ret < 0)
+				return ret;
+			ret = 1;

sound/usb/babyfacepro/babyfacepro-ctl.c int babyface_create_panel(struct snd_usb_babyface *chip) · line 3889

Quoted 10 lines
sound/usb/babyfacepro/babyfacepro-ctl.c +10 -0
+		}
+	} else if (v && *v != nv) {
+		*v = nv;
+		ret = bf_eq_update_strip(chip, strip);
+		if (ret < 0)
+			return ret;
+		ret = 1;
+	}
+	return ret;
+}

[Severity: Medium]
As reported by Sashiko on v7 [1], in bf_eq_put(), e->on or *v is updated before calling bf_eq_update_strip() and is not restored if bf_eq_update_strip() fails (for example, if kmemdup() returns -ENOMEM or usb_bulk_msg() fails in bf_eq_upload()).

Because chip->eq[strip] retains the new value nv, the host cache becomes desynchronized from the hardware DSP, and a subsequent retry writing the same value nv sees e->on == !!nv or *v == nv and returns 0 without re-uploading the coefficients. In addition, while bf_eq_upload() sleeps, concurrent lockless readers in bf_eq_get() observe the uncommitted value. Should bf_eq_put() restore the previous value on failure (like bf_phantom_put() and bf_reflevel_put())?

Quoted 4 lines
sound/usb/babyfacepro/babyfacepro.c +0 -0
index ece2bd0c4e63a..ed17600aba2f0 100644

[ ... ]

sound/usb/babyfacepro/babyfacepro.c static int babyface_probe(struct usb_interface *intf, · line 1647

Quoted 9 lines
sound/usb/babyfacepro/babyfacepro.c +2 -0
@@ -1638,6 +1640,8 @@ static int babyface_probe(struct usb_interface *intf,
 			BF_REF_LEVEL_4DBU : BF_REF_LEVEL_MINUS10DBV;
 	}
 
+	bf_eq_defaults(chip);
+
 	/* The 0x16 clear zeroed the mixer registers: apply the factory
 	 * default routing to keep the outputs live out of the box.  alsactl
 	 * restores the user's settings once the card is registered.

[Severity: Medium]
Does babyface_probe() also need to call bf_eq_reupload(chip) after bf_eq_defaults(chip) to reset the hardware DSP EQ state?

As noted in the commit message and bf_resume_locked(), bf_cold_init() does not clear the DSP EQ on bulk endpoint 0x0A. If the device already had EQ filters active before a driver reload, warm reboot, or re-probe, bf_eq_defaults() sets e->on = false and default parameters in chip->eq[] without uploading identity blocks to the hardware. When alsactl subsequently restores a state where EQ is disabled (or at default values), bf_eq_put() sees e->on == !!nv and *v == nv and skips bf_eq_update_strip(), leaving the stale EQ filters active in the hardware DSP while the ALSA controls report EQ Enable as Off.

[1] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=7
#15 lore raw

PATCH v8 8/8 Documentation: sound: add the Babyface Pro proprietary-mode design doc

In reply to 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.

---
 Documentation/sound/cards/babyface-pro.rst | 218 +++++++++++++++++++++
 Documentation/sound/cards/index.rst        |   1 +
 2 files changed, 219 insertions(+)
 create mode 100644 Documentation/sound/cards/babyface-pro.rst
Documentation/sound/cards/babyface-pro.rst new file +218 -0
new file mode 100644
index 000000000..2b79a52d9
@@ -0,0 +1,218 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=================================================
+RME Babyface Pro / Pro FS (snd-usb-babyface-pro)
+=================================================
+
+This document describes the design of the ``snd-usb-babyface-pro``
+driver - what problem the driver solves, why it is a standalone
+driver instead of a snd-usb-audio quirk, and the four design
+decisions (protocol shape, stream model, mixer state across re-probes
+and resume, front-panel emulation) that shape most of the code.
+
+Two USB personalities, one device
+==================================
+
+The RME Babyface Pro and Babyface Pro FS present two different USB
+configurations depending on a physical/firmware switch: a
+class-compliant one, already handled by ``snd-usb-audio``, and a
+proprietary one (USB ID ``2a39:3fc0``) that this driver covers.  The
+two hardware models share the same USB ID, ``bcdDevice`` and
+``iProduct`` string shape; nothing in the descriptors tells them
+apart, and the driver runs unmodified on both.
+
+In proprietary mode, interface 5 carries the PCM stream on two
+INTERRUPT endpoints (``0x01`` OUT, ``0x82`` IN) instead of the
+isochronous endpoints the USB Audio Class specifies.  Isochronous
+transfers are rejected there with ``-EINVAL``.  ``snd-usb-audio`` has
+no interrupt-PCM transport, so this mode cannot be a quirk on top of
+it; the driver is standalone, modelled on ``snd-usb-caiaq`` (another
+interrupt-streaming RME/NI-style device).
+
+Why interrupt endpoints and not isochronous is a hardware/firmware
+choice on RME's side, not something this driver can change - the
+class-compliant mode already exists on the same device for users who
+want a fully standard, quirk-free path with a subset of the
+functionality (no mixer, no front panel).  This driver is for users
+who want the full mixer, routing matrix, and hardware DSP EQ that
+only the proprietary mode exposes.
+
+The vendor protocol: writes only, no readback
+==============================================
+
+Every mixer and clock function is one of a handful of USB vendor
+control requests (``bmRequestType 0x40``, i.e. host-to-device,
+vendor, device-recipient), each identified by its request number and
+a 16-bit value/index pair - there is no larger command structure.
+The commonly used ones are:
+
+======  ========================================
+0x10    settings word, rate family, stream trigger
+0x12    16-bit crosspoint and output-master writes
+0x14    stream session arm
+0x16    cold-init register clear
+0x17    front-panel + preamp state (read and write)
+0x1a    8-bit gain / output-master writes
+0x1b    varispeed (DDS quad)
+0x1d    stream session start
+======  ========================================
+
+The full register map, decoded from Windows USB captures and
+cross-checked against hardware, lives in the driver's own development
+repository (not shipped in-tree) - the constants and the comments
+next to each vendor write in the source are the authoritative
+in-tree reference.
+
+The one property that shapes the rest of the driver: **almost nothing
+here can be read back**.  The 0x17 request returns the front-panel
+and preamp state, but the crosspoint matrix, the output masters, the
+routing flags and the clock all have to be tracked host-side - the
+device will accept a write blindly and never confirm what it actually
+holds.  Two consequences follow directly from this:
+
+* The ``struct snd_usb_babyface`` device state (see
+  ``babyfacepro.h``) is not a cache in the usual sense of "avoid a
+  slow read" - it is the *only* record of what the hardware should
+  currently hold.  Every mixer control's ``.get`` callback reads this
+  state directly; none of them ever talks to the device.
+
+* A full reset of the device's registers - which the cold init at
+  probe and at resume does - has to be followed by replaying the
+  *entire* cached state back, in the right order, or the card comes
+  back silent or at the wrong levels.  This is what
+  ``babyface_restore_state()`` and ``bf_state_apply_flags()`` do (see
+  "Mixer-state persistence" below).
+
+The stream model
+================
+
+Playback and capture share one physical stream: the device only
+advances it while both interrupt endpoints have a pending URB, so the
+IN and OUT URBs are always submitted as a pair, and both directions run
+at one sample rate.  The driver runs one stream *session* for both
+substreams:
+
+* ``hw_params`` counts a substream as a user of the session;
+* ``prepare`` starts the session if it is not running - the rate write,
+  the session trigger pair, the IN/OUT URBs, then the arm - which is
+  what the RME Windows driver sends at a stream start;
+* trigger START/STOP only decides whether the URB handlers move that
+  substream's audio.  The URBs keep running either way, carrying
+  silence while nothing plays, so an xrun restart does not restart the
+  session;
+* ``hw_free`` drops the user, and the last one stops the session.
+
+The device keeps its mixer registers between sessions, so a session
+start writes no mixer state.  A session triggered within about 15 ms of
+the previous one stopping comes up with the outputs silent, so a
+session start waits until 50 ms have passed since the last stop.
+
+Both directions share one clock, so while another application has the
+other direction set up, the rate belongs to it: ``open()`` offers that
+rate alone, and the sound server resamples, rather than the device
+changing rate under a running stream - as with RME's own drivers, which
+grey the sample rate out while a stream runs.  The application that
+holds both directions may change the rate or the buffer size itself, as
+a DAW does from its settings; the direction it left running then stops
+with an xrun and is set up again.
+
+The size of the URBs follows the period the application asks for, so
+the latency follows the application's buffer instead of a fixed queue:
+``hw_params`` splits the period into the fewest URBs of at most
+``frames_per_urb`` frames, each a whole number of the device's IN
+packets, and keeps two periods' worth in flight (at most ``nurbs``).
+The URB handlers copy to and from the ring the core allocates and take
+the application's position from the shared control page, so the ring can
+also be mapped by the application, which JACK requires.
+``runtime->delay`` reports the audio queued in the URBs plus the fixed
+delay of the converters and of the device, per speed, so that an
+application that uses ALSA directly can line up what it records.
+
+The playback handler copies the ring as it is and moves the pointer on
+by the whole URB, so the pointer follows the device even for an
+application that runs the stream free, as dmix does on the hardware PCM
+it opens; an underrun is the PCM core's to detect and report.
+
+Capture exposes the device's twelve input words in the device's own
+order, one channel per word: AN1-4, AS1/2, then ADAT3-8.  AS1/2 is the
+first optical pair, so it carries ADAT 1/2 when the input receives ADAT
+and S/PDIF when it receives S/PDIF.  At 96/192 kHz the frame has fewer
+words and the channels past them read zero.
+
+URB completions run in interrupt context; the work that needs to sleep
+(stopping the session after repeated URB errors) runs from
+``stream_work``.
+
+Mixer state across re-probes and resume
+=======================================
+
+A userspace client can claim the proprietary interface directly via
+``usbfs`` (``USBDEVFS_DISCONNECT_CLAIM``) - both PipeWire grabbing the
+device for a sink and the project's own TuxMix userspace daemon do
+this via libusb.  That detaches the kernel driver and the ALSA card
+disappears for the duration; when the client releases the interface,
+the driver re-probes.  The cold init the probe runs clears the mixer
+registers, and the driver does not carry its cached state across the
+re-probe: alsactl restores the settings stored at the last shutdown
+once the card is registered again, as for any other card.  A resume
+that follows a device reset goes through ``reset_resume`` instead of a
+re-probe, so a reset does not lose the state that way.
+
+A system suspend does lose the registers.  On resume the driver re-runs
+the cold init and replays the in-memory state (masters, crosspoints,
+routing flags, EQ) through the same paths the controls use.  Since
+almost nothing can be read back (see above), that in-memory state is
+still the only record of what the hardware should hold.
+
+The front-panel SELECT selection is the exception: it is not in the
+0x17 readback, so the driver has to be told it once and cannot know
+whether it changed meanwhile.  The driver tracks whether a unit has
+been probed since the module was loaded.  After a boot it takes the
+values alsactl restores; a re-probe of an already-seen unit ignores
+them and leaves the pairs unknown, because SELECT may have been pressed
+since the last store.  The "Front Panel Selection" controls let
+software correct the selection (see below).
+
+Front-panel emulation: the driver plays TotalMix's role
+==========================================================
+
+The front panel (IN/OUT/SET/MIX/SELECT/DIM buttons, the rotary
+wheel) has no on-device intelligence of its own for turning a wheel
+click into a mixer change - on Windows/Mac, RME's TotalMix
+application polls the same 0x17 status register this driver polls,
+decodes button/wheel deltas, and performs the resulting mixer writes
+itself.  Standalone (no host software) mode exists on the hardware,
+but the proprietary USB mode this driver targets always has a host
+attached, so this driver has to do what TotalMix does: poll 0x17 on
+a delayed work item (``panel_poll_ms`` module parameter, default
+20 ms to match TotalMix's own ~50 Hz), decode the button flash and
+signed wheel delta, and apply the resulting change (an output level
+step, a preamp gain step, a monitoring level step, a phantom toggle)
+exactly like the corresponding ALSA control's ``.put`` would.  A DIM
+press is only counted, for a mixer application to act on.  The
+front-panel ALSA
+controls this driver exposes are the read side of this: a way for
+userspace (WirePlumber, TuxMix) to observe what the physical panel is
+doing, not a way to drive the hardware.
+
+Some panel state - which channel SELECT currently has chosen, for
+instance - is not part of the 0x17 readback at all and exists only on
+the device's own internal state machine, which the driver cannot
+read.  The unit keeps one such selection for each IN pair, across IN
+switches and even across a power cycle; its LEDs stay dark until the
+next SELECT press, which only shows the selection again, and later
+presses step it.  The driver follows the presses and exposes the
+selection of each pair as a "Front Panel Selection" control (index 0
+is Ch 1/2), so that alsactl keeps it across boots like any other
+mixer setting.  It cannot know what the unit holds before it has been
+told once, so a pair starts out unknown, and SET and the wheel then do
+nothing instead of acting on a channel that may not be the lit one;
+setting the control to what the LEDs show for the pair tells it.  The
+values alsactl restores right after a probe are the ones of its last
+store: after a boot that is the newest record there is, but a re-probe
+of a unit already seen may follow SELECT presses made since, so the
+driver ignores them then and leaves the pairs unknown.  The stored
+values can be wrong only if the selection was changed while the driver
+was not running (the unit used on its own, or with another host).  The relevant code
+comments (``babyface_panel_start()``, the ``panel_select_armed``
+handling in ``bf_panel_tick()``) explain the details.
Documentation/sound/cards/index.rst +1 -0
index e68bbb13c..31051b7a4 100644
@@ -12,6 +12,7 @@ Card-Specific Information
    emu10k1-jack
    via82xx-mixer
    audiophile-usb
+   babyface-pro
    mixart
    bt87x
    maya44
Signature · 1 line
2.56.0
#16 lore raw

Re: [PATCH v8 8/8] Documentation: sound: add the Babyface Pro proprietary-mode design doc

Sashiko has reviewed this patch and found no issues.
Quoted 11 lines

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.

Sashiko has reviewed this patch and found no issues.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8