From nobody Sat Sep 26 10:02:09 2026 Received: from mail-wm1-f53.google.com (mail-wm1-f53.google.com [209.85.128.53]) (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 8D0B21A7264 for ; Wed, 2 Sep 2026 10:11:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.53 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; cv=none; b=n2ccObC/Bjwy6wSDMuiJ2iP8r4lp0kSLq2FYqd+6lAWt80B8cUJsYrsUixJQd5cdHoRGyZDXCn8j07+TG551/g19r04QzI4o2ZYxnDB8ESjfaEi6uxXkYTKktEBNFMUKQwJVbrDGUYzMucREryQ6eDI4jqNoRchkUSy5+k9FWTs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; c=relaxed/simple; bh=6Z0eFUkqXrxbtgdE4GAW6xJaJOUwx6d0Fjk0jRYI71w=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=us5qSuUvuwyGXwkrd5vDkULB3Ic1P22/t4l/IEZzOmna66VpF1Dibx9eV6LA97F8gNAGyKxiBTrkZfxEip3Sh9vMdiqF81Bw/ELVNP2N2MLPC0qsLDtRN259AOhalT/uZmVDFx4oO+zNdnf/z0kJ72KDiLmDi2bq4QZUTcZ7sgI= 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=q+yIU6zJ; arc=none smtp.client-ip=209.85.128.53 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="q+yIU6zJ" Received: by mail-wm1-f53.google.com with SMTP id 5b1f17b1804b1-499ae1c6471so4831775e9.3 for ; Wed, 02 Sep 2026 03:11:33 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788343892; x=1788948692; 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=4plUdQhCNUHEIQUXuB0OxfWKxrIkE7jTGeea8rp5u1o=; b=q+yIU6zJTL8E2jr6HosBaX8cEMpitcRWw1McJhcqP0acWV9EBJmYrdqWvddTlEwBE4 jz1Xx0Ort0IQTS1PBTAIj2rEsHbY5cvAANdYayyk1ThFfjXocuAPMV8tzpJQCYdT6ttW J7amfDsdETD+Dbd8Oo4v/1a5Bvu+nLLgmaZpg7IAScouX2JkPyx0BtmfBGYXruudMxiW WU6EO8Kbd/1kwxw4x4XdSGf/aCS7uWRP7LhhmT6eD81l1181NZcZ+YKb4XV5cUq1QYl9 hL+o0sbNsE/fexicLzvxqhLhI/1ol2wKsTIRkds9mOhMbYwsOfn/g3WFiZtTUbCzdKAb Tc5g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788343892; x=1788948692; 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=4plUdQhCNUHEIQUXuB0OxfWKxrIkE7jTGeea8rp5u1o=; b=VXGq6w7PlFu7ghhzuwFXEMmwpyUEvqrm5dutk33RKpmayPAXNc+gDdcnixLCaWjpX5 rHuVWYPzkfMTscOPv62p7rEpToT8dZssQDfA9Xsqm+Ay0St4b9L0wNcxPGAxtvuaIKcG fc5s7/38piWYHxi2psvhjVwo+4dmBUv1xMkQZ1sul/KmBWxwo7hBBiePWokP2nZPMWwj epOiZXigx6VsOwMQAaU0CHfKNZ3qvHSAHnnKT4bosKBQ7y8oCdTsZGzfEaUwINaXGyUm q6vEj7k/LpsRXc7X9zSTOItIMe9x3cpJj+McrwTOK492amFTRWywpqQuiy+vD35Tki/H PGCQ== X-Forwarded-Encrypted: i=1; AHgh+RqzJuimBPS7J8G8l/qR7VTac4dQNVkje/wZNVRVmEU0kdSIHwj55veoE4O4UxU9rJCpR6nMR8YPQsp99qI=@vger.kernel.org X-Gm-Message-State: AFuF++k8gQwyLH7/H9+NmIKdQrZiTTWqEN60QIgg8/3/jrRqz46tDfPh JVK4mLNgri/VR8k7T+4LwXp7qqQul5oGP8Wjaxi46OJ14GabgO6KNiMu X-Gm-Gg: AR+sD10FG3KkoPlOnuDrFHL4WTZFbb6K9ityJ2xbbkS/K5wjrFOHQooSC18Ll/jVFzH ZKIxEPIU8cznkcAcfTe8HS111P7B2CGytzqrEXw4n1f3WI2l0YTeQ7+6L6NAR62atsGhUZJCggq bJEso5CH0yIvK4/FXssMT/XFd0NG7ntKWyrycWqg5hEkdcpp6NjAi64Uy3odpKg7xr2JdfbDbF1 Q/fLfSiQJn3Ch0SLR7vt+Ig8l5Y5zLVSaomsY/2o4LQVur8oSU5zIQgEZU2JI40z0OwLaePCdM/ 93BFnyxocqoGYXlgLZRrNig0BkYSSqF8T0M1c/EYaVjHysmG45qGdwMpFmGa4TopJjhJO/2TGxq rAEtnCNNbaSSYVNg1DD6XrTcTf5VepbA0b4a76algwel+dqWwuLM3Jop66UboEyreYr2Jt2aXJC 6Ywd1HjgKwSDX34WjmtPLmpuk68GIrtdlLKlF3mdqq22d0QQzEsv42YTid2W515YthJgRoBJfN9 fHry4xZrrHLqBc1 X-Received: by 2002:a05:600c:8b29:b0:499:a5c8:c6f3 with SMTP id 5b1f17b1804b1-49ce580d875mr50937315e9.3.1788343890904; Wed, 02 Sep 2026 03:11:30 -0700 (PDT) Received: from cachyos ([196.77.120.180]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49ce47885b1sm57294265e9.14.2026.09.02.03.11.29 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 02 Sep 2026 03:11:30 -0700 (PDT) From: =?UTF-8?q?Isma=C3=AFl=20Bahloul?= To: linux-sound@vger.kernel.org Cc: linux-usb@vger.kernel.org, alsa-devel@alsa-project.org, perex@perex.cz, tiwai@suse.com, linux-kernel@vger.kernel.org, =?UTF-8?q?Isma=C3=AFl=20Bahloul?= Subject: [RFC PATCH v3 1/4] ALSA: usb: add RME Babyface Pro FS driver (proprietary mode) - core + PCM Date: Wed, 2 Sep 2026 11:11:14 +0100 Message-ID: <20260902101117.11671-2-i.bahloul01@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260902101117.11671-1-i.bahloul01@gmail.com> References: <20260902101117.11671-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: quoted-printable Add the core RME Babyface Pro FS driver (proprietary mode, VID 0x2a39 PID 0x3fc0): USB vendor-request protocol + cold init, interrupt-URB PCM streaming on interface 5, mixer-state persistence across re-probes/resume, and the card lifecycle (probe/disconnect/PM/ module entry). Stream model (big picture): the PCM stream is fully asynchronous. A trigger(START/STOP) only toggles the shared stream_users counter (0..2, one per running substream) and schedules stream_work, which runs in process context (control transfers and usb_submit_urb must sleep). The first user cold-inits and arms the session and submits the interrupt URBs; the last user kills them. So the device has one live stream regardless of substream count, shared by playback and capture. See the comment above babyface_stream_work(). The control surface (mixer, front panel, DSP EQ) is stubbed here so the module links; the follow-up patches in this series implement each part. Signed-off-by: Isma=C3=AFl Bahloul Assisted-by: DeepSeek V4 Flash --- MAINTAINERS | 6 + sound/usb/Kconfig | 18 + sound/usb/Makefile | 2 +- sound/usb/babyfacepro/Makefile | 3 + sound/usb/babyfacepro/babyfacepro-ctl.c | 127 ++ sound/usb/babyfacepro/babyfacepro.c | 1488 +++++++++++++++++++++++ sound/usb/babyfacepro/babyfacepro.h | 402 ++++++ 7 files changed, 2045 insertions(+), 1 deletion(-) 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 diff --git a/MAINTAINERS b/MAINTAINERS index b91655b34..f6de3b5ae 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23654,6 +23654,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 =20 +RME BABYFACE PRO FS DRIVER (PROPRIETARY MODE) +M: Isma=C3=AFl Bahloul +L: alsa-devel@alsa-project.org (moderated for non-subscribers) +S: Maintained +F: sound/usb/babyfacepro/ + RNBD BLOCK DRIVERS M: Md. Haris Iqbal M: Jack Wang diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig index b4588915e..14d759814 100644 --- a/sound/usb/Kconfig +++ b/sound/usb/Kconfig @@ -204,6 +204,24 @@ config SND_USB_AUDIO_QMI To compile this driver as a module, choose M here: the module will be called snd-usb-audio-qmi. =20 +config SND_USB_BABYFACE_PRO + tristate "RME Babyface Pro FS (proprietary mode)" + select SND_PCM + help + Say Y here to include support for the RME Babyface Pro FS in + its proprietary mode (VID 0x2a39, PID 0x3fc0). + + 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). It + exposes the PCM stream plus the output masters, mutes, mic + phantom/PAD and preamp gains as standard ALSA controls. + + To compile this driver as a module, choose M here: the module + will be called snd-usb-babyface-pro. + + source "sound/usb/line6/Kconfig" =20 endif # SND_USB diff --git a/sound/usb/Makefile b/sound/usb/Makefile index e62794a87..2f83f5881 100644 --- a/sound/usb/Makefile +++ b/sound/usb/Makefile @@ -35,5 +35,5 @@ obj-$(CONFIG_SND_USB_UA101) +=3D snd-usbmidi-lib.o obj-$(CONFIG_SND_USB_USX2Y) +=3D snd-usbmidi-lib.o obj-$(CONFIG_SND_USB_US122L) +=3D snd-usbmidi-lib.o =20 -obj-$(CONFIG_SND) +=3D misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/ +obj-$(CONFIG_SND) +=3D misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/ b= abyfacepro/ obj-$(CONFIG_SND_USB_LINE6) +=3D line6/ diff --git a/sound/usb/babyfacepro/Makefile b/sound/usb/babyfacepro/Makefile new file mode 100644 index 000000000..44e6b5a4d --- /dev/null +++ b/sound/usb/babyfacepro/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +snd-usb-babyface-pro-y :=3D babyfacepro.o babyfacepro-ctl.o +obj-$(CONFIG_SND_USB_BABYFACE_PRO) +=3D snd-usb-babyface-pro.o diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepr= o/babyfacepro-ctl.c new file mode 100644 index 000000000..ec2640e0b --- /dev/null +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * RME Babyface Pro FS =E2=80=94 proprietary-mode USB audio driver + * + * ALSA control surface (mixer, front panel, DSP EQ). This is the + * initial slice of the series: the card's PCM stream + lifecycle come + * from babyfacepro.c, while the control surface is stubbed out here so + * the module links. The real mixer, front-panel and DSP EQ land in the + * follow-up patches (mixer, panel, eq) =E2=80=94 each replaces its stubs. + * + * See babyfacepro.h for the shared device state and register map. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "babyfacepro.h" + +const struct bf_source bf_sources[14] =3D { + { "AN1", 0, 0 }, + { "AN2", 1, 1 }, + { "AN3", 2, 2 }, + { "AN4", 3, 3 }, + { "AS1/2", 4, 5 }, + { "ADAT3/4", 6, 7 }, + { "ADAT5/6", 8, 9 }, + { "ADAT7/8", 10, 11 }, + { "PB1", 12, 13 }, + { "PB2", 14, 15 }, + { "PB3", 16, 17 }, + { "PB4", 18, 19 }, + { "PB5", 20, 21 }, + { "PB6", 22, 23 }, +}; + +/* Crosspoint-map output order vs the master-map order =E2=80=94 HARDWARE- + * VERIFIED 2026-08-24: the block that feeds the Phones is the FIRST + * crosspoint block (0x34), while the Phones master is the SECOND + * (0x03E2/0x0006). The crosspoint map lists the Phones first (the + * monitor output); the master map lists AN1/2 first. Control index =3D + * the canonical order (AN1/2=3D0, PH3/4=3D1, ...) so the crosspoint and + * master controls line up; this table maps to the register block. + */ +const u8 bf_xpoint_block[6] =3D { 1, 0, 2, 3, 4, 5 }; + +/* =E2=94=80=E2=94=80 control-surface stubs =E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80 + * Filled in by the mixer / panel / eq patches. The core driver + * (babyfacepro.c) calls these from probe() and the stream/state + * paths, so they must exist for the module to link. Until then the + * card exposes the PCM stream only. + */ + +int babyface_write_default_mixer(struct snd_usb_babyface *chip) +{ + return 0; +} + +int bf_apply_masters(struct snd_usb_babyface *chip) +{ + return 0; +} + +int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on) +{ + return 0; +} + +int bf_preamp_state_write(struct snd_usb_babyface *chip) +{ + return 0; +} + +int babyface_create_controls(struct snd_usb_babyface *chip) +{ + return 0; +} + +int babyface_create_xpoints(struct snd_usb_babyface *chip) +{ + return 0; +} + +int babyface_create_flags(struct snd_usb_babyface *chip) +{ + return 0; +} + +int babyface_create_panel(struct snd_usb_babyface *chip) +{ + return 0; +} + +int babyface_create_eq(struct snd_usb_babyface *chip) +{ + return 0; +} + +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_reupload(struct snd_usb_babyface *chip) +{ +} + +u8 bf_gain_raw(int mic, int db) +{ + return 0; +} diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/ba= byfacepro.c new file mode 100644 index 000000000..9f7c81764 --- /dev/null +++ b/sound/usb/babyfacepro/babyfacepro.c @@ -0,0 +1,1488 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * RME Babyface Pro FS - proprietary-mode USB audio driver + * + * Core driver: USB vendor requests + cold init, interrupt-URB PCM + * streaming, mixer-state persistence across re-probes/resume, and + * the card lifecycle (probe/disconnect/PM/module entry). + * + * See babyfacepro.h for the shared device state and register map, + * and babyfacepro-ctl.c for the ALSA control surface (mixer, front + * panel, DSP EQ). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "babyfacepro.h" + +/* The transaction-flag counter cycle on 16-bit writes. */ +const u16 bf_flag_cycle[4] =3D { 0xc000, 0x4000, 0x8000, 0x0000 }; + +/* -- sample-rate / alt classes -------------------- */ + +static const struct bf_rate bf_rates[] =3D { + { 32000, BF_ALT_1, 56, 8 }, + { 44100, BF_ALT_1, 56, 8 }, + { 48000, BF_ALT_1, 56, 8 }, + { 64000, BF_ALT_1, 56, 8 }, + { 88200, BF_ALT_1, 56, 8 }, + { 96000, BF_ALT_2, 40, 16 }, + { 128000, BF_ALT_2, 40, 16 }, + { 176400, BF_ALT_3, 32, 32 }, + { 192000, BF_ALT_3, 32, 32 }, +}; + +static const unsigned int bf_rate_list[ARRAY_SIZE(bf_rates)] =3D { + 32000, 44100, 48000, 64000, 88200, + 96000, 128000, 176400, 192000, +}; + +const struct snd_pcm_hw_constraint_list bf_rates_constraint =3D { + .count =3D ARRAY_SIZE(bf_rate_list), + .list =3D bf_rate_list, + .mask =3D 0, +}; + +const struct bf_rate *bf_rate_lookup(unsigned int rate) +{ + int i; + + for (i =3D 0; i < ARRAY_SIZE(bf_rates); i++) + if (bf_rates[i].rate =3D=3D rate) + return &bf_rates[i]; + return NULL; +} + +/* -- vendor requests ----------------------- */ + +/* Timeout for every vendor control transfer (ms). */ +#define BF_CTL_TIMEOUT 1000 + +int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 id= x) +{ + return usb_control_msg_send(chip->dev, 0, req, + USB_DIR_OUT | USB_TYPE_VENDOR | + USB_RECIP_DEVICE, + val, idx, NULL, 0, BF_CTL_TIMEOUT, GFP_KERNEL); +} + +int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf) +{ + return usb_control_msg_recv(chip->dev, 0, req, + USB_DIR_IN | USB_TYPE_VENDOR | + USB_RECIP_DEVICE, + 0, idx, buf, 4, BF_CTL_TIMEOUT, GFP_KERNEL); +} + +/* Write with the per-transaction flag-cycle word OR'd into idx. The + * device wants the flag word (0xc000/0x4000/0x8000/0x0000, rotating) + * set on every 0x12/0x1a write; this is the hot path for the mixer + * puts, so it is factored out. + */ +int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, = u16 idx) +{ + u16 flag =3D bf_flag_cycle[chip->flag_cnt]; + + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + return bf_vendor_write(chip, req, val, idx | flag); +} + +/* The cold-start session init (cap_coldplug.pcap), verbatim from the + * user-space reference (protocol::streaming_init). Without it the + * firmware never validates a stream. + */ +int bf_cold_init(struct snd_usb_babyface *chip) +{ + int ret, i; + + for (i =3D 0; i <=3D 0x3d; i++) { + if (i =3D=3D 0x1e || i =3D=3D 0x1f) + continue; + ret =3D bf_vendor_write(chip, BF_REQ_REG_CLEAR, 0x0000, i); + if (ret < 0) + return ret; + } + /* 48-kHz DDS clock quads (banked 0x1B). */ + ret =3D bf_vendor_write(chip, BF_REQ_DDS, 0xc350, 0x0000); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, 0x8db8, 0xd201); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, 0x8234, 0xd302); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, 0x7cff, 0xf803); + if (ret < 0) + return ret; + /* 0x1C status - the hardware-validated reference (protocol:: + * streaming_init) sends it as an OUT write; Windows reads it. + * Both are tolerated; match the validated path. + */ + ret =3D bf_vendor_write(chip, BF_REQ_STATUS_2, 0x0000, 0x0000); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0021, BF_REG_KEEPALIVE_= INIT); + if (ret < 0) + return ret; + /* 0x17 wIdx=3D0x0000 does NOT touch the preamp state (0x003F). */ + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, 0x000c, 0x0000); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000); + if (ret < 0) + return ret; + for (i =3D 0; i < 2; i++) { + ret =3D bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0000, 0x3000); + if (ret < 0) + return ret; + } + for (i =3D 0; i < 3; i++) { + ret =3D bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0800, 0x0800); + if (ret < 0) + return ret; + } + return 0; +} + +/* The 0x16 cold-init clear covers only 0x00-0x3D - the "cross" + * registers of a block (L-reg odd / R-reg even of the stereo + * sources) survive from the previous session and would sum L+R into + * BOTH channels of the output (mono). Zero them explicitly: 10 odd + * L-registers (5,7,...23) + 10 even R-registers (4,6,...22). + */ +int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip, + unsigned int blk) +{ + int ret, k; + + for (k =3D BF_CROSS_L_FIRST; k <=3D BF_CROSS_L_LAST; k +=3D 2) { + ret =3D bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000, + BF_REG_CROSS_BASE_L + + BF_REG_CROSS_STRIDE * blk + k); + if (ret < 0) + return ret; + } + for (k =3D BF_CROSS_R_FIRST; k <=3D BF_CROSS_R_LAST; k +=3D 2) { + ret =3D bf_vendor_write_cycle(chip, BF_REQ_CROSSPOINT, 0x0000, + BF_REG_CROSS_BASE_R + + BF_REG_CROSS_STRIDE * blk + k); + if (ret < 0) + return ret; + } + return 0; +} + +/* -- mixer-state persistence across interface re-probes -------- + * A userspace client can claim the proprietary interface via usbfs + * (USBDEVFS_DISCONNECT_CLAIM - seen with PipeWire grabbing the + * device when a stream targets the sink, and with the TuxMix + * user-space daemon's libusb). That detaches us and the card + * disappears for the duration; on release the interface re-probes. + * The device keeps its registers across the detach, but our cold + * init clears them - so save the mixer state at disconnect and + * restore it at the next probe. + */ + +static LIST_HEAD(bf_saved_list); +static DEFINE_MUTEX(bf_saved_mutex); + +/* Re-apply the whole cached mixer state after a resume (the device + * lost its registers across a system suspend - TotalMix does the same + * re-apply). Caller holds chip->mutex. + */ +int babyface_restore_state(struct snd_usb_babyface *chip) +{ + int out, src, mic, ret; + u16 flag; + + /* Preamp state + commit. */ + ret =3D bf_preamp_state_write(chip); + if (ret < 0) + return ret; + + /* The four mic gains (the counter restarts). */ + for (mic =3D 0; mic < 4; mic++) { + u8 counter =3D (mic % 3 =3D=3D 0) ? 0x20 : (mic % 3 =3D=3D 1) ? 0x00 : 0= x40; + + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, + (u16)((bf_gain_raw(mic, chip->gain[mic]) & 0x1f) | + counter), + BF_REG_GAIN + mic); + if (ret < 0) + return ret; + } + chip->gain_cycle =3D 1; + + /* Masters (8-bit =3D the real volume) + mutes. */ + ret =3D bf_apply_masters(chip); + if (ret < 0) + return ret; + + /* Crosspoints (canonical out -> register block). */ + for (out =3D 0; out < 6; out++) { + unsigned int blk =3D bf_xpoint_block[out]; + + for (src =3D 0; src < 14; src++) { + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->xpoint[out][src][0], + (BF_REG_CROSS_BASE_L + + BF_REG_CROSS_STRIDE * blk + + bf_sources[src].idx_l) | flag); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->xpoint[out][src][1], + (BF_REG_CROSS_BASE_R + + BF_REG_CROSS_STRIDE * blk + + bf_sources[src].idx_r) | flag); + if (ret < 0) + return ret; + } + ret =3D bf_crosspoint_clear_cross(chip, blk); + if (ret < 0) + return ret; + } + + /* Pitch (the DDS quad) + the clock keepalive. */ + if (chip->pitch) { + u32 dds24 =3D div_u64(12800000000ULL + (u32)(1000 + chip->pitch) / 2, + 1000 + chip->pitch); + u16 dds16 =3D dds24 >> 8; + u16 frac =3D dds24 & 0xff; + + ret =3D bf_vendor_write(chip, BF_REQ_DDS, dds16, (frac << 8) | 0); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, + (u16)div_u64(dds16 * 72562ull + 50000, 100000), 0x0001); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, (u16)((dds16 * 2 + 1) / 3), + 0x0002); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, 0x7cff, 0x0003); + if (ret < 0) + return ret; + } + return bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0001, + BF_REG_KEEPALIVE_SETTINGS); +} + +/* Re-apply the non-master flags (loopback / AN1>2 / link / width / + * FX send / MS) after a state restore. The write patterns mirror the + * corresponding _put() handlers. Caller holds chip->mutex. + */ +int bf_state_apply_flags(struct snd_usb_babyface *chip) +{ + int out, ret, on_out =3D -1; + u16 l, r; + + /* Loopback: the full 30-channel map from the cached state (the + * single-active invariant keeps at most one pair at 0x0001). + */ + for (out =3D 0; out < 6; out++) { + if (chip->loopback[out]) { + on_out =3D out; + break; + } + } + ret =3D bf_loopback_write_map(chip, on_out, on_out >=3D 0); + if (ret < 0) + return ret; + + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, + (chip->linked ? 0x0400 : 0x0000) | + (chip->an12 ? 0x1000 : 0x0000), 0x1000); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000); + if (ret < 0) + return ret; + + l =3D (u16)(((0x2000 * (100 + chip->width) / 2) + 50) / 100); + r =3D 0x2000 - l; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b); + if (ret < 0) + return ret; + + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0138); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->fx_send, 0x0153); + if (ret < 0) + return ret; + + if (chip->ms_proc) { + /* Same ON pattern as bf_ms_put (cap_ms2.pcap): mute the AN2 + * (side) crosspoints, both maps. + */ + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b); + if (ret < 0) + return ret; + } + + /* Re-apply an engaged DIM (the fixed -20 dB Phones pair + flag). */ + if (chip->dim) { + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, 0xcb, + BF_REG_MASTER_8 + 2 * 1); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, 0xcb, + BF_REG_MASTER_8 + 2 * 1 + 1); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333, + (BF_REG_MASTER_16 + 2 * 1) | + bf_flag_cycle[chip->flag_cnt]); + if (ret < 0) + return ret; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333, + (BF_REG_MASTER_16 + 2 * 1 + 1) | + bf_flag_cycle[chip->flag_cnt]); + if (ret < 0) + return ret; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000); + if (ret < 0) + return ret; + } + return 0; +} + +void bf_state_save(struct snd_usb_babyface *chip) +{ + struct bf_saved *s; + const char *key =3D chip->dev->serial ? chip->dev->serial : + dev_name(&chip->dev->dev); + bool found =3D false; + + mutex_lock(&bf_saved_mutex); + list_for_each_entry(s, &bf_saved_list, list) { + if (strcmp(s->key, key)) + continue; + found =3D true; + break; + } + if (!found) { + s =3D kzalloc_obj(*s, GFP_KERNEL); + if (!s) { + mutex_unlock(&bf_saved_mutex); + return; + } + strscpy(s->key, key, sizeof(s->key)); + list_add_tail(&s->list, &bf_saved_list); + } + + s->preamp =3D chip->preamp; + memcpy(s->gain, chip->gain, sizeof(s->gain)); + s->gain_cycle =3D chip->gain_cycle; + s->flag_cnt =3D chip->flag_cnt; + memcpy(s->master, chip->master, sizeof(s->master)); + memcpy(s->muted, chip->muted, sizeof(s->muted)); + memcpy(s->xpoint, chip->xpoint, sizeof(s->xpoint)); + s->pitch =3D chip->pitch; + memcpy(s->loopback, chip->loopback, sizeof(s->loopback)); + s->an12 =3D chip->an12; + s->linked =3D chip->linked; + s->ms_proc =3D chip->ms_proc; + s->width =3D chip->width; + s->fx_send =3D chip->fx_send; + s->dim =3D chip->dim; + mutex_unlock(&bf_saved_mutex); +} + +/* Copy a saved state (if any) into a freshly probed chip and push it + * to the device. Returns 1 when restored, -ENOENT when there is none, + * or a negative error from the vendor writes. + */ +int bf_state_restore(struct snd_usb_babyface *chip) +{ + struct bf_saved *s; + const char *key =3D chip->dev->serial ? chip->dev->serial : + dev_name(&chip->dev->dev); + int ret =3D -ENOENT; + + mutex_lock(&bf_saved_mutex); + list_for_each_entry(s, &bf_saved_list, list) { + if (strcmp(s->key, key)) + continue; + chip->preamp =3D s->preamp; + memcpy(chip->gain, s->gain, sizeof(chip->gain)); + chip->gain_cycle =3D s->gain_cycle; + chip->flag_cnt =3D s->flag_cnt; + memcpy(chip->master, s->master, sizeof(chip->master)); + memcpy(chip->muted, s->muted, sizeof(chip->muted)); + memcpy(chip->xpoint, s->xpoint, sizeof(chip->xpoint)); + chip->pitch =3D s->pitch; + memcpy(chip->loopback, s->loopback, sizeof(chip->loopback)); + chip->an12 =3D s->an12; + chip->linked =3D s->linked; + chip->ms_proc =3D s->ms_proc; + chip->width =3D s->width; + chip->fx_send =3D s->fx_send; + chip->dim =3D s->dim; + ret =3D 1; + break; + } + mutex_unlock(&bf_saved_mutex); + if (ret !=3D 1) + return ret; + + mutex_lock(&chip->mutex); + ret =3D babyface_restore_state(chip); + if (ret =3D=3D 0) + ret =3D bf_state_apply_flags(chip); + mutex_unlock(&chip->mutex); + return ret ? ret : 1; +} + +void bf_state_purge(void) +{ + struct bf_saved *s, *tmp; + + mutex_lock(&bf_saved_mutex); + list_for_each_entry_safe(s, tmp, &bf_saved_list, list) { + list_del(&s->list); + kfree(s); + } + mutex_unlock(&bf_saved_mutex); +} + +/* -- stream (interrupt URBs, caiaq-style) ---------------- */ + +static bool babyface_capture_copy(struct snd_usb_babyface *chip, + struct snd_pcm_substream *subs, + const u8 *data, unsigned int frames) +{ + struct snd_pcm_runtime *rt =3D subs->runtime; + unsigned int buf_frames =3D rt->buffer_size; + unsigned int words =3D chip->frame_bytes / 4; + unsigned int chans =3D rt->channels; + unsigned int pos, f, i; + unsigned long new_period; + bool crossed =3D false; + u8 *dst; + + spin_lock(&chip->lock); + pos =3D chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] % buf_frames; + for (f =3D 0; f < frames; f++) { + const __le32 *w =3D (const __le32 *)(data + f * chip->frame_bytes); + + dst =3D rt->dma_area + frames_to_bytes(rt, pos); + for (i =3D 0; i < chans; i++) { + /* Channel map: app ch0-3 =3D device words 0-3 (AN1-4); + * app ch4-9 =3D words 6-11 (ADAT/SPDIF); app ch10/11 =3D + * words 12/13 =3D a FIXED-GAIN playback tap (observed + * 2026-08-25: the playback echoes there at ~-27 dB, + * independent of the output masters - NOT the output + * bus; the ADAT/SPDIF range is words 6-11 only). The + * device words 4/5 are a fixed marker, not audio - + * skipped. At 96/192 kHz the frame has fewer words; + * missing ones read as zero. + */ + static const u8 map[12] =3D { 0, 1, 2, 3, 6, 7, 8, 9, + 10, 11, 12, 13 }; + u8 wi =3D i < 12 ? map[i] : 0xff; + s32 s =3D 0; + + if (wi < words) { + /* 24-bit sample in bits 31-8 of the 32-bit word; + * S32_LE with 24 msbits, so copy the word as-is. + */ + s =3D (s32)le32_to_cpu(w[wi]); + } + put_unaligned_le32((u32)s, dst + i * 4); + } + pos++; + if (pos >=3D buf_frames) + pos =3D 0; + } + chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] +=3D frames; + new_period =3D chip->hw_ptr[SNDRV_PCM_STREAM_CAPTURE] / rt->period_size; + if (new_period !=3D chip->prev_period[SNDRV_PCM_STREAM_CAPTURE]) { + chip->prev_period[SNDRV_PCM_STREAM_CAPTURE] =3D new_period; + crossed =3D true; + } + spin_unlock(&chip->lock); + + return crossed; +} + +static bool babyface_playback_copy(struct snd_usb_babyface *chip, + struct snd_pcm_substream *subs, + u8 *data, unsigned int frames) +{ + struct snd_pcm_runtime *rt =3D subs->runtime; + unsigned int buf_frames =3D rt->buffer_size; + unsigned int words =3D chip->frame_bytes / 4; + unsigned int chans =3D rt->channels; + unsigned int pos, f, i; + unsigned long new_period; + bool crossed =3D false; + const u8 *src; + + spin_lock(&chip->lock); + /* Never copy frames the app has not prepared. With several URBs in + * flight the device can transiently be ahead of the app ring at low + * periods, so hw_ptr may reach past appl_ptr; reading beyond appl_ptr + * would trip a spurious XRUN. The device just repeats the last + * frames instead. (Both counters are unbounded, so clamp on the + * signed difference.) + */ + { + snd_pcm_sframes_t avail =3D + (snd_pcm_sframes_t)(rt->control->appl_ptr - + chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK]); + + if (avail < 0) + avail =3D 0; + else if (avail > (snd_pcm_sframes_t)buf_frames) + avail =3D buf_frames; + if ((snd_pcm_uframes_t)avail < frames) + frames =3D (unsigned int)avail; + } + pos =3D chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] % buf_frames; + for (f =3D 0; f < frames; f++) { + __le32 *w =3D (__le32 *)(data + f * chip->frame_bytes); + + src =3D rt->dma_area + frames_to_bytes(rt, pos); + /* App ch n feeds the device word n (PB1-6 =3D words 0-11); + * words 12/13 stay zero. At 96/192 kHz the frame is + * shorter - the extra app channels are dropped. + */ + for (i =3D 0; i < chans && i < words; i++) { + u32 s =3D get_unaligned_le32(src + i * 4); + + /* S32_LE msbits=3D24: the app's 24 valid bits are already + * left-justified (bits 31-8) - copy the word as-is. + */ + w[i] =3D cpu_to_le32(s); + } + for (; i < words; i++) + w[i] =3D 0; + pos++; + if (pos >=3D buf_frames) + pos =3D 0; + } + chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] +=3D frames; + new_period =3D chip->hw_ptr[SNDRV_PCM_STREAM_PLAYBACK] / rt->period_size; + if (new_period !=3D chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK]) { + chip->prev_period[SNDRV_PCM_STREAM_PLAYBACK] =3D new_period; + crossed =3D true; + } + spin_unlock(&chip->lock); + + return crossed; +} + +static void babyface_complete_in(struct urb *urb) +{ + struct snd_usb_babyface *chip =3D urb->context; + struct snd_pcm_substream *subs; + unsigned long flags; + unsigned int frames; + bool crossed =3D false; + int ret; + + if (urb->status < 0) { + if (urb->status =3D=3D -ESHUTDOWN || urb->status =3D=3D -ENOENT || + urb->status =3D=3D -ECONNRESET) + return; /* killed */ + dev_dbg_ratelimited(&chip->dev->dev, "IN urb status %d\n", + urb->status); + if (atomic_inc_return(&chip->urb_err) >=3D BF_URB_ERR_STOP) + schedule_work(&chip->stream_work); + goto resubmit; + } + atomic_set(&chip->urb_err, 0); + + subs =3D READ_ONCE(chip->subs[SNDRV_PCM_STREAM_CAPTURE]); + if (subs) { + snd_pcm_stream_lock_irqsave(subs, flags); + if (snd_pcm_running(subs)) { + frames =3D urb->actual_length / chip->frame_bytes; + if (frames) + crossed =3D babyface_capture_copy(chip, subs, + urb->transfer_buffer, + frames); + } + snd_pcm_stream_unlock_irqrestore(subs, flags); + if (crossed) + snd_pcm_period_elapsed(subs); + } +resubmit: + ret =3D usb_submit_urb(urb, GFP_ATOMIC); + if (ret < 0) { + dev_err_ratelimited(&chip->dev->dev, + "IN resubmit failed: %d\n", ret); + if (atomic_inc_return(&chip->urb_err) >=3D BF_URB_ERR_STOP) + schedule_work(&chip->stream_work); + } +} + +static void babyface_complete_out(struct urb *urb) +{ + struct snd_usb_babyface *chip =3D urb->context; + struct snd_pcm_substream *subs; + unsigned long flags; + unsigned int frames; + bool crossed =3D false; + int ret; + + if (urb->status < 0) { + if (urb->status =3D=3D -ESHUTDOWN || urb->status =3D=3D -ENOENT || + urb->status =3D=3D -ECONNRESET) + return; /* killed */ + dev_dbg_ratelimited(&chip->dev->dev, "OUT urb status %d\n", + urb->status); + if (atomic_inc_return(&chip->urb_err) >=3D BF_URB_ERR_STOP) + schedule_work(&chip->stream_work); + goto resubmit; + } + atomic_set(&chip->urb_err, 0); + + subs =3D READ_ONCE(chip->subs[SNDRV_PCM_STREAM_PLAYBACK]); + if (subs) { + snd_pcm_stream_lock_irqsave(subs, flags); + if (snd_pcm_running(subs)) { + frames =3D chip->frames_per_urb; + crossed =3D babyface_playback_copy(chip, subs, + urb->transfer_buffer, frames); + } + snd_pcm_stream_unlock_irqrestore(subs, flags); + if (crossed) + snd_pcm_period_elapsed(subs); + } else { + /* No consumer: silence the OUT frames. */ + memset(urb->transfer_buffer, 0, urb->transfer_buffer_length); + } +resubmit: + ret =3D usb_submit_urb(urb, GFP_ATOMIC); + if (ret < 0) { + dev_err_ratelimited(&chip->dev->dev, + "OUT resubmit failed: %d\n", ret); + if (atomic_inc_return(&chip->urb_err) >=3D BF_URB_ERR_STOP) + schedule_work(&chip->stream_work); + } +} + +void babyface_stream_kill(struct snd_usb_babyface *chip) +{ + int i; + + for (i =3D 0; i < chip->nurbs; i++) { + usb_kill_urb(chip->urbs_in[i]); + usb_kill_urb(chip->urbs_out[i]); + } + chip->streaming =3D false; +} + +/* Stream start/stop run in process context (control transfers sleep). + * The trigger only toggles stream_users and schedules this work. + */ + +/* Stop both PCM substreams (if running) so apps blocked in read/write + * wake with a clean error: XRUN for a recoverable stream error, or + * DISCONNECTED when the card is going away. + */ +void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t= state) +{ + int s; + + for (s =3D 0; s < 2; s++) { + struct snd_pcm_substream *subs =3D READ_ONCE(chip->subs[s]); + + if (subs && snd_pcm_running(subs)) + snd_pcm_stop(subs, state); + } +} + +/* Re-count stream_users from the substream running states. The apps + * can recover (re-prepare + trigger) while the stream work runs, so a + * hard `=3D 0` would wipe a fresh increment and leave a RUNNING + * substream with no URBs (hang). Called on the error paths with the + * mutex held. + */ +static void bf_recount_users(struct snd_usb_babyface *chip) +{ + unsigned long flags; + int s, users =3D 0; + + for (s =3D 0; s < 2; s++) { + struct snd_pcm_substream *subs =3D READ_ONCE(chip->subs[s]); + + if (subs && snd_pcm_running(subs)) + users++; + } + spin_lock_irqsave(&chip->lock, flags); + chip->stream_users =3D users; + spin_unlock_irqrestore(&chip->lock, flags); +} + +/* Stream model (big picture): + * + * The PCM stream is entirely asynchronous. A trigger(START/STOP) does + * not touch the hardware; it only changes the shared stream_users + * counter (0..2, one per running substream, counted under chip->lock) + * and schedules stream_work. The work runs in process context (the + * control transfers and usb_submit_urb() calls must sleep): + * + * - 1st user (users 0 -> 1): cold-init + the session trigger pair, + * then the interrupt URBs (nurbs in each direction) are submitted and + * the session is armed; the mixer state is re-applied afterwards. + * - last user (users 1 -> 0): the URBs are killed and the session + * disarmed. + * + * So the device has exactly one live stream regardless of how many + * substreams run, and a playback+capture pair shares it. An app that + * triggers while the work is running just increments stream_users; the + * work's re-count on the error path (bf_recount_users) reflects the + * RUNNING state so a recovered app re-arms from a clean slate. + */ +void babyface_stream_work(struct work_struct *work) +{ + struct snd_usb_babyface *chip =3D + container_of(work, struct snd_usb_babyface, stream_work); + unsigned int urbsize =3D chip->frame_bytes * chip->frames_per_urb; + unsigned long flags; + int i, ret; + int users; + + mutex_lock(&chip->mutex); + + if (chip->shutdown) { + mutex_unlock(&chip->mutex); + return; + } + + /* Persistent URB errors (bad link, device wedged): stop the stream + * and wake the apps with -EPIPE. stream_users is re-counted from + * the (now stopped) substreams so an app recovery (prepare+start) + * re-arms the session from a clean slate. + */ + if (atomic_read(&chip->urb_err) >=3D BF_URB_ERR_STOP) { + dev_err(&chip->dev->dev, + "stream error: %d consecutive bad URBs, stopping (apps re-arm)\n", + BF_URB_ERR_STOP); + babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN); + if (chip->streaming) + babyface_stream_kill(chip); + bf_recount_users(chip); + atomic_set(&chip->urb_err, 0); + mutex_unlock(&chip->mutex); + return; + } + + spin_lock_irqsave(&chip->lock, flags); + users =3D chip->stream_users; + spin_unlock_irqrestore(&chip->lock, flags); + + if (users > 0 && !chip->streaming) { + /* The firmware only validates a stream session that is + * preceded by the full cold-init (the user-space reference + * sends streaming_init at every session start - without it + * the outputs stay silent). The 0x16 clear wipes the mixer + * registers, so the cached state is re-applied after the arm. + */ + ret =3D bf_cold_init(chip); + if (ret < 0) + goto err; + + /* Stream trigger pair (cap_audio): 0x10 0x8000 + 0x1D. */ + ret =3D bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0000, 0x8000); + if (ret < 0) + goto err; + ret =3D bf_vendor_write(chip, BF_REQ_SESSION_START, 0x0000, 0x0000); + if (ret < 0) + goto err; + + for (i =3D 0; i < chip->nurbs; i++) { + 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); + } + for (i =3D 0; i < chip->nurbs; i++) { + ret =3D usb_submit_urb(chip->urbs_in[i], GFP_KERNEL); + if (ret < 0) + goto err; + ret =3D usb_submit_urb(chip->urbs_out[i], GFP_KERNEL); + if (ret < 0) + goto err; + } + /* Session arm (cap_audio frame 5829, after the URBs). */ + ret =3D bf_vendor_write(chip, BF_REQ_SESSION_ARM, 0x0000, 0xc000); + if (ret < 0) + goto err; + + /* The cold init above cleared the mixer registers; push the + * cached state back (preamp, gains, masters, crosspoints, + * pitch) so the session starts at the user's levels. + */ + ret =3D babyface_restore_state(chip); + if (ret < 0) + goto err; + + /* The 0x16 clear also wipes the flag registers (loopback, + * AN1>2, stereo link, width, FX send, MS) - re-apply them. + */ + ret =3D bf_state_apply_flags(chip); + if (ret < 0) + goto err; + + chip->streaming =3D true; + dev_dbg(&chip->dev->dev, "stream started (%u frames/URB, %u URBs)\n", + chip->frames_per_urb, chip->nurbs); + } else if (users =3D=3D 0 && chip->streaming) { + babyface_stream_kill(chip); + dev_dbg(&chip->dev->dev, "stream stopped\n"); + } + + mutex_unlock(&chip->mutex); + return; + +err: + dev_err(&chip->dev->dev, "failed to start stream: %d\n", ret); + babyface_stream_kill(chip); + /* The apps already got a successful trigger - wake them with an + * XRUN so a failed start (device wedged, cold-init error) does not + * leave them hung in read/write with no URBs in flight. + */ + babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_XRUN); + bf_recount_users(chip); + mutex_unlock(&chip->mutex); +} + +/* -- PCM --------------------------- */ + +static const struct snd_pcm_hardware babyface_pcm_hw =3D { + .info =3D SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER, + .formats =3D SNDRV_PCM_FMTBIT_S32_LE, + .rate_min =3D 32000, + .rate_max =3D 192000, + .channels_min =3D 2, + .channels_max =3D 12, + .buffer_bytes_max =3D 1 << 20, + .period_bytes_max =3D 1 << 18, + .periods_min =3D 2, + .periods_max =3D 16, +}; + +static int babyface_pcm_open(struct snd_pcm_substream *subs) +{ + struct snd_usb_babyface *chip =3D snd_pcm_substream_chip(subs); + struct snd_pcm_runtime *rt =3D subs->runtime; + unsigned long flags; + int ret; + + rt->hw =3D babyface_pcm_hw; + ret =3D 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 =3D snd_pcm_hw_constraint_msbits(rt, 0, 32, 24); + if (ret < 0) + return ret; + /* One URB delivers frames_per_urb frames per interrupt; a period must + * span at least one URB so a completion crosses at most one period + * boundary. Constrain in frames (not bytes) so the minimum period + * does not balloon at low channel counts: 2 ch @ 48 kHz -> 256 + * frames (5.3 ms) instead of 1536 frames from a 12-ch byte clamp. + */ + ret =3D snd_pcm_hw_constraint_minmax(rt, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, + chip->frames_per_urb, 1 << 18); + if (ret < 0) + return ret; + + spin_lock_irqsave(&chip->lock, flags); + chip->subs[subs->stream] =3D subs; + spin_unlock_irqrestore(&chip->lock, flags); + return 0; +} + +static int babyface_pcm_close(struct snd_pcm_substream *subs) +{ + struct snd_usb_babyface *chip =3D snd_pcm_substream_chip(subs); + unsigned long flags; + + /* Wait for the stream stop work so the URB callbacks (which + * touch subs) are done before the substream can be freed. + */ + flush_work(&chip->stream_work); + spin_lock_irqsave(&chip->lock, flags); + chip->subs[subs->stream] =3D NULL; + spin_unlock_irqrestore(&chip->lock, flags); + return 0; +} + +static int babyface_pcm_hw_params(struct snd_pcm_substream *subs, + struct snd_pcm_hw_params *params) +{ + struct snd_usb_babyface *chip =3D snd_pcm_substream_chip(subs); + const struct bf_rate *r; + int ret =3D 0; + + r =3D bf_rate_lookup(params_rate(params)); + if (!r) + return -EINVAL; + + /* The stream URBs must be at least one alt packet wide: the device + * delivers its IN data in alt-sized packets (448/640/1024 B for + * alt 1/2/3), and a smaller URB buffer makes the host controller + * discard the transfer with -EOVERFLOW (babble) - seen at + * 176.4/192 kHz with frames_per_urb below 32. Return a clean + * error instead of a silently dead capture stream. + */ + if (chip->frames_per_urb < r->min_fpu) { + dev_err(&chip->dev->dev, + "rate %u Hz needs frames_per_urb >=3D %u (module has %u)\n", + r->rate, r->min_fpu, chip->frames_per_urb); + return -EINVAL; + } + + mutex_lock(&chip->mutex); + if (r->rate !=3D chip->rate) { + /* Both directions share one clock, so a rate change must not + * race live transfers. Stop the URBs, re-point the bandwidth + * class and let the stream work restart the session at the + * new rate - the other running substream briefly sees a rate + * step (PipeWire re-negotiates via its resampler) instead of + * this open failing with -EBUSY (which killed the PW sink). + */ + if (chip->streaming) { + unsigned long flags; + + babyface_stream_kill(chip); + spin_lock_irqsave(&chip->lock, flags); + if (chip->stream_users > 0) + schedule_work(&chip->stream_work); + spin_unlock_irqrestore(&chip->lock, flags); + } + ret =3D usb_set_interface(chip->dev, BF_IFACE, r->alt); + if (ret < 0) + goto out; + chip->rate =3D r->rate; + chip->alt =3D r->alt; + chip->frame_bytes =3D r->frame_bytes; + /* The DSP EQ coefficients depend on fs: re-upload. */ + bf_eq_reupload(chip); + dev_dbg(&chip->dev->dev, "rate %u Hz (alt %u)\n", + chip->rate, chip->alt); + } +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int babyface_pcm_hw_free(struct snd_pcm_substream *subs) +{ + /* The device buffer is host-side; nothing to release here. */ + return 0; +} + +static int babyface_pcm_prepare(struct snd_pcm_substream *subs) +{ + struct snd_usb_babyface *chip =3D snd_pcm_substream_chip(subs); + unsigned long flags; + + spin_lock_irqsave(&chip->lock, flags); + chip->hw_ptr[subs->stream] =3D 0; + chip->prev_period[subs->stream] =3D 0; + spin_unlock_irqrestore(&chip->lock, flags); + return 0; +} + +static int babyface_pcm_trigger(struct snd_pcm_substream *subs, int cmd) +{ + struct snd_usb_babyface *chip =3D snd_pcm_substream_chip(subs); + unsigned long flags; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + spin_lock_irqsave(&chip->lock, flags); + chip->hw_ptr[subs->stream] =3D 0; + chip->prev_period[subs->stream] =3D 0; + /* stream_users is shared by the two substreams (separate + * locks) - serialize the ++/-- so a concurrent trigger on + * the other direction can't lose an increment (which would + * stop the stream while a substream still runs). + */ + if (chip->stream_users++ =3D=3D 0) + schedule_work(&chip->stream_work); + spin_unlock_irqrestore(&chip->lock, flags); + return 0; + case SNDRV_PCM_TRIGGER_STOP: + spin_lock_irqsave(&chip->lock, flags); + if (chip->stream_users > 0 && --chip->stream_users =3D=3D 0) + schedule_work(&chip->stream_work); + spin_unlock_irqrestore(&chip->lock, flags); + return 0; + } + return -EINVAL; +} + +static snd_pcm_uframes_t babyface_pcm_pointer(struct snd_pcm_substream *su= bs) +{ + struct snd_usb_babyface *chip =3D snd_pcm_substream_chip(subs); + unsigned long flags; + snd_pcm_uframes_t pos; + + spin_lock_irqsave(&chip->lock, flags); + pos =3D chip->hw_ptr[subs->stream] % subs->runtime->buffer_size; + spin_unlock_irqrestore(&chip->lock, flags); + return pos; +} + +static const struct snd_pcm_ops babyface_pcm_ops =3D { + .open =3D babyface_pcm_open, + .close =3D babyface_pcm_close, + .ioctl =3D snd_pcm_lib_ioctl, + .hw_params =3D babyface_pcm_hw_params, + .hw_free =3D babyface_pcm_hw_free, + .prepare =3D babyface_pcm_prepare, + .trigger =3D babyface_pcm_trigger, + .pointer =3D babyface_pcm_pointer, +}; + +static int index[SNDRV_CARDS] =3D SNDRV_DEFAULT_IDX; +static char *id[SNDRV_CARDS] =3D SNDRV_DEFAULT_STR; +static int frames_per_urb =3D BF_FRAMES_PER_URB_DEFAULT; +static int nurbs =3D BF_NURBS_DEFAULT; +static int panel_poll_ms =3D BF_PANEL_POLL_MS_DEFAULT; + +module_param_array(index, int, NULL, 0444); +MODULE_PARM_DESC(index, "Index value for the Babyface Pro FS sound card."); +module_param_array(id, charp, NULL, 0444); +MODULE_PARM_DESC(id, "ID string for the Babyface Pro FS sound card."); +module_param(frames_per_urb, int, 0644); +MODULE_PARM_DESC(frames_per_urb, "Audio frames per URB, 8..1024 (16 =3D lo= w-latency floor, 256 =3D default)."); +module_param(nurbs, int, 0644); +MODULE_PARM_DESC(nurbs, "URBs in flight per direction, 1..16 (16 =3D low-l= atency)."); +module_param(panel_poll_ms, int, 0644); +MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000= (20 =3D default, matches Windows' ~50 Hz)."); + +/* -- USB driver ------------------------- */ + +static void babyface_private_free(struct snd_card *card) +{ + struct snd_usb_babyface *chip =3D 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 =3D chip->frame_bytes * chip->frames_per_urb; + for (i =3D 0; i < chip->nurbs; i++) { + if (chip->urbs_in[i]) { + usb_kill_urb(chip->urbs_in[i]); + usb_free_urb(chip->urbs_in[i]); + } + if (chip->urbs_out[i]) { + usb_kill_urb(chip->urbs_out[i]); + usb_free_urb(chip->urbs_out[i]); + } + usb_free_coherent(chip->dev, urbsize, chip->buf_in[i], + chip->dma_in[i]); + usb_free_coherent(chip->dev, urbsize, chip->buf_out[i], + chip->dma_out[i]); + } + } + kfree(chip->urbs_in); + kfree(chip->urbs_out); + kfree(chip->buf_in); + kfree(chip->buf_out); + kfree(chip->dma_in); + kfree(chip->dma_out); + usb_put_dev(chip->dev); +} + +static int babyface_probe(struct usb_interface *intf, + const struct usb_device_id *usb_id) +{ + struct usb_device *dev =3D interface_to_usbdev(intf); + struct snd_usb_babyface *chip; + struct snd_card *card; + struct snd_pcm *pcm; + unsigned int urbsize; + u8 st[4]; + int i, err; + + if (intf->cur_altsetting->desc.bInterfaceNumber !=3D 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 =3D snd_card_new(&intf->dev, index[0], id[0], THIS_MODULE, + sizeof(*chip), &card); + if (err < 0) { + dev_err(&intf->dev, "snd_card_new failed: %d\n", err); + return err; + } + chip =3D card->private_data; + chip->card =3D card; + + chip->dev =3D 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 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 =3D intf; + chip->nurbs =3D clamp(nurbs, 1, 16); + chip->frames_per_urb =3D clamp(frames_per_urb, 8, 1024) & ~7; + chip->panel_poll_ms =3D clamp(panel_poll_ms, 10, 1000); + chip->rate =3D 48000; + chip->alt =3D BF_ALT_1; + chip->frame_bytes =3D 56; + chip->preamp =3D BF_PREAMP_BASE; + 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 =3D babyface_private_free; + + strscpy(chip->card->driver, "BabyfaceProFS", + sizeof(chip->card->driver)); + strscpy(chip->card->shortname, "Babyface Pro FS", + sizeof(chip->card->shortname)); + snprintf(chip->card->longname, sizeof(chip->card->longname), + "RME Babyface Pro FS (proprietary mode) at %s", + dev_name(&dev->dev)); + strscpy(chip->card->mixername, "Babyface Pro FS", + sizeof(chip->card->mixername)); + + /* alt 1 =3D the default 48-kHz bandwidth class. */ + err =3D usb_set_interface(dev, BF_IFACE, BF_ALT_1); + if (err < 0) { + dev_err(&intf->dev, "usb_set_interface failed: %d\n", err); + goto error; + } + + err =3D bf_cold_init(chip); + if (err < 0) { + dev_err(&intf->dev, "cold init failed: %d\n", err); + goto error; + } + + /* Sync the preamp state from the 0x17 readback (byte 0 mirrors + * the 48V/PAD bits; it persists across power cycles). + */ + err =3D bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PREAMP, st); + if (err < 0) + dev_dbg(&intf->dev, "preamp readback failed: %d\n", err); + else + chip->preamp =3D st[0]; + + /* Restore the mixer state saved at the last disconnect (if any); + * the device keeps its registers across a usbfs detach, but the + * cold init above cleared them, so push the user's settings back. + */ + err =3D bf_state_restore(chip); + if (err =3D=3D -ENOENT) { + /* No saved state: the 0x16 clear zeroed the mixer registers, + * so restore the factory default routing to keep the outputs + * live out of the box. + */ + err =3D babyface_write_default_mixer(chip); + if (err < 0) { + dev_err(&intf->dev, "default mixer restore failed: %d\n", err); + goto error; + } + } else if (err < 0) { + dev_err(&intf->dev, "mixer state restore failed: %d\n", err); + goto error; + } + + urbsize =3D chip->frame_bytes * chip->frames_per_urb; + chip->urbs_in =3D kcalloc(chip->nurbs, sizeof(*chip->urbs_in), GFP_KERNEL= ); + chip->urbs_out =3D kcalloc(chip->nurbs, sizeof(*chip->urbs_out), GFP_KERN= EL); + chip->buf_in =3D kcalloc(chip->nurbs, sizeof(*chip->buf_in), GFP_KERNEL); + chip->buf_out =3D kcalloc(chip->nurbs, sizeof(*chip->buf_out), GFP_KERNEL= ); + chip->dma_in =3D kcalloc(chip->nurbs, sizeof(*chip->dma_in), GFP_KERNEL); + chip->dma_out =3D kcalloc(chip->nurbs, sizeof(*chip->dma_out), GFP_KERNEL= ); + if (!chip->urbs_in || !chip->urbs_out || !chip->buf_in || + !chip->buf_out || !chip->dma_in || !chip->dma_out) + goto error; + + for (i =3D 0; i < chip->nurbs; i++) { + chip->urbs_in[i] =3D usb_alloc_urb(0, GFP_KERNEL); + chip->urbs_out[i] =3D usb_alloc_urb(0, GFP_KERNEL); + chip->buf_in[i] =3D usb_alloc_coherent(dev, urbsize, GFP_KERNEL, + &chip->dma_in[i]); + chip->buf_out[i] =3D usb_alloc_coherent(dev, urbsize, GFP_KERNEL, + &chip->dma_out[i]); + if (!chip->urbs_in[i] || !chip->urbs_out[i] || + !chip->buf_in[i] || !chip->buf_out[i]) + goto error; + } + + err =3D snd_pcm_new(chip->card, "Babyface Pro FS", 0, 1, 1, &pcm); + if (err < 0) { + dev_err(&intf->dev, "snd_pcm_new failed: %d\n", err); + goto error; + } + pcm->private_data =3D chip; + strscpy(pcm->name, "Babyface Pro FS", sizeof(pcm->name)); + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &babyface_pcm_ops); + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &babyface_pcm_ops); + + /* The PCM buffer is host-side (the URB callbacks copy in/out of + * it); vmalloc is the standard choice for that. + */ + err =3D snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC, + NULL, 0, 1 << 20); + if (err < 0) { + dev_err(&intf->dev, "buffer allocation failed: %d\n", err); + goto error; + } + + err =3D babyface_create_controls(chip); + if (err < 0) { + dev_err(&intf->dev, "control creation failed: %d\n", err); + goto error; + } + + err =3D babyface_create_xpoints(chip); + if (err < 0) { + dev_err(&intf->dev, "crosspoint creation failed: %d\n", err); + goto error; + } + + err =3D babyface_create_flags(chip); + if (err < 0) { + dev_err(&intf->dev, "flag control creation failed: %d\n", err); + goto error; + } + + err =3D babyface_create_panel(chip); + if (err < 0) { + dev_err(&intf->dev, "front-panel control creation failed: %d\n", err); + goto error; + } + + err =3D babyface_create_eq(chip); + if (err < 0) { + dev_err(&intf->dev, "EQ control creation failed: %d\n", err); + goto error; + } + + /* The DSP coefficient stream (EQ, bulk ep 0x0A) lives on interface + * 1, which has a single altsetting (alt 0) already active in the + * default configuration - the endpoint is scheduled, no + * SET_INTERFACE or interface claim is needed (the earlier + * -EAGAIN was the on-stack transfer buffer, and SET_INTERFACE on + * interface 1 wedged the iface-5 audio stream - playback URBs + * never completed). + */ + + err =3D 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 FS: card %i, %u frames/URB, %u URBs/direction\n", + chip->card->number, chip->frames_per_urb, chip->nurbs); + return 0; + +error: + usb_set_intfdata(intf, NULL); + /* Balance the probe()-time usb_disable_autosuspend(): disconnect() + * is never called for a failed probe, so the disable would leak and + * leave autosuspend off on this usb_device until a physical unplug. + */ + usb_enable_autosuspend(chip->dev); + snd_card_free(chip->card); + return err; +} + +static void babyface_disconnect(struct usb_interface *intf) +{ + struct snd_usb_babyface *chip =3D usb_get_intfdata(intf); + + if (!chip) + return; + + /* Idempotence guard: a disconnect can race a re-probe (usbfs + * detach/re-attach) - tear the card down exactly once. + */ + usb_set_intfdata(intf, NULL); + if (chip->shutdown) + return; + + /* Keep the mixer state for the next probe: a userspace usbfs + * claim (PipeWire sink grab, TuxMix daemon) detaches us and the + * cold init of the re-probe would otherwise wipe the settings. + */ + bf_state_save(chip); + + chip->shutdown =3D true; + cancel_work_sync(&chip->stream_work); + babyface_panel_stop(chip); + /* Balance the probe()-time usb_disable_autosuspend(): the usb_device + * outlives this interface claim (a usbfs detach re-probes without + * the physical device ever disconnecting), so leaving autosuspend + * disabled here would wrongly affect whatever claims the device next. + */ + usb_enable_autosuspend(chip->dev); + /* Wake apps blocked in read/write: the card is going away. */ + dev_info(&chip->dev->dev, "disconnect: stopping PCM substreams\n"); + babyface_pcm_stop_both(chip, SNDRV_PCM_STATE_DISCONNECTED); + mutex_lock(&chip->mutex); + if (chip->streaming) + babyface_stream_kill(chip); + mutex_unlock(&chip->mutex); + + snd_card_disconnect(chip->card); + /* NEVER snd_card_free() here: it blocks until the last user + * closes the card, and an open client (e.g. PipeWire) deadlocks + * the disconnect (seen live: pipewire stuck in snd_card_free, + * D state). free_when_closed frees on the last close. + */ + snd_card_free_when_closed(chip->card); +} + +static int babyface_suspend(struct usb_interface *intf, pm_message_t messa= ge) +{ + struct snd_usb_babyface *chip =3D usb_get_intfdata(intf); + + struct snd_device *sdev; + + if (!chip) + return 0; + list_for_each_entry(sdev, &chip->card->devices, list) { + if (sdev->type =3D=3D SNDRV_DEV_PCM) + snd_pcm_suspend_all(sdev->device_data); + } + cancel_work_sync(&chip->stream_work); + babyface_panel_stop(chip); + mutex_lock(&chip->mutex); + if (chip->streaming) + babyface_stream_kill(chip); + mutex_unlock(&chip->mutex); + return 0; +} + +static int babyface_resume(struct usb_interface *intf) +{ + struct snd_usb_babyface *chip =3D usb_get_intfdata(intf); + int err; + + if (!chip) + return 0; + + /* The device lost its state across the suspend; re-run the cold + * init and re-apply the cached mixer state. Suspended PCM + * substreams are woken by the core - apps get -ESTRPIPE and + * restart (the trigger re-arms the stream). + */ + mutex_lock(&chip->mutex); + err =3D usb_set_interface(chip->dev, BF_IFACE, chip->alt); + if (err < 0) + goto out; + err =3D bf_cold_init(chip); + if (err < 0) + goto out; + err =3D babyface_restore_state(chip); +out: + mutex_unlock(&chip->mutex); + if (!err) + babyface_panel_start(chip); + return err; +} + +static const struct usb_device_id babyface_ids[] =3D { + { USB_DEVICE(USB_VENDOR_RME, USB_PRODUCT_BABYFACE_PRO_FS) }, + { } +}; +MODULE_DEVICE_TABLE(usb, babyface_ids); + +static struct usb_driver babyface_driver =3D { + .name =3D "snd-usb-babyface-pro", + .probe =3D babyface_probe, + .disconnect =3D babyface_disconnect, + .suspend =3D babyface_suspend, + .resume =3D babyface_resume, + .id_table =3D babyface_ids, +}; + +static int __init babyface_init(void) +{ + return usb_register(&babyface_driver); +} + +static void __exit babyface_exit(void) +{ + bf_state_purge(); + usb_deregister(&babyface_driver); +} + +module_init(babyface_init); +module_exit(babyface_exit); + +MODULE_AUTHOR("Isma=C3=AFl Bahloul "); +MODULE_DESCRIPTION("RME Babyface Pro FS (proprietary mode) USB audio drive= r"); +MODULE_LICENSE("GPL"); diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/ba= byfacepro.h new file mode 100644 index 000000000..f8326397f --- /dev/null +++ b/sound/usb/babyfacepro/babyfacepro.h @@ -0,0 +1,402 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * RME Babyface Pro FS - proprietary-mode USB audio driver + * + * The Babyface Pro FS presents two personalities on the USB bus: a + * class-compliant one (handled by snd-usb-audio) and a proprietary one + * (VID 0x2a39 / PID 0x3fc0) whose PCM stream runs on INTERRUPT + * endpoints (interface 5, ep 0x01 OUT / 0x82 IN). Isochronous + * transfers are rejected there with EINVAL, and snd-usb-audio has no + * interrupt-PCM path, so this driver is standalone (snd-usb-caiaq-style + * interrupt streaming) instead of an snd-usb-audio quirk. + * + * The protocol (vendor requests + 14x32-bit frame layout) was + * reverse-engineered from Windows captures and validated on hardware - + * tools/usbdump/PROTOCOL.md is the authoritative reference. + * + * Stream notes (hardware-validated 2026-08): + * - frames_per_urb is tunable 8..1024 (multiple of 8) but must be at + * least one alt packet wide - the device delivers IN data in + * alt-sized packets (448/640/1024 B for alt 1/2/3), smaller URBs + * get -EOVERFLOW (babble). So frames_per_urb >=3D 8/16/32 for + * alt 1/2/3; the driver rejects violating rates in hw_params. + * - Validated sweep 256->128->64->32->16 (<=3D 128 kHz): with nurbs=3D8= the + * period floor is 32 frames (0.67 ms @ 48 kHz) without glitches; + * nurbs=3D16 drops it to 16 frames (0.33 ms). Soaks (5-15 min, + * 2026-08-25) refine this: period 32 is the zero-glitch floor + * (0 xruns both directions); period 16 is rock-solid on playback + * but the capture side drops ~1 buffer per 7 s (0.67 ms each - + * any scheduler hiccup overruns a 0.33 ms ring) - fine for + * monitoring, not for clean recording. Defaults (256x8) match + * the RME TotalMix 256-sample buffer; the low-latency profile is + * 16x16. + * - The device only advances the stream while BOTH endpoints have a + * pending URB - IN and OUT are always submitted as a pair. + * - Sample rate =3D SET_INTERFACE(5, alt) only; the alt is a bandwidth + * class (alt 1 =3D 32/44.1/48/64/88.2 kHz, alt 2 =3D 96/128 kHz, + * alt 3 =3D 176.4/192 kHz), not a 1:1 rate code. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define USB_VENDOR_RME 0x2a39 +#define USB_PRODUCT_BABYFACE_PRO_FS 0x3fc0 + +/* The proprietary audio interface (interface 5, interrupt endpoints). */ +#define BF_IFACE 5 +#define BF_EP_OUT 0x01 +#define BF_EP_IN 0x82 + +#define BF_ALT_1 1 /* 32/44.1/48/64/88.2 kHz, 448-B packets */ +#define BF_ALT_2 2 /* 96/128 kHz, 640-B packets */ +#define BF_ALT_3 3 /* 176.4/192 kHz, 1024-B packets */ + +/* Default stream geometry - conservative, matches the RME TotalMix + * 256-sample buffer. Both are tunable via module params; the + * low-latency profile (validated) is frames_per_urb=3D16 nurbs=3D16. + */ +#define BF_FRAMES_PER_URB_DEFAULT 256 +#define BF_NURBS_DEFAULT 8 + +/* 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 + +#define BF_WORDS_PER_FRAME 14 /* 14 x 32-bit words per frame */ + +/* Consecutive URB errors (CRC/babble/protocol or a failed resubmit) + * before the stream is stopped and the apps get a clean -EPIPE. + */ +#define BF_URB_ERR_STOP 3 + +/* Vendor requests (bmRequestType 0x40, value in wValue, no data phase). */ +#define BF_REQ_KEEPALIVE 0x10 /* settings word / stream trigger */ +#define BF_REQ_STATUS 0x11 /* read 4 B */ +#define BF_REQ_CROSSPOINT 0x12 /* 16-bit crosspoint / master */ +#define BF_REQ_SESSION_STOP 0x13 /* disarm - never sent mid-run */ +#define BF_REQ_SESSION_ARM 0x14 +#define BF_REQ_REG_CLEAR 0x16 /* cold-init register clear */ +#define BF_REQ_PREAMP 0x17 /* 48V/PAD state + readback */ +#define BF_REQ_GAIN 0x1a /* 8-bit gain / master companion */ +#define BF_REQ_DDS 0x1b /* clock quads */ +#define BF_REQ_STATUS_2 0x1c /* read 4 B */ +#define BF_REQ_SESSION_START 0x1d +#define BF_REQ_PREAMP_COMMIT 0x21 /* commit after 0x17 */ +#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 =3D + * the pair at 0x0001 + the other 28 at 0x0000; OFF =3D all 0x0000). + * wIdx =3D 2xout_index: AN1/2 =3D 0/1, PH3/4 =3D 2/3, AS1/2 =3D 4/5, ... + */ +#define BF_LOOPBACK_CHANNELS 30 + +/* Register addresses. */ +#define BF_REG_PREAMP 0x003f +#define BF_REG_MASTER_16 0x03e0 /* + 2*out (bReq 0x12) */ +#define BF_REG_MASTER_8 0x0004 /* + 2*out (bReq 0x1a) */ +#define BF_REG_GAIN 0x0000 /* + mic 0-3 (bReq 0x1a) */ +#define BF_REG_CROSS_BASE_L 0x0034 /* + 0x34*out + src (bReq 0x12) */ +#define BF_REG_CROSS_BASE_R 0x004e /* + 0x34*out + src */ +#define BF_REG_CROSS_STRIDE 0x0034 +#define BF_REG_KEEPALIVE_SETTINGS 0x05cf +#define BF_REG_KEEPALIVE_INIT 0x05ff + +/* The "cross" register block within each output: the L-registers sit at + * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo + * source pairs that can be cross-linked). bf_crosspoint_clear_cross() + * zeroes them because the cold-init clear does not cover them. + */ +#define BF_CROSS_L_FIRST 5 +#define BF_CROSS_L_LAST 23 +#define BF_CROSS_R_FIRST 4 +#define BF_CROSS_R_LAST 22 + +/* Front-panel readback (babyfacepro-ctl.c): 0x17 read at wIdx 0x0000 - th= e index + * the Windows driver polls (cap_buttons2.pcap). byte0 =3D preamp 48V/PAD, + * byte1 =3D OUT sel + DIM/MIX bits, byte2 =3D IN sel + wheel counter, + * byte3 =3D 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.pca= p). + */ +#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 + +/* Preamp state byte (0x17, wIdx 0x003F - full state, verified). + * NOTE 2026-08-26 (cap_reflevel3.pcap): the 0x0C "base" is NOT a + * constant - it is the Instr 3/4 REF-LEVEL bits (bits 2-3, +4dBu =3D + * 0x0C set; -10dBV/Boost =3D clear; Boost additionally commits 0x21 + * wVal 0x0003). Keeping it always set =3D forcing the default +4dBu, + * which is correct for the driver (no ref-level control). + */ +#define BF_PREAMP_REF_4DBU 0x000c +#define BF_PREAMP_BASE BF_PREAMP_REF_4DBU +#define BF_PREAMP_48V_MIC1 0x0001 +#define BF_PREAMP_48V_MIC2 0x0002 +#define BF_PREAMP_PAD_MIC1 0x0010 +#define BF_PREAMP_PAD_MIC2 0x0020 + +/* Calibrated master value: 0 dB =3D 0x2000 (+6 dB =3D 0x4000). See + * CALIBRATION.md. The crosspoint fader curve is DIFFERENT (0 dB =3D + * 0x16a0, top 0x2d41 - see below). + */ +#define BF_MASTER_0DB 0x2000 + +/* The 8-bit master is the REAL output volume (hardware-verified + * 2026-08-24: writing it changes the level, the 16-bit does not). + * Scale: 0.5 dB per step, 0xf3 =3D 0 dB (the scene-load default), + * bottom 0x73 =3D -64 dB (silence), top 0xff =3D +6 dB. The 16-bit + * register is a companion kept in sync (TotalMix writes both). + * The mute value is 0x3B. + */ +#define BF_MASTER_8_0DB 0xf3 +#define BF_MASTER_8_MIN 0x73 +#define BF_MASTER_MUTE 0x3b +#define BF_MASTER_UNMUTE 0xf3 + +/* 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 + +/* Crosspoint fader curve: 0 dB =3D 0x16a0, +6 dB =3D 0x2d41 (fader curve, + * DIFFERENT from the master 0x4000 top - see CALIBRATION.md). + */ +#define BF_FADER_0DB 0x16a0 +#define BF_FADER_TOP 0x2d41 + +/* The crosspoint matrix sources (14 controls per output). */ +struct bf_source { + const char *name; + u8 idx_l; + u8 idx_r; +}; + +/* Crosspoint-source order + register block maps (babyfacepro-ctl.c). */ +extern const struct bf_source bf_sources[14]; +extern const u8 bf_xpoint_block[6]; + +/* Calibrated preamp gain: 65 dB over 20 raw steps (3.25 dB/step). */ +#define BF_GAIN_MAX_DB 65 + +struct snd_usb_babyface { + struct snd_card *card; + struct usb_device *dev; + struct usb_interface *iface; + + struct mutex mutex; /* controls + stream geometry */ + spinlock_t lock; /* hw_ptr / subs */ + + /* stream */ + struct urb **urbs_in; + struct urb **urbs_out; + void **buf_in; + void **buf_out; + dma_addr_t *dma_in; + dma_addr_t *dma_out; + unsigned int nurbs; + unsigned int frames_per_urb; + unsigned int frame_bytes; /* 56/40/32 for alt 1/2/3 */ + unsigned int rate; + unsigned int alt; + int stream_users; /* PCM substreams sharing the stream */ + bool streaming; /* URBs actually in flight */ + bool shutdown; + atomic_t urb_err; /* consecutive bad URBs (stops the stream) */ + struct work_struct stream_work; + + struct snd_pcm_substream *subs[2]; + unsigned long hw_ptr[2]; + unsigned long prev_period[2]; + + /* mixer state (no gain readback exists - host-side mirror) */ + u16 preamp; /* 48V/PAD bits, base 0x0c */ + u8 gain[4]; /* preamp gain in dB 0-65/9 (raw derived + * at write: mic 3.25 dB/step, instr + * 0.5 dB/step) + */ + u8 gain_cycle; /* 0x20/0x00/0x40 transaction counter */ + u8 flag_cnt; /* 0xc000/0x4000/0x8000/0x0000 */ + u16 master[6][2]; /* cached 16-bit masters */ + bool muted[6]; + u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */ + bool dim; /* DIM engaged (fixed -20 dB on Phones) */ + u16 xpoint[6][14][2]; /* cached crosspoints (out, src, L/R) */ + int pitch; /* varispeed in 0.1% (-500..+500) */ + bool loopback[6]; + bool an12; /* AN 1>2 copy */ + bool linked; /* AN1/2 input link */ + bool ms_proc; /* MS processor engaged */ + int width; /* width knob -100..+100 */ + u16 fx_send; /* FX send level 0..0x1000 */ + + /* DSP EQ (babyfacepro-ctl.c) - 4 analog-input strips, params kept in sta= te */ + struct bf_eq_channel { + bool on; /* EQ engaged (else identity blocks) */ + s32 slope_db; /* low-cut slope 6/12/18/24 (0 =3D off) */ + s32 lc_hz; /* low-cut freq, 0 =3D off */ + u32 lc_raw; /* cached 0x38 word */ + u8 slope; /* cached slope byte (2^n - 1) */ + s32 band_type[3]; /* 0 off, 1 bell, 2 low shelf, 3 high shelf */ + s32 band_freq[3]; /* Hz */ + s32 band_q[3]; /* Q x 100 */ + s32 band_gain[3]; /* dB x 10 */ + s32 words[3][4]; /* cached c0..c3 */ + s32 shared; /* cached c4 (shared by the slots) */ + } eq[4]; + + /* front panel (babyfacepro-ctl.c) - 0x17 readback poll */ + struct delayed_work panel_work; + unsigned int panel_poll_ms; /* front-panel poll interval, module param */ + u8 panel_prev[4]; /* last 0x17 snapshot */ + bool panel_seen; /* first snapshot taken */ + bool panel_select_armed; /* device SELECT cycle armed (IN switch disarms)= */ + unsigned long panel_start; /* jiffies at panel_start (boot re-assert) */ + int panel_button; /* latched button event (consumed on get) */ + int panel_wheel; /* accumulated wheel delta (consumed on get) */ + int panel_in; /* enum: 0 unknown, 1 Ch1/2, 2 Ch3/4, 3 Opt */ + int panel_out; /* enum: 0 unknown, 1 Ch1/2, 2 Phones, 3 Opt */ + bool panel_mix; /* MIX engaged - HOST-latched (like TotalMix): + * set by the 0x44 flash ack, NOT by the readback + * 0x80 bit (the raw press has none) + */ + bool panel_dim; /* DIM sticky (byte1 bit 0x20) */ + bool panel_saw_fader; /* device observed in fader mode (byte2 0x0x) + * - gates the device-driven MIX exit + */ + int panel_select; /* SELECT state: 0 L, 1 R, 2 both, 3 none + * (host-tracked - not in the readback) + */ + int panel_sel_hold; /* consecutive ticks with byte3 =3D 0x50 + * (SELECT held > 200 ms =3D the OUT-balance + * gesture; a tap flashes only ~100-150 ms, + * selhold_probe2 - no engaged bit) + */ + u16 panel_mix_raw; /* MIX-mode monitoring level (fader raw) */ + u8 panel_mix_disp[4]; /* MIX-mode VU display shadow per mic + * (0x1A 0x000A+mic - written on change + * so the input VU follows the wheel) + */ + struct snd_kcontrol *panel_kctl[7]; /* for snd_ctl_notify */ +}; + +struct bf_saved { + struct list_head list; + char key[32]; + u16 preamp; + u8 gain[4]; + u8 gain_cycle; + u8 flag_cnt; + u16 master[6][2]; + bool muted[6]; + u16 xpoint[6][14][2]; + int pitch; + bool loopback[6]; + bool an12; + bool linked; + bool ms_proc; + int width; + u16 fx_send; + bool dim; +}; + +struct bf_rate { + unsigned int rate; + unsigned int alt; + unsigned int frame_bytes; + unsigned int min_fpu; /* frames/URB floor =3D one alt packet (448/640/102= 4 B) */ +}; + +/* Sample-rate / alt classes (babyfacepro.c). */ +const struct bf_rate *bf_rate_lookup(unsigned int rate); + +/* -- shared driver state ---------------------- */ +extern const u16 bf_flag_cycle[4]; +extern const struct bf_source bf_sources[14]; + +/* babyfacepro-ctl.c - the DSP EQ (struct snd_usb_babyface is defined abov= e). */ +void bf_eq_band_words(s32 *w, int type, s32 freq_hz, s32 q100, + s32 gain_x10, s32 fs); +void bf_eq_reupload(struct snd_usb_babyface *chip); +int babyface_create_eq(struct snd_usb_babyface *chip); +extern const u8 bf_xpoint_block[6]; +extern const struct snd_pcm_hw_constraint_list bf_rates_constraint; + +/* -- babyfacepro.c ------------------------ */ +int bf_vendor_write(struct snd_usb_babyface *chip, u8 req, u16 val, u16 id= x); +int bf_vendor_write_cycle(struct snd_usb_babyface *chip, u8 req, u16 val, = u16 idx); +int bf_vendor_read(struct snd_usb_babyface *chip, u8 req, u16 idx, u8 *buf= ); +int bf_cold_init(struct snd_usb_babyface *chip); +int bf_crosspoint_clear_cross(struct snd_usb_babyface *chip, + unsigned int blk); +const struct bf_rate *bf_rate_lookup(unsigned int rate); +void babyface_stream_kill(struct snd_usb_babyface *chip); +void babyface_pcm_stop_both(struct snd_usb_babyface *chip, snd_pcm_state_t= state); +void babyface_stream_work(struct work_struct *work); + +/* -- babyfacepro-ctl.c ----------------------- */ +int babyface_write_default_mixer(struct snd_usb_babyface *chip); +int bf_apply_masters(struct snd_usb_babyface *chip); +int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on); +int bf_preamp_state_write(struct snd_usb_babyface *chip); +int babyface_create_controls(struct snd_usb_babyface *chip); +int babyface_create_xpoints(struct snd_usb_babyface *chip); +int babyface_create_flags(struct snd_usb_babyface *chip); + +/* Master + gain law helpers - shared with the front-panel wheels. */ +int bf_master_half_db(u16 vol16); /* 16-bit master -> dBx2 */ +int bf_master_16bit(int half_db); /* dBx2 -> 16-bit master */ +u8 bf_master_8bit(u16 vol16); /* 16-bit master -> 8-bit companion */ +int bf_gain_max_db(int mic); +int bf_gain_db(int mic, u8 raw); u8 bf_gain_raw(int mic, int db); + +/* -- babyfacepro-ctl.c ----------------------- */ +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); + +/* -- babyfacepro.c ------------------------ */ +void bf_state_save(struct snd_usb_babyface *chip); +int bf_state_restore(struct snd_usb_babyface *chip); +void bf_state_purge(void); +int babyface_restore_state(struct snd_usb_babyface *chip); +int bf_state_apply_flags(struct snd_usb_babyface *chip); --=20 2.55.0 From nobody Sat Sep 26 10:02:09 2026 Received: from mail-wm1-f54.google.com (mail-wm1-f54.google.com [209.85.128.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 E28BB4334AD for ; Wed, 2 Sep 2026 10:11:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.54 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; cv=none; b=tRTIkWjbicK3DF5mdV7xgwY1IJ/3D017jd7x6f0BS03Jp6t8eWDaFmjk0A3A2HJ6gziSKZA5JjMGKCvSb5VmkSgZlHZsfoSz4mV7Z0m1oSvx65+k4pafvRcmDM4g+L2dZptrTKlC6dEQ72dGh+P4O966zB8GDqejDP33jj0houw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; c=relaxed/simple; bh=/R6EqUqD5f/3lTrKgpD1cd6gUJJ3nIRJ3i8LjUo88e0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=C2bBKyiuNGdPSS2jkaSboGAjiXGWvwwkbye95E32Oc2CwxaqZX+flrCKxhrjKUCiX2NOUE5Wo83Z00bQazcMyMg0w77rsWWacl0UX/62VqTl6Il1LUmvrOnlfWj/sU7VLi+DoXk+R/KaiRfd7+PWStK/yMftMpzP+Sn6dDIKLCs= 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=YfoWtRx/; arc=none smtp.client-ip=209.85.128.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="YfoWtRx/" Received: by mail-wm1-f54.google.com with SMTP id 5b1f17b1804b1-495437bb891so6937805e9.1 for ; Wed, 02 Sep 2026 03:11:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788343893; x=1788948693; 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=Xlk5HjgSlhRNe3S56qTNTA+jFTBrEv6KxGXOW5vBX3k=; b=YfoWtRx/j+BI1DP5YCgOV2Ii4jF8PDp9zx5whO3mT78qI2Rmz5uWH1qWIoRKESiEly m7WOr8DByf+xOfnKF+05q+LLzY/5jNjR5K7cn4HZYaiEnuyIpWrbDkXyhotMgpz2muT0 5+dpEgYhmsqHt+QhErq6Duq1Wr68J4nFs6hr4O4R5aE35K1WLc4cTd6eQpZqcuI6zPq7 7sJSHTMqKW5zdgCGwdT2VfaP/rkDVijvfqIxHslBnEtbjf6z/qLQwWxaYvRjuVdrXCzk VhnivNratKQfHLZNRh3KfAD+Q+sbHvZWOGQqafnqa+5k9+XYtpxSdlPJIkLqA7SObdQE 5zIA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788343893; x=1788948693; 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=Xlk5HjgSlhRNe3S56qTNTA+jFTBrEv6KxGXOW5vBX3k=; b=MD1T7gVdJip00pNQlhXQEQqustGLHVb5wAjQS1wa5on6VVxvYsSgNQo3OzQWE9/fT3 x6VF1R5emJkJlZmLtpXoRXUK6SnImI8YOLiQJ3MHopqdwISCeHkvRD0nVclaSGn8AGYy iq/jhgCmTu2P9ITkl4c4DLWIPVwlaz/ZLO1GQr30D7BuG1eT1aV24btIgWCptMPZE5W6 OGrfr0+e8EvZe6myM0SB5CC5F7SzjeHEnFRvd41+ChwxQ2q+uK7foJUdGa/0KiMM9RrR pU8ZVVNaAdOe5RY2ExNmNXra5laZh13yVPtkWdrBS/2kvHJ5V2lWk4Bk5xnG9qENF+k7 z70w== X-Forwarded-Encrypted: i=1; AHgh+RrLc7BUVvwB4bbzxIg35zmYtr2Gd8ap6SmyNtL+F4HckpQgPiRm61m40mmabF3YGiUINsfTR5bHgsFXlmI=@vger.kernel.org X-Gm-Message-State: AFuF++n1kwt5abjjTHm2LlyNqX6C2bX5VIFhSsJVX0HUsVjJtQies7x/ G5GlXFyDO526n7+qD5f7Ye06vFIEf9VeaAt7+DXQxZD6Gs+vcUdNyisy X-Gm-Gg: AR+sD13D/8nWZiSGxsOL9C63kaU+euli7TiRtfW+qZ3oZAJxA6uL+7jBrKf3cCKB7Wo BG+TIV2BHZ7Qb3i7G6KBMl7gGOVMDszy8ou+p5YGz6lVIyc3ZnL0KfM3VBLOLSRK5Xv4eFxl0ae spAiqvPIePMUeNK72bjYragQWXgfgXfeglsx/THYj32I2Qh80KyB3aRZD+0uaVmWS8Abkje35x7 I4LTYeJaV3E6/nodaKbiGANkaBNPhC52uuL5SD/3n/NBduTiO6VhKgH7qIH+unvHHxwiKG0NmJ8 fz6pIX5OLfywTsIIzjmT6Ex057tcsZD+NREnxKYDbqBL82yG8DSWzYTbitwy85nmpSEea/avjUq WxPqPXdn7+1fJoi61H0gbTrFoXrOYRgqYg1+1jbgl1D29Scs8EV/18+aMijoEfQw695elIXVgJQ 2S5GfF9jWKNHX7yWulwonT3jxAWOwfcRuR/+vGn0G7nb454vIJ4SoJ5MlI75RqBTfNac8E58Z1y 7W7g8jNSRx4p2yJ X-Received: by 2002:a05:600c:3e14:b0:49c:e37e:4389 with SMTP id 5b1f17b1804b1-49ce7c03f51mr33704995e9.4.1788343892314; Wed, 02 Sep 2026 03:11:32 -0700 (PDT) Received: from cachyos ([196.77.120.180]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49ce47885b1sm57294265e9.14.2026.09.02.03.11.31 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 02 Sep 2026 03:11:31 -0700 (PDT) From: =?UTF-8?q?Isma=C3=AFl=20Bahloul?= To: linux-sound@vger.kernel.org Cc: linux-usb@vger.kernel.org, alsa-devel@alsa-project.org, perex@perex.cz, tiwai@suse.com, linux-kernel@vger.kernel.org, =?UTF-8?q?Isma=C3=AFl=20Bahloul?= Subject: [RFC PATCH v3 2/4] ALSA: usb: babyface-pro: add the mixer control surface Date: Wed, 2 Sep 2026 11:11:15 +0100 Message-ID: <20260902101117.11671-3-i.bahloul01@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260902101117.11671-1-i.bahloul01@gmail.com> References: <20260902101117.11671-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: quoted-printable Implement the ALSA mixer: the six output masters + mutes, the 6x14 crosspoint matrix, the four preamp gains (mic 3.25 dB/step, instrument 0.5 dB/step), phantom power and PAD, pitch/varispeed, loopback, AN1>2, stereo link, MS processor, width, FX send and DIM. The front panel and DSP EQ remain stubbed here; they land in the next patches of the series. Signed-off-by: Isma=C3=AFl Bahloul Assisted-by: DeepSeek V4 Flash --- sound/usb/babyfacepro/babyfacepro-ctl.c | 1294 ++++++++++++++++++++++- 1 file changed, 1273 insertions(+), 21 deletions(-) diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepr= o/babyfacepro-ctl.c index ec2640e0b..168bcb35b 100644 --- a/sound/usb/babyfacepro/babyfacepro-ctl.c +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * RME Babyface Pro FS =E2=80=94 proprietary-mode USB audio driver + * RME Babyface Pro FS - proprietary-mode USB audio driver * - * ALSA control surface (mixer, front panel, DSP EQ). This is the - * initial slice of the series: the card's PCM stream + lifecycle come - * from babyfacepro.c, while the control surface is stubbed out here so - * the module links. The real mixer, front-panel and DSP EQ land in the - * follow-up patches (mixer, panel, eq) =E2=80=94 each replaces its stubs. + * ALSA control surface: mixer (masters, preamp, crosspoints, flags, + * gains), front-panel poll + controls, and the hardware DSP EQ + * (3-band + low cut). * - * See babyfacepro.h for the shared device state and register map. + * See babyfacepro.h for the shared device state and register map, + * and babyfacepro.c for the core driver (protocol, PCM streaming, + * state persistence, card lifecycle). */ #include #include @@ -43,7 +43,7 @@ const struct bf_source bf_sources[14] =3D { { "PB6", 22, 23 }, }; =20 -/* Crosspoint-map output order vs the master-map order =E2=80=94 HARDWARE- +/* Crosspoint-map output order vs the master-map order - HARDWARE- * VERIFIED 2026-08-24: the block that feeds the Phones is the FIRST * crosspoint block (0x34), while the Phones master is the SECOND * (0x03E2/0x0006). The crosspoint map lists the Phones first (the @@ -53,54 +53,1302 @@ const struct bf_source bf_sources[14] =3D { */ const u8 bf_xpoint_block[6] =3D { 1, 0, 2, 3, 4, 5 }; =20 -/* =E2=94=80=E2=94=80 control-surface stubs =E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80 - * Filled in by the mixer / panel / eq patches. The core driver - * (babyfacepro.c) calls these from probe() and the stream/state - * paths, so they must exist for the module to link. Until then the - * card exposes the PCM stream only. +/* Master-register output order - the master map lists AN1/2 first + * (0x03E0) and the Phones master SECOND (0x03E2, HARDWARE-VERIFIED + * 2026-08-24); the crosspoint blocks are in the opposite order + * (Phones =3D block 0x34 first, hence bf_xpoint_block above). Control + * index -> canonical output (AN1/2=3D0, PH3/4=3D1, ...) =3D the master + * register position directly: the names 'AN1/2 Playback Volume' etc. + * must match the register they write (corrected 2026-08-26 - the + * previous {1,0,...} swap made 'AN1/2' drive the Phones and 'PH3/4' + * drive the AN1/2 analog out). */ +static const u8 bf_master_out[6] =3D { 0, 1, 2, 3, 4, 5 }; =20 +/* The 16-bit master value -> the 8-bit companion code (0.5 dB/step). + * Integer-only: half_db =3D 12*log2(v/0x2000) via ilog2 + an 8-bit + * fractional-octave table (12*log2(1 + n/256), ~0.05 dB resolution - + * fine enough for the +/-0.5 dB panel wheel to track the round-trip). + */ +static const u8 bf_lg2_frac[256] =3D { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, +}; + +/* 16-bit master -> dBx2 (12 half-dB per octave; 0x2000 =3D 0 dB). + * Shared by the 8-bit companion and the front-panel OUT wheel. + */ +int bf_master_half_db(u16 vol16) +{ + unsigned int k, frac; + + vol16 =3D clamp(vol16, 1, 0x4000); + k =3D ilog2(vol16); + frac =3D ((vol16 - (1u << k)) << 8) >> k; + return 12 * (int)k - 156 + bf_lg2_frac[frac]; +} + +/* dBx2 -> 16-bit master (0x2000*2^(half_db/12), rounded). The + * inverse of bf_master_half_db - the 12th-root table 2^(n/12). + */ +static const u16 bf_twelfth[12] =3D { + 0x1000, 0x10f4, 0x11f6, 0x1307, 0x1429, 0x155c, + 0x16a1, 0x17f9, 0x1966, 0x1ae9, 0x1c82, 0x1e34, +}; + +int bf_master_16bit(int half_db) +{ + int k =3D half_db / 12; + int n =3D half_db % 12; + u32 v; + + if (n < 0) { + n +=3D 12; + k--; + } + v =3D (u32)bf_twelfth[n] << 1; /* 0x2000*2^(n/12) */ + if (k >=3D 0) { + v <<=3D k; + } else { + v +=3D 1u << (-k - 1); /* round-half-up */ + v >>=3D -k; + } + return (u16)clamp(v, 1, 0x4000); +} + +u8 bf_master_8bit(u16 vol16) +{ + if (vol16 =3D=3D 0) + return BF_MASTER_MUTE; + return (u8)clamp(0xf3 + bf_master_half_db(vol16), BF_MASTER_8_MIN, 0xff); +} + +/* The cold-init register clear zeroes the mixer registers TotalMix + * re-uploads afterwards. The kernel driver has no saved scene (no + * readback for faders), so it applies TotalMix's factory default: + * every source routed to every output at unity, masters at 0 dB and + * unmuted - the user/TuxMix can restore its own scene on top. + */ int babyface_write_default_mixer(struct snd_usb_babyface *chip) { - return 0; + int out, src, ret; + u16 flag; + + /* Output masters: 0 dB (0x2000) + the unmute companion (0xf3). */ + for (out =3D 0; out < 6; out++) { + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_UNMUTE, + BF_REG_MASTER_8 + 2 * out); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_UNMUTE, + BF_REG_MASTER_8 + 2 * out + 1); + if (ret < 0) + return ret; + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_MASTER_0DB, + (BF_REG_MASTER_16 + 2 * out) | flag); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_MASTER_0DB, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + if (ret < 0) + return ret; + chip->master[out][0] =3D BF_MASTER_0DB; + chip->master[out][1] =3D BF_MASTER_0DB; + chip->muted[out] =3D false; + } + + /* Every source into every output pair, L and R, at 0 dB (the + * standard map; the low map is only a shadow). The addresses use + * the source's idx_l/idx_r on the canonical block - writing the raw + * index on both bases would put PB1 R on the L side and PB1 L on + * the R side (L+R on both =3D mono). The "cross" registers + * (L-reg idx_r / R-reg idx_l) are left at 0; the restore at stream + * start re-writes the same addresses from the cache. + */ + for (out =3D 0; out < 6; out++) { + unsigned int blk =3D bf_xpoint_block[out]; + + for (src =3D 0; src < 14; src++) { + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_FADER_0DB, + (BF_REG_CROSS_BASE_L + + BF_REG_CROSS_STRIDE * blk + + bf_sources[src].idx_l) | flag); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, BF_FADER_0DB, + (BF_REG_CROSS_BASE_R + + BF_REG_CROSS_STRIDE * blk + + bf_sources[src].idx_r) | flag); + if (ret < 0) + return ret; + } + ret =3D bf_crosspoint_clear_cross(chip, blk); + if (ret < 0) + return ret; + } + + /* Mirror the defaults into the control cache (14 controls/output). */ + for (out =3D 0; out < 6; out++) + for (src =3D 0; src < 14; src++) { + chip->xpoint[out][src][0] =3D BF_FADER_0DB; + chip->xpoint[out][src][1] =3D BF_FADER_0DB; + } + + /* Host settings word: clock Internal (0x0001). */ + return bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0001, + BF_REG_KEEPALIVE_SETTINGS); } =20 +/* The device resets its output masters to mute when a stream session + * starts (hardware-verified 2026-08-24: after a stream start the + * output stays silent until a master write lands - only a write + * un-mutes the 8-bit register). Re-apply the six output masters + + * mutes from the cache; also used by the PM restore path. + */ int bf_apply_masters(struct snd_usb_babyface *chip) { + int out, ret; + u16 flag; + + for (out =3D 0; out < 6; out++) { + u16 l =3D chip->muted[out] ? 0 : chip->master[out][0]; + u16 r =3D chip->muted[out] ? 0 : chip->master[out][1]; + u8 l8 =3D chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(l); + u8 r8 =3D chip->muted[out] ? BF_MASTER_MUTE : bf_master_8bit(r); + + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, l8, + BF_REG_MASTER_8 + 2 * out); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, r8, + BF_REG_MASTER_8 + 2 * out + 1); + if (ret < 0) + return ret; + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_MASTER_16 + 2 * out) | flag); + if (ret < 0) + return ret; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + if (ret < 0) + return ret; + } + return 0; +} + +/* -- mixer controls ------------------------ */ + +/* dB TLV for the output masters: 0x2000 =3D 0 dB, 0x4000 =3D +6 dB + * (CALIBRATION.md) with the hardware 20*log10(v/0x2000) law - the raw + * 16-bit value IS the linear amplitude. WirePlumber needs this to map + * the volume 1:1 to the hardware control instead of applying a software + * volume on top (which left the output ~30 dB down). + */ +static const DECLARE_TLV_DB_RANGE(bf_master_tlv, + 0, 0x2000, TLV_DB_LINEAR_ITEM(-6500, 0), + 0x2000, 0x4000, TLV_DB_LINEAR_ITEM(0, 600) +); + +static int bf_master_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 2; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D 0x4000; /* +6 dB =3D 2 x 0dB(0x2000) */ + uinfo->value.integer.step =3D 1; return 0; } =20 -int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on) +static int bf_master_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) { + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D bf_master_out[kctl->private_value]; + + ucontrol->value.integer.value[0] =3D chip->master[out][0]; + ucontrol->value.integer.value[1] =3D chip->master[out][1]; return 0; } =20 +static int bf_master_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D bf_master_out[kctl->private_value]; + u16 l =3D ucontrol->value.integer.value[0]; + u16 r =3D ucontrol->value.integer.value[1]; + u16 flag; + int ret =3D 0; + + /* The control is declared 0..0x4000 (+6 dB); reject anything outside + * so the 16-bit companion register and the cache stay in spec (the + * ALSA core only enforces this with CONFIG_SND_CTL_INPUT_VALIDATION). + */ + if (l > 0x4000 || r > 0x4000) + return -EINVAL; + + mutex_lock(&chip->mutex); + if (l =3D=3D chip->master[out][0] && r =3D=3D chip->master[out][1]) + goto out; + + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + + /* The 8-bit register is the real volume; the 16-bit is its + * companion (kept in sync like TotalMix). + */ + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l), + BF_REG_MASTER_8 + 2 * out); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r), + BF_REG_MASTER_8 + 2 * out + 1); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_MASTER_16 + 2 * out) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + if (ret < 0) + goto out; + + chip->master[out][0] =3D l; + chip->master[out][1] =3D r; + chip->muted[out] =3D false; + /* A Phones change while DIM is engaged re-bases the restore point. */ + if (chip->dim && out =3D=3D 1) { + chip->dim_saved[0] =3D l; + chip->dim_saved[1] =3D r; + } + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_mute_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_BOOLEAN; + uinfo->count =3D 2; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D 1; + return 0; +} + +static int bf_mute_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D bf_master_out[kctl->private_value]; + + /* ALSA convention: 1 =3D enabled (sound on) =3D not muted. */ + ucontrol->value.integer.value[0] =3D !chip->muted[out]; + ucontrol->value.integer.value[1] =3D !chip->muted[out]; + return 0; +} + +static int bf_mute_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D bf_master_out[kctl->private_value]; + bool muted =3D !ucontrol->value.integer.value[0]; + u16 flag; + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (muted =3D=3D chip->muted[out]) + goto out; + + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + + if (muted) { + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE, + BF_REG_MASTER_8 + 2 * out); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_MUTE, + BF_REG_MASTER_8 + 2 * out + 1); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, + (BF_REG_MASTER_16 + 2 * out) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + if (ret < 0) + goto out; + } else { + /* Unmute restores the cached volume (TotalMix keeps the + * pre-mute fader value host-side), 8-bit + 16-bit. + */ + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, + bf_master_8bit(chip->master[out][0]), + BF_REG_MASTER_8 + 2 * out); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, + bf_master_8bit(chip->master[out][1]), + BF_REG_MASTER_8 + 2 * out + 1); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->master[out][0], + (BF_REG_MASTER_16 + 2 * out) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->master[out][1], + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + if (ret < 0) + goto out; + } + chip->muted[out] =3D muted; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + int bf_preamp_state_write(struct snd_usb_babyface *chip) { + int ret; + + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, chip->preamp, BF_REG_PREAMP); + if (ret < 0) + return ret; + return bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000); +} + +/* -- crosspoint matrix (6 outputs x 14 sources) -------------- */ + +static int bf_xpoint_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 2; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D BF_FADER_TOP; /* +6 dB fader top */ + uinfo->value.integer.step =3D 1; return 0; } =20 -int babyface_create_controls(struct snd_usb_babyface *chip) +static int bf_xpoint_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) { + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D kctl->private_value >> 8; + int src =3D kctl->private_value & 0xff; + + ucontrol->value.integer.value[0] =3D chip->xpoint[out][src][0]; + ucontrol->value.integer.value[1] =3D chip->xpoint[out][src][1]; return 0; } =20 +static int bf_xpoint_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D kctl->private_value >> 8; + int src =3D kctl->private_value & 0xff; + unsigned int blk =3D bf_xpoint_block[out]; + const struct bf_source *s =3D &bf_sources[src]; + u16 l =3D ucontrol->value.integer.value[0]; + u16 r =3D ucontrol->value.integer.value[1]; + u16 flag; + int ret =3D 0; + + if (l > BF_FADER_TOP || r > BF_FADER_TOP) + return -EINVAL; + + mutex_lock(&chip->mutex); + if (l =3D=3D chip->xpoint[out][src][0] && r =3D=3D chip->xpoint[out][src]= [1]) + goto out; + + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + + /* L register =3D 0x0034 + 0x34*blk + idx, R =3D 0x004E + 0x34*blk + idx + * (mono sources use the same idx on both sides). + */ + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk + + s->idx_l) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk + + s->idx_r) | flag); + if (ret < 0) + goto out; + + chip->xpoint[out][src][0] =3D l; + chip->xpoint[out][src][1] =3D r; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + int babyface_create_xpoints(struct snd_usb_babyface *chip) { + struct snd_kcontrol *kctl; + int out, src, err; + + for (out =3D 0; out < 6; out++) { + for (src =3D 0; src < 14; src++) { + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Playback Volume", + .index =3D out * 14 + src, + .info =3D bf_xpoint_info, + .get =3D bf_xpoint_get, + .put =3D bf_xpoint_put, + .private_value =3D (out << 8) | src, + }, chip); + /* Name the control by its source: "AN1 Playback Volume", + * "PB1 Playback Volume"... with a unique index. + */ + strscpy(kctl->id.name, bf_sources[src].name, + sizeof(kctl->id.name)); + strlcat(kctl->id.name, " Playback Volume", + sizeof(kctl->id.name)); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + } + } + return 0; +} + +/* -- flags / special controls (pitch, loopback, link, width, FX) -- */ + +static int bf_switch_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_BOOLEAN; + uinfo->count =3D 1; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D 1; + return 0; +} + +static int bf_pitch_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D -50; /* -5.0 % */ + uinfo->value.integer.max =3D 50; /* +5.0 % */ + uinfo->value.integer.step =3D 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 =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->pitch; + return 0; +} + +static int bf_pitch_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int p =3D ucontrol->value.integer.value[0]; + u32 dds24, dds16; + u16 frac, b1, b2; + int ret =3D 0; + + if (p < -50 || p > 50) + return -EINVAL; + + mutex_lock(&chip->mutex); + if (p =3D=3D chip->pitch) + goto out; + + /* The 0x1B DDS quad (16.8 fixed point, banked). p is 0.1 % steps: + * DDS_24 =3D round(50000*256/(1+p/1000)) =3D round(12800000000/(1000+p)). + */ + dds24 =3D div_u64(12800000000ULL + (u32)(1000 + p) / 2, 1000 + p); + dds16 =3D dds24 >> 8; + frac =3D dds24 & 0xff; + b1 =3D (u16)div_u64(dds16 * 72562ull + 50000, 100000); + b2 =3D (u16)((dds16 * 2 + 1) / 3); + + ret =3D bf_vendor_write(chip, BF_REQ_DDS, (u16)dds16, (frac << 8) | 0); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, b1, 0x0001); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, b2, 0x0002); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_DDS, 0x7cff, 0x0003); + if (ret < 0) + goto out; + /* Every quad must be followed by the clock keepalive. */ + ret =3D bf_vendor_write(chip, BF_REQ_KEEPALIVE, 0x0001, + BF_REG_KEEPALIVE_SETTINGS); + if (ret < 0) + goto out; + + chip->pitch =3D p; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_loopback_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int out =3D kctl->private_value; + + ucontrol->value.integer.value[0] =3D chip->loopback[out]; + ucontrol->value.integer.value[1] =3D 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 =3D 0; ch < BF_LOOPBACK_CHANNELS; ch++) { + u16 val =3D (on && (ch =3D=3D out * 2 || ch =3D=3D out * 2 + 1)) + ? 0x0001 : 0x0000; + + ret =3D 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 =3D snd_kcontrol_chip(kctl); + int out =3D kctl->private_value; + bool on =3D ucontrol->value.integer.value[0]; + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (on =3D=3D chip->loopback[out]) + goto out; + ret =3D bf_loopback_write_map(chip, out, on); + if (ret < 0) + goto out; + /* Single-active model (TotalMix writes one pair at 0x0001, the + * rest 0x0000): toggling one output clears the others. + */ + memset(chip->loopback, 0, sizeof(chip->loopback)); + chip->loopback[out] =3D on; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_an12_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->an12; + return 0; +} + +static int bf_an12_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + bool an12 =3D ucontrol->value.integer.value[0]; + u16 v; + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (an12 =3D=3D chip->an12) + goto out; + v =3D (chip->linked ? 0x0400 : 0x0000) | (an12 ? 0x1000 : 0x0000); + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000); + if (ret < 0) + goto out; + chip->an12 =3D an12; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_link_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->linked; + return 0; +} + +static int bf_link_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + bool linked =3D ucontrol->value.integer.value[0]; + u16 v; + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (linked =3D=3D chip->linked) + goto out; + v =3D (linked ? 0x0400 : 0x0000) | (chip->an12 ? 0x1000 : 0x0000); + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, v, 0x1000); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP_COMMIT, 0x0000, 0x0000); + if (ret < 0) + goto out; + chip->linked =3D linked; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_ms_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->ms_proc; + return 0; +} + +/* MS-proc: engage per the cap_ms2.pcap ON pattern - write 0x0000 to + * ALL FOUR AN2 (side) crosspoints: standard map 0x0035/0x004F (L/R) + * + low map 0x0001/0x001B (L/R) - the side path is muted (ear- + * verified 2026-08-26 with the mic on AN2: MS ON =3D silence); release + * restores the cached fader values (host-side, like TotalMix). + * (The 0x1000/0x0004 writes are the DISENGAGE restore values seen in + * cap_ms2 - the driver had them inverted on the engage path.) + */ +static int bf_ms_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + bool on =3D ucontrol->value.integer.value[0]; + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (on =3D=3D chip->ms_proc) + goto out; + if (on) { + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0035); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x004f); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x0001); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0000, 0x001b); + if (ret < 0) + goto out; + } else { + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->xpoint[1][1][0], 0x0001); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->xpoint[1][1][0], 0x0035); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->xpoint[1][1][1], 0x001b); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->xpoint[1][1][1], 0x004f); + if (ret < 0) + goto out; + } + chip->ms_proc =3D on; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +/* DIM - cap_dim2.pcap: an absolute -20 dB on the Phones master + * (out 1: 8-bit 0xCB / 16-bit 0x0333) regardless of the current level, + * plus the 0x17 wVal=3D0x2000 wIdx=3D0x2000 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 =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->dim; + return 0; +} + +static int bf_dim_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + bool on =3D ucontrol->value.integer.value[0]; + u16 flag; + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (on =3D=3D chip->dim) + goto out; + if (on) { + chip->dim_saved[0] =3D chip->master[1][0]; + chip->dim_saved[1] =3D chip->master[1][1]; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, 0xcb, + BF_REG_MASTER_8 + 2 * 1); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, 0xcb, + BF_REG_MASTER_8 + 2 * 1 + 1); + if (ret < 0) + goto out; + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333, + (BF_REG_MASTER_16 + 2 * 1) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, 0x0333, + (BF_REG_MASTER_16 + 2 * 1 + 1) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, 0x2000, 0x2000); + if (ret < 0) + goto out; + } else { + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, + bf_master_8bit(chip->dim_saved[0]), + BF_REG_MASTER_8 + 2 * 1); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, + bf_master_8bit(chip->dim_saved[1]), + BF_REG_MASTER_8 + 2 * 1 + 1); + if (ret < 0) + goto out; + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (chip->flag_cnt + 1) & 3; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->dim_saved[0], + (BF_REG_MASTER_16 + 2 * 1) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, + chip->dim_saved[1], + (BF_REG_MASTER_16 + 2 * 1 + 1) | flag); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_PREAMP, 0x0000, 0x2000); + if (ret < 0) + goto out; + } + chip->dim =3D on; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_width_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D -100; + uinfo->value.integer.max =3D 100; + uinfo->value.integer.step =3D 1; return 0; } =20 +static int bf_width_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->width; + return 0; +} + +static int bf_width_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int w =3D ucontrol->value.integer.value[0]; + u16 l, r; + int ret =3D 0; + + if (w < -100 || w > 100) + return -EINVAL; + + mutex_lock(&chip->mutex); + if (w =3D=3D chip->width) + goto out; + /* Width spread: L =3D 0x1000*(1+w), R =3D 0x1000*(1-w), L+R =3D 0x2000. + * TotalMix writes the strip's src pair on BOTH maps (cap_width3-7, + * PROTOCOL.md "Width strip mapping"): the low map (0x0000+src L / + * 0x001A+src R) and the std block-0 map (0x0034+src L / + * 0x004E+src R) - the stereo pair spreads L/R in opposition, the + * mirror src (AN2) gets the swapped values. + */ + l =3D (u16)(((0x2000 * (100 + w) / 2) + 50) / 100); + r =3D 0x2000 - l; + /* Low map: AN1 L=3D0x0000, R=3D0x001A; AN2 L=3D0x0001, R=3D0x001B. */ + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0000); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x001a); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0001); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x001b); + if (ret < 0) + goto out; + /* Std block-0 map (item 0b, the missing half): AN1 L=3D0x0034, + * R=3D0x004E; AN2 L=3D0x0035, R=3D0x004F. (The playback strips PB2-6 + * target block n-2 - 0x00AE family - reserved for the per-strip + * controls.) + */ + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x0034); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x004e); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, 0x0035); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, 0x004f); + if (ret < 0) + goto out; + chip->width =3D w; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +static int bf_fx_send_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D 0x1000; + uinfo->value.integer.step =3D 1; + return 0; +} + +static int bf_fx_send_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D 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 =3D snd_kcontrol_chip(kctl); + u16 v =3D ucontrol->value.integer.value[0]; + int ret =3D 0; + + if (v > 0x1000) + return -EINVAL; + + mutex_lock(&chip->mutex); + if (v =3D=3D chip->fx_send) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0138); + if (ret < 0) + goto out; + ret =3D bf_vendor_write(chip, BF_REQ_CROSSPOINT, v, 0x0153); + if (ret < 0) + goto out; + chip->fx_send =3D v; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + int babyface_create_flags(struct snd_usb_babyface *chip) { + struct snd_kcontrol *kctl; + int i, err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Varispeed Pitch", + .info =3D bf_pitch_info, + .get =3D bf_pitch_get, + .put =3D bf_pitch_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + for (i =3D 0; i < 6; i++) { + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Loopback Switch", + .index =3D i, + .info =3D bf_mute_info, + .get =3D bf_loopback_get, + .put =3D bf_loopback_put, + .private_value =3D i, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + } + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "AN 1>2 Switch", + .info =3D bf_switch_info, + .get =3D bf_an12_get, + .put =3D bf_an12_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "AN1/2 Link Switch", + .info =3D bf_switch_info, + .get =3D bf_link_get, + .put =3D bf_link_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "MS Processor Switch", + .info =3D bf_switch_info, + .get =3D bf_ms_get, + .put =3D bf_ms_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Dim Switch", + .info =3D bf_switch_info, + .get =3D bf_dim_get, + .put =3D bf_dim_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Width", + .info =3D bf_width_info, + .get =3D bf_width_get, + .put =3D bf_width_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "FX Send Volume", + .info =3D bf_fx_send_info, + .get =3D bf_fx_send_get, + .put =3D bf_fx_send_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + return 0; +} + +static int bf_bool_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_BOOLEAN; + uinfo->count =3D 1; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D 1; return 0; } =20 -int babyface_create_panel(struct snd_usb_babyface *chip) +static int bf_phantom_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) { + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D + !!(chip->preamp & kctl->private_value); return 0; } =20 -int babyface_create_eq(struct snd_usb_babyface *chip) +static int bf_phantom_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + u16 bit =3D kctl->private_value; + bool on =3D ucontrol->value.integer.value[0]; + bool cur =3D !!(chip->preamp & bit); + int ret =3D 0; + + mutex_lock(&chip->mutex); + if (on =3D=3D cur) + goto out; + chip->preamp =3D on ? (chip->preamp | bit) : (chip->preamp & ~bit); + ret =3D bf_preamp_state_write(chip); + if (ret < 0) + goto out; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +/* Gain scales: the mic preamps (AN1/2) span 0-65 dB over raw 0-20 + * (3.25 dB/step); the Hi-Z instrument inputs (AN3/4) are digitally + * limited to 9 dB over raw 0-18 (0.5 dB/step) - manual sec. 10, raw + * ranges verified from cap_gain12/cap_gain34.pcap. Shared by the GUI + * controls and the front-panel gain wheel. + */ +int bf_gain_max_db(int mic) +{ + return mic < 2 ? BF_GAIN_MAX_DB : 9; +} + +int bf_gain_db(int mic, u8 raw) +{ + return mic < 2 ? (raw * 13) / 4 : raw / 2; +} + +u8 bf_gain_raw(int mic, int db) +{ + return mic < 2 ? (db * 8 + 13) / 26 : db * 2; +} + +static int bf_gain_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D bf_gain_max_db(kctl->private_value); + uinfo->value.integer.step =3D 1; + return 0; +} + +static int bf_gain_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + int mic =3D kctl->private_value; + + /* chip->gain[] tracks the dB (the raw is derived at write time - + * the 3.25 dB/step mic grid would otherwise make a +/-1 dB wheel + * stick on a raw boundary). + */ + ucontrol->value.integer.value[0] =3D 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 =3D snd_kcontrol_chip(kctl); + int mic =3D kctl->private_value; + int db =3D ucontrol->value.integer.value[0]; + u8 raw, counter; + int ret =3D 0; + + if (db < 0 || db > bf_gain_max_db(mic)) + return -EINVAL; + + mutex_lock(&chip->mutex); + if (db =3D=3D chip->gain[mic]) + goto out; + raw =3D bf_gain_raw(mic, db); + counter =3D (chip->gain_cycle % 3 =3D=3D 0) ? 0x20 : + (chip->gain_cycle % 3 =3D=3D 1) ? 0x00 : 0x40; + chip->gain_cycle =3D (chip->gain_cycle + 1) % 3; + + ret =3D bf_vendor_write(chip, BF_REQ_GAIN, + (u16)((raw & 0x1f) | counter), + BF_REG_GAIN + mic); + if (ret < 0) + goto out; + chip->gain[mic] =3D db; + ret =3D 1; +out: + mutex_unlock(&chip->mutex); + return ret; +} + +int babyface_create_controls(struct snd_usb_babyface *chip) +{ + static const char * const out_names[6] =3D { + "AN1/2", "PH3/4", "AS1/2", "ADAT3/4", "ADAT5/6", "ADAT7/8" + }; + struct snd_kcontrol *kctl; + int i, err; + + for (i =3D 0; i < 6; i++) { + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D out_names[i], + .index =3D i, + .access =3D SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info =3D bf_master_info, + .get =3D bf_master_get, + .put =3D bf_master_put, + .tlv.p =3D bf_master_tlv, + .private_value =3D i, + }, chip); + strlcat(kctl->id.name, " Playback Volume", sizeof(kctl->id.name)); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D out_names[i], + .index =3D i, + .info =3D bf_mute_info, + .get =3D bf_mute_get, + .put =3D bf_mute_put, + .private_value =3D i, + }, chip); + strlcat(kctl->id.name, " Playback Switch", sizeof(kctl->id.name)); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + + dev_dbg(&chip->dev->dev, "output %d =3D %s\n", i, out_names[i]); + } + + for (i =3D 0; i < 2; i++) { + u16 bit =3D i =3D=3D 0 ? BF_PREAMP_48V_MIC1 : BF_PREAMP_48V_MIC2; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Phantom Power Mic 1", + .index =3D i, + .info =3D bf_bool_info, + .get =3D bf_phantom_get, + .put =3D bf_phantom_put, + .private_value =3D bit, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + } + + for (i =3D 0; i < 2; i++) { + u16 bit =3D i =3D=3D 0 ? BF_PREAMP_PAD_MIC1 : BF_PREAMP_PAD_MIC2; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Pad Mic 1", + .index =3D i, + .info =3D bf_bool_info, + .get =3D bf_phantom_get, + .put =3D bf_phantom_put, + .private_value =3D bit, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + } + + for (i =3D 0; i < 4; i++) { + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Mic 1 Capture Volume", + .index =3D i, + .info =3D bf_gain_info, + .get =3D bf_gain_get, + .put =3D bf_gain_put, + .private_value =3D i, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + } + return 0; +} + + +/* =E2=94=80=E2=94=80 front-panel stubs =E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80 + * Replaced by the panel patch. The core driver calls these from + * probe()/disconnect()/suspend()/resume() and the delayed-work init. + */ +int babyface_create_panel(struct snd_usb_babyface *chip) { return 0; } @@ -117,11 +1365,15 @@ void babyface_panel_work(struct work_struct *work) { } =20 -void bf_eq_reupload(struct snd_usb_babyface *chip) +/* =E2=94=80=E2=94=80 DSP-EQ stubs =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80 + * Replaced by the eq patch. The core driver calls these from + * probe() and hw_params() (fs-dependent re-upload). + */ +int babyface_create_eq(struct snd_usb_babyface *chip) { + return 0; } =20 -u8 bf_gain_raw(int mic, int db) +void bf_eq_reupload(struct snd_usb_babyface *chip) { - return 0; } --=20 2.55.0 From nobody Sat Sep 26 10:02:09 2026 Received: from mail-wm1-f48.google.com (mail-wm1-f48.google.com [209.85.128.48]) (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 A134C389111 for ; Wed, 2 Sep 2026 10:11:36 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.48 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; cv=none; b=FhgnGt/e0RcEMBcywuNOA1tZEBQvbQqs7tumJGJbUu76XeJkbnf5vVpnreO2D+Ozqx99Tq7BBxU09F3mvf/fzULB9XpNaiau8XLrv53LK5d+O26yEmhrYSJwtlVD5+vcb9sfC5mUovmLe4rqgMpXibD1G6bu/XEYz9GHLtlIHpM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; c=relaxed/simple; bh=moQ3XffSn5MFQ1kYkfgAeSPxHmZzlPmCaYUBtJwRxPc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=okWX6173MDcnpWjl75fXAjOtRjLTUF+30+dEYzSSP8wD5UvGQn+Pr6bo3S1RaL2ItHuBzmFmWxEl7qJtDx8B0xb1HEQMNade1p/pGGcNFmGYqXUxmQXv7BvmVqhMxpJCaf5vMQXKzSJdM5+ZojHqpQC/JOwywsrWYLf9uKaXgdY= 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=dK6ZcIu4; arc=none smtp.client-ip=209.85.128.48 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="dK6ZcIu4" Received: by mail-wm1-f48.google.com with SMTP id 5b1f17b1804b1-49b0d8bc2aaso7856395e9.0 for ; Wed, 02 Sep 2026 03:11:36 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788343894; x=1788948694; 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=Od45HnMgkq5G9N1ottRDVDY4K6zXxNVVciKF2j3QFiI=; b=dK6ZcIu4oK+x3iv5Y7gzVdnOZ5rwNWh9rbI9Vk88Z8FeQNhxVwjdkGoV8K7NejNHLs gjuldxOF2HgzIpLG0W0FI2dCipsWhTysWYNjwuzj3/roN0Utx1w5c1ff5T7q5TgLf0YN eD8pJBF2is0BitEkwHUEzU2WZuITJduVZr/7rq28YZqr24PP91yRmZyM2w8z0DKqhiYg lAI7/wRUnNDYD3p9AX5j+zHhWZM2r22AZ1rlqrDccZkTDWn91NDK7LWqdnWO4EQuFmqV jIM21Ax7mROX3t9jxvSp4jdciNnNVxvPF97uWjIJo9qF6KxnYIcLoEcGpYfVzEYfAIoD /m8g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788343894; x=1788948694; 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=Od45HnMgkq5G9N1ottRDVDY4K6zXxNVVciKF2j3QFiI=; b=LQP/CJ67m6VQ1cGatUZFA80iqqLtGJrxkYFWTnaXTw4geJ6F8+GhahDnSXYe1YEMN6 uVHSy1bw6L03mSQlYKF0RrdXX6Lzg2xd0Z+Z0AgvWbuxBBQ40fIEmWk75ubRESMxna0c c8iL6Zm70wK4gixfE3Z9AMTZzyOxdUCMXKpeAGGGLLZNdy4wLXGl9anpidgiZpj/KQnH OKeOVC5EtmBOJbf7lfFNJ9VW5/wBscQsGIL1Ilo6OWfCQ+RpXkY15Emw8NdJ1tTRwlYG ExOl0shKy0YYp7d6Em5guEPPNndcl+yYoWZfKaBlQ6r5gc2p0Q5sI/5JhXKbxb4uyJSy 7tuA== X-Forwarded-Encrypted: i=1; AHgh+Rqa65kRZwJtr9ftKlJmaRulh1A5EJFXnsjpNO+BPcAKXm2zNrEfVa9Akt1/u9DEROKnuAULuDTmWIzMnrU=@vger.kernel.org X-Gm-Message-State: AFuF++kSOR1LvHz8Xv5iZyH2/1kybE0CEBLjmlDR6wKgAIz2qA0DBdE3 E5tdicJv9cVzmuGrT7a2LfL08DinJEglHLhUsXoGjbkONuxeu5+HDLiK X-Gm-Gg: AR+sD12Yw/skLArn48vAF5KBHyG4z7UQN65QVwYRU/Rclf+3NZ3V1hnrxCw3enltfUa F/F2GLpd31GfuPjKBPKOfvUTmIdMTc+0nLZGvaQdcqsuNFkuuRLcyXGikKFD7TNcgZHp48q3Syi 3DWY3dWr9lRxEXJWaGtI8UqnRLelFUt2zShNOp4ixRJRxshdY3o+UvqHNh69ZaudUgBmo6ipG3G cVZoWlJ1cS1EE9cQX1lDEgO5NrCsaVejZ0Ze/qNSUyHv2K2v+2OJAIFwkaPrrTgnG4DeVNFZzY+ RA3+Cs0IWLIJuzyh45twUQb81zemXy+cMbuNErR9RB2PfnhgSOJyMfK/9Xs60/jBH6EKh79sm/Z E+IIv8eshyxi4SDhbpmyip5b1JyyC36W9jTZ/9kLnRdd3asBhD6MRtOMrYBeFMaOxOc19bQl6eK hhjj6iKbNoFruAJeHyyYNQEVHKNBfW5j5+4ViyZ10KsFLqDrtKSDheWrBHqkgj6vvfb6XHmdiYT G2o0+9kS1zMdq8f X-Received: by 2002:a05:600c:4e44:b0:49c:dcf8:5216 with SMTP id 5b1f17b1804b1-49ce57e7f55mr67498845e9.6.1788343893717; Wed, 02 Sep 2026 03:11:33 -0700 (PDT) Received: from cachyos ([196.77.120.180]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49ce47885b1sm57294265e9.14.2026.09.02.03.11.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 02 Sep 2026 03:11:33 -0700 (PDT) From: =?UTF-8?q?Isma=C3=AFl=20Bahloul?= To: linux-sound@vger.kernel.org Cc: linux-usb@vger.kernel.org, alsa-devel@alsa-project.org, perex@perex.cz, tiwai@suse.com, linux-kernel@vger.kernel.org, =?UTF-8?q?Isma=C3=AFl=20Bahloul?= Subject: [RFC PATCH v3 3/4] ALSA: usb: babyface-pro: add the front-panel poll + controls Date: Wed, 2 Sep 2026 11:11:16 +0100 Message-ID: <20260902101117.11671-4-i.bahloul01@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260902101117.11671-1-i.bahloul01@gmail.com> References: <20260902101117.11671-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: quoted-printable Emulate the Babyface Pro FS front panel (the host is in the loop like TotalMix): the 50 Hz 0x17 readback poll drives read-only button/wheel/ IN/OUT/MIX/DIM controls, and the wheel is translated into master fader, gain, MIX-monitoring and balance edits on the corresponding mixer controls. The DSP EQ remains stubbed here; it lands in the final patch. Signed-off-by: Isma=C3=AFl Bahloul Assisted-by: DeepSeek V4 Flash --- sound/usb/babyfacepro/babyfacepro-ctl.c | 926 +++++++++++++++++++++++- 1 file changed, 920 insertions(+), 6 deletions(-) diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepr= o/babyfacepro-ctl.c index 168bcb35b..9422a73d3 100644 --- a/sound/usb/babyfacepro/babyfacepro-ctl.c +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c @@ -1344,27 +1344,941 @@ int babyface_create_controls(struct snd_usb_babyfa= ce *chip) } =20 =20 -/* =E2=94=80=E2=94=80 front-panel stubs =E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80 - * Replaced by the panel patch. The core driver calls these from - * probe()/disconnect()/suspend()/resume() and the delayed-work init. +/* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */ +enum { + BF_PANEL_KCTL_BUTTON, + BF_PANEL_KCTL_WHEEL, + BF_PANEL_KCTL_IN, + BF_PANEL_KCTL_OUT, + BF_PANEL_KCTL_MIX, + BF_PANEL_KCTL_DIM, + BF_PANEL_KCTL_SELECT, + BF_PANEL_KCTL_NUM, +}; + +static const char *const bf_panel_in_texts[] =3D { + "Unknown", "Ch 1/2", "Ch 3/4", "Opt", NULL +}; + +static const char *const bf_panel_out_texts[] =3D { + "Unknown", "Ch 1/2", "Phones", "Opt", NULL +}; + +static const char *const bf_panel_select_texts[] =3D { + "Left", "Right", "Both", "None", NULL +}; + +/* byte3 button flash -> event code (0 =3D none). The idle byte3 is 0x40; + * a press flashes the value below the base for one or two poll frames. */ -int babyface_create_panel(struct snd_usb_babyface *chip) +static int bf_panel_button_decode(u8 flash) { - return 0; + 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 =3D IN position 4/5/6 -> enum index (0 =3D not in rang= e). */ +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 =3D OUT position. Two encodings seen in captures: the + * gain-display mode 0x04/0x05/0x06 (cap_dim.pcap, cap_buttons2.pcap) + * and the base mode 0x01/0x02/0x00 (cap_buttons.pcap; 0x01 is also the + * idle byte1 of cap_padpan.pcap and the live device). Accept both; + * 0x00 is ambiguous (could be Opt or no selection) so keep previous. + */ +static int bf_panel_out_decode(u8 v) +{ + switch (v) { + case BF_PANEL_OUT_CH12: return 1; + case BF_PANEL_OUT_PHONES: return 2; + case BF_PANEL_OUT_OPT: return 3; + case 0x01: return 1; /* base-mode Ch 1/2 */ + case 0x02: return 2; /* base-mode Phones */ + default: return 0; + } +} + +/* -- MIX-mode monitoring level (fader curve) ---------------- + * Calibrated crosspoint-fader curve (AN1->AN1/2, cap_calib.pcap + * 2026-08-22; the same table as tuxmix-core/src/usb.rs FADER_CURVE). + * dB stored x2 (half-dB grid): the MIX wheel steps +/-0.5 dB per click + * on this curve (cap_mix.pcap). 0x0000 =3D -inf (digital mute), + * 0x0003 =3D -62 dB, ... 0x2D41 =3D +6 dB. Raw values interpolate linear= ly + * between the 1-dB points. + */ +#define BF_FADER_DB2_INF (-130) /* -65 dB =3D the wheel's -inf floor */ + +static const struct bf_fader_pt { + s16 db2; /* dB x 2 */ + u16 raw; +} bf_fader_curve[] =3D { + { -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 =3D -inf). */ +static int bf_fader_raw_to_db2(u16 raw) +{ + int i; + + if (raw =3D=3D 0 || raw < bf_fader_curve[0].raw) + return BF_FADER_DB2_INF; + for (i =3D 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) { + if (raw <=3D bf_fader_curve[i + 1].raw) { + u32 num =3D (u32)(raw - bf_fader_curve[i].raw) * + (u32)(bf_fader_curve[i + 1].db2 - bf_fader_curve[i].db2); + u32 den =3D 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 =3D mute 0). */ +static u16 bf_fader_db2_to_raw(int db2) +{ + int i; + + if (db2 <=3D bf_fader_curve[0].db2) + return db2 < bf_fader_curve[0].db2 ? 0 : bf_fader_curve[0].raw; + for (i =3D 0; i < ARRAY_SIZE(bf_fader_curve) - 1; i++) { + if (db2 <=3D bf_fader_curve[i + 1].db2) { + u32 num =3D (u32)(db2 - bf_fader_curve[i].db2) * + (u32)(bf_fader_curve[i + 1].raw - bf_fader_curve[i].raw); + u32 den =3D 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; +} + +/* 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[] =3D { + { -124, 0 }, { -108, 1 }, { -96, 2 }, { -85, 3 }, + { -70, 4 }, { -57, 5 }, { -15, 10 }, { -13, 11 }, + { -9, 12 }, + }; + int i; + + if (db2 <=3D pts[0].db2) + return 0; + for (i =3D 0; i < ARRAY_SIZE(pts) - 1; i++) { + if (db2 <=3D pts[i + 1].db2) { + u32 num =3D (u32)(db2 - pts[i].db2) * + (u32)(pts[i + 1].disp - pts[i].disp); + u32 den =3D pts[i + 1].db2 - pts[i].db2; + + return pts[i].disp + (int)((num + den / 2) / den); + } + } + /* Above -4.6 dB: keep the last slope (2 dB/step) up to +6 dB. */ + return pts[ARRAY_SIZE(pts) - 1].disp + + clamp((db2 - pts[ARRAY_SIZE(pts) - 1].db2) / 4, 0, 12); +} + +/* The kernel driver plays the TotalMix role for the MIX button (the + * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs + * + usb.rs): one wheel click in fader mode =3D +/-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 =3D Ch1/2, + * 2 =3D Phones, 3 =3D Opt): AN1/2, PH3/4, ADAT7/8 (the optical + * output) respectively. + */ + int out =3D chip->panel_out =3D=3D 3 ? 5 : + chip->panel_out =3D=3D 2 ? 1 : 0; + unsigned int blk =3D bf_xpoint_block[out]; + u8 targets[2]; + int n =3D 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 =3D nothing selected =3D no-op wheel). + * Source indices: AN1/AN2 =3D 0/1, AN3/AN4 =3D 2/3, AS1/2 =3D 4. + */ + if (chip->panel_in =3D=3D 3) { + targets[0] =3D 4; /* Opt: the AS1/2 pair */ + n =3D 1; + } else if (chip->panel_select !=3D 3) { + int base =3D chip->panel_in =3D=3D 2 ? 2 : 0; + + targets[0] =3D base + (chip->panel_select =3D=3D 1 ? 1 : 0); + n =3D 1; + if (chip->panel_select =3D=3D 2) + targets[n++] =3D base + 1; + } + + mutex_lock(&chip->mutex); + db2 =3D bf_fader_raw_to_db2(chip->panel_mix_raw); + db2 =3D clamp(db2 + delta, BF_FADER_DB2_INF, 12); + raw =3D bf_fader_db2_to_raw(db2); + chip->panel_mix_raw =3D raw; + for (i =3D 0; i < n; i++) { + const struct bf_source *s =3D &bf_sources[targets[i]]; + + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (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] =3D raw; + chip->xpoint[out][targets[i]][1] =3D raw; + /* MIX-mode VU display shadow (0x1A 0x000A+mic): TotalMix + * mirrors the monitoring level into the panel display family + * (cap_mix/cap_panel.pcap) - the input VU segments follow it. + * Written only on change (the captures show TotalMix updating + * it on segment crossings). Law =3D bf_mix_display (TODO 0g + * pending the exact full-range capture). + */ + if (targets[i] < 4) { + int disp =3D bf_mix_display(db2); + + if (disp !=3D 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]] =3D disp; + } + } + } + mutex_unlock(&chip->mutex); +} + +/* Write an output's L/R masters (8-bit companions + 16-bit with the + * transaction flag) and mirror into the cache - shared by the OUT + * volume wheel and the balance wheel. Caller holds the mutex. + */ +static void bf_panel_write_master(struct snd_usb_babyface *chip, int out, + u16 l, u16 r) +{ + u16 flag; + + flag =3D bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt =3D (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] =3D l; + chip->master[out][1] =3D r; + chip->muted[out] =3D false; + /* A Phones change while DIM is engaged re-bases the restore. */ + if (chip->dim && out =3D=3D 1) { + chip->dim_saved[0] =3D l; + chip->dim_saved[1] =3D r; + } +} + +/* OUT-mode wheel: the master fader of the OUT-selected output, +/-0.5 dB + * per click (cap_set2/cap_dim.pcap: the wheel writes the 16-bit master + * 0x03E0+2*out on the master curve 0x2000*2^(dB/6); the driver keeps + * the 8-bit companion in sync like bf_master_put - the 8-bit is the + * real volume). BOTH sides move by the same dB so an existing + * balance (hold-SELECT) is preserved. Same output mapping as the MIX + * wheel (Phones =3D canon 1, Opt =3D ADAT7/8 =3D canon 5, else AN1/2). + */ +static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta) +{ + int out =3D chip->panel_out =3D=3D 3 ? 5 : + chip->panel_out =3D=3D 2 ? 1 : 0; + int hl, hr; + u16 l, r; + + mutex_lock(&chip->mutex); + hl =3D bf_master_half_db(chip->master[out][0]) + delta; + hr =3D bf_master_half_db(chip->master[out][1]) + delta; + l =3D bf_master_16bit(clamp(hl, -128, 12)); + r =3D bf_master_16bit(clamp(hr, -128, 12)); + bf_panel_write_master(chip, out, l, r); + 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 =3D no target. + */ +static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta) +{ + u8 mics[2]; + int n =3D 0; + int i; + + if (chip->panel_in =3D=3D 3 || chip->panel_select =3D=3D 3) + return; + { + int base =3D chip->panel_in =3D=3D 2 ? 2 : 0; + + mics[0] =3D base + (chip->panel_select =3D=3D 1 ? 1 : 0); + n =3D 1; + if (chip->panel_select =3D=3D 2) + mics[n++] =3D base + 1; + } + + mutex_lock(&chip->mutex); + for (i =3D 0; i < n; i++) { + int mic =3D mics[i]; + int db =3D clamp((int)chip->gain[mic] + delta, + 0, bf_gain_max_db(mic)); + u8 raw =3D bf_gain_raw(mic, db); + + bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic); + chip->gain[mic] =3D db; + } + mutex_unlock(&chip->mutex); +} + +/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output + * Balance"): moves the stereo image of the OUT-selected output by + * attenuating ONE side, linear in raw (cap_pan_stereo.pcap: the varied + * side =3D 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 delt= a) +{ + int out =3D chip->panel_out =3D=3D 3 ? 5 : + chip->panel_out =3D=3D 2 ? 1 : 0; + u16 l, r; + int bal; /* -100..+100; + =3D image right (left varies) */ + u16 fixed, varied; + + mutex_lock(&chip->mutex); + /* Read under the lock so the L/R pair is consistent with the + * master/mute/dim writers (they update chip->master[] under the + * same mutex). + */ + l =3D chip->master[out][0]; + r =3D chip->master[out][1]; + /* Balance from the L/R ratio: the louder side is the fixed one. */ + if (l >=3D r) { + bal =3D r ? -(100 - (100 * r) / l) : -100; + fixed =3D l; + } else { + bal =3D l ? (100 - (100 * l) / r) : 100; + fixed =3D r; + } + bal =3D clamp(bal + delta * 2, -100, 100); + varied =3D (u16)((u32)fixed * (100 - abs(bal)) / 100); + l =3D bal >=3D 0 ? varied : fixed; + r =3D bal >=3D 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 =3D no target. + */ +static void bf_panel_set_phantom(struct snd_usb_babyface *chip) +{ + u16 bits =3D 0; + int m; + + if (chip->panel_mix || chip->panel_in !=3D 1 || + chip->panel_select =3D=3D 3) + return; + if (chip->panel_select !=3D 1) + bits |=3D BF_PREAMP_48V_MIC1; + if (chip->panel_select !=3D 0) + bits |=3D BF_PREAMP_48V_MIC2; + + mutex_lock(&chip->mutex); + /* One channel selected: toggle it. Both selected: ALIGN both to + * the same state, so repeated SET presses cycle all-on <-> all-off + * (a mixed phantom state cannot persist with both selected). + */ + if (chip->panel_select =3D=3D 2) { + if ((chip->preamp & bits) =3D=3D bits) + chip->preamp &=3D ~bits; + else + chip->preamp |=3D bits; + } else { + chip->preamp ^=3D bits; + } + bf_preamp_state_write(chip); + for (m =3D 0; m < 4; m++) + chip->panel_mix_disp[m] =3D 0; + mutex_unlock(&chip->mutex); +} + +static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl) +{ + if (chip->panel_kctl[ctl]) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &chip->panel_kctl[ctl]->id); +} + +/* One 0x17 read + decode. Called from the poll work; no locking needed - + * the worker is the only writer and the control get callbacks run under + * the ALSA controls lock (chip->panel_button/wheel are consumed there). + */ +static void bf_panel_tick(struct snd_usb_babyface *chip) +{ + u8 st[4]; + int delta, in, out; + bool dim; + u8 cls, pcls; + int btn; + bool mix_flash, fader_now; + + if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0) + return; /* device gone / busy - retry next tick */ + + if (!chip->panel_seen) { + chip->panel_seen =3D true; + memcpy(chip->panel_prev, st, sizeof(st)); + /* Seed the state controls from the first snapshot. */ + in =3D bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7); + if (in) + chip->panel_in =3D in; + out =3D bf_panel_out_decode(st[1] & 0x07); + if (out) + chip->panel_out =3D out; + chip->panel_mix =3D !!(st[0] & 0x80); + chip->panel_saw_fader =3D (st[2] >> 4) =3D=3D 0x0; + chip->panel_dim =3D !!(st[1] & 0x20); + return; + } + + /* The udev alsactl restore (~100 ms after probe) clobbers the host + * SELECT with a stale stored value (the control is VOLATILE but + * this alsactl stores/restores it anyway) - re-assert the device's + * power-on state (nothing selected, cycle ARMED) for the first + * ~3 s so the boot always starts in sync. + */ + if (time_is_after_jiffies(chip->panel_start + 3 * HZ)) + chip->panel_select =3D 3; + + /* Button flash (byte3 over the 0x40 idle base). */ + btn =3D bf_panel_button_decode(st[3]); + if (btn) + chip->panel_button =3D btn; + + /* Wheel: signed 4-bit wrap delta of the byte2 low nibble - only + * while the mode class is unchanged. A mode switch (IN 0x4x -> + * fader 0x0x on a MIX press, or the OUT counter carrying 0x8F -> + * 0x90 - the OUT counter is a full byte, cap_set2.pcap) must not + * be read as a wheel jump. Class: 0 =3D fader (0x0x), 1 =3D OUT + * (0x8x/0x9x), 2 =3D IN (0x4x/0x5x/0x6x). + */ + cls =3D (st[2] >> 4) =3D=3D 0x8 || (st[2] >> 4) =3D=3D 0x9 ? 1 : + (st[2] >> 4) =3D=3D 0x0 ? 0 : 2; + pcls =3D (chip->panel_prev[2] >> 4) =3D=3D 0x8 || + (chip->panel_prev[2] >> 4) =3D=3D 0x9 ? 1 : + (chip->panel_prev[2] >> 4) =3D=3D 0x0 ? 0 : 2; + delta =3D (int)(st[2] & 0x0f) - (int)(chip->panel_prev[2] & 0x0f); + if (delta > 8) + delta -=3D 16; + else if (delta < -8) + delta +=3D 16; + if (delta && cls =3D=3D pcls) { + chip->panel_wheel =3D clamp(chip->panel_wheel + delta, + SHRT_MIN, SHRT_MAX); + bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL); + /* Wheel by mode (LINUX-VALIDATION sec. 12, the TotalMix + * emulator): MIX -> monitoring level, OUT (0x8x/0x9x) -> the + * selected output master (or its balance while SELECT is + * held), IN (0x4x/0x5x/0x6x) -> the SELECT-chosen preamp + * gain. + */ + if (chip->panel_mix) + bf_panel_mix_wheel(chip, delta); + else if (chip->panel_sel_hold >=3D 10 && cls =3D=3D 1) + bf_panel_balance_wheel(chip, delta); + else if (cls =3D=3D 1) + bf_panel_out_wheel(chip, delta); + else if (cls =3D=3D 2) + bf_panel_gain_wheel(chip, delta); + } + + /* Selections - keep the previous when the field is not in range + * (the fader-mode readback drops the IN position bits). + */ + in =3D bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7); + if (in && in !=3D chip->panel_in) { + chip->panel_in =3D in; + /* The card CLEARS its L/R/both selection on an IN pair + * switch (user-verified 2026-08-27): re-sync the host- + * tracked SELECT so SET / the wheel / MIX target nothing + * until the user picks a channel again. This is the main + * anti-desync hook (the physical state is not readable). + */ + if (chip->panel_select !=3D 3) { + chip->panel_select =3D 3; + bf_panel_notify(chip, BF_PANEL_KCTL_SELECT); + } + /* An IN-pair switch disarms the device's SELECT cycle: the + * next press only re-arms it (no step), the one after that + * cycles (device behavior, user-verified 2026-08-28). + */ + chip->panel_select_armed =3D false; + bf_panel_notify(chip, BF_PANEL_KCTL_IN); + } + out =3D bf_panel_out_decode(st[1] & 0x07); + if (out && out !=3D chip->panel_out) { + chip->panel_out =3D 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] =3D=3D BF_PANEL_FLASH_SELECT && + chip->panel_prev[3] !=3D BF_PANEL_FLASH_SELECT) { + if (!chip->panel_select_armed) { + /* Disarmed (IN switch since the last step): the press + * only re-arms the cycle - the device steps on the + * NEXT press (user-verified 2026-08-28). + */ + chip->panel_select_armed =3D true; + } else { + chip->panel_select =3D (chip->panel_select + 1) & 3; + } + bf_panel_notify(chip, BF_PANEL_KCTL_SELECT); + } + /* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output + * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz + * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and + * byte0 does NOT gain the 0x80 engaged bit - so the duration is + * the only discriminator: >=3D 10 ticks (200 ms at 50 Hz) =3D held. + */ + if (st[3] =3D=3D BF_PANEL_FLASH_SELECT) + chip->panel_sel_hold++; + else + chip->panel_sel_hold =3D 0; + + /* SET (A) press: host-side 48V phantom toggle on the + * SELECT-chosen mic(s) (see bf_panel_set_phantom). + */ + if (st[3] =3D=3D BF_PANEL_FLASH_SET && + chip->panel_prev[3] !=3D BF_PANEL_FLASH_SET) + bf_panel_set_phantom(chip); + + /* MIX (fader mode) - HOST-latched, like TotalMix (cap_mix.pcap, + * cap_select2.pcap): the raw press readback is `0D 0D 41 44` - + * byte3 flash 0x44, NO engaged bit, byte2 still in the current + * mode. The host acks the flash with `0x17 0x8480 0x8C80` -> the + * device latches fader mode (byte0/1 gain the 0x80 bit, byte2 =3D + * 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 =3D st[3] =3D=3D BF_PANEL_FLASH_MIX && + chip->panel_prev[3] !=3D BF_PANEL_FLASH_MIX; + fader_now =3D (st[2] >> 4) =3D=3D 0x0; + + if (mix_flash) { + if (chip->panel_mix) { + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000); + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080); + chip->panel_mix =3D false; + chip->panel_saw_fader =3D false; + } else { + int ref, out; + int m; + + bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80); + chip->panel_mix =3D true; + /* Seed the monitoring level at the reference + * crosspoint's current value so the first wheel + * click doesn't jump from -inf (the reference =3D + * the first SELECT-chosen channel of the IN pair; + * Opt =3D the AS1/2 pair). + */ + out =3D chip->panel_out =3D=3D 3 ? 5 : + chip->panel_out =3D=3D 2 ? 1 : 0; + ref =3D chip->panel_in =3D=3D 3 ? 4 : + (chip->panel_in =3D=3D 2 ? 2 : 0) + + (chip->panel_select =3D=3D 1 ? 1 : 0); + chip->panel_mix_raw =3D 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 =3D 0; m < 4; m++) + chip->panel_mix_disp[m] =3D 0; + if (ref < 4) { + int db2 =3D bf_fader_raw_to_db2(chip->panel_mix_raw); + int disp =3D bf_mix_display(db2); + + bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp, + BF_REG_PANEL_GAIN + ref); + chip->panel_mix_disp[ref] =3D disp; + } + } + bf_panel_notify(chip, BF_PANEL_KCTL_MIX); + } + if (fader_now) { + chip->panel_saw_fader =3D true; + } else if (chip->panel_mix && chip->panel_saw_fader && + st[3] !=3D BF_PANEL_FLASH_MIX) { + /* device left fader mode by itself (IN/OUT/SET press) */ + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000); + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080); + chip->panel_mix =3D false; + chip->panel_saw_fader =3D false; + bf_panel_notify(chip, BF_PANEL_KCTL_MIX); + } + + dim =3D !!(st[1] & 0x20); + if (dim !=3D chip->panel_dim) { + chip->panel_dim =3D 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 =3D container_of(work, + struct snd_usb_babyface, panel_work.work); + + if (chip->shutdown) + return; + bf_panel_tick(chip); + schedule_delayed_work(&chip->panel_work, + msecs_to_jiffies(chip->panel_poll_ms)); } =20 void babyface_panel_start(struct snd_usb_babyface *chip) { + chip->panel_seen =3D false; + /* The device boots with NOTHING selected (the SELECT cycle starts + * at none -> AN1 -> AN2 -> both -> none) - the unreadable selection + * must start there too, or every later SET is off by one channel + * (host at AN1 while the LEDs show nothing -> first SELECT makes + * the device blink AN1 but the host believes AN2). + */ + chip->panel_select =3D 3; /* none */ + chip->panel_select_armed =3D true; + chip->panel_start =3D jiffies; + schedule_delayed_work(&chip->panel_work, 0); } =20 void babyface_panel_stop(struct snd_usb_babyface *chip) { + cancel_delayed_work_sync(&chip->panel_work); } =20 -void babyface_panel_work(struct work_struct *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 =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D 0; + uinfo->value.integer.max =3D BF_PANEL_BTN_DIM; + uinfo->value.integer.step =3D 1; + return 0; +} + +static int bf_panel_button_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D chip->panel_button; + return 0; } =20 +static int bf_panel_wheel_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D SHRT_MIN; + uinfo->value.integer.max =3D SHRT_MAX; + uinfo->value.integer.step =3D 1; + return 0; +} + +static int bf_panel_wheel_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D 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 =3D snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] =3D 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 =3D snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] =3D chip->panel_out; + return 0; +} + +static int bf_panel_select_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_select_texts); +} + +static int bf_panel_select_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] =3D chip->panel_select; + return 0; +} + +/* Writable so software (or the user, after a driver reload) can + * re-sync the host-tracked SELECT state to the physical card - the + * L/R/both/none state is NOT in the 0x17 readback, so a reload starts + * at "Left" while the card may sit at any position; a desync makes + * SET / the wheel / MIX target the wrong channel. Writing the + * physical state re-aligns the emulation (TotalMix parity: it also + * lets software select channels directly). + */ +static int bf_panel_select_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + unsigned int v =3D ucontrol->value.enumerated.item[0]; + int ret =3D 0; + + if (v > 3) + return -EINVAL; + if (v !=3D chip->panel_select) { + chip->panel_select =3D v; + bf_panel_notify(chip, BF_PANEL_KCTL_SELECT); + ret =3D 1; + } + return ret; +} + +/* Shared boolean get - private_value selects mix (0) / dim (1). */ +static int bf_panel_bool_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] =3D + kctl->private_value ? chip->panel_dim : chip->panel_mix; + return 0; +} + +int babyface_create_panel(struct snd_usb_babyface *chip) +{ + struct snd_kcontrol *kctl; + int err; + + memset(chip->panel_kctl, 0, sizeof(chip->panel_kctl)); + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel Button", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D bf_panel_button_info, + .get =3D bf_panel_button_get, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_BUTTON] =3D kctl; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel Wheel", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D bf_panel_wheel_info, + .get =3D bf_panel_wheel_get, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_WHEEL] =3D kctl; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel In", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D bf_panel_in_info, + .get =3D bf_panel_in_get, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_IN] =3D kctl; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel Out", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D bf_panel_out_info, + .get =3D bf_panel_out_get, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_OUT] =3D kctl; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel Mix", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D snd_ctl_boolean_mono_info, + .get =3D bf_panel_bool_get, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_MIX] =3D kctl; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel Dim", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D snd_ctl_boolean_mono_info, + .get =3D bf_panel_bool_get, + .private_value =3D 1, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_DIM] =3D kctl; + + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "Front Panel Select", + .access =3D SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_WRITE | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info =3D bf_panel_select_info, + .get =3D bf_panel_select_get, + .put =3D bf_panel_select_put, + }, chip); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_SELECT] =3D kctl; + + return 0; +} + + /* =E2=94=80=E2=94=80 DSP-EQ stubs =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80 * Replaced by the eq patch. The core driver calls these from * probe() and hw_params() (fs-dependent re-upload). --=20 2.55.0 From nobody Sat Sep 26 10:02:09 2026 Received: from mail-wr1-f51.google.com (mail-wr1-f51.google.com [209.85.221.51]) (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 B25F442EEA3 for ; Wed, 2 Sep 2026 10:11:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.51 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; cv=none; b=j7t+r/bqLafasCP+Twl52cXJHaSfkUd4hy1tnMREkVh3mg3hNNQwOBvfiBYXLo3qkCpWcI2MQnEDIpoSnm0CgwYSslrl64OJMMwCMp33+7deA/s9HPQ/dJ1z12yCPe8eHpcPaSOlNabQ4fr8SqbotBNtqZQ850Vh+OmeLVY1L74= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788343903; c=relaxed/simple; bh=Czq3MNo7sXIZWnn8uBDLBQFXIm6qfEkwxwLoghWYtU4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=BdVhpX2lBbxaukSvVRu+W6XDVGhVhlwW0QnsFHuDfq+cy7iHCM/esGwoyAYb8uzeqOSZPkUkqSZlKnTKNKUmdgI2k1dkmIUekYXdL9QzIpS3L0ZzCw6XE5GfSKl2BTxQerzUoV5a+2taUfsLDgHyhAqJcrtFaqovXDJS8xzqnEs= 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=q/xM/9Ak; arc=none smtp.client-ip=209.85.221.51 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="q/xM/9Ak" Received: by mail-wr1-f51.google.com with SMTP id ffacd0b85a97d-47ddf7b09e5so875803f8f.1 for ; Wed, 02 Sep 2026 03:11:36 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788343895; x=1788948695; 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=zGakF+5UgC/Qyae+DfDML9mtCfg1kDn0jWYGe/geyPE=; b=q/xM/9AkP3AEpHp50ib/1eJFfWxCN7ZZ9Yh/oJBUJ8Nu6R7ej2otffkMs+twBio8Vr jdPaMblunJ0Rebqhh74y+XYLRTi6gN+2ovCvVjl0V0ziXe1cg3d8Htu4dcVnVnZd+mD8 VPcWNYz1nfVUmeOLpeQKJOt/d38TraxL7UsbOxz5c16HGZf6RDO8HQgDWJxL11KUlqDi h4XeNA52DVFOvUvcVB4qOTeMdzp2gtBb+Ry1Nn8alv5cGzKS6t56wNZJDm6wXAZpS1XA VpZbvkPwVnV86GpN/d9XW0l3kb6EDpuU1OBQHHdVuyKicxh674Lm6qz/k3qdUIfUOwcz U0uQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788343895; x=1788948695; 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=zGakF+5UgC/Qyae+DfDML9mtCfg1kDn0jWYGe/geyPE=; b=bLIZpjxBg/j8EpqgI88nAN84sg8VHmVTz8i8E/idKjaOCZmOGkM3EgU8GAELqagvZ8 02sBgvYbHzv9yVWNnz8/gusAzy7r8lMpK9h0QlSo+GawAODcHXxWuAuGpGuwpGHtw+mm QHe379IeRBShvTnLfisZBzza5DFhxwBI91LklH0cL1tbe0KtKefh/BbE+7fzkkWw1QPe th8uEVdwAHHHzADdvRv+xJ0oDCJdxQ/TsKfB+yBMPp+qh9yj0TKKSDVIhRmUdB8Aw1MK oxP+KcZUDgHkkplPEfq+cfdaDOOF7YA3qq0S2Fb3I2h3Th8xFfKnhjHbDDnCedO73Zr0 WU4A== X-Forwarded-Encrypted: i=1; AHgh+RqM+fXe89vz3N9sv59BQFu4bQCpZeOg5rph7uxweriUCGwrWximPSt8oU7fdE8YcaUol/sY25McQAkCYlI=@vger.kernel.org X-Gm-Message-State: AFuF++nV0c8qOpVcARqWlb2XIMTPO4efmxB+gyEhj1rhQyzJycw20Au+ UprpsdwjZR88GzUOVwLsGRdfjBd8YW6N6ZtX+Eek+PZBW56lSnyazbS1 X-Gm-Gg: AR+sD13FIMlc5OsOOBkG4hG8AJzzdR/a3MNKuq3uSZeg5EAgtSUD/9R7zlG8au28mC6 KThFaNCJF2aLN0rcKPwGIoD4HDZU+YESlDzfwD1wg/YadmBXHkB9dcdheRFGnjw4U+IhwIuY0OX oqdgKCnTTB98Nh6IditqQKvpjhohO00aa46/4QeFG9gb5/NC6fX6prOW2dbIo2REPy9goKlHsJW rTVd1roziAf3vPXcWLQ5q4pUyydkOegr7nZ1s/4K6LljBkYQ1Eeb8MX7QUz5ZiJ+xnHaw5U55rM mNZGMBokmOj6mO8uGrcbV/A3//GZtD6RVeD6pjMOcLkkdp3PbczAbnwuIt/vOzaeDEry7aTmxr8 3ohyVLBxTZKOayySJ5KA1qqdExmqrsdcMfDIKHGeARYSWp/ixG+eK/aBq5QwK+aqlW6mYPWRT47 C1j/O4JWpXNutfg4GmCPqGKQWUwL2beGTlohBzwc+OIFR1WLTN7S7tWslQrsccIP9pe2tTzk/hi 6MA5GQRSj1PX2QM X-Received: by 2002:a05:600c:63c6:b0:49c:eb16:9fd with SMTP id 5b1f17b1804b1-49ceb160bb1mr12572905e9.3.1788343894994; Wed, 02 Sep 2026 03:11:34 -0700 (PDT) Received: from cachyos ([196.77.120.180]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49ce47885b1sm57294265e9.14.2026.09.02.03.11.33 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 02 Sep 2026 03:11:34 -0700 (PDT) From: =?UTF-8?q?Isma=C3=AFl=20Bahloul?= To: linux-sound@vger.kernel.org Cc: linux-usb@vger.kernel.org, alsa-devel@alsa-project.org, perex@perex.cz, tiwai@suse.com, linux-kernel@vger.kernel.org, =?UTF-8?q?Isma=C3=AFl=20Bahloul?= Subject: [RFC PATCH v3 4/4] ALSA: usb: babyface-pro: add the hardware DSP EQ Date: Wed, 2 Sep 2026 11:11:17 +0100 Message-ID: <20260902101117.11671-5-i.bahloul01@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260902101117.11671-1-i.bahloul01@gmail.com> References: <20260902101117.11671-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: quoted-printable Program the four EQ strips (3-band bell/shelf + low cut) via the fixed-point Q27 coefficient synthesis (CORDIC sin/cos, exp2) and bulk uploads on EP 0x0A, exposed as the standard ALSA control set. The coefficients depend on the stream rate, so they are re-uploaded on hw_params. Signed-off-by: Isma=C3=AFl Bahloul Assisted-by: DeepSeek V4 Flash --- sound/usb/babyfacepro/babyfacepro-ctl.c | 570 +++++++++++++++++++++++- 1 file changed, 565 insertions(+), 5 deletions(-) diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepr= o/babyfacepro-ctl.c index 9422a73d3..938e47368 100644 --- a/sound/usb/babyfacepro/babyfacepro-ctl.c +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c @@ -1343,7 +1343,6 @@ int babyface_create_controls(struct snd_usb_babyface = *chip) return 0; } =20 - /* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */ enum { BF_PANEL_KCTL_BUTTON, @@ -2278,16 +2277,577 @@ int babyface_create_panel(struct snd_usb_babyface = *chip) return 0; } =20 +#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] =3D { + 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, +}; =20 -/* =E2=94=80=E2=94=80 DSP-EQ stubs =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80 - * Replaced by the eq patch. The core driver calls these from - * probe() and hw_params() (fs-dependent re-upload). +/* ---- fixed-point helpers (Q27 in/out, s64 intermediates) ---- */ + +/* sin/cos of an angle in [0, pi/2] (Q27). Simultaneous CORDIC, 28 + * iterations (~1e-8 residual). eq_selftest.c verifies the whole + * pipeline against the double-precision reference. */ -int babyface_create_eq(struct snd_usb_babyface *chip) +static void bf_sincos(s64 ang, s64 *sn, s64 *cs) +{ + s64 x =3D 0x4DBA76D; /* 1/1.64676 x 2^27 (CORDIC gain) */ + s64 y =3D 0; + s64 z =3D ang; + int i; + + for (i =3D 0; i < 28; i++) { + s64 d =3D z >=3D 0 ? 1 : -1; + s64 nx =3D x - d * (y >> i); + s64 ny =3D y + d * (x >> i); + + x =3D nx; + y =3D ny; + z -=3D d * bf_atan_tab[i]; + } + *cs =3D x; + *sn =3D y; +} + +/* 2^u for u in Q27, u in [-2, 2] (gain-amplitude range). */ +static s64 bf_exp2(s64 u) { + s64 n =3D u >> 27; + s64 r =3D u - (n << 27); + s64 rl =3D (r * 0x58B90C0 + (1 << 26)) >> 27; /* r.ln2 */ + s64 e =3D BF_EQ_Q27; + s64 term =3D BF_EQ_Q27; + int k; + + for (k =3D 1; k <=3D 10; k++) { + term =3D div_s64((term * rl + (1 << 26)) >> 27, k); + e +=3D term; + } + return n >=3D 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 =3D Q x 100; gain_x10 =3D 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 =3D freq_hz; + s64 w0, c, s, alpha, A, sq; + s64 b0, b1, b2, a0, a1, a2; + s64 pi =3D 0x1921FB54; /* pi, Q27 */ + s64 hpi =3D 0xC90FDAA; /* pi/2, Q27 */ + s64 t; + int both =3D 0, cflip =3D 0; + + if (gain_x10 =3D=3D 0 || q100 <=3D 0) { + /* Inactive band: identity words (also guards the alpha + * division below against the default Q=3D0 the controls start + * with - a user setting gain before Q used to hit a kernel + * divide-by-zero oops). + */ + w[0] =3D 0; + w[1] =3D 0; + w[2] =3D 0; + w[3] =3D 0; + return; + } + + /* w0 =3D 2.pi.f/fs (Q27), reduced to [0, pi/2]. */ + w0 =3D div_s64(f * BF_EQ_Q27, fs); + w0 =3D (w0 * 0x3243F6A9) >> 27; /* x 2.pi */ + t =3D w0; + if (t > pi) { + t -=3D pi; + both =3D 1; + } + if (t > hpi) { + t =3D pi - t; + cflip =3D 1; + } + bf_sincos(t, &s, &c); + if (both) { + s =3D -s; + c =3D -c; + } + if (cflip) + c =3D -c; + + alpha =3D div64_s64(s * 100 + q100, 2 * (s64)q100); /* sin(w0)/(2Q) */ + /* A =3D 10^(g/40), sqrt(A): g =3D gain_x10/10 dB */ + A =3D bf_exp2((s64)gain_x10 * 0x11021E); + sq =3D bf_exp2((s64)gain_x10 * 0x8810F); + + if (type =3D=3D 1) { + s64 ta =3D (alpha * A + (1 << 26)) >> 27; + + b0 =3D BF_EQ_Q27 + ta; + b1 =3D -2 * c; + b2 =3D BF_EQ_Q27 - ta; + a0 =3D BF_EQ_Q27 + div64_s64(alpha * BF_EQ_Q27 + A / 2, A); + a1 =3D -2 * c; + a2 =3D BF_EQ_Q27 - div64_s64(alpha * BF_EQ_Q27 + A / 2, A); + } else { + s64 ap1 =3D A + BF_EQ_Q27; + s64 am1 =3D A - BF_EQ_Q27; + s64 cp0 =3D (am1 * c + (1 << 26)) >> 27; /* (A-1).c */ + s64 cp1 =3D (ap1 * c + (1 << 26)) >> 27; /* (A+1).c */ + s64 ab =3D (2 * sq * alpha + (1 << 26)) >> 27; + + if (type =3D=3D 2) { /* low shelf */ + b0 =3D (A * (ap1 - cp0 + ab) + (1 << 26)) >> 27; + b1 =3D (2 * A * (am1 - cp1) + (1 << 26)) >> 27; + b2 =3D (A * (ap1 - cp0 - ab) + (1 << 26)) >> 27; + a0 =3D ap1 + cp0 + ab; + a1 =3D -2 * (am1 + cp1); + a2 =3D ap1 + cp0 - ab; + } else { /* high shelf */ + b0 =3D (A * (ap1 + cp0 + ab) + (1 << 26)) >> 27; + b1 =3D (-2 * A * (am1 + cp1) + (1 << 26)) >> 27; + b2 =3D (A * (ap1 + cp0 - ab) + (1 << 26)) >> 27; + a0 =3D ap1 - cp0 + ab; + a1 =3D -2 * (am1 - cp1); + a2 =3D ap1 - cp0 - ab; + } + } + + w[0] =3D (s32)div64_s64(a1 * BF_EQ_Q27 + a0 / 2, a0); + w[1] =3D (s32)div64_s64(a2 * BF_EQ_Q27 + a0 / 2, a0); + w[2] =3D (s32)div64_s64(b1 * BF_EQ_Q27 + b0 / 2, b0); + w[3] =3D (s32)div64_s64(b2 * BF_EQ_Q27 + b0 / 2, b0); + w[4] =3D (s32)div64_s64(b0 * BF_EQ_Q27 + a0 / 2, a0); +} + +/* ---- low cut ---- */ + +/* Slope byte: 2^n-1 (n poles) -> 6/12/18/24 dB per oct; 0 =3D 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; } =20 +/* The 0x38 low-cut frequency word: round(K.f'.(11656)/(11656+f')) with + * K =3D 11508, f' =3D 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 <=3D 0) + return BF_EQ_LC_OFF; + f =3D freq_hz; + switch (slope_db) { + case 6: + f =3D f * 15267 / 10000; + break; + + case 18: + f =3D f * 8061 / 10000; + break; + + case 24: + f =3D f * 6977 / 10000; + break; + } + word =3D (11508 * f * 11656 + (11656 + f) / 2) / (11656 + f); + return (u32)word; +} + +/* ---- block build + bulk write ---- */ + +static void bf_eq_build_block(u8 *b, int ch, u8 slope, + const s32 bands[3][4], s32 shared, u32 lc) +{ + int slot, k; + + memset(b, 0, BF_EQ_BLOCK_LEN); + b[0] =3D ch; + b[1] =3D slope; + b[2] =3D ch; + b[3] =3D 0x80; /* EQ engine active */ + for (slot =3D 0; slot < 3; slot++) { + for (k =3D 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 =3D kmemdup(block, BF_EQ_BLOCK_LEN, GFP_KERNEL); + if (!buf) + return -ENOMEM; + ret =3D 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 =3D strip x 2). */ +static int bf_eq_write_strip(struct snd_usb_babyface *chip, int strip) +{ + struct bf_eq_channel *e =3D &chip->eq[strip]; + u8 b[BF_EQ_BLOCK_LEN]; + s32 identity[3][4] =3D { { 0 }, { 0 }, { 0 } }; + s32 shared =3D e->on ? e->shared : BF_EQ_Q27; + u32 lc =3D 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 =3D off made the device apply a + * garbage-frequency cut (ear-verified: "low cut off" left only + * highs). cap_eq7: byte1 =3D 0x00 + 0x38 =3D 0x04000000 when off. + */ + u8 slope =3D (e->on && e->lc_hz > 0) ? e->slope : 0; + int ch, ret; + + for (ch =3D 0; ch < 2; ch++) { + bf_eq_build_block(b, strip * 2 + ch, slope, + e->on ? e->words : identity, shared, lc); + ret =3D bf_eq_upload(chip, b); + if (ret < 0) + return ret; + } + return 0; +} + +/* Recompute one strip's words + low cut from its params, re-upload. + * Lock-free by convention: every caller must already hold chip->mutex + * (bf_eq_put() and bf_eq_reupload() do) - asserting it here catches a + * future caller that forgets, instead of a silent self-deadlock. + */ +static void bf_eq_update_strip(struct snd_usb_babyface *chip, int strip) +{ + struct bf_eq_channel *e =3D &chip->eq[strip]; + s32 fs =3D chip->rate ? chip->rate : 48000; + s32 last_c4 =3D BF_EQ_Q27; + int band, i; + + lockdep_assert_held(&chip->mutex); + + for (band =3D 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 =3D 0; i < 4; i++) + e->words[band][i] =3D w[i]; + if (e->band_type[band] && e->band_gain[band]) + last_c4 =3D w[4]; /* shared scale: the last band */ + } + e->shared =3D last_c4; + e->lc_raw =3D bf_eq_lc_freq_raw(e->lc_hz, e->slope_db); + e->slope =3D bf_eq_lc_slope_byte(e->slope_db); + bf_eq_write_strip(chip, strip); +} + +/* Recompute + re-upload all four strips (rate change). Caller must + * hold chip->mutex - bf_eq_update_strip()/bf_eq_write_strip() are + * lock-free by convention (see bf_eq_put()) and the only caller, + * babyface_pcm_hw_params(), already holds the lock across the rate + * change; locking here too self-deadlocked it (hung-task: "blocked + * on a mutex likely owned by" itself, hit via regress.sh's rate + * sweep). + */ void bf_eq_reupload(struct snd_usb_babyface *chip) { + int strip; + + for (strip =3D 0; strip < 4; strip++) + bf_eq_update_strip(chip, strip); +} + +/* ---- ALSA controls (4 strips x 19 controls) ---- */ + +#define EQ_STRIP(pv) ((pv) >> 8) +#define EQ_PARAM(pv) ((pv) & 0xff) +/* params: 0 enable, 1-3 type, 4-6 freq, 7-9 q, 10-12 gain, 13 lc freq, 14= lc slope */ + +static const char *const bf_eq_type_texts[] =3D { + "Off", "Bell", "Low Shelf", "High Shelf", NULL +}; + +static const char *const bf_eq_slope_texts[] =3D { + "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 =3D EQ_PARAM(kctl->private_value); + + if (param =3D=3D 0) { + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_BOOLEAN; + uinfo->count =3D 1; + return 0; + } + if (param =3D=3D 1 || param =3D=3D 2 || param =3D=3D 3) + return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_type_texts); + if (param =3D=3D 14) + return snd_ctl_enum_info(uinfo, 1, 4, bf_eq_slope_texts); + + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D (param =3D=3D 10 || param =3D=3D 11 || + param =3D=3D 12) ? -240 : + (param =3D=3D 7 || param =3D=3D 8 || + param =3D=3D 9) ? 5 : 0; + uinfo->value.integer.max =3D (param =3D=3D 7 || param =3D=3D 8 || + param =3D=3D 9) ? 1000 : + (param =3D=3D 10 || param =3D=3D 11 || + param =3D=3D 12) ? 240 : 20000; + uinfo->value.integer.step =3D 1; + return 0; +} + +static int bf_eq_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip =3D snd_kcontrol_chip(kctl); + struct bf_eq_channel *e =3D &chip->eq[EQ_STRIP(kctl->private_value)]; + int param =3D EQ_PARAM(kctl->private_value); + int band =3D (param - 1) % 3; + s32 *v =3D NULL; + + switch (param) { + case 0: + break; + + case 1: + case 2: + case 3: + v =3D &e->band_type[band]; + break; + + case 4: + case 5: + case 6: + v =3D &e->band_freq[band]; + break; + + case 7: + case 8: + case 9: + v =3D &e->band_q[band]; + break; + + case 10: + case 11: + case 12: + v =3D &e->band_gain[band]; + break; + + case 13: + v =3D &e->lc_hz; + break; + + case 14: + v =3D &e->slope_db; + break; + } + if (param =3D=3D 0) { + ucontrol->value.integer.value[0] =3D e->on; + } else if (param =3D=3D 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 =3D v ? *v : 6; + + ucontrol->value.enumerated.item[0] =3D + slope >=3D 24 ? 3 : slope >=3D 18 ? 2 : slope >=3D 12 ? 1 : 0; + } else if (param =3D=3D 1 || param =3D=3D 2 || param =3D=3D 3) { + /* ENUMERATED band type: use the enumerated union member. */ + ucontrol->value.enumerated.item[0] =3D v ? *v : 0; + } else { + ucontrol->value.integer.value[0] =3D 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 =3D snd_kcontrol_chip(kctl); + int strip =3D EQ_STRIP(kctl->private_value); + int param =3D EQ_PARAM(kctl->private_value); + struct bf_eq_channel *e =3D &chip->eq[strip]; + int band =3D (param - 1) % 3; + s32 nv; + s32 *v =3D NULL; + int ret =3D 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 =3D=3D 1 || param =3D=3D 2 || param =3D=3D 3 || param =3D=3D 14) + nv =3D (s32)ucontrol->value.enumerated.item[0]; + else + nv =3D (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 >=3D 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 =3D NULL; + break; + + case 1: + case 2: + case 3: + v =3D &e->band_type[band]; + break; + + case 4: + case 5: + case 6: + v =3D &e->band_freq[band]; + break; + + case 7: + case 8: + case 9: + v =3D &e->band_q[band]; + break; + + case 10: + case 11: + case 12: + v =3D &e->band_gain[band]; + break; + + case 13: + v =3D &e->lc_hz; + break; + + case 14: + v =3D &e->slope_db; + break; + } + if (param =3D=3D 14) /* slope enum items are 6/12/18/24 */ + nv =3D nv =3D=3D 0 ? 6 : nv =3D=3D 1 ? 12 : nv =3D=3D 2 ? 18 : 24; + + mutex_lock(&chip->mutex); + if (param =3D=3D 0) { + if (e->on !=3D !!nv) { + e->on =3D !!nv; + bf_eq_update_strip(chip, strip); + ret =3D 1; + } + } else if (v && *v !=3D nv) { + *v =3D nv; + bf_eq_update_strip(chip, strip); + ret =3D 1; + } + mutex_unlock(&chip->mutex); + return ret; +} + +int babyface_create_eq(struct snd_usb_babyface *chip) +{ + static const char *const names[4] =3D { "AN1", "AN2", "AN3", "AN4" }; + static const char *const params[] =3D { + "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 =3D 0; strip < 4; strip++) { + for (i =3D 0; i < 15; i++) { + struct snd_kcontrol *kctl; + char name[64]; + + snprintf(name, sizeof(name), "%s %s", names[strip], + params[i]); + kctl =3D snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .name =3D "EQ", + .index =3D 0, + .info =3D bf_eq_info, + .get =3D bf_eq_get, + .put =3D bf_eq_put, + .private_value =3D (strip << 8) | i, + }, chip); + if (!kctl) + return -ENOMEM; + strscpy(kctl->id.name, name, sizeof(kctl->id.name)); + err =3D snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + } + } + return 0; } --=20 2.55.0