From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-wr1-f54.google.com (mail-wr1-f54.google.com [209.85.221.54]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C516948F855 for ; Sat, 3 Oct 2026 19:42:14 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.54 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1791056544; cv=none; b=WHcQgwAWYaaDFjxUZMza9hfCeBjUQ5E4g17H/4XcGTWO1zQBFNN0xpTLuMcfkbGiKJlryEU6qRmEQZtcdgYXvnpu4q4v3p+iu2HvwKx8pKX7FFVDqRz/3CmTld/C7GT6Zd/E5eyFQCN0CxX86Xk8OnrtLfj/8cLnABlInl616XA= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1791056544; c=relaxed/simple; bh=P4owZ2nh0s0WABbvJUG2IgV+zCYa+z04xnL5yOZZLMA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=gLPUCrFjmaTMjFUn+lo7YXwZsVQFxwPPJUw4K7/lKwbkAo4yVX5K+1SriICvonO5fiLB5jySp4BNcNYWXC4GkLEA5i9joKeNXW+9E5HX/gD+WKTMmO9CUZ7/34VlX1sk63H5mUD2OYuPqB9+/CoBlxsI/lqPzmp4XpNpVfKAB4s= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=TJJdEp7W; arc=none smtp.client-ip=209.85.221.54 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="TJJdEp7W" Received: by mail-wr1-f54.google.com with SMTP id ffacd0b85a97d-48afcfc4bf5so413579f8f.1 for ; Sat, 03 Oct 2026 12:42:14 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1791056532; x=1791661332; darn=vger.kernel.org; h=content-transfer-encoding:content-type:mime-version:references :in-reply-to:message-id:date:subject:cc:to:from:from:to:cc:subject :date:message-id:reply-to:content-type; bh=Fs/2x9B6vgEMOGNi4tcMPglLh+a6OD7ZgKR7xBWSVi4=; b=TJJdEp7W8qa4s9q4BwEL7cp6qjKRd4ut2/29SXIyA8g8H563+kIDURTCp6aScTo54L bpdFVW9ZsPde8v2Yk9f4A2J8v8C7Jn0QnW8q4z/Pgd9GXtkTsOpkghUGRkojIGRVl/Io y6eSvmiuzaTK5p9/2NO5r+k9SVS+vZoZdfdDSwkHtivUlf7eAaP00QZbOhPFrkPYfnZ1 swRvyYNBvxPPpZv4EyqXTd1/Q+JA9WetdYAj24QssDN9+hOcGJt4EiBJdlwlbQnS60Ys V46p7Lcw9Ie4KrL636TEhCHysQS7ByfK1ByXvA9ROZB/eshP6DHB8FUjaobYJBMpKDUY tSxg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1791056532; x=1791661332; h=content-transfer-encoding:content-type:mime-version:references :in-reply-to:message-id:date:subject:cc:to:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=Fs/2x9B6vgEMOGNi4tcMPglLh+a6OD7ZgKR7xBWSVi4=; b=UsAlM/+Pu5i314Gd3Nu/cMgPfxHAwEfoGV5lcCIEodRjqpAVVMGVCMbMckB+0GQtiJ a1WyJkZdkcEdLvOm6Aq2mBtZN60oto79SaAAjpuYhdKs2buWy5MXl2pCInkW59B/QEHJ INanpGxgAhIX+zKtblFIyYH9HFCzhwQ1evt7cNX017XHXOuZFT1E/Gp2JfNAsAu8Zt1T DC47gCeuKUT/fWGi/gGeBNRA7jpHjMw/4TMPjNHE9NablRz1P40wLjTFbRMmG++1YA1m dQoqtxp9runbpFrYyg4oG3Fvhp6WrrXow/ZZ4bPg7u06+oaJJXNKbtC1LuO+9XywPAAX wVTA== X-Forwarded-Encrypted: i=1; AKwUvBwCM/MzI8Ag3icYy6FBtJfajlhGgwedZhzl9xhiW2zFgVOPxedNAxK41etR8nWb0fo76mKXUkKHcQfNtmI=@vger.kernel.org X-Gm-Message-State: AFuF++mBBD6fYc3951Pp6457265OWiBlKaAgSX3sJgMzZamSCkygKt1N sI5QAZl+RZrXfq0Js4sYK11JOL8OK6t2/OPvC3RzRILIXUOjzdHiG41H X-Gm-Gg: AYBFou1qYKwgApzk6dyutIiClAu7Xw/VLPEttVkL8iOIYwM8BrQsXZniAwB3HTeG+aZ M5GooX8ovxpjo3IHb0amEE008fsVRmZfYpOZE56HNrAOYvJhfuulU0wjGYjddIQkQ2oRarlOZvl NCv+B3ujq1tJrzi0IjD8h4hFxVSsoGciwICuHCaQjj2lSR93lECSWjVWncTtGU3lVqFe0sEnY9Z Unm+QI2+LwR28yaCLRoF6GNOm5LjgsEXvWZyYME+Fl5VKnSG2LEGDq3sMXTKdOGT97R9TGpV910 oiBXzD6WkZHDQXYhw4EL9d+VhjT22IcT0VoULSfLvyXmsv8G2ZVJAfjmNVrOfnOmJUWugZVmd5n wRtYhPenLq4grepDnd8LdEnNLYuI+Z7ul5ME0PE6f7kZk08MbkYyTapbHGYthWlvW9ihTig/izA 4nttSYfM4gR1oUsJxegdwKTx3pUP3zdvDdFbeQ8yqtfz/EScA+sFrag2lXzIBq4b6IEqUKCVvOe OAB7uJtn0VkGLaDNUa56cwhEj3g7bZ2BGm0ovOoRmFduWUUWdodBja4TR6QegjMde0g7VacBpY0 x1Dsppt/IEelWU2MDqs3RH5W X-Received: by 2002:a05:600c:a49:b0:4a0:eae0:769a with SMTP id 5b1f17b1804b1-4a168100e0dmr46701095e9.28.1791056531941; Sat, 03 Oct 2026 12:42:11 -0700 (PDT) Received: from cachyos.home (2a01cb0c82caf900a186612f304bfea5.ipv6.abo.wanadoo.fr. [2a01:cb0c:82ca:f900:a186:612f:304b:fea5]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4a162e867e8sm161520495e9.15.2026.10.03.12.42.11 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 03 Oct 2026 12:42:11 -0700 (PDT) From: =?UTF-8?q?Isma=C3=AFl=20Bahloul?= To: linux-sound@vger.kernel.org Cc: tiwai@suse.com, perex@perex.cz, linux-usb@vger.kernel.org, alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org, corbet@lwn.net, skhan@linuxfoundation.org, rdunlap@infradead.org, linux-doc@vger.kernel.org, =?UTF-8?q?Isma=C3=AFl=20Bahloul?= , David Fredman Subject: [RFC PATCH v6 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls Date: Sat, 3 Oct 2026 21:41:35 +0200 Message-ID: <20261003194137.86176-7-i.bahloul01@gmail.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261003194137.86176-1-i.bahloul01@gmail.com> References: <20261003194137.86176-1-i.bahloul01@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this mode the front panel is host-driven: the device reports its buttons, wheel and IN/OUT/SELECT/MIX/DIM state in a 4-byte readback (request 0x17), and the host software is expected to act on them, as TotalMix does. Poll it (every 20 ms by default, panel_poll_ms) and expose the decoded state as read-only controls, notified on change. The driver acts on SET (phantom power of the selected microphone, the host standing in for the unit's standalone function), MIX and the wheel: - With OUT selected, the firmware moves the analog output level itself on each click, smoothed over about 12 ms, by a step that depends on the level and on how fast the wheel turns (0.5 to 3 dB, doubled below a ~62 ms gap between clicks). The driver follows it: it writes only the 16-bit (digital) master during the gesture, tracks the level per side in half-dB, polls every 5 ms for 200 ms after a click to tell fast clicks apart, and writes the 8-bit master once the wheel has rested for 150 ms, as TotalMix does at the end of a gesture. A muted output ignores the wheel. The volume controls are notified. - With IN selected, the wheel moves the gain of the selected input. - With SELECT held, the wheel moves the balance of the selected output. - In MIX mode, the wheel moves the monitoring level of the selected input into the selected output, and the input VU display follows. The controls these change - the output volumes, the preamp gain, the 48V switches and the monitoring crosspoint - are notified, so that a mixer application follows the unit. The channel selection of SELECT (left, right, both, none) is not in the readback. The unit keeps one per IN pair, across IN switches and power cycles; its LEDs go dark on an IN switch, and the next press only shows the selection again, later presses step it. The driver follows the presses and exposes the selection of each pair as a "Front Panel Selection" control (index 0 is Ch 1/2, 1 Ch 3/4, 2 Opt), which is what alsactl keeps across boots. A pair it has not been told starts unknown, and SET and the wheel then do nothing, with one message, rather than act on a channel that may not be the lit one. A re-probe keeps what the driver had; the values alsactl restores after a probe are taken for a pair that is not known and ignored for one that is. A DIM press only increments "DIM Button Press Count", for a mixer application to act on; the driver changes no level on it. The OUT selector reports 0/1/2 for Ch 1/2, Phones and Opt on both the Pro and the Pro FS. The poll is stopped around a system suspend, and at disconnect before the mixer state is saved, since the poll worker writes that state. Co-developed-by: David Fredman Signed-off-by: David Fredman Signed-off-by: Ismaïl Bahloul --- sound/usb/babyfacepro/babyfacepro-ctl.c | 1235 ++++++++++++++++++++++- sound/usb/babyfacepro/babyfacepro.c | 39 +- sound/usb/babyfacepro/babyfacepro.h | 123 ++- 3 files changed, 1389 insertions(+), 8 deletions(-) diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c index f72879502..183647e25 100644 --- a/sound/usb/babyfacepro/babyfacepro-ctl.c +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c @@ -3,7 +3,8 @@ * RME Babyface Pro / Pro FS - proprietary-mode USB audio driver * * ALSA control surface: the hardware mixer (output masters and mutes, - * the crosspoint matrix, preamps, routing flags, varispeed pitch). + * the crosspoint matrix, preamps, routing flags, varispeed pitch), + * and the front-panel poll and controls. * * See babyfacepro.h for the shared device state and register map, * and babyfacepro.c for the core driver (protocol, PCM streaming, @@ -664,6 +665,7 @@ int babyface_create_xpoints(struct snd_usb_babyface *chip) err = snd_ctl_add(chip->card, kctl); if (err < 0) return err; + chip->xpoint_kctl[out][src] = kctl; } } @@ -1409,6 +1411,7 @@ int babyface_create_preamp(struct snd_usb_babyface *chip) err = snd_ctl_add(chip->card, kctl); if (err < 0) return err; + chip->phantom_kctl[i] = kctl; } for (i = 0; i < 2; i++) { @@ -1455,6 +1458,7 @@ int babyface_create_preamp(struct snd_usb_babyface *chip) err = snd_ctl_add(chip->card, kctl); if (err < 0) return err; + chip->gain_kctl[i] = kctl; } return 0; } @@ -2098,3 +2102,1232 @@ int babyface_create_flags(struct snd_usb_babyface *chip) return 0; } +/* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */ +enum { + BF_PANEL_KCTL_BUTTON, + BF_PANEL_KCTL_WHEEL, + BF_PANEL_KCTL_IN, + BF_PANEL_KCTL_OUT, + BF_PANEL_KCTL_MIX, + BF_PANEL_KCTL_DIM, + BF_PANEL_KCTL_NUM, +}; + +static const char *const bf_panel_in_texts[] = { + "Unknown", "Ch 1/2", "Ch 3/4", "Opt", NULL +}; + +static const char *const bf_panel_out_texts[] = { + "Unknown", "Ch 1/2", "Phones", "Opt", NULL +}; + +static const char *const bf_panel_select_texts[] = { + "Left", "Right", "Both", "None", "Unknown", NULL +}; + +/* byte3 button flash -> event code (0 = none). The idle byte3 is 0x40; + * a press flashes the value below the base for one or two poll frames. + */ +static int bf_panel_button_decode(u8 flash) +{ + switch (flash) { + case BF_PANEL_FLASH_IN: return BF_PANEL_BTN_IN; + case BF_PANEL_FLASH_SET: return BF_PANEL_BTN_SET; + case BF_PANEL_FLASH_MIX: return BF_PANEL_BTN_MIX; + case BF_PANEL_FLASH_OUT: return BF_PANEL_BTN_OUT; + case BF_PANEL_FLASH_SELECT: return BF_PANEL_BTN_SELECT; + case BF_PANEL_FLASH_DIM: return BF_PANEL_BTN_DIM; + default: return BF_PANEL_BTN_NONE; + } +} + +/* (byte2 >> 4) & 7 = IN position 4/5/6 -> enum index (0 = not in range). */ +static int bf_panel_in_decode(u8 nib) +{ + switch (nib) { + case BF_PANEL_IN_CH12: return 1; + case BF_PANEL_IN_CH34: return 2; + case BF_PANEL_IN_OPT: return 3; + default: return 0; + } +} + +/* byte1 & 7 = OUT position. 0/1/2 for physical Ch 1/2 / Phones / Opt, + * LED-correlated capture, hardware-verified on both an original + * Babyface Pro and the Pro FS. + */ +static int bf_panel_out_decode(u8 v) +{ + switch (v) { + case 0x00: + case BF_PANEL_OUT_CH12: return 1; + case 0x01: + case BF_PANEL_OUT_PHONES: return 2; + case 0x02: + case BF_PANEL_OUT_OPT: return 3; + default: return 0; + } +} + +/* MIX-mode VU display law - monitoring dBx2 -> the 0x1A 0x000A display + * value. Piecewise-linear through the captured (dB, display) points + * (cap_mix.pcap 2026-08-23: (-62,0) (-54,1) (-48,2) (-42.5,3) + * (-35,4) (-28.4,5); cap_panel.pcap: (-7.4,10) (-6.7,11) + * (-4.6,12)) - a log-ish VU scale (coarse at the bottom, ~1.4 dB/step + * near 0). The -28..-8 dB middle is interpolated; the exact law is + * pending the cap_mixdisp.pcap full-range sweep (TODO 0g). + */ +static int bf_mix_display(int db2) +{ + static const struct { + s16 db2; + u8 disp; + } pts[] = { + { -124, 0 }, { -108, 1 }, { -96, 2 }, { -85, 3 }, + { -70, 4 }, { -57, 5 }, { -15, 10 }, { -13, 11 }, + { -9, 12 }, + }; + int i; + + if (db2 <= pts[0].db2) + return 0; + for (i = 0; i < ARRAY_SIZE(pts) - 1; i++) { + if (db2 <= pts[i + 1].db2) { + u32 num = (u32)(db2 - pts[i].db2) * + (u32)(pts[i + 1].disp - pts[i].disp); + u32 den = pts[i + 1].db2 - pts[i].db2; + + return pts[i].disp + (int)((num + den / 2) / den); + } + } + /* Above -4.6 dB: keep the last slope (2 dB/step) up to +6 dB. */ + return pts[ARRAY_SIZE(pts) - 1].disp + + clamp((db2 - pts[ARRAY_SIZE(pts) - 1].db2) / 4, 0, 12); +} + +/* Tell ALSA clients that the front panel changed a mixer control. */ +static void bf_panel_notify_kctl(struct snd_usb_babyface *chip, + struct snd_kcontrol *kctl) +{ + if (kctl) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, &kctl->id); +} + +/* The selection of the IN pair now shown, from panel_sel[]. */ +static void bf_panel_sel_load(struct snd_usb_babyface *chip) +{ + int pair = READ_ONCE(chip->panel_in) - 1; + int sel = pair >= 0 && pair < 3 ? chip->panel_sel[pair] : -1; + + chip->panel_select_known = sel >= 0; + WRITE_ONCE(chip->panel_select, sel >= 0 ? sel : 3); +} + +/* SET and the wheel have nothing to act on while the selection is not + * known: say so once, since the cause is not visible from the unit. + */ +static void bf_panel_sel_hint(struct snd_usb_babyface *chip) +{ + if (!chip->panel_select_known) + dev_info_once(&chip->dev->dev, + "front panel: the SELECT channel selection is not known (the unit keeps it, and the driver cannot read it), so SET and the wheel do nothing; press SELECT to see it, then set the \"Front Panel Selection\" control of the IN pair shown (index 0 = Ch 1/2) to match\n"); +} + +/* The kernel driver plays the TotalMix role for the MIX button (the + * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs + * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT- + * chosen channel(s) of the IN-selected pair, into the OUT-selected + * output's crosspoint block - the STANDARD map only (cap_mix.pcap / + * cap_select2.pcap, no low-map mirror). Mirrors the change into the + * xpoint cache so the ALSA controls follow the wheel. Takes the mutex + * (the 0x12 writes cycle the transaction flag like the mixer puts). + */ +static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta) +{ + /* Canonical output of the OUT selection (enum 1 = Ch1/2, + * 2 = Phones, 3 = Opt): AN1/2, PH3/4, ADAT7/8 (the optical + * output) respectively. + */ + int out = READ_ONCE(chip->panel_out) == 3 ? 5 : + READ_ONCE(chip->panel_out) == 2 ? 1 : 0; + unsigned int blk = bf_xpoint_block[out]; + u8 targets[2]; + int n = 0; + int db2; + u16 raw, flag; + int i; + + /* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT + * steps left/right/both; none = nothing selected = no-op wheel). + * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4. + */ + if (READ_ONCE(chip->panel_in) == 3) { + targets[0] = 4; /* Opt: the AS1/2 pair */ + n = 1; + } else if (READ_ONCE(chip->panel_select) != 3) { + int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0; + + targets[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0); + n = 1; + if (READ_ONCE(chip->panel_select) == 2) + targets[n++] = base + 1; + } else { + bf_panel_sel_hint(chip); + } + + mutex_lock(&chip->mutex); + db2 = bf_fader_raw_to_db2(chip->panel_mix_raw); + db2 = clamp(db2 + delta, BF_FADER_DB2_INF, 12); + raw = bf_fader_db2_to_raw(db2); + chip->panel_mix_raw = raw; + for (i = 0; i < n; i++) { + const struct bf_source *s = &bf_sources[targets[i]]; + + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw, + (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk + + s->idx_l) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw, + (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk + + s->idx_r) | flag); + chip->xpoint[out][targets[i]][0] = raw; + chip->xpoint[out][targets[i]][1] = raw; + bf_panel_notify_kctl(chip, chip->xpoint_kctl[out][targets[i]]); + /* MIX-mode VU display shadow (0x1A 0x000A+mic): TotalMix + * mirrors the monitoring level into the panel display family + * (cap_mix/cap_panel.pcap) - the input VU segments follow it. + * Written only on change (the captures show TotalMix updating + * it on segment crossings). Law = bf_mix_display (TODO 0g + * pending the exact full-range capture). + */ + if (targets[i] < 4) { + int disp = bf_mix_display(db2); + + if (disp != chip->panel_mix_disp[targets[i]]) { + bf_vendor_write(chip, BF_REQ_GAIN, + (u16)disp, + BF_REG_PANEL_GAIN + targets[i]); + chip->panel_mix_disp[targets[i]] = disp; + } + } + } + mutex_unlock(&chip->mutex); +} + +/* A muted output ignores the wheel, as it does under TotalMix. The + * firmware still moves the analog level by itself, up from the mute + * code, so the output would become faintly audible while the mute switch + * still reads off: put the mute back. Caller holds the mutex. + */ +static void bf_panel_remute(struct snd_usb_babyface *chip, int out) +{ + bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE, + BF_REG_MASTER_8 + 2 * out); + bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE, + BF_REG_MASTER_8 + 2 * out + 1); +} + +/* Write an output's L/R masters (8-bit companions + 16-bit with the + * transaction flag) and mirror into the cache - used by the balance + * wheel on an unmuted output. Caller holds the mutex. + */ +static void bf_panel_write_master(struct snd_usb_babyface *chip, int out, + u16 l, u16 r) +{ + u16 flag; + + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l), + BF_REG_MASTER_8 + 2 * out); + bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r), + BF_REG_MASTER_8 + 2 * out + 1); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_MASTER_16 + 2 * out) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + chip->master[out][0] = l; + chip->master[out][1] = r; + /* A Phones change while DIM is engaged re-bases the restore. */ + if (chip->dim && out == 1) { + chip->dim_saved[0] = l; + chip->dim_saved[1] = r; + } + if (chip->master_kctl[out]) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &chip->master_kctl[out]->id); +} + +/* Front-panel OUT wheel, measured on hardware 2026-09-17 with a tone + * looped from the headphone output into IN3/IN4: + * + * - The firmware moves an analog output's level by itself when the + * wheel turns, with its own ~12 ms smoothing, even when the host + * writes nothing. See bf_out_wheel_step() for the step size. + * - A host write to the 8-bit master (the analog gain) overrides that. + * Writing it on every poll from the host's own count made the level + * saw-tooth by 1-3 dB while turning, heard as heavy zipper noise. + * - The 16-bit master does not affect the analog level, but it is the + * digital outputs' level. TotalMix writes only the 16-bit during a + * wheel gesture, and the 8-bit once the wheel is at rest. + * + * So while the wheel turns, only the 16-bit is written, and the cache + * follows the firmware's own count so the ALSA controls read the real + * level. A muted output ignores the wheel (see bf_panel_remute()). + * Both sides move by the louder side's step, so a balance (hold-SELECT) + * is kept. Same output mapping as the MIX wheel (Phones = canon 1, + * Opt = ADAT7/8 = canon 5, else AN1/2). + */ +static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out, + u16 l, u16 r) +{ + u16 flag; + + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_MASTER_16 + 2 * out) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + chip->master[out][0] = l; + chip->master[out][1] = r; + /* A Phones change while DIM is engaged re-bases the restore. */ + if (chip->dim && out == 1) { + chip->dim_saved[0] = l; + chip->dim_saved[1] = r; + } + if (chip->master_kctl[out]) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &chip->master_kctl[out]->id); +} + +/* A click that follows the previous one within this gap moves twice as + * far. In the recordings the firmware doubled clicks up to 58 ms apart + * and not from 67 ms. A normal poll every ~21 ms can't tell those + * apart, so after an OUT wheel click the panel is polled every + * BF_PANEL_FAST_POLL_MS for BF_PANEL_FAST_HOLD_MS, and each click is + * dated at the middle of the interval it was seen in. A wrong guess is + * fixed by the resync at rest below. + */ +#define BF_OUT_WHEEL_ACCEL_MS 62 +#define BF_PANEL_FAST_POLL_MS 5 +#define BF_PANEL_FAST_HOLD_MS 200 + +/* Quiet time after the last click before the resync write. */ +#define BF_OUT_WHEEL_RESYNC_MS 150 + +/* The device's own OUT wheel range: below -90 dB a side is muted. + * BF_OUT_WHEEL_MUTED is where a muted louder side is kept. + */ +#define BF_OUT_WHEEL_FLOOR (-180) /* half-dB */ +#define BF_OUT_WHEEL_MUTED (BF_OUT_WHEEL_FLOOR - 1) +/* Lowest level the 8-bit master is known to take (BF_MASTER_8_MIN). */ +#define BF_OUT_WHEEL_8BIT_MIN (BF_MASTER_8_MIN - BF_MASTER_8_0DB) + +/** + * bf_out_wheel_step - the firmware's OUT wheel step, in half-dB + * @half_db: the louder side's level before the click, in half-dB + * + * Measured on the headphone output (a tone looped into IN3/IN4, the + * driver writing nothing): + * + * -9.5 dB and up 0.5 dB per click + * -10 to -25.5 dB 1 dB + * -26 to -41.5 dB 1.5 dB + * -42 to -57.5 dB 2 dB + * -58 dB and down 3 dB + * + * A fast click moves twice as far. Both sides move by the step of the + * louder one, so a balance is kept. + */ +static int bf_out_wheel_step(int half_db) +{ + if (half_db >= -19) + return 1; + if (half_db >= -51) + return 2; + if (half_db >= -83) + return 3; + if (half_db >= -115) + return 4; + return 6; +} + +/* The wheel's view of one side: its own tracked level while the master + * is still what the wheel wrote, else the master's level. + */ +static int bf_out_wheel_level(struct snd_usb_babyface *chip, int out, int ch) +{ + u16 raw = chip->master[out][ch]; + + if (raw == chip->panel_master_last[out][ch]) + return chip->panel_out_hdb[out][ch]; + if (!raw) + return BF_OUT_WHEEL_MUTED; + return bf_master_half_db(raw); +} + +/* One click on both sides. They move together, so a balance is kept. + * When the louder side would go below the floor, both stop there and the + * louder side mutes; from mute, turning up continues from the floor + * (-90 dB up one step is -87). A quieter side below the floor is muted + * but keeps its offset, so the balance comes back as the level rises. + */ +static void bf_out_wheel_click(int db[2], int step) +{ + int louder = max(db[0], db[1]); + int shift = step; + + if (louder < BF_OUT_WHEEL_FLOOR) { + if (step < 0) + return; + shift = BF_OUT_WHEEL_FLOOR - louder + step; + } else if (louder + step < BF_OUT_WHEEL_FLOOR) { + shift = BF_OUT_WHEEL_MUTED - louder; + } else if (louder + step > 12) { + shift = 12 - louder; + } + db[0] += shift; + db[1] += shift; +} + +static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta) +{ + int out = READ_ONCE(chip->panel_out) == 3 ? 5 : + READ_ONCE(chip->panel_out) == 2 ? 1 : 0; + int dir = delta > 0 ? 1 : -1; + int clicks = abs(delta); + ktime_t now = ktime_get(); + bool fast; + int ch, i, step, louder; + int db[2]; + u16 next[2]; + + chip->panel_fast_until = jiffies + + msecs_to_jiffies(BF_PANEL_FAST_HOLD_MS); + /* The click came some time since the previous poll: take the + * middle. Clicks after the first in this poll came within one + * interval, so only the first can be slow. A reversal starts a + * new turn. + */ + if (chip->panel_poll_t) + now = ktime_sub(now, + ktime_divns(ktime_sub(now, chip->panel_poll_t), 2)); + + mutex_lock(&chip->mutex); + if (chip->muted[out]) { + bf_panel_remute(chip, out); + mutex_unlock(&chip->mutex); + return; + } + fast = chip->panel_out_wheel_t && + dir == chip->panel_out_wheel_dir && + out == chip->panel_out_wheel_out && + ktime_ms_delta(now, chip->panel_out_wheel_t) < + BF_OUT_WHEEL_ACCEL_MS; + for (ch = 0; ch < 2; ch++) + db[ch] = bf_out_wheel_level(chip, out, ch); + for (i = 0; i < clicks; i++) { + /* Muted, the louder side counts as the floor. */ + louder = max3(db[0], db[1], BF_OUT_WHEEL_FLOOR); + step = dir * bf_out_wheel_step(louder); + if (fast || i) + step *= 2; + bf_out_wheel_click(db, step); + } + for (ch = 0; ch < 2; ch++) + next[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 : + bf_master_16bit(db[ch]); + bf_panel_out_wheel_write(chip, out, next[0], next[1]); + for (ch = 0; ch < 2; ch++) { + chip->panel_master_last[out][ch] = next[ch]; + chip->panel_out_hdb[out][ch] = db[ch]; + } + chip->panel_out_wheel_t = now; + chip->panel_out_wheel_dir = dir; + chip->panel_out_wheel_out = out; + chip->panel_out_resync = true; + mutex_unlock(&chip->mutex); +} + +/* Once the wheel has rested, write the wheel's level to the 8-bit (and + * 16-bit) master, as TotalMix does at the end of a gesture. The level + * already follows the firmware's step rule, so this normally changes + * nothing audible. It fixes the rare click whose speed the driver + * guessed wrong: one zone step per such click, smoothed by the + * firmware. Nothing is written below -64 dB, where the 8-bit codes the + * device takes are unknown, nor after a mixer application has set the + * master since (that write already set the device). + */ +static void bf_panel_out_resync(struct snd_usb_babyface *chip) +{ + int out, ch; + int db[2]; + u16 flag; + + if (!chip->panel_out_resync || + ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) < + BF_OUT_WHEEL_RESYNC_MS) + return; + + mutex_lock(&chip->mutex); + chip->panel_out_resync = false; + out = chip->panel_out_wheel_out; + for (ch = 0; ch < 2; ch++) { + if (chip->master[out][ch] != chip->panel_master_last[out][ch]) + goto unlock; + db[ch] = chip->panel_out_hdb[out][ch]; + if (db[ch] < BF_OUT_WHEEL_8BIT_MIN) + goto unlock; + } + if (!chip->muted[out]) { + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[0], + BF_REG_MASTER_8 + 2 * out); + bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[1], + BF_REG_MASTER_8 + 2 * out + 1); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][0], + (BF_REG_MASTER_16 + 2 * out) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][1], + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + } +unlock: + mutex_unlock(&chip->mutex); +} + +/* IN-mode wheel: the gain of the SELECT-chosen channel(s) of the + * IN-selected pair, +/-1 dB per click (manual sec. 5.1: SELECT steps + * left/right/both, then the wheel changes the gain). Writes the PANEL + * gain registers 0x1A 0x000A+mic (cap_select.pcap 2026-08-24 - the + * "ADC gain" family, which drives the same preamp as the GUI + * 0x0000+mic; the cache tracks the raw either way). Opt has no + * preamp and SELECT None = no target. + */ +static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta) +{ + u8 mics[2]; + int n = 0; + int i; + + if (READ_ONCE(chip->panel_in) == 3) + return; + if (READ_ONCE(chip->panel_select) == 3) { + bf_panel_sel_hint(chip); + return; + } + { + int base = READ_ONCE(chip->panel_in) == 2 ? 2 : 0; + + mics[0] = base + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0); + n = 1; + if (READ_ONCE(chip->panel_select) == 2) + mics[n++] = base + 1; + } + + mutex_lock(&chip->mutex); + for (i = 0; i < n; i++) { + int mic = mics[i]; + int db = clamp((int)chip->gain[mic] + delta, + 0, bf_gain_max_db(mic)); + u8 raw = bf_gain_raw(mic, db); + + bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic); + if (db != chip->gain[mic]) { + chip->gain[mic] = db; + bf_panel_notify_kctl(chip, chip->gain_kctl[mic]); + } + } + mutex_unlock(&chip->mutex); +} + +/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output + * Balance"): moves the stereo image of the OUT-selected output by + * attenuating ONE side, linear in raw (cap_pan_stereo.pcap: the varied + * side = fixed*(1-|pan|), ~0x9C raw step per click at 0 dB - the PAN + * of the stereo hardware output in TotalMix). The balance position is + * derived from the L/R master ratio (the louder side is the fixed + * one), so the gesture needs no extra state - and the OUT wheel below + * moves both sides by the same dB to preserve an existing balance. + */ +static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta) +{ + int out = READ_ONCE(chip->panel_out) == 3 ? 5 : + READ_ONCE(chip->panel_out) == 2 ? 1 : 0; + u16 l, r; + int bal; /* -100..+100; + = image right (left varies) */ + u16 fixed, varied; + + mutex_lock(&chip->mutex); + if (chip->muted[out]) { + bf_panel_remute(chip, out); + mutex_unlock(&chip->mutex); + return; + } + /* Read under the lock so the L/R pair is consistent with the + * master/mute/dim writers (they update chip->master[] under the + * same mutex). + */ + l = chip->master[out][0]; + r = chip->master[out][1]; + /* Balance from the L/R ratio: the louder side is the fixed one. */ + if (l >= r) { + bal = r ? -(100 - (100 * r) / l) : -100; + fixed = l; + } else { + bal = l ? (100 - (100 * l) / r) : 100; + fixed = r; + } + bal = clamp(bal + delta * 2, -100, 100); + varied = (u16)((u32)fixed * (100 - abs(bal)) / 100); + l = bal >= 0 ? varied : fixed; + r = bal >= 0 ? fixed : varied; + + bf_panel_write_master(chip, out, l, r); + mutex_unlock(&chip->mutex); +} + +/* SET press (byte3 0x42 flash): toggle 48V phantom on the + * SELECT-chosen mic(s) of the IN-selected pair. The hardware only + * does this in standalone mode (online, TotalMix ignores SET - no USB + * write in the captures), but the driver IS the host: it writes the + * preamp state itself and the P48 LEDs follow (the tuxmix-core + * emulator, hardware-verified). Restricted to IN mode + Ch1/2 (the + * phantom-capable pair); Opt/Ch3/4 and SELECT None = no target. + */ +static void bf_panel_set_phantom(struct snd_usb_babyface *chip) +{ + u16 bits = 0; + u16 old; + int m; + + if (READ_ONCE(chip->panel_mix) || READ_ONCE(chip->panel_in) != 1) + return; + if (READ_ONCE(chip->panel_select) == 3) { + bf_panel_sel_hint(chip); + return; + } + if (READ_ONCE(chip->panel_select) != 1) + bits |= BF_PREAMP_48V_MIC1; + if (READ_ONCE(chip->panel_select) != 0) + bits |= BF_PREAMP_48V_MIC2; + + mutex_lock(&chip->mutex); + old = chip->preamp; + /* One channel selected: toggle it. Both selected: ALIGN both to + * the same state, so repeated SET presses cycle all-on <-> all-off + * (a mixed phantom state cannot persist with both selected). + */ + if (READ_ONCE(chip->panel_select) == 2) { + if ((chip->preamp & bits) == bits) + chip->preamp &= ~bits; + else + chip->preamp |= bits; + } else { + chip->preamp ^= bits; + } + if (bf_preamp_state_write(chip) < 0) + chip->preamp = old; + if ((chip->preamp ^ old) & BF_PREAMP_48V_MIC1) + bf_panel_notify_kctl(chip, chip->phantom_kctl[0]); + if ((chip->preamp ^ old) & BF_PREAMP_48V_MIC2) + bf_panel_notify_kctl(chip, chip->phantom_kctl[1]); + for (m = 0; m < 4; m++) + chip->panel_mix_disp[m] = 0; + mutex_unlock(&chip->mutex); +} + +static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl) +{ + if (chip->panel_kctl[ctl]) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &chip->panel_kctl[ctl]->id); +} + +/* One 0x17 read + decode. Called from the poll work. The decoded state + * (panel_button/wheel/in/out/select/mix/dim) is also read by the control + * get callbacks, and panel_select is written by the SELECT control's + * put(): those fields are shared between the worker and the ALSA control + * layer, so every access to them goes through READ_ONCE()/WRITE_ONCE(), + * which keeps the lock-free access defined and tear-free rather than a + * plain data race (the worker is the only writer of the other fields). + */ +static void bf_panel_tick(struct snd_usb_babyface *chip) +{ + u8 st[4]; + int delta, in, out; + bool dim; + u8 cls, pcls; + int btn; + bool mix_flash, fader_now; + + if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0) + return; /* device gone / busy - retry next tick */ + + if (!chip->panel_seen) { + chip->panel_seen = true; + memcpy(chip->panel_prev, st, sizeof(st)); + /* Seed the state controls from the first snapshot. */ + in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7); + if (in) + WRITE_ONCE(chip->panel_in, in); + /* The IN pair is only known from here on: pick up its + * selection (kept across a re-probe). + */ + bf_panel_sel_load(chip); + out = bf_panel_out_decode(st[1] & 0x07); + if (out) + WRITE_ONCE(chip->panel_out, out); + WRITE_ONCE(chip->panel_mix, !!(st[0] & 0x80)); + WRITE_ONCE(chip->panel_saw_fader, (st[2] >> 4) == 0x0); + WRITE_ONCE(chip->panel_dim, !!(st[1] & 0x20)); + return; + } + + /* Button flash (byte3 over the 0x40 idle base). Notify on the + * edge, as the other flash-driven controls do, so a subscriber sees + * each press once rather than on every poll while it is held. + */ + btn = bf_panel_button_decode(st[3]); + if (btn && chip->panel_prev[3] != st[3]) { + WRITE_ONCE(chip->panel_button, btn); + bf_panel_notify(chip, BF_PANEL_KCTL_BUTTON); + } + + /* Wheel: signed 4-bit wrap delta of the byte2 low nibble - only + * while the mode class is unchanged. A mode switch (IN 0x4x -> + * fader 0x0x on a MIX press, or the OUT counter carrying 0x8F -> + * 0x90 - the OUT counter is a full byte, cap_set2.pcap) must not + * be read as a wheel jump. Class: 0 = fader (0x0x), 1 = OUT + * (0x8x/0x9x), 2 = IN (0x4x/0x5x/0x6x). + */ + cls = (st[2] >> 4) == 0x8 || (st[2] >> 4) == 0x9 ? 1 : + (st[2] >> 4) == 0x0 ? 0 : 2; + pcls = (chip->panel_prev[2] >> 4) == 0x8 || + (chip->panel_prev[2] >> 4) == 0x9 ? 1 : + (chip->panel_prev[2] >> 4) == 0x0 ? 0 : 2; + delta = (int)(st[2] & 0x0f) - (int)(chip->panel_prev[2] & 0x0f); + if (delta > 8) + delta -= 16; + else if (delta < -8) + delta += 16; + if (delta && cls == pcls) { + WRITE_ONCE(chip->panel_wheel, + clamp(READ_ONCE(chip->panel_wheel) + delta, + SHRT_MIN, SHRT_MAX)); + bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL); + /* Wheel by mode (LINUX-VALIDATION sec. 12, the TotalMix + * emulator): MIX -> monitoring level, OUT (0x8x/0x9x) -> the + * selected output master (or its balance while SELECT is + * held), IN (0x4x/0x5x/0x6x) -> the SELECT-chosen preamp + * gain. + */ + if (READ_ONCE(chip->panel_mix)) + bf_panel_mix_wheel(chip, delta); + else if (chip->panel_sel_hold >= 10 && cls == 1) + bf_panel_balance_wheel(chip, delta); + else if (cls == 1) + bf_panel_out_wheel(chip, delta); + else if (cls == 2) + bf_panel_gain_wheel(chip, delta); + } + bf_panel_out_resync(chip); + + /* Selections - keep the previous when the field is not in range + * (the fader-mode readback drops the IN position bits). + */ + in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7); + if (in && in != READ_ONCE(chip->panel_in)) { + WRITE_ONCE(chip->panel_in, in); + /* The unit keeps one SELECT selection per IN pair, and an + * IN switch does not clear it (hardware-verified + * 2026-10-02: back on Ch 1/2 after a trip through Ch 3/4, + * the first SELECT press showed the L it had left, while + * Ch 3/4 showed nothing). Switch to the new pair's. + */ + bf_panel_sel_load(chip); + /* The LEDs go dark on an IN switch: the next SELECT press + * only shows the selection again (no step), the one after + * that steps it (user-verified 2026-08-28). + */ + chip->panel_select_armed = false; + bf_panel_notify(chip, BF_PANEL_KCTL_IN); + } + out = bf_panel_out_decode(st[1] & 0x07); + if (out && out != READ_ONCE(chip->panel_out)) { + WRITE_ONCE(chip->panel_out, out); + bf_panel_notify(chip, BF_PANEL_KCTL_OUT); + } + + /* SELECT press cycles the channel selection L -> R -> both -> none + * -> L (manual sec. 5.1). The state is NOT in the readback + * (panelprobe 2026-08-24), so it is tracked host-side. + */ + if (st[3] == BF_PANEL_FLASH_SELECT && + chip->panel_prev[3] != BF_PANEL_FLASH_SELECT && + chip->panel_select_known) { + if (!chip->panel_select_armed) { + /* Disarmed (IN switch since the last step): the press + * only shows the selection - the device steps it on + * the NEXT press (user-verified 2026-08-28). + */ + chip->panel_select_armed = true; + } else { + int sel = (READ_ONCE(chip->panel_select) + 1) & 3; + struct snd_kcontrol *kctl = + chip->panel_select_kctl[READ_ONCE(chip->panel_in) - 1]; + + WRITE_ONCE(chip->panel_select, sel); + chip->panel_sel[READ_ONCE(chip->panel_in) - 1] = sel; + if (kctl) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &kctl->id); + } + } + /* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output + * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz + * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and + * byte0 does NOT gain the 0x80 engaged bit - so the duration is + * the only discriminator: >= 10 ticks (200 ms at 50 Hz) = held. + */ + if (st[3] == BF_PANEL_FLASH_SELECT) + chip->panel_sel_hold++; + else + chip->panel_sel_hold = 0; + + /* SET (A) press: host-side 48V phantom toggle on the + * SELECT-chosen mic(s) (see bf_panel_set_phantom). + */ + if (st[3] == BF_PANEL_FLASH_SET && + chip->panel_prev[3] != BF_PANEL_FLASH_SET) + bf_panel_set_phantom(chip); + + /* DIM is a software-assignable button. Report presses without + * choosing a monitor output or changing any gain in the driver. + */ + if (st[3] == BF_PANEL_FLASH_DIM && + chip->panel_prev[3] != BF_PANEL_FLASH_DIM) { + chip->dim_press_count = (chip->dim_press_count + 1) & 0x7fffffff; + if (chip->dim_press_kctl) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &chip->dim_press_kctl->id); + } + + /* MIX (fader mode) - HOST-latched, like TotalMix (cap_mix.pcap, + * cap_select2.pcap): the raw press readback is `0D 0D 41 44` - + * byte3 flash 0x44, NO engaged bit, byte2 still in the current + * mode. The host acks the flash with `0x17 0x8480 0x8C80` -> the + * device latches fader mode (byte0/1 gain the 0x80 bit, byte2 = + * 0x00+n counter) and STAYS there after the physical release; the + * SECOND 0x44 flash exits it (`0x17 0x0400 0x8000` + `0x8080`). + * A mode button (IN/OUT/SET) pressed during MIX makes the device + * leave fader mode by itself -> same exit writes (the user: IN + * must return to gain control). `panel_saw_fader` gates the + * device-driven exit so a pre-ack readback (byte2 still 0x4x + * while the 0x44 flash shows) never ends MIX before it started. + */ + mix_flash = st[3] == BF_PANEL_FLASH_MIX && + chip->panel_prev[3] != BF_PANEL_FLASH_MIX; + fader_now = (st[2] >> 4) == 0x0; + + if (mix_flash) { + if (READ_ONCE(chip->panel_mix)) { + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000); + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080); + WRITE_ONCE(chip->panel_mix, false); + WRITE_ONCE(chip->panel_saw_fader, false); + } else { + int ref, out; + int m; + + bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80); + WRITE_ONCE(chip->panel_mix, true); + /* Seed the monitoring level at the reference + * crosspoint's current value so the first wheel + * click doesn't jump from -inf (the reference = + * the first SELECT-chosen channel of the IN pair; + * Opt = the AS1/2 pair). + */ + out = READ_ONCE(chip->panel_out) == 3 ? 5 : + READ_ONCE(chip->panel_out) == 2 ? 1 : 0; + ref = READ_ONCE(chip->panel_in) == 3 ? 4 : + (READ_ONCE(chip->panel_in) == 2 ? 2 : 0) + + (READ_ONCE(chip->panel_select) == 1 ? 1 : 0); + chip->panel_mix_raw = chip->xpoint[out][ref][0]; + /* Seed the VU display shadow at the CURRENT level + * (cap_panel.pcap: TotalMix writes the display value of + * the current fader on engage - 10 in that session - + * not a hard 0; cap_mix's 0 was because the fader sat + * at the bottom). Only the channels the wheel can move. + */ + for (m = 0; m < 4; m++) + chip->panel_mix_disp[m] = 0; + if (ref < 4) { + int db2 = bf_fader_raw_to_db2(chip->panel_mix_raw); + int disp = bf_mix_display(db2); + + bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp, + BF_REG_PANEL_GAIN + ref); + chip->panel_mix_disp[ref] = disp; + } + } + bf_panel_notify(chip, BF_PANEL_KCTL_MIX); + } + if (fader_now) { + WRITE_ONCE(chip->panel_saw_fader, true); + } else if (READ_ONCE(chip->panel_mix) && READ_ONCE(chip->panel_saw_fader) && + st[3] != BF_PANEL_FLASH_MIX) { + /* device left fader mode by itself (IN/OUT/SET press) */ + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000); + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080); + WRITE_ONCE(chip->panel_mix, false); + WRITE_ONCE(chip->panel_saw_fader, false); + bf_panel_notify(chip, BF_PANEL_KCTL_MIX); + } + + dim = !!(st[1] & 0x20); + if (dim != READ_ONCE(chip->panel_dim)) { + WRITE_ONCE(chip->panel_dim, dim); + bf_panel_notify(chip, BF_PANEL_KCTL_DIM); + } + + memcpy(chip->panel_prev, st, sizeof(st)); +} + +void babyface_panel_work(struct work_struct *work) +{ + struct snd_usb_babyface *chip = container_of(work, + struct snd_usb_babyface, panel_work.work); + unsigned int ms; + + if (chip->shutdown) + return; + bf_panel_tick(chip); + chip->panel_poll_t = ktime_get(); + /* Poll fast for a moment after an OUT wheel click. */ + ms = time_before(jiffies, chip->panel_fast_until) ? + BF_PANEL_FAST_POLL_MS : chip->panel_poll_ms; + schedule_delayed_work(&chip->panel_work, msecs_to_jiffies(ms)); +} + +void babyface_panel_start(struct snd_usb_babyface *chip) +{ + chip->panel_seen = false; + /* The SELECT channel selection is not in the readback, so the + * driver follows it from the presses. The unit keeps one per IN + * pair across a driver reload and even a power cycle, with the LEDs + * dark until the next SELECT press, which shows it again instead of + * stepping (hardware-verified 2026-09-29: after an unplug, the + * first press lit both channels). What panel_sel[] holds comes + * from before (a re-probe keeps it, see bf_saved) or is not known, + * and an unknown selection targets nothing: SET, the IN wheel and + * the MIX wheel then do nothing rather than act on the wrong + * channel, until "Front Panel Select" is set to what the LEDs show. + */ + bf_panel_sel_load(chip); + chip->panel_select_armed = false; + chip->panel_start = jiffies; + chip->panel_fast_until = jiffies; + chip->panel_poll_t = 0; + /* No master holds 0xffff: the wheel starts from the masters. */ + memset(chip->panel_master_last, 0xff, sizeof(chip->panel_master_last)); + schedule_delayed_work(&chip->panel_work, 0); +} + +void babyface_panel_stop(struct snd_usb_babyface *chip) +{ + cancel_delayed_work_sync(&chip->panel_work); +} + +/* -- controls -------------------------- */ + +/* The button/wheel controls hold the LATEST state and are NOT consumed + * on read: wireplumber subscribes to every notifying control and reads + * it, so a clear-on-get would let another reader eat the event. Each + * consumer tracks its own baseline and acts on changes (the button is a + * last-press code, the wheel an accumulated signed delta). VOLATILE + * keeps alsactl from caching them. + */ +static int bf_panel_button_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = BF_PANEL_BTN_DIM; + uinfo->value.integer.step = 1; + return 0; +} + +static int bf_panel_button_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = READ_ONCE(chip->panel_button); + return 0; +} + +static int bf_panel_wheel_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = SHRT_MIN; + uinfo->value.integer.max = SHRT_MAX; + uinfo->value.integer.step = 1; + return 0; +} + +static int bf_panel_wheel_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = READ_ONCE(chip->panel_wheel); + return 0; +} + +static int bf_panel_in_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_in_texts); +} + +static int bf_panel_in_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] = READ_ONCE(chip->panel_in); + return 0; +} + +static int bf_panel_out_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_out_texts); +} + +static int bf_panel_out_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] = READ_ONCE(chip->panel_out); + return 0; +} + +static int bf_panel_select_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 5, bf_panel_select_texts); +} + +static int bf_panel_select_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + s8 sel = READ_ONCE(chip->panel_sel[kctl->private_value]); + + ucontrol->value.enumerated.item[0] = sel >= 0 ? sel : 4; + return 0; +} + +/* Writable so software (or the user, after a driver reload) can + * re-sync the host-tracked SELECT state to the physical card - the + * L/R/both/none state is NOT in the 0x17 readback, and the unit keeps + * one per IN pair across reloads and power cycles, so the driver cannot + * know it after a load. Writing what the LEDs show for the pair now + * selected tells it (TotalMix parity: it also lets software select + * channels directly). + */ +static int bf_panel_select_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + unsigned int v = ucontrol->value.enumerated.item[0]; + int pair = kctl->private_value; + /* alsactl restores the values stored at the last shutdown shortly + * after every probe. + */ + bool restore = time_is_after_jiffies(chip->panel_start + 3 * HZ); + s8 sel = v < 4 ? v : -1; + + if (v > 4) + return -EINVAL; + /* After a boot nothing is known and the stored values are what there + * is; after a re-probe the driver kept what it tracked, which is + * newer than anything stored. + */ + if (restore && chip->panel_sel[pair] >= 0) + return 0; + if (pair == READ_ONCE(chip->panel_in) - 1 && !restore && sel >= 0) + /* Set while the LEDs show the selection: the next press steps + * it. + */ + chip->panel_select_armed = true; + if (sel == chip->panel_sel[pair]) + return 0; + chip->panel_sel[pair] = sel; + if (pair == READ_ONCE(chip->panel_in) - 1) + bf_panel_sel_load(chip); + return 1; +} + +/* Shared boolean get - private_value selects mix (0) / dim (1). */ +static int bf_panel_bool_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = + kctl->private_value ? READ_ONCE(chip->panel_dim) : READ_ONCE(chip->panel_mix); + return 0; +} + +static int bf_dim_press_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = 0x7fffffff; + return 0; +} + +static int bf_dim_press_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = READ_ONCE(chip->dim_press_count); + return 0; +} + +int babyface_create_panel(struct snd_usb_babyface *chip) +{ + struct snd_kcontrol *kctl; + int err, pair; + + memset(chip->panel_kctl, 0, sizeof(chip->panel_kctl)); + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Button", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_button_info, + .get = bf_panel_button_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_BUTTON] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Wheel", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_wheel_info, + .get = bf_panel_wheel_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_WHEEL] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel In", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_in_info, + .get = bf_panel_in_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_IN] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Out", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_out_info, + .get = bf_panel_out_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_OUT] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Mix", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = snd_ctl_boolean_mono_info, + .get = bf_panel_bool_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_MIX] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Dim", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = snd_ctl_boolean_mono_info, + .get = bf_panel_bool_get, + .private_value = 1, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_DIM] = kctl; + + /* One per IN pair (index 0 = Ch 1/2, 1 = Ch 3/4, 2 = Opt): the + * unit keeps one selection per pair, and a plain control, not a + * volatile one, is what alsactl keeps across boots. Named apart from + * the single volatile "Front Panel Select" this replaces, whose stored + * value alsactl must not apply. + */ + for (pair = 0; pair < 3; pair++) { + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Selection", + .index = pair, + .info = bf_panel_select_info, + .get = bf_panel_select_get, + .put = bf_panel_select_put, + .private_value = pair, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_select_kctl[pair] = kctl; + } + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "DIM Button Press Count", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_dim_press_info, + .get = bf_dim_press_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->dim_press_kctl = kctl; + + return 0; +} + diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c index aaa74ee68..18b8ac08d 100644 --- a/sound/usb/babyfacepro/babyfacepro.c +++ b/sound/usb/babyfacepro/babyfacepro.c @@ -537,6 +537,7 @@ void bf_state_save(struct snd_usb_babyface *chip) s->width = chip->width; s->fx_send = chip->fx_send; s->dim = chip->dim; + memcpy(s->panel_sel, chip->panel_sel, sizeof(s->panel_sel)); mutex_unlock(&bf_saved_mutex); } @@ -574,6 +575,7 @@ int bf_state_restore(struct snd_usb_babyface *chip) chip->width = s->width; chip->fx_send = s->fx_send; chip->dim = s->dim; + memcpy(chip->panel_sel, s->panel_sel, sizeof(chip->panel_sel)); ret = 1; break; } @@ -1582,6 +1584,7 @@ static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR; static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT; static int urb_frames_min = 32; static int nurbs = BF_NURBS_DEFAULT; +static int panel_poll_ms = BF_PANEL_POLL_MS_DEFAULT; module_param_array(index, int, NULL, 0444); MODULE_PARM_DESC(index, "Index value for the Babyface Pro sound card."); @@ -1593,6 +1596,8 @@ module_param(urb_frames_min, int, 0444); MODULE_PARM_DESC(urb_frames_min, "Smallest URB in audio frames, 8..1024 (32 = default, 16 = low-latency floor)."); module_param(nurbs, int, 0444); MODULE_PARM_DESC(nurbs, "Most URBs in flight per direction, 2..16 (8 = default)."); +module_param(panel_poll_ms, int, 0444); +MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz)."); /* -- USB driver ------------------------- */ @@ -1671,11 +1676,12 @@ static int babyface_probe(struct usb_interface *intf, chip->dev = usb_get_dev(dev); /* USB autosuspend is untested: babyface_suspend()/_resume() don't * check PMSG_IS_AUTO, and nothing in this driver holds a PM - * reference while streaming, so an autosuspend request could race - * a live stream. Disable it explicitly rather than ship an - * untested code path - full autosuspend support (correct - * autopm_get/put pairing around the stream) is a deliberate - * follow-up, not an oversight. + * reference while streaming or while the panel poll/keepalive + * timers are running, so an autosuspend request could race a + * live stream or panel tick. Disable it explicitly rather than + * ship an untested code path - full autosuspend support (correct + * autopm_get/put pairing around the stream and the panel/keepalive + * work) is a deliberate follow-up, not an oversight. */ usb_disable_autosuspend(chip->dev); chip->iface = intf; @@ -1690,10 +1696,13 @@ static int babyface_probe(struct usb_interface *intf, chip->alt = BF_ALT_1; chip->frame_bytes = 56; chip->preamp = BF_PREAMP_BASE; + memset(chip->panel_sel, -1, sizeof(chip->panel_sel)); + chip->panel_poll_ms = clamp(panel_poll_ms, 10, 1000); mutex_init(&chip->mutex); spin_lock_init(&chip->lock); atomic_set(&chip->urb_err, 0); INIT_WORK(&chip->stream_work, babyface_stream_work); + INIT_DELAYED_WORK(&chip->panel_work, babyface_panel_work); chip->card->private_free = babyface_private_free; /* Model-neutral on purpose. The FS and the original (2015) @@ -1856,12 +1865,23 @@ static int babyface_probe(struct usb_interface *intf, goto error; } + err = babyface_create_panel(chip); + if (err < 0) { + dev_err(&intf->dev, "front-panel control creation failed: %d\n", err); + goto error; + } + err = snd_card_register(chip->card); if (err < 0) { dev_err(&intf->dev, "snd_card_register failed: %d\n", err); goto error; } + /* The panel poll mirrors the physical buttons/wheel into the + * Front Panel controls; it runs for the whole card lifetime. + */ + babyface_panel_start(chip); + usb_set_intfdata(intf, chip); dev_info(&intf->dev, "Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n", @@ -1904,9 +1924,13 @@ static void babyface_disconnect(struct usb_interface *intf) babyface_stream_kill(chip); mutex_unlock(&chip->mutex); cancel_work_sync(&chip->stream_work); + babyface_panel_stop(chip); /* Keep the mixer state for the next probe: a userspace usbfs claim * (PipeWire sink grab, TuxMix daemon) detaches us and the cold init - * of the re-probe would otherwise wipe the settings. + * of the re-probe would otherwise wipe the settings. Saved after + * the panel poll is stopped: the worker writes master/gain/xpoint + * under chip->mutex and this copy does not take it, so an unplug + * during a wheel turn could otherwise snapshot a torn state. */ bf_state_save(chip); /* Balance the probe()-time usb_disable_autosuspend(): the usb_device @@ -1946,6 +1970,7 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message) babyface_stream_kill(chip); mutex_unlock(&chip->mutex); cancel_work_sync(&chip->stream_work); + babyface_panel_stop(chip); return 0; } @@ -1975,6 +2000,8 @@ static int babyface_resume(struct usb_interface *intf) err = bf_state_apply_flags(chip); out: mutex_unlock(&chip->mutex); + if (!err) + babyface_panel_start(chip); return err; } diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h index e324e8411..1959bdf2e 100644 --- a/sound/usb/babyfacepro/babyfacepro.h +++ b/sound/usb/babyfacepro/babyfacepro.h @@ -14,7 +14,7 @@ * babyfacepro.c holds the card lifecycle, the PCM stream and the * mixer-state persistence across re-probes and resume; * babyfacepro-ctl.c holds the ALSA control surface - the hardware - * mixer. + * mixer and the front panel. * * The protocol (vendor requests + 14x32-bit frame layout) was * reverse-engineered from Windows captures and validated on hardware - @@ -49,6 +49,7 @@ * bf_clock_write / bf_pitch_write. */ +#include #include #include #include @@ -190,6 +191,53 @@ */ #define BF_LOOPBACK_CHANNELS 30 +/* Front-panel poll interval default - Windows polls the 5-register + * status set at ~50 cycles/s (20 ms); match that. Tunable via the + * panel_poll_ms module param for reviewers/distros who want a slower + * (or faster) rate than the Windows-matching default. + */ +#define BF_PANEL_POLL_MS_DEFAULT 20 + +/* Front-panel readback (babyfacepro-ctl.c): 0x17 read at wIdx 0x0000 - the index + * the Windows driver polls (cap_buttons2.pcap). byte0 = preamp 48V/PAD, + * byte1 = OUT sel + DIM/MIX bits, byte2 = IN sel + wheel counter, + * byte3 = button flash (see babyfacepro-ctl.c for the full layout). + */ +#define BF_REG_PANEL_READ 0x0000 +#define BF_PANEL_IN_SHIFT 4 +#define BF_PANEL_IN_CH12 0x04 +#define BF_PANEL_IN_CH34 0x05 +#define BF_PANEL_IN_OPT 0x06 +/* OUT selection - the gain-display-mode encoding (cap_dim.pcap); + * babyfacepro-ctl.c also accepts the base-mode 0x01/0x02 (cap_buttons.pcap). + */ +#define BF_PANEL_OUT_CH12 0x04 +#define BF_PANEL_OUT_PHONES 0x05 +#define BF_PANEL_OUT_OPT 0x06 +#define BF_PANEL_FLASH_IN 0x41 +#define BF_PANEL_FLASH_SET 0x42 +#define BF_PANEL_FLASH_MIX 0x44 +#define BF_PANEL_FLASH_OUT 0x48 +#define BF_PANEL_FLASH_SELECT 0x50 +#define BF_PANEL_FLASH_DIM 0x60 +#define BF_PANEL_BTN_NONE 0 +#define BF_PANEL_BTN_IN 1 +#define BF_PANEL_BTN_SET 2 +#define BF_PANEL_BTN_MIX 3 +#define BF_PANEL_BTN_OUT 4 +#define BF_PANEL_BTN_SELECT 5 +#define BF_PANEL_BTN_DIM 6 + +/* The front-panel gain/display family (0x1A, wIdx 0x000A + mic 0-3; + * cap_panel/cap_mix.pcap): in gain mode the wheel writes the "ADC + * gain" here (drives the same preamp as the GUI 0x0000+mic); in MIX + * (fader) mode the same registers carry the VU DISPLAY shadow - + * TotalMix writes the monitoring level display value (0..~31) and the + * card lights the input VU segments accordingly (hardware-verified + * 2026-08-26 live: sweeping 0x1A values moved the input VU). + */ +#define BF_REG_PANEL_GAIN 0x000a + /* The "cross" register block within each output: the L-registers sit at * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo * source pairs that can be cross-linked). bf_crosspoint_clear_cross() @@ -301,6 +349,12 @@ struct snd_usb_babyface { u16 master[6][2]; /* cached 16-bit masters */ bool muted[6]; u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */ + /* The controls the front panel changes on its own (the MIX wheel, + * the IN wheel, SET), for snd_ctl_notify. + */ + struct snd_kcontrol *xpoint_kctl[6][14]; + struct snd_kcontrol *gain_kctl[4]; + struct snd_kcontrol *phantom_kctl[2]; u16 preamp; /* 48V/PAD bits, base 0x0c */ u8 gain[4]; /* preamp gain in dB 0-65/9 (raw derived * at write: mic packed coarse/fine, @@ -342,6 +396,68 @@ struct snd_usb_babyface { u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */ bool dim; /* DIM engaged (fixed -20 dB on Phones) */ struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */ + + /* front panel (babyfacepro-ctl.c) - 0x17 readback poll */ + struct delayed_work panel_work; + unsigned int panel_poll_ms; /* front-panel poll interval, module param */ + u8 panel_prev[4]; /* last 0x17 snapshot */ + bool panel_seen; /* first snapshot taken */ + bool panel_select_armed; /* the LEDs show the selection: the next SELECT + * press steps it (an IN switch clears this) + */ + bool panel_select_known; /* panel_select matches the unit */ + s8 panel_sel[3]; /* the SELECT selection of each IN pair (Ch 1/2, + * Ch 3/4, Opt): -1 not known, else as + * panel_select. The unit keeps one per pair, + * across IN switches and power cycles; the + * "Front Panel Select" controls expose them + * so that alsactl can keep them across boots. + */ + unsigned long panel_start; /* jiffies at panel_start (boot re-assert) */ + int panel_button; /* latched button event (consumed on get) */ + int panel_wheel; /* accumulated wheel delta (consumed on get) */ + int panel_in; /* enum: 0 unknown, 1 Ch1/2, 2 Ch3/4, 3 Opt */ + int panel_out; /* enum: 0 unknown, 1 Ch1/2, 2 Phones, 3 Opt */ + bool panel_mix; /* MIX engaged - HOST-latched (like TotalMix): + * set by the 0x44 flash ack, NOT by the readback + * 0x80 bit (the raw press has none) + */ + bool panel_dim; /* DIM sticky (byte1 bit 0x20) */ + bool panel_saw_fader; /* device observed in fader mode (byte2 0x0x) + * - gates the device-driven MIX exit + */ + int panel_select; /* SELECT state of the IN pair shown: 0 L, 1 R, + * 2 both, 3 none (3 too when not known); + * host-tracked, not in the readback + */ + int panel_sel_hold; /* consecutive ticks with byte3 = 0x50 + * (SELECT held > 200 ms = the OUT-balance + * gesture; a tap flashes only ~100-150 ms, + * selhold_probe2 - no engaged bit) + */ + /* The OUT wheel's own level per side, in half-dB, valid while the + * master still holds the value the wheel last wrote (the 16-bit + * value can't hold every half-dB step, nor the device's range + * below -64 dB, nor its mute). 0xffff = nothing written yet. + */ + u16 panel_master_last[6][2]; + s16 panel_out_hdb[6][2]; + /* OUT wheel acceleration and resync at rest (babyfacepro-ctl.c) */ + ktime_t panel_out_wheel_t; /* estimated time of the last count */ + ktime_t panel_poll_t; /* when the previous poll ran */ + unsigned long panel_fast_until; /* jiffies: poll fast until then */ + int panel_out_wheel_dir; /* its direction, +1 or -1 */ + int panel_out_wheel_out; /* the output it moved */ + bool panel_out_resync; /* 8-bit write due once at rest */ + u16 panel_mix_raw; /* MIX-mode monitoring level (fader raw) */ + u8 panel_mix_disp[4]; /* MIX-mode VU display shadow per mic + * (0x1A 0x000A+mic - written on change + * so the input VU follows the wheel) + */ + struct snd_kcontrol *panel_kctl[6]; /* for snd_ctl_notify */ + struct snd_kcontrol *panel_select_kctl[3]; /* per IN pair, likewise */ + struct snd_kcontrol *dim_press_kctl; + u32 dim_press_count; /* front-panel DIM presses, wraps */ }; /* The mixer state cached across interface re-probes/resume (see @@ -369,6 +485,7 @@ struct bf_saved { int width; u16 fx_send; bool dim; + s8 panel_sel[3]; }; struct bf_rate { @@ -424,6 +541,10 @@ int bf_gain_db(int mic, u8 raw); u8 bf_gain_raw(int mic, int db); int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on); int babyface_create_flags(struct snd_usb_babyface *chip); +int babyface_create_panel(struct snd_usb_babyface *chip); +void babyface_panel_start(struct snd_usb_babyface *chip); +void babyface_panel_stop(struct snd_usb_babyface *chip); +void babyface_panel_work(struct work_struct *work); /* Master gain-law helpers (babyfacepro-ctl.c). */ int bf_master_half_db(u16 vol16); /* 16-bit master -> dBx2 */ -- 2.56.0