From nobody Mon Sep 28 06:35:37 2026 Received: from mail-lf1-f53.google.com (mail-lf1-f53.google.com [209.85.167.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 21BEB3FA5E6 for ; Tue, 25 Aug 2026 11:12:51 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.167.53 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787656376; cv=none; b=Bk979/I97MItX6fA6YpNxfYICEH9/UAWW8mC10NgU3+QigwrfBfC8z1u3PCAHuAvLsKgfdpmXQfBrbhqdmJC0wk1IZ4Foxm2fq1lCOBzTUcm98NJJF9nZtdd6ho3daLx0Jzo20wm9jjSjapBLw2WSLGQc3NYgIrxGvr90F09rOc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787656376; c=relaxed/simple; bh=e4O+894/U/qjYHaV+YB7BDVdt58FpIOBtEN6/T/zUc8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=AkHGnXBR2sZHS1rjRymfCIIGNPv4eD4/eNlQc4BKtA/mI9swaObcubwrYPCP0JilZM1Iist6bIMp5AWCAi/lUL3fVFF8CkBHvoaCVgMKsYDhtLemEuHHL++rbXY8x2kQpXeHK8WLCfuThvJd7GHrQvnx/nVLVaRIznOv6pzJZFU= 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=ETAkvJFb; arc=none smtp.client-ip=209.85.167.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="ETAkvJFb" Received: by mail-lf1-f53.google.com with SMTP id 2adb3069b0e04-5aec201b582so5196826e87.1 for ; Tue, 25 Aug 2026 04:12:51 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787656370; x=1788261170; darn=vger.kernel.org; h=content-transfer-encoding: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=ZwXJNrHEyVA4kQM8LtElYDcotphDpMcc+BYajQN+I90=; b=ETAkvJFb5JVd486ZrK+I0MNq3rtx8bqMcYnY7LszvUZ1X2zhpQ9AW6bzZxfKLSuyr+ jEsNnwFbiZFa94s9UhYiJQLEL47T+QR1e/4BarxV30kF1NsafmqPJh6KJUhjW4fZb3GO Ut3xK/Q+eZT4Si7NyDth7UqKUFAo85SQkqRITSUx3sAfU3C1OQQ9jqOjss5+5rA6LodM zo7nw/RP98db9/d91xXIz70hasH9DBNGsqW4B2rdlfVsMH8+rwtMRRjIgtIIFek+EzaL 2lr+eQvIYfqROjR9RZsqj563ctnKRw/CnGb8nTd1ORJRfALrggHUXgoDDYy2BBwlwbXu pW2w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787656370; x=1788261170; h=content-transfer-encoding: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=ZwXJNrHEyVA4kQM8LtElYDcotphDpMcc+BYajQN+I90=; b=kJ3kNpFRXQWS36Pqh/N+7tf4RoQh/jcvgWH0UTw/dezwTOp0vKZCfg+xYudkWAyMDN vv7kfFJz15N2Z1JZ7RnXZbzWBKMOgKMBMjvcJnymY6E0BNL8pI2AMQAmPAlNX4VoRNAZ hy6BdOIUEHKh9dTIcHO17YGDX7MpGAZG7Q1ZFWL2aJfwen44SIE1/RP3Ztq7JCzoIbds gceMSAvvicqoqLeb2uZRuBGCTPwmeUzQ+kS5XIOZpcqNyjkDj+UkPpFii2LbwttvQLFv o/Uwnh/+YzEy+4aOI2HqTdqWq3gUPkHf6IG6FL53ihiuXuGmmpJAzrE5pJPNMeqXkXh6 gmpA== X-Forwarded-Encrypted: i=1; AHgh+Rq9Hr0K7Q+9+G8j0IWHOfx438ENA+DVHMDv548FnOrx14b8gEflAbYM3rFrKRXbi0FIkvPT8khia6VEXzk=@vger.kernel.org X-Gm-Message-State: AFuF++lCveYgVU+SJiU3FOXSEzGrzRv264Mp8oSgSQINyrjMVtkuS6hG GsKu+gol8XrV8nsA90DDbVxxmkmQ818Y5S9ei7tGOiMpmJ/b8gRubhOR X-Gm-Gg: AR+sD11OJaDTgqryf76OsIgdbmjWKQcSX2keiUaxptawER6qLb9SykSOTr8vIOqvkPJ Kq06C7JreABICkHWLMB+cvrm0DnY5tvVYe7qALhlRkp66qnZmHpLl+vlpDNZrNB6bKVYvknS8dH NvpGjK9cx8yXdbLLpTtGXPuJLP5zpxcgDNr+qG+iMPCJQCmcXzX+yJQAcaExrFgjKfmYUPH0upB 3W/BNqP870tQksO7IcruHgKVtQlW6rGAAFiF6VNDfXeUi4JhClDUdyR6mCJqBjZICtlcnczyJvn 8rCdlEe3Uwj7McS28pzRLBFlSA9K8upixpsCxAsHEH67eGT7YwpEMdjR47eM19uivF8jf9kt6W/ dtGosZ4kF2/n4vIr5O/3RQK89joOyQq8rchE0JIQeMfNKzRgJB5JzPr3HfcHRFXsXs6JXhtfR9E DG4eSERvblikHQs0K71xsdj31CASFWPS1Sa7Ruwmh6tviU5Cy2TG85ONqppsPrXm/RR/AlohILF y9G1NiPVvyru/YeEZFUCqLokYlrrD62xcA39YseC+PiGIaIY8KqSHtVh2V9/l9pxwzPhSE= X-Received: by 2002:a05:6512:3daa:b0:5b0:22a6:6b13 with SMTP id 2adb3069b0e04-5b48b440e02mr7879055e87.0.1787656369403; Tue, 25 Aug 2026 04:12:49 -0700 (PDT) Received: from localhost ([188.234.148.119]) by smtp.gmail.com with ESMTPSA id 2adb3069b0e04-5b48ce3a89fsm2337244e87.83.2026.08.25.04.12.48 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 25 Aug 2026 04:12:48 -0700 (PDT) From: Mikhail Gavrilov To: tiwai@suse.com Cc: perex@perex.cz, jikos@kernel.org, bentiss@kernel.org, linux-sound@vger.kernel.org, linux-input@vger.kernel.org, linux-kernel@vger.kernel.org, Mikhail Gavrilov Subject: [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Date: Tue, 25 Aug 2026 16:12:38 +0500 Message-ID: <20260825111239.24834-2-mikhail.v.gavrilov@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260825111239.24834-1-mikhail.v.gavrilov@gmail.com> References: <20260825085659.52675-1-mikhail.v.gavrilov@gmail.com> <20260825111239.24834-1-mikhail.v.gavrilov@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The M62 (152a:875c) keeps its analogue input gains and its output volumes behind a vendor protocol on a HID-class interface, and exposes none of them through UAC. What UAC does offer on the capture side is a digital trim after the converter, which cannot buy signal-to-noise: raising it lifts the converter's own floor along with the signal. A noise-floor ladder against the card shows exactly that, so on Linux today the one knob worth setting is the one that cannot be reached, and a measurement has to begin by asking a human to touch the front panel. The protocol was read off the vendor application's traffic, the way mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen bytes -- start magic, a constant, a target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS over the middle stored big-endian, end magic -- and rebuilding all 2619 captured frames from that description reproduces them byte for byte. The device says nothing until it is subscribed; one write starts the stream, a second makes it announce its whole state, after which every change arrives unsolicited, including a front panel press. So the controls are populated by asking rather than by caching what was written, which matters here because the vendor application on another host pushes its own cached state onto the card on connect. The control pipe cannot carry this: GET_REPORT and SET_REPORT stall with EPIPE for every report type, so the interrupt endpoints on the HID interface are the only route and this driver has to own that interface. hid_ignore_list keeps usbhid away. Nothing is lost by that: the report descriptor the device offers is a fig leaf -- a Generic Desktop application collection, eight unnamed usages, sixteen bytes in and out, no report ID -- so hid-generic can only make a nonexistent mouse of it. The controls are a table: a name, the target and property that carry the knob, the second target that must be written in step with it, the range and the scale. Adding a knob is adding a row. Six rows here -- the two microphone preamps in whole decibels, AUX and Bluetooth on the input side, headphone and OTG on the output side -- and the outputs come in pairs because the device answers on only one of each pair and the other would drift away unheard. The two volume tapers are measured, not guessed: index 0 is mute, index 99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as well. Both express as DB_RANGE. The microphone preamps are ordinary 1 dB steps from 0 to 88. One thing a mixer quirk cannot do for itself: usb_audio_driver is private to card.c, so claiming an interface the audio class knows nothing about needs a helper there. snd_usb_claim_iface() is that helper, and it is the only change outside the new file and its dispatch. Seven rows. OTG IN was the one gap when this was first posted -- it has no front panel control, so it never announced itself and its property was unknown; a capture of the vendor application moving it named it as target 0x27, and its taper is the same family as Bluetooth, confirmed by the indices the application dwelt on matching the decibels it displayed. The subscription lapses, so it is renewed: the vendor application repeats the same subscribe every two seconds for as long as it runs, and a device that hears nothing stops reporting. A listener that subscribed once got the meters and the identification block and then very little; one that kept repeating got the gains too, about five seconds in. Nothing in the frame says "keep alive" and nothing acknowledges it -- it is the subscribe again -- so a plain periodic write does it. Two seconds is what the vendor uses; the device presumably tolerates longer, but there is no reason to find the edge. The device is reached under the shutdown lock, the way the rest of this directory reaches hardware: the teardown waits for everyone holding it before the card is taken apart, so nothing here can be talking to a device that has gone. A mutex spans each write from the comparison to the cache update, so two writers cannot arrive at the device in one order and at the cache in the other. Suspend and resume are handled rather than survived. The URB does not outlive a system sleep, and a device that has heard nothing for a while stops reporting anyway, so the resume path resubmits, subscribes again and asks for the state -- which also refreshes a cache that may have gone stale while the panel was reachable and this driver was not. The claimed interface is given back, on the error path and at teardown alike, so that unbinding and binding again works instead of failing at the claim. Allocation on the resume path asks for no I/O, the way the mixer core does beside it, since reclaim there can wait on a block device that has not woken yet; and a URB that usb_kill_urb() has already refused is not reported as a failure, since -EPERM at that point is the ordinary sound of an unplug. The device is woken before the write lock is taken, not after. Waking it can run the resume callback on the calling thread, and that callback writes to the device too; entering the lock first would meet it already held, by the same thread, with nothing able to release it. Waking is the caller's business, not the frame writer's. A write asked for by a hand wakes what is asleep; the keepalive and the resume path do not, the first because a sleeping device has no subscription worth renewing -- resume renews it -- and the second because it is the resume. When the keepalive woke the device itself, a runtime suspend arriving at the same moment waited in cancel_delayed_work_sync() for a worker that was in turn waiting for that suspend to finish. It also means the card can now reach runtime suspend at all, which a write every two seconds had prevented. Bus noise does not end the listening. A frame that arrives with -EPROTO or -EILSEQ is dropped and the URB goes back; only the three statuses that mean the URB or the device is gone stop it, which is what the mixer's own status handler does next door. The resume path forbids I/O reclaim for everything under it, not just for the frame buffer: usb_interrupt_msg() allocates a URB of its own with GFP_KERNEL, so a polite flag on our own allocation settles nothing. And the claimed interface is held with a reference of its own. Claiming does not keep it alive -- on unplug the core deletes and puts every interface -- while this driver hands the pointer back to the core when the card goes away. SND_USB_AUDIO selects CRC16, since the frames are checked with it and nothing else in the directory pulled it in. The cache takes a new value before the write reaches the device rather than after. The lock cannot be held across a send, and a hand on the front panel during that window produces a notification the URB handler stores; updating afterwards would throw that away and leave the driver claiming a value the device had already moved away from. Written first, the device's own report is simply the last word. A failed write puts the old value back, unless the device has spoken in the meantime. The keepalive stops renewing a subscription nobody is listening to: a write that fails because the device has gone ends the cycle instead of scheduling another two seconds later. Those two errors are also no longer logged, being an unplug rather than a fault. Signed-off-by: Mikhail Gavrilov --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Kconfig | 1 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 654 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 10 files changed, 702 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index 549df316f487..f5b0bd36be22 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27497,6 +27497,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ =20 +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 341bf587863b..092b2a942b4c 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1470,6 +1470,9 @@ #define USB_DEVICE_ID_TIVO_SLIDE 0x1201 #define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203 =20 +#define USB_VENDOR_ID_TOPPING 0x152a +#define USB_DEVICE_ID_TOPPING_M62 0x875c + #define USB_VENDOR_ID_TOPRE 0x0853 #define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148 #define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146 diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c index 8a0b51d47040..3c156d1420d1 100644 --- a/drivers/hid/hid-quirks.c +++ b/drivers/hid/hid-quirks.c @@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] =3D= { { HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) }, { HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) }, #endif + /* the M62's vendor control channel, driven by snd-usb-audio */ + { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) }, { HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K= ) }, { HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA= _5473) }, { } diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig index b4588915efa1..5af29599e494 100644 --- a/sound/usb/Kconfig +++ b/sound/usb/Kconfig @@ -17,6 +17,7 @@ config SND_USB_AUDIO select SND_PCM select SND_UMP if SND_USB_AUDIO_MIDI_V2 select BITREVERSE + select CRC16 select SND_USB_AUDIO_USE_MEDIA_CONTROLLER if MEDIA_CONTROLLER && (MEDIA_S= UPPORT=3Dy || MEDIA_SUPPORT=3DSND_USB_AUDIO) help Say Y here to include support for USB audio and USB MIDI diff --git a/sound/usb/Makefile b/sound/usb/Makefile index e62794a87e73..151b481df795 100644 --- a/sound/usb/Makefile +++ b/sound/usb/Makefile @@ -14,6 +14,7 @@ snd-usb-audio-y :=3D card.o \ mixer_quirks.o \ mixer_scarlett.o \ mixer_scarlett2.o \ + mixer_topping.o \ mixer_us16x08.o \ mixer_s1810c.o \ pcm.o \ diff --git a/sound/usb/card.c b/sound/usb/card.c index 24112e491779..191391822092 100644 --- a/sound/usb/card.c +++ b/sound/usb/card.c @@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio = *chip, int ctrlif, int int return 0; } =20 +/* + * Claim an interface of this device for snd-usb-audio. + * + * A mixer quirk may need an interface the audio class knows nothing + * about -- a vendor control channel that happens to wear the HID class, + * for instance -- and cannot claim it itself, because usb_audio_driver + * is private to this file. + */ +int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *= iface) +{ + return usb_driver_claim_interface(&usb_audio_driver, iface, + USB_AUDIO_IFACE_UNUSED); +} + +void snd_usb_release_iface(struct usb_interface *iface) +{ + usb_driver_release_interface(&usb_audio_driver, iface); +} + /* * parse audio control descriptor and create pcm/midi streams */ diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c index a1f5592cc5d5..10f33026cdff 100644 --- a/sound/usb/mixer_quirks.c +++ b/sound/usb/mixer_quirks.c @@ -36,6 +36,7 @@ #include "mixer_quirks.h" #include "mixer_scarlett.h" #include "mixer_scarlett2.h" +#include "mixer_topping.h" #include "mixer_us16x08.h" #include "mixer_s1810c.h" #include "helper.h" @@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixe= r_interface *mixer) err =3D snd_fcp_init(mixer); break; =20 + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err =3D snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err =3D snd_soundblaster_e1_switch_create(mixer); break; diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c new file mode 100644 index 000000000000..933a8bbe57b9 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,654 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov + * + * The M62 (152a:875c) puts its analogue input gains and its output + * volumes behind a vendor protocol on a HID-class interface, and + * exposes nothing of them through UAC. What UAC does expose on the + * capture side is a digital trim AFTER the converter, which cannot buy + * signal-to-noise: raising it lifts the converter's own floor with the + * signal. So the only knob worth automating is unreachable, and a + * measurement application on Linux has to ask a human to set it by + * hand on the front panel. + * + * The protocol was read off the vendor application's traffic. Frames + * are fifteen bytes: + * + * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77 + * + * with TT a target (an input, an output, or the device itself), PP a + * property of that target, and the checksum CRC-16/MODBUS over bytes + * 2..10 stored most significant byte first. Reports arriving from the + * device are the same frame plus one trailing pad byte; an idle poll + * returns sixteen zeroes. The vendor application sends 00 00 in place + * of the checksum and the device accepts it, so the device evidently + * does not verify what it receives -- this driver signs its writes + * anyway, and validates what it reads. + * + * The device says nothing until it is subscribed: one write of + * 0x11/0x24 starts the notification stream, after which every change, + * including a front panel button, arrives unsolicited. A second + * write, 0x11/0x26, makes the device announce its whole state, which + * is how the controls are populated without caching what we wrote. + * + * Note that the control pipe is not an option here: GET_REPORT and + * SET_REPORT both stall with EPIPE for every report type, so the + * interrupt endpoints on the HID interface are the only route and this + * driver has to own that interface. hid_ignore_list keeps usbhid off + * it; the report descriptor it would bind to describes nothing anyway + * (a Generic Desktop application collection with eight unnamed usages + * and no report ID), so no HID functionality is lost. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "usbaudio.h" +#include "mixer.h" +#include "mixer_topping.h" + +#define TOPPING_FRAME_LEN 15 /* what we send */ +#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */ +#define TOPPING_EP_BUF 64 /* the endpoints' packet size */ + +/* device-scope properties */ +#define TOPPING_TT_DEVICE 0x11 +#define TOPPING_PP_SUBSCRIBE 0x24 +#define TOPPING_PP_ANNOUNCE 0x26 + +/* + * The two volume tapers, measured against the vendor application's own + * readout: index 0 is always mute, index 99 always the maximum, the + * step is 0.5 dB above -10 dB and 1 dB below it, and the family that + * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well. + */ +static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0); + +static const unsigned int topping_tlv_out_9[] =3D { + TLV_DB_RANGE_HEAD(4), + 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1), + 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0), + 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0), + 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0), +}; + +static const unsigned int topping_tlv_out_0[] =3D { + TLV_DB_RANGE_HEAD(3), + 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1), + 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0), + 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0), +}; + +/* + * One row per knob. A row is the whole description of a control: what + * to call it, which target and property carry it, the second target + * that has to be written in step with the first, the range, and the + * scale. Adding a knob is adding a row. + * + * The outputs come in pairs and the device announces only the second + * of each pair, so both are written and the second is the one listened + * for. + */ +struct topping_ctl_desc { + const char *name; + u8 target; /* the target that reports */ + u8 target_pair; /* written too, or 0 */ + u8 prop; + int min, max; + const unsigned int *tlv; +}; + +static const struct topping_ctl_desc topping_m62_ctls[] =3D { + { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88, + topping_tlv_gain }, + { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88, + topping_tlv_gain }, + { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99, + topping_tlv_out_9 }, + { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99, + topping_tlv_out_0 }, + { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99, + topping_tlv_out_0 }, + { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99, + topping_tlv_out_9 }, + { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99, + topping_tlv_out_0 }, +}; + +struct topping_mixer { + struct usb_mixer_interface *mixer; + struct usb_interface *iface; + bool claimed; /* iface is ours to give back */ + const struct topping_ctl_desc *ctls; + int num_ctls; + struct urb *urb; + u8 *inbuf; + dma_addr_t inbuf_dma; + unsigned int pipe_in, pipe_out; + int interval; + struct delayed_work keepalive; + struct mutex write_lock; /* one writer at a time, end to end */ + spinlock_t lock; /* guards val[] against the URB */ + int *val; + struct snd_kcontrol **kctl; +}; + +static void topping_build(u8 *f, u8 target, u8 prop, s32 value) +{ + u16 crc; + + f[0] =3D 0x22; + f[1] =3D 0x33; + f[2] =3D 0x20; + f[3] =3D 0x01; + f[4] =3D 0x01; + f[5] =3D target; + f[6] =3D prop; + put_unaligned_be32(value, f + 7); + crc =3D crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] =3D 0x66; + f[14] =3D 0x77; +} + +/* + * The frame goes out as it is; waking the device is the CALLER's + * business. A write asked for by a hand takes the shutdown lock + * first, which wakes what is asleep. The keepalive and the resume + * path deliberately do not: the first because a sleeping device has + * no subscription worth renewing -- resume renews it -- and the + * second because it IS the resume. + * + * That division is also what keeps the keepalive out of a deadlock. + * When it woke the device itself, a runtime suspend arriving at the + * same moment would wait in cancel_delayed_work_sync() for a worker + * that was in turn waiting for that suspend to finish. + */ +static int topping_send(struct topping_mixer *tm, u8 target, u8 prop, + s32 value) +{ + /* + * NOIO rather than KERNEL: this is called from the resume path + * too, where reclaim can wait on a block device that has not + * woken yet. The frame is fifteen bytes; nothing is lost by + * asking for it without I/O. + */ + u8 *buf __free(kfree) =3D kzalloc(TOPPING_EP_BUF, GFP_NOIO); + int err, actual; + + if (!buf) + return -ENOMEM; + + topping_build(buf, target, prop, value); + err =3D usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + /* ENODEV and ESHUTDOWN are an unplug, not a fault worth a line */ + if (err < 0 && err !=3D -ENODEV && err !=3D -ESHUTDOWN) + usb_audio_err(tm->mixer->chip, + "Topping: write %02x/%02x failed: %d\n", + target, prop, err); + return err; +} + +/* -1 when this frame is not one of ours */ +static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop) +{ + int i; + + for (i =3D 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target =3D=3D target && tm->ctls[i].prop =3D=3D prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm =3D urb->context; + const u8 *f =3D urb->transfer_buffer; + int idx, value, err; + bool changed; + + /* + * Bus noise gives -EPROTO and -EILSEQ, and those are not the end + * of anything: the frame is dropped and the listening goes on. + * Only the three that mean the URB or the device is gone stop it, + * which is what the mixer's own status handler does next door. + */ + if (urb->status) + goto resubmit; + if (urb->actual_length < TOPPING_FRAME_LEN) + goto resubmit; + if (f[0] !=3D 0x22 || f[1] !=3D 0x33 || f[13] !=3D 0x66 || f[14] !=3D 0x7= 7) + goto resubmit; + if (get_unaligned_be16(f + 11) !=3D crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx =3D topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value =3D get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed =3D false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] !=3D value) { + tm->val[idx] =3D value; + changed =3D true; + } + } + + if (changed && tm->kctl[idx]) + snd_ctl_notify(tm->mixer->chip->card, + SNDRV_CTL_EVENT_MASK_VALUE, + &tm->kctl[idx]->id); + +resubmit: + if (urb->status =3D=3D -ENOENT || urb->status =3D=3D -ECONNRESET || + urb->status =3D=3D -ESHUTDOWN) + return; + err =3D usb_submit_urb(urb, GFP_ATOMIC); + /* + * ENODEV, ESHUTDOWN and EPERM are the sound of an unplug -- the + * last being a URB that usb_kill_urb() has already refused. + */ + if (err < 0 && err !=3D -ENODEV && err !=3D -ESHUTDOWN && err !=3D -EPERM) + usb_audio_err(tm->mixer->chip, + "Topping: cannot resubmit: %d\n", err); +} + +/* + * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24 + * every two seconds for as long as it is running, and a device that + * hears nothing stops reporting -- which is why a listener that + * subscribed once saw the meters and not much else. Nothing in the + * frame says "keep alive"; it is simply the same subscribe again. + */ +#define TOPPING_KEEPALIVE_MS 2000 + +static void topping_keepalive(struct work_struct *work) +{ + struct topping_mixer *tm =3D container_of(work, struct topping_mixer, + keepalive.work); + int err; + + err =3D topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + if (err =3D=3D -ENODEV || err =3D=3D -ESHUTDOWN) + return; /* the device has gone; there is nothing to renew */ + + schedule_delayed_work(&tm->keepalive, + msecs_to_jiffies(TOPPING_KEEPALIVE_MS)); +} + +static int topping_ctl_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + struct usb_mixer_elem_info *elem =3D kctl->private_data; + struct topping_mixer *tm =3D elem->head.mixer->private_data; + int idx =3D elem->control; + + uinfo->type =3D SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count =3D 1; + uinfo->value.integer.min =3D tm->ctls[idx].min; + uinfo->value.integer.max =3D tm->ctls[idx].max; + uinfo->value.integer.step =3D 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem =3D kctl->private_data; + struct topping_mixer *tm =3D elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] =3D tm->val[elem->control]; + return 0; +} + +static int topping_ctl_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem =3D kctl->private_data; + struct usb_mixer_interface *mixer =3D elem->head.mixer; + struct topping_mixer *tm =3D mixer->private_data; + const struct topping_ctl_desc *d =3D &tm->ctls[elem->control]; + int value, prev, err; + + value =3D ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + /* + * THE ORDER OF THESE TWO MATTERS. Waking the device can run the + * resume callback on this very thread, and that callback takes + * write_lock to write the selectors back; taking write_lock first + * would meet it already held, by us. So the device is woken + * first and the lock is entered with nothing left to run under + * it. This is also the wake topping_send() relies on, since it + * does none of its own. + */ + CLASS(snd_usb_lock, pm)(mixer->chip); + if (pm.err < 0) + return -EIO; + + /* + * Held from the comparison to the cache update, so that two + * writers cannot reach the device in one order and the cache in + * the other. + */ + guard(mutex)(&tm->write_lock); + + /* + * The cache takes the new value BEFORE the write, not after. The + * lock cannot be held across a send, and a hand on the front + * panel during that window produces a notification the URB + * handler stores; updating afterwards would throw that away and + * leave the driver claiming a value the device had already moved + * away from. Written first, the device's own report is simply + * the last word, which is the right bias. + */ + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[elem->control] =3D=3D value) + return 0; + prev =3D tm->val[elem->control]; + tm->val[elem->control] =3D value; + } + + err =3D topping_send(tm, d->target, d->prop, value); + if (!err && d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err =3D topping_send(tm, d->target_pair, d->prop, value); + } + if (err < 0) { + /* put back what was there, unless the device has spoken */ + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] =3D=3D value) + tm->val[elem->control] =3D prev; + return err; + } + + return 1; +} + +static const struct snd_kcontrol_new topping_ctl =3D { + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .access =3D SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info =3D topping_ctl_info, + .get =3D topping_ctl_get, + .put =3D topping_ctl_put, +}; + +static int topping_add_ctl(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + int err; + + elem =3D kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer =3D tm->mixer; + elem->head.id =3D 0; + elem->control =3D idx; + elem->channels =3D 1; + elem->val_type =3D USB_MIXER_BESPOKEN; + + kctl =3D snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free =3D snd_usb_mixer_elem_free; + kctl->tlv.p =3D tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err =3D snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] =3D kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm =3D mixer->private_data; + + if (!tm) + return; + cancel_delayed_work_sync(&tm->keepalive); + if (tm->urb) + usb_kill_urb(tm->urb); +} + +static int topping_resume(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm =3D mixer->private_data; + unsigned int noio; + int err; + + if (!tm) + return 0; + + /* + * Everything below runs without I/O reclaim: usb_interrupt_msg() + * allocates a URB of its own with GFP_KERNEL, so asking for the + * frame buffer politely is not enough, and reclaim here can wait + * on a block device that has not woken yet. + */ + noio =3D memalloc_noio_save(); + + if (tm->urb) { + err =3D usb_submit_urb(tm->urb, GFP_NOIO); + if (err < 0) { + memalloc_noio_restore(noio); + return err; + } + } + + /* + * Subscribing again is not a formality: the device stops + * reporting to a host it has not heard from, and asking for the + * state refreshes a cache that may have gone stale while the + * panel was reachable and this driver was not. + */ + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1); + schedule_delayed_work(&tm->keepalive, + msecs_to_jiffies(TOPPING_KEEPALIVE_MS)); + memalloc_noio_restore(noio); + return 0; +} + +static void topping_private_free(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm =3D mixer->private_data; + + if (!tm) + return; + cancel_delayed_work_sync(&tm->keepalive); + if (tm->urb) { + usb_kill_urb(tm->urb); + usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF, + tm->inbuf, tm->inbuf_dma); + usb_free_urb(tm->urb); + } + if (tm->claimed) { + snd_usb_release_iface(tm->iface); + usb_put_intf(tm->iface); + } + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + mixer->private_data =3D NULL; +} + +/* the HID interface, by class rather than by a number in a comment */ +static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip, + int *ep_in, int *ep_out, + int *interval) +{ + struct usb_device *dev =3D chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i =3D 0; i < 256; i++) { + iface =3D usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts =3D &iface->altsetting[0]; + if (alts->desc.bInterfaceClass !=3D USB_CLASS_HID) + continue; + *ep_in =3D *ep_out =3D 0; + for (e =3D 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep =3D &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in =3D usb_endpoint_num(ep); + *interval =3D ep->bInterval; + } else { + *ep_out =3D usb_endpoint_num(ep); + } + } + if (*ep_in && *ep_out) + return iface; + } + return NULL; +} + +int snd_topping_init(struct usb_mixer_interface *mixer) +{ + struct snd_usb_audio *chip =3D mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in =3D 0, ep_out =3D 0, interval =3D 5; + int i, err; + + iface =3D topping_find_iface(chip, &ep_in, &ep_out, &interval); + if (!iface) { + usb_audio_err(chip, "Topping: no vendor HID interface\n"); + return 0; /* not fatal: the card still plays */ + } + if (usb_interface_claimed(iface)) { + usb_audio_err(chip, + "Topping: the HID interface is already claimed\n"); + return 0; + } + + tm =3D kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer =3D mixer; + tm->iface =3D iface; + tm->ctls =3D topping_m62_ctls; + tm->num_ctls =3D ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in =3D usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out =3D usb_sndintpipe(chip->dev, ep_out); + tm->interval =3D interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val =3D kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl =3D kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err =3D -ENOMEM; + goto fail; + } + + err =3D snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed =3D true; + /* + * Claiming does not keep the interface alive. On unplug the core + * deletes and puts every interface, and this pointer is handed + * back to it later, so it holds a reference of its own until then. + */ + usb_get_intf(iface); + + tm->urb =3D usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err =3D -ENOMEM; + goto fail; + } + tm->inbuf =3D usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err =3D -ENOMEM; + goto fail; + } + usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in, + tm->inbuf, TOPPING_EP_BUF, + topping_urb_complete, tm, tm->interval); + tm->urb->transfer_dma =3D tm->inbuf_dma; + tm->urb->transfer_flags |=3D URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data =3D tm; + mixer->private_free =3D topping_private_free; + mixer->private_suspend =3D topping_suspend; + mixer->private_resume =3D topping_resume; + + for (i =3D 0; i < tm->num_ctls; i++) { + err =3D topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err =3D usb_submit_urb(tm->urb, GFP_KERNEL); + if (err < 0) { + usb_audio_err(chip, "Topping: cannot listen: %d\n", err); + return err; + } + + /* + * Subscribe, then ask for the state. The device answers in two + * waves -- identification at once, the gains about 3.7 s later, + * which is the same delay a phantom rail takes to settle -- so + * nothing here waits for them: each value lands through the URB + * and notifies its own control. + */ + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1); + schedule_delayed_work(&tm->keepalive, + msecs_to_jiffies(TOPPING_KEEPALIVE_MS)); + return 0; + +fail: + if (tm->claimed) { + snd_usb_release_iface(iface); + usb_put_intf(iface); + } + if (tm->inbuf) + usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf, + tm->inbuf_dma); + usb_free_urb(tm->urb); + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + return err; +} diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h new file mode 100644 index 000000000000..15e16b509eb9 --- /dev/null +++ b/sound/usb/mixer_topping.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef __USB_MIXER_TOPPING_H +#define __USB_MIXER_TOPPING_H + +int snd_topping_init(struct usb_mixer_interface *mixer); + +#endif /* __USB_MIXER_TOPPING_H */ diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h index c49709d7ad25..16b8c6a8031c 100644 --- a/sound/usb/usbaudio.h +++ b/sound/usb/usbaudio.h @@ -80,6 +80,10 @@ struct snd_usb_audio { =20 #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) =20 +int snd_usb_claim_iface(struct snd_usb_audio *chip, + struct usb_interface *iface); +void snd_usb_release_iface(struct usb_interface *iface); + #define usb_audio_err(chip, fmt, args...) \ dev_err(&(chip)->dev->dev, fmt, ##args) #define usb_audio_err_ratelimited(chip, fmt, args...) \ --=20 2.55.0 From nobody Mon Sep 28 06:35:37 2026 Received: from mail-lf1-f41.google.com (mail-lf1-f41.google.com [209.85.167.41]) (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 066A53FC5AA for ; Tue, 25 Aug 2026 11:12:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.167.41 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787656376; cv=none; b=Al/pPsjtR2KR8SrJcPfh109xp9Rsv0hmzp/i2JSI8G2fOA5JApVDZ7NiGTOJtlrkO5NFt062EpLKosM/VQxwMl6/eTv++wojInQOY3+jYKnvuMElaDw9FQjQNYYu5bBev4DndWDvRx8yRrY0wjPm//t0AEwNANqmIimh9NqFUQE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787656376; c=relaxed/simple; bh=vMtWDNwbwcpelyaCoK5Kmwd4wqMxpsqiZD7dK5PEs7s=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=lL+aF77/I1hXKb6tbvAiqPoz8KGSULFWSsC4fmQfSq44Pu7ULDkuOeAAlA3pvzTEvy+kcImxRh68yNmdv1SxKP4yq8v5BNG8DHfYKHfqvVfHnVH7s+3mqBYHIR9BJWdlGvrw070rfDW99Hz9cfd52nWQRgUuwE3TeEcGzs4DWN8= 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=U/JbF3sG; arc=none smtp.client-ip=209.85.167.41 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="U/JbF3sG" Received: by mail-lf1-f41.google.com with SMTP id 2adb3069b0e04-5b011edaf7dso5148385e87.2 for ; Tue, 25 Aug 2026 04:12:53 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787656372; x=1788261172; darn=vger.kernel.org; h=content-transfer-encoding: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=lymENNeZhP352EEGP9sBVket9fGxGtuJScsbcL5Oefo=; b=U/JbF3sGiE98wRd9XSfDTNCO22ziIQ8ccviO053ukdTu/ZyZ2U9Ie4PwC7dOjnaIhp FC5SG5BTuhVKbRxbZ39WxCibShVX9J0f7M3F0UoLjj98Q9K3S8uM9px2Jm1O7smYNmFI VoxFD+NKrGwq48Vir4vupeG7UZXttUh3ahC907cLj1CyB6hLle1wC+tWMGTkHcj2if47 AijOuQKpmN4rUntbC+R6jcYY01Hc9Hk3wRFU1No5rhs3HGGLr/laK5PtWQZvE9KXNPsB iTkeUrEBDq1oKFeDY220hNau5kEFK+yZNEfRWOJeyJrPhfFqcdImnmB1YAI/F4L0Nmj1 1B3Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787656372; x=1788261172; h=content-transfer-encoding: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=lymENNeZhP352EEGP9sBVket9fGxGtuJScsbcL5Oefo=; b=CCI2Sog8TzAijgcHqjo4VD9qi+q8ip6AvF65yrkDHHaqoSX4WbNMFeT9RFjTkDHLGl xkeuhPYVtVMss03Xa3yqPPxtKJ1z+tFQL7Ol8V6SvZ/E6hOZQkw6NZZ0iW+D0cglD9uk bhE4O50LScjz6geyuuqlAzp4EBCzkCVk8Cfth6K9eBqL9q1K239vPVBnTw2Vbvp7CAz0 TG3BptrT/cQLNv98j+2OjMpMQfh9moDC8LZ5rTGOHTW1yxFM9kk+nr8Mjre0zfJLqMih 260hFcEwsF3oxlhdYDsG3UTkQHQ1yxWuSCW/fLaEOCRJW+EuZvmssWmfjMM1TCOD+CDW QN7A== X-Forwarded-Encrypted: i=1; AHgh+RoX4pwTM65BxUAAPn9sazd3tDnbPkgzSwpQ84kUd92uiz5IPJb/Vec6oBrrWhGcIWUd3RyQYkDY9un9++s=@vger.kernel.org X-Gm-Message-State: AFuF++mxWX664TdPpfEcl8u6OcI4/yseQ1jnpCkoxhkojfV4tQW/JBsa bbojI3u4IrgEoZcjlwCxKERRFK8c1KqYa+3aAb7AhEpGlrcF+KPBlcml X-Gm-Gg: AR+sD12mgC+6SiPqvOVCdTy9QwhbdNxjQ1RkPknZb3oXrmIT9BroOk9f7vSmW4xOwfr BUdGKNu+XKHFKNPRBGedxeLO3NOXz6lnOiE0cp3l7V8lE2kSvs38DeANNhS5EqeOWQTB+6qyic3 +P8F6xOnMsbavn7bmuUQuuUYbrUA3ZVmhmK88T8TYo8F6hnxJ6UzXy8/jTCp8yV6AS7yHDmER8n 8x2m8zFxQDS+tsV5JFrIW9Xdr/g/BWkVPmU25JlMyHPGzbGrCBbKTtjJaQb/Rjh12ZJ1naGPHEX Ym4Jl2n1hixutQVotLfLRbblkxIfYleoLn/hwnwEP2o1vBhyrLGKVvl4Ksdn59Ffm6yhg4qZm+N sPA4KmIA96P8xjm+pRElf0xQAI3Mf2pL4oTWALXt4woghCp5d3fIwILUsYuo6dOJhUH883c2XAL qVWGGGxDAYZfOG+96N08zR5qPBlVKNTpJOnnHXZLFbZClxkxDM2oAg4QcADgc363ehDwSEAMbHE +yfFjdlcz2OIGGEtyFQD22JKt9Jz5QtYk6zAdpSqlB3TisU0Q96PMFw32KGT+Lfld/GCvA= X-Received: by 2002:a05:6512:118a:b0:5b2:9f7a:29aa with SMTP id 2adb3069b0e04-5b4841a9e27mr10019236e87.0.1787656371650; Tue, 25 Aug 2026 04:12:51 -0700 (PDT) Received: from localhost ([188.234.148.119]) by smtp.gmail.com with ESMTPSA id 2adb3069b0e04-5b48ce3a89fsm2337244e87.83.2026.08.25.04.12.49 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 25 Aug 2026 04:12:51 -0700 (PDT) From: Mikhail Gavrilov To: tiwai@suse.com Cc: perex@perex.cz, jikos@kernel.org, bentiss@kernel.org, linux-sound@vger.kernel.org, linux-input@vger.kernel.org, linux-kernel@vger.kernel.org, Mikhail Gavrilov Subject: [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Date: Tue, 25 Aug 2026 16:12:39 +0500 Message-ID: <20260825111239.24834-3-mikhail.v.gavrilov@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260825111239.24834-1-mikhail.v.gavrilov@gmail.com> References: <20260825085659.52675-1-mikhail.v.gavrilov@gmail.com> <20260825111239.24834-1-mikhail.v.gavrilov@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Each output on this card has a source selector: it can take one of the three internal mixes, or any input, or any playback bus straight from USB. Which it is decides everything downstream -- point the headphones at a playback bus and the card's mixer leaves the path entirely, along with every question about what is summed into it. That matters more here than it would elsewhere. There is no control panel for the M62 on Linux, so a user who never runs the vendor's application on another machine has no way to see or change this, and inherits whatever the card was last told. One enumerated control per output gives them the whole choice, and it does so without exposing the sixty-cell mixer matrix, which without a graphical representation would confuse far more than it helps. The item list has "Unknown" first, and it is deliberate rather than tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not to the vendor's own application, which on connect pushes its entire workspace to the card rather than asking it anything. So the current setting cannot be learned at probe, and saying so is the only honest thing a control can do until a hand has chosen. Selecting "Unknown" is refused, since it is a report and not a choice. The numbering the card uses has a hole where 4 and 5 would be, so the item index and the value written are kept as separate tables rather than one being computed from the other. Writing "Unknown" changes nothing and says so quietly. It is what the control reports until a hand has chosen, and alsactl stores and restores it like any other value, so refusing it would fail a restore of the driver's own report -- once at every boot, and again whenever a saved state is put back over a chosen one. The choice is written again on resume. The gains need no such help -- the device announces them and the cache re-syncs by itself -- but a selector is never reported, so if the card came up on its own defaults while the host slept, the driver's idea of it would be silently wrong and writing the remembered value back would look like no change at all. This write wakes the device before taking the lock for the same reason the gain controls do: the wake can run the resume callback on this thread, and that callback writes the selectors back under the same lock. Signed-off-by: Mikhail Gavrilov --- sound/usb/mixer_topping.c | 162 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c index 933a8bbe57b9..7d6ba7257807 100644 --- a/sound/usb/mixer_topping.c +++ b/sound/usb/mixer_topping.c @@ -127,6 +127,45 @@ static const struct topping_ctl_desc topping_m62_ctls[= ] =3D { topping_tlv_out_0 }, }; =20 +/* + * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs + * and the loopback returns, and it has a hole where 4 and 5 would be, + * so the index of a control item is not the value the card wants and + * the two are kept side by side. + * + * "Unknown" is first and is not a choice: the device NEVER reports a + * selector, not to us and not to the vendor's own application, which + * pushes its whole workspace on connect rather than asking. So a + * driver cannot learn where an output is pointing, and the only honest + * thing it can show until a hand has chosen is that it does not know. + */ +static const char * const topping_sources[] =3D { + "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2", + "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4", + "Playback 5/6", "Playback 7/8", "Playback 9/10", +}; + +static const u8 topping_source_value[] =3D { + 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, +}; + +struct topping_enum_desc { + const char *name; + u8 target; + u8 prop; +}; + +/* + * The selector answers on ONE target of an output's pair, unlike the + * volume and the mute which must be written to both. + */ +static const struct topping_enum_desc topping_m62_enums[] =3D { + { "Headphone Playback Source", 0x64, 0x02 }, + { "OTG Playback Source", 0x62, 0x02 }, +}; + +#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums) + struct topping_mixer { struct usb_mixer_interface *mixer; struct usb_interface *iface; @@ -143,6 +182,7 @@ struct topping_mixer { spinlock_t lock; /* guards val[] against the URB */ int *val; struct snd_kcontrol **kctl; + int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */ }; =20 static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -387,6 +427,71 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } =20 +static int topping_sel_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources), + topping_sources); +} + +static int topping_sel_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem =3D kctl->private_data; + struct topping_mixer *tm =3D elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] =3D tm->sel[elem->control]; + return 0; +} + +static int topping_sel_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem =3D kctl->private_data; + struct topping_mixer *tm =3D elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item =3D ucontrol->value.enumerated.item[0]; + if (item >=3D ARRAY_SIZE(topping_sources)) + return -EINVAL; + + /* the wake before the lock, for the reason given in _ctl_put */ + CLASS(snd_usb_lock, pm)(elem->head.mixer->chip); + if (pm.err < 0) + return -EIO; + + guard(mutex)(&tm->write_lock); + + /* + * "Unknown" is what this control reports until a hand has chosen, + * and alsactl stores and restores it like any other value. It is + * not a choice, so writing it changes nothing -- quietly, rather + * than failing a restore of the driver's own report. + */ + if (!item || tm->sel[elem->control] =3D=3D item) + return 0; + + d =3D &topping_m62_enums[elem->control]; + err =3D topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] =3D item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel =3D { + .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, + .access =3D SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info =3D topping_sel_info, + .get =3D topping_sel_get, + .put =3D topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl =3D { .iface =3D SNDRV_CTL_ELEM_IFACE_MIXER, .access =3D SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -429,6 +534,57 @@ static int topping_add_ctl(struct topping_mixer *tm, i= nt idx) return 0; } =20 +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem =3D kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer =3D tm->mixer; + elem->head.id =3D 0; + elem->control =3D idx; + elem->channels =3D 1; + elem->val_type =3D USB_MIXER_BESPOKEN; + + kctl =3D snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free =3D snd_usb_mixer_elem_free; + strscpy(kctl->id.name, topping_m62_enums[idx].name, + sizeof(kctl->id.name)); + + return snd_usb_mixer_add_control(&elem->head, kctl); +} + +/* + * The gains come back by themselves, since the device announces them, + * but a selector is never reported: if the card came up on its own + * defaults while the host slept, this driver's idea of it would be + * silently wrong, and writing the remembered value would then look + * like no change at all. So the choice a hand made is written again + * -- which is what the mixer core does for every control that is not + * marked as the driver's own to handle. + */ +static void topping_restore_sel(struct topping_mixer *tm) +{ + const struct topping_enum_desc *d; + int i; + + guard(mutex)(&tm->write_lock); + for (i =3D 0; i < TOPPING_NUM_ENUMS; i++) { + if (!tm->sel[i]) + continue; /* nothing was ever chosen */ + d =3D &topping_m62_enums[i]; + topping_send(tm, d->target, d->prop, + topping_source_value[tm->sel[i]]); + } +} + static void topping_suspend(struct usb_mixer_interface *mixer) { struct topping_mixer *tm =3D mixer->private_data; @@ -473,6 +629,7 @@ static int topping_resume(struct usb_mixer_interface *m= ixer) */ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1); + topping_restore_sel(tm); schedule_delayed_work(&tm->keepalive, msecs_to_jiffies(TOPPING_KEEPALIVE_MS)); memalloc_noio_restore(noio); @@ -618,6 +775,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i =3D 0; i < TOPPING_NUM_ENUMS; i++) { + err =3D topping_add_sel(tm, i); + if (err < 0) + return err; + } =20 err =3D usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { --=20 2.55.0