* snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) @ 2026-08-12 17:10 Mikhail Gavrilov 2026-08-13 7:24 ` Takashi Iwai 0 siblings, 1 reply; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-12 17:10 UTC (permalink / raw) To: linux-sound; +Cc: tiwai, perex, g, jikos, bentiss, linux-input Hello, I would like to add ALSA mixer controls for the analogue input gain of a USB audio interface whose control channel is a vendor-specific HID interface, and I would like to agree on the shape before writing code, because it crosses into drivers/hid. The device is a Topping Professional M62, USB 152a:875c. In its multichannel modes it presents 10 playback and 16 capture channels on interfaces 1 and 2, a DFU interface, and interface 4 of class 3 (HID) with a vendor-defined usage page, one 16-byte Input report and one 16-byte Output report, and no report IDs. The problem: the microphone preamplifier gain, 0..88 dB per the specification, is not reachable through ALSA. The card does expose a 'Mic Capture Volume', but a gain ladder measured in silence shows that control to be a digital trim after the converter. The recorded noise floor is flat at about -172 dBFS at the bottom of its range, far below any converter's own noise floor, so what is being measured there is the sample word running out of bits; above that the floor rises with unity slope, i.e. one fixed analogue noise being divided down. The analogue stage is reachable only over the HID interface, which is what the vendor's own application uses. I have the protocol. It was reverse engineered from captures of the vendor application's traffic, the same way sound/usb/mixer_scarlett2.c describes in its header. Frames are 15 bytes: 22 33 | 20 01 01 | target | property | s32 big endian | CRC | 66 77 with CRC-16/MODBUS over bytes 2..10, stored big endian. Rebuilding every frame of a capture from the decoded fields reproduces all 2619 of them byte for byte. Inbound reports are that frame plus one pad byte. The device stays silent until the host sends a subscription frame, after which it reports every state change including front-panel button presses, and it answers a "report your state" frame with a full dump. The analogue gain of each microphone input is a single property carrying whole decibels, 0..88, so a plain TLV_DB_SCALE fits it. The constraint, and my question. This device accepts nothing on the control pipe: SET_REPORT and GET_REPORT both stall with EPIPE, for report types Output, Input and Feature alike. So the pattern used by snd_soundblaster_e1_switch_update() in sound/usb/mixer_quirks.c, which sends HID_REQ_SET_REPORT through snd_usb_ctl_msg(), is not available here. The only usable transport is the interrupt endpoints of interface 4, which usbhid binds. Would it be acceptable for a mixer quirk in sound/usb to own that interface? Concretely: an entry in hid_ignore_list so that usbhid stays away, the quirk claiming interface 4, an interrupt IN URB whose completion handler parses the vendor frame, updates cached values and calls snd_ctl_notify(), and usb_interrupt_msg() in the put callbacks. The notification half looks like what snd_usb_mixer_status_create() already does for the audio control interface's status endpoint. Or would you prefer a different layout for this? A first patch would be deliberately minimal: two controls for the analogue gain of the two microphone inputs, with a dB TLV, and nothing else. The line-level inputs and the outputs use index scales with a piecewise taper, which I have measured but would rather submit separately. For context, an ALSA UCM configuration for the same card is already proposed as alsa-project/alsa-ucm-conf#826. That is what would designate the new control as the capture volume, so that userspace moves the hardware gain instead of the digital trim. I can post the full protocol notes and the captures if that would be useful. -- Thanks, Mikhail Gavrilov ^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) 2026-08-12 17:10 snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) Mikhail Gavrilov @ 2026-08-13 7:24 ` Takashi Iwai 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov 0 siblings, 1 reply; 29+ messages in thread From: Takashi Iwai @ 2026-08-13 7:24 UTC (permalink / raw) To: Mikhail Gavrilov Cc: linux-sound, tiwai, perex, g, jikos, bentiss, linux-input On Wed, 12 Aug 2026 19:10:34 +0200, Mikhail Gavrilov wrote: > > Hello, > > I would like to add ALSA mixer controls for the analogue input gain of a > USB audio interface whose control channel is a vendor-specific HID > interface, and I would like to agree on the shape before writing code, > because it crosses into drivers/hid. > > The device is a Topping Professional M62, USB 152a:875c. In its > multichannel modes it presents 10 playback and 16 capture channels on > interfaces 1 and 2, a DFU interface, and interface 4 of class 3 (HID) > with a vendor-defined usage page, one 16-byte Input report and one > 16-byte Output report, and no report IDs. > > The problem: the microphone preamplifier gain, 0..88 dB per the > specification, is not reachable through ALSA. The card does expose a > 'Mic Capture Volume', but a gain ladder measured in silence shows that > control to be a digital trim after the converter. The recorded noise > floor is flat at about -172 dBFS at the bottom of its range, far below > any converter's own noise floor, so what is being measured there is the > sample word running out of bits; above that the floor rises with unity > slope, i.e. one fixed analogue noise being divided down. The analogue > stage is reachable only over the HID interface, which is what the > vendor's own application uses. > > I have the protocol. It was reverse engineered from captures of the > vendor application's traffic, the same way sound/usb/mixer_scarlett2.c > describes in its header. Frames are 15 bytes: > > 22 33 | 20 01 01 | target | property | s32 big endian | CRC | 66 77 > > with CRC-16/MODBUS over bytes 2..10, stored big endian. Rebuilding > every frame of a capture from the decoded fields reproduces all 2619 of > them byte for byte. Inbound reports are that frame plus one pad byte. > The device stays silent until the host sends a subscription frame, > after which it reports every state change including front-panel button > presses, and it answers a "report your state" frame with a full dump. > The analogue gain of each microphone input is a single property carrying > whole decibels, 0..88, so a plain TLV_DB_SCALE fits it. > > The constraint, and my question. This device accepts nothing on the > control pipe: SET_REPORT and GET_REPORT both stall with EPIPE, for > report types Output, Input and Feature alike. So the pattern used by > snd_soundblaster_e1_switch_update() in sound/usb/mixer_quirks.c, which > sends HID_REQ_SET_REPORT through snd_usb_ctl_msg(), is not available > here. The only usable transport is the interrupt endpoints of interface > 4, which usbhid binds. > > Would it be acceptable for a mixer quirk in sound/usb to own that > interface? Concretely: an entry in hid_ignore_list so that usbhid stays > away, the quirk claiming interface 4, an interrupt IN URB whose > completion handler parses the vendor frame, updates cached values and > calls snd_ctl_notify(), and usb_interrupt_msg() in the put callbacks. > The notification half looks like what snd_usb_mixer_status_create() > already does for the audio control interface's status endpoint. Or would > you prefer a different layout for this? > > A first patch would be deliberately minimal: two controls for the > analogue gain of the two microphone inputs, with a dB TLV, and nothing > else. The line-level inputs and the outputs use index scales with a > piecewise taper, which I have measured but would rather submit > separately. > > For context, an ALSA UCM configuration for the same card is already > proposed as alsa-project/alsa-ucm-conf#826. That is what would designate > the new control as the capture volume, so that userspace moves the > hardware gain instead of the digital trim. > > I can post the full protocol notes and the captures if that would be > useful. I believe we can judge better with the comparison of the actual code. You can try implementing PoC's for both usb-audio mixer quirk and a HID driver, then compare which would fit better. If either of them looks significantly harder, you don't fulfill the implementation, of course. My gut feeling is that we can take it as a mixer quirk, but it really depends on the complexity. thanks, Takashi ^ permalink raw reply [flat|nested] 29+ messages in thread
* [RFC 0/2] Two ways to reach the Topping M62's analogue gains 2026-08-13 7:24 ` Takashi Iwai @ 2026-08-20 15:13 ` Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (4 more replies) 0 siblings, 5 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-20 15:13 UTC (permalink / raw) To: tiwai, jikos, bentiss; +Cc: perex, linux-sound, linux-input, linux-kernel You asked for PoCs of both roads and a comparison of the actual code rather than of arguments. Here are both. They are alternatives, not a series: each is written against mainline 98f21c54f995 on its own, and either can be applied alone. 1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface 2/2 HID: topping: a HID driver that registers a card of its own Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for reading, for unsolicited notification from the front panel, and for writing. What the device is ================== The M62 keeps its two microphone preamp gains, its AUX and Bluetooth input volumes and its headphone and OTG output volumes behind a vendor protocol on a HID-class interface, and exposes none 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: a noise-floor ladder against the card shows the converter's own floor rising with the signal. So on Linux today the one knob worth setting is the one that cannot be reached, and a measurement application has to begin by asking a human to touch the front panel. The protocol is fifteen-byte frames -- 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. 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 and a second makes it announce its whole state, after which every change arrives unasked, including a front panel press. The control pipe is not an option: GET_REPORT and SET_REPORT stall with EPIPE for every report type, so the interrupt endpoints on the HID interface are the only route. What is identical in both ========================= The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...) is CRC-16/MODBUS, so no private table), and the control table. A knob is a row of { name, target, paired target, property, min, max, TLV } so adding one is adding a row. Six rows today. The outputs come in pairs because the device answers on only one target of each pair and the other would drift away unheard. Where they differ ================= 1/2 claims the HID interface for snd-usb-audio and puts the elements on the card the device already has. The cost is two-sided: an entry in hid_ignore_list to keep usbhid off the interface, and one new helper in sound/usb/card.c, because usb_audio_driver is static there and a quirk cannot claim an interface without it. That helper is the only change in 1/2 outside the new file and its dispatch. Nothing is lost by taking the interface: the report descriptor is a Generic Desktop application collection with eight unnamed usages, sixteen bytes in and out and no report ID, so hid-generic can only build an input device for a mouse that does not exist -- which is what it does today. 2/2 binds as a HID driver, and the protocol half is if anything smaller there: usbhid owns the endpoints, so hid_hw_output_report replaces a hand-built interrupt URB out, raw_event replaces the one in, and no interface has to be claimed. It needs nothing in sound/usb. But these are mixer controls for an audio device, and the audio device's card belongs to snd-usb-audio. A HID driver cannot put an element there. There is no interface for it, and inventing one means exporting from sound/usb both a lookup from struct usb_device to the card and an add-element call, and then answering, for a single device, what happens when the two drivers probe in either order and when either disconnects first, given that the element would live in one module and its private data in another. So 2/2 does what a HID driver can do alone: it registers a card of its own. That works, and the cost is visible from userspace rather than theoretical: $ cat /proc/asound/cards 0 [ToppingCtl ]: Topping - Topping M62 control ... 4 [M62 ]: USB-Audio - M62 $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33 amixer: Cannot find the given element from control sysdefault:4 One device, two cards; the gains on a card with no PCM beside them; and anything that looks for a device's mixer next to its streams -- alsamixer -c, UCM profiles, PipeWire's device model -- does not find them there. Against my own preference, two honest notes. The phantom input device 2/2 leaves at boot (hid-generic binds first, the specific driver being a module outside the initramfs) is a packaging artefact, not a property of that road. And 1/2's claim helper is new API surface in sound/usb, small as it is. Field results ============= With 1/2: the interface belongs to snd-usb-audio while a neighbouring device's HID interface still belongs to usbhid, so the ignore entry is precise. Values arrive by themselves -- the headphone volume came up at 51 while the zero-initialised cache would have said 0. One front panel press produces exactly one control event. A write reaches the hardware: the device reports the written value back, and its meters answer. With 2/2: the same, on its own card. One device fact worth recording: a written gain takes effect at once, but when the device commits it to non-volatile memory is the firmware's business, and a value written and then torn off the bus can come back as the older one. Nothing in either driver depends on that -- neither treats itself as the source of truth, both ask the device -- but it is easy to mistake for a driver bug while testing. Where I come out ================ The knobs belong on the card the device already has, and 2/2 cannot put them there without a new cross-subsystem interface built for one device. 1/2's cost is one static-variable problem solved by one helper in the file that owns it. So I would take 1/2, which is also your gut feeling -- but the comparison is what you asked for, and either patch stands alone if you read it the other way. Not covered by either: the OTG input's gain. It has no front panel control and therefore never announced itself in any capture, so its property is unknown. It is one row when it is known. Mikhail ^ permalink raw reply [flat|nested] 29+ messages in thread
* [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov @ 2026-08-20 15:13 ` Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov ` (3 subsequent siblings) 4 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-20 15:13 UTC (permalink / raw) To: tiwai, jikos, bentiss Cc: perex, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Not included: OTG IN, which has no front panel control and therefore never announced itself in any capture, so its property is unknown. It is one row when it is known. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 14 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 495 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 3 + 9 files changed, 536 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index d2b7ed2..41ec6b7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27349,6 +27349,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 341bf58..092b2a9 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 +#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 8a0b51d..3c156d1 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[] = { { 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/Makefile b/sound/usb/Makefile index e62794a..151b481 100644 --- a/sound/usb/Makefile +++ b/sound/usb/Makefile @@ -14,6 +14,7 @@ snd-usb-audio-y := 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 24112e4..e99b934 100644 --- a/sound/usb/card.c +++ b/sound/usb/card.c @@ -325,6 +325,20 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int return 0; } +/* + * 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); +} + /* * parse audio control descriptor and create pcm/midi streams */ diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c index a1f5592..10f3302 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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 0000000..0c89a51 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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. OTG IN is missing on purpose: it has no front panel control, + * so it never announced itself in any capture, and its property is not + * known. It is one row when it is. + */ +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[] = { + { "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 }, + { "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; + 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; + 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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +static int topping_send(struct topping_mixer *tm, u8 target, u8 prop, + s32 value) +{ + u8 *buf; + int err, actual; + + buf = kzalloc(TOPPING_EP_BUF, GFP_KERNEL); + if (!buf) + return -ENOMEM; + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + kfree(buf); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = urb->transfer_buffer; + unsigned long flags; + int idx, value, err; + + if (urb->status) + return; /* resubmitted below only when running */ + if (urb->actual_length < TOPPING_FRAME_LEN) + goto resubmit; + if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + spin_lock_irqsave(&tm->lock, flags); + if (tm->val[idx] == value) { + spin_unlock_irqrestore(&tm->lock, flags); + goto resubmit; + } + tm->val[idx] = value; + spin_unlock_irqrestore(&tm->lock, flags); + + if (tm->kctl[idx]) + snd_ctl_notify(tm->mixer->chip->card, + SNDRV_CTL_EVENT_MASK_VALUE, + &tm->kctl[idx]->id); + +resubmit: + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err < 0 && err != -ENODEV && err != -ESHUTDOWN) + usb_audio_err(tm->mixer->chip, + "Topping: cannot resubmit: %d\n", err); +} + +static int topping_ctl_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + unsigned long flags; + + spin_lock_irqsave(&tm->lock, flags); + ucontrol->value.integer.value[0] = tm->val[elem->control]; + spin_unlock_irqrestore(&tm->lock, flags); + return 0; +} + +static int topping_ctl_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + unsigned long flags; + int value, err; + + value = ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + spin_lock_irqsave(&tm->lock, flags); + if (tm->val[elem->control] == value) { + spin_unlock_irqrestore(&tm->lock, flags); + return 0; + } + spin_unlock_irqrestore(&tm->lock, flags); + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + spin_lock_irqsave(&tm->lock, flags); + tm->val[elem->control] = value; + spin_unlock_irqrestore(&tm->lock, flags); + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_private_free(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = mixer->private_data; + + if (!tm) + return; + 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); + } + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + mixer->private_data = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + + tm->urb = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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); + return 0; + +fail: + 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 0000000..15e16b5 --- /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 c49709d..4f9770a 100644 --- a/sound/usb/usbaudio.h +++ b/sound/usb/usbaudio.h @@ -80,6 +80,9 @@ struct snd_usb_audio { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +int snd_usb_claim_iface(struct snd_usb_audio *chip, + 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...) \ -- 2.43.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [RFC 2/2] HID: topping: driver for the M62's vendor control channel 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-20 15:13 ` Mikhail Gavrilov 2026-08-21 11:23 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov ` (2 subsequent siblings) 4 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-20 15:13 UTC (permalink / raw) To: tiwai, jikos, bentiss Cc: perex, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov The alternative to doing this as a snd-usb-audio mixer quirk, written so the two can be compared rather than argued about. The device and the protocol are the same: fifteen-byte frames carrying a target, a property and a signed value, CRC-16/MODBUS over the middle, one write to subscribe and one to make the device announce its state, after which every change arrives unasked, front panel presses included. That part is if anything smaller here than in the quirk -- usbhid owns the endpoints, so hid_hw_output_report replaces a hand-built interrupt URB out and raw_event replaces the one in, and the interface needs no claiming because this driver is what binds to it. The control table is identical, so adding a knob is still adding a row. WHERE THIS ROAD RUNS OUT is the reason to write it. These are mixer controls for an audio device, and the audio device's card belongs to snd-usb-audio. A HID driver cannot put an element there: there is no interface for it, and inventing one means exporting from sound/usb both a lookup from struct usb_device to the card and an add-element call -- and then answering, for a single device, what happens when the two drivers probe in either order, and when either disconnects first, given that the element would live in one module and its private data in another. So this driver does what a HID driver can do alone: it registers a card of its own. That works, and the cost is visible from userspace rather than theoretical -- one physical device appears as two cards, the gains land on a card with no PCM beside them, and anything that looks for a device's mixer next to its streams (alsamixer -c, UCM profiles, PipeWire's device model) does not find them there. hid_hw_start is called with no connect mask on purpose. The report descriptor is a Generic Desktop application collection with eight unnamed usages and no report ID, so hid-generic would build an input device for a mouse that does not exist. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/Kconfig | 13 ++ drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-topping.c | 382 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 drivers/hid/hid-topping.c diff --git a/MAINTAINERS b/MAINTAINERS index d2b7ed2..ec302fa 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27349,6 +27349,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 HID CONTROL DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/hid/hid-topping.c + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index aa7fa11..712390a 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -1283,6 +1283,19 @@ config HID_TIVO help Say Y if you have a TiVo Slide Bluetooth remote control. +config HID_TOPPING + tristate "Topping M62 vendor control channel" + depends on USB_HID + depends on SND + help + Say Y here if you have a Topping M62 audio interface and want + its analogue input gains and output volumes as ALSA mixer + controls. The device keeps them behind a vendor protocol on a + HID interface and exposes none of them through USB audio. + + Note that the controls appear on a card of this driver's own, + not on the card snd-usb-audio creates for the same device. + config HID_TOPSEED tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 48a863b..6e1f8fc 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o obj-$(CONFIG_HID_TIVO) += hid-tivo.o +obj-$(CONFIG_HID_TOPPING) += hid-topping.o obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o obj-$(CONFIG_HID_TOPRE) += hid-topre.o obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 341bf58..092b2a9 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 +#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-topping.c b/drivers/hid/hid-topping.c new file mode 100644 index 0000000..9d58bce --- /dev/null +++ b/drivers/hid/hid-topping.c @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for the Topping M62's vendor control channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * The same device and the same protocol as the snd-usb-audio mixer + * quirk this is meant to be compared against; only the road differs. + * Here the vendor channel is taken as what it claims to be -- a HID + * interface -- and the driver rides usbhid instead of claiming the + * interface for the audio driver. + * + * The protocol: fifteen-byte frames, + * + * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77 + * + * a target TT, a property PP of it, CRC-16/MODBUS over bytes 2..10 + * stored big-endian. Reports arriving are the same plus a pad byte. + * One write of 0x11/0x24 subscribes, one of 0x11/0x26 makes the device + * announce its whole state, and every later change arrives unasked, + * including a front panel press. + * + * WHERE THIS ROAD RUNS OUT, which is the point of writing it: these + * are mixer controls for an audio device, and the audio device's card + * belongs to snd-usb-audio. A HID driver cannot put a control there. + * There is no interface for it, and inventing one means exporting from + * sound/usb both a way to find the card behind a struct usb_device and + * a way to add an element to it -- and then answering, for a single + * device, what happens when the two drivers probe in either order and + * when either disconnects first, since the control's private data + * would live in this module while the element lives in that card. + * + * So this driver does what a HID driver CAN do by itself: it makes a + * card of its own. That works, and it is exactly the wart to weigh -- + * one physical device shows up twice in userspace, the gains land on a + * card that has no PCM, and anything that looks for a device's mixer + * beside its streams (alsamixer -c, UCM, PipeWire) does not find them + * there. + */ + +#include <linux/crc16.h> +#include <linux/hid.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/unaligned.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/initval.h> +#include <sound/tlv.h> + +#include "hid-ids.h" + +#define TOPPING_FRAME_LEN 15 +#define TOPPING_REPORT_LEN 16 + +#define TOPPING_TT_DEVICE 0x11 +#define TOPPING_PP_SUBSCRIBE 0x24 +#define TOPPING_PP_ANNOUNCE 0x26 + +static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0); + +static const unsigned int topping_tlv_out_9[] = { + 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[] = { + 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), +}; + +/* identical to the quirk's: a knob is a row */ +struct topping_ctl_desc { + const char *name; + u8 target; + u8 target_pair; + u8 prop; + int min, max; + const unsigned int *tlv; +}; + +static const struct topping_ctl_desc topping_m62_ctls[] = { + { "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 }, + { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99, + topping_tlv_out_9 }, + { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99, + topping_tlv_out_0 }, +}; + +#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls) + +struct topping_hid { + struct hid_device *hdev; + struct snd_card *card; + spinlock_t lock; /* guards val[] against raw_event */ + int val[TOPPING_NUM_CTLS]; + struct snd_kcontrol *kctl[TOPPING_NUM_CTLS]; +}; + +static void topping_build(u8 *f, u8 target, u8 prop, s32 value) +{ + f[0] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + put_unaligned_be16(crc16(0xffff, f + 2, 9), f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +static int topping_send(struct topping_hid *th, u8 target, u8 prop, + s32 value) +{ + u8 *buf; + int err; + + buf = kzalloc(TOPPING_FRAME_LEN, GFP_KERNEL); + if (!buf) + return -ENOMEM; + topping_build(buf, target, prop, value); + err = hid_hw_output_report(th->hdev, buf, TOPPING_FRAME_LEN); + kfree(buf); + if (err < 0) + hid_err(th->hdev, "write %02x/%02x failed: %d\n", + target, prop, err); + return err; +} + +static int topping_index_of(u8 target, u8 prop) +{ + int i; + + for (i = 0; i < TOPPING_NUM_CTLS; i++) + if (topping_m62_ctls[i].target == target && + topping_m62_ctls[i].prop == prop) + return i; + return -1; +} + +static int topping_raw_event(struct hid_device *hdev, + struct hid_report *report, u8 *f, int size) +{ + struct topping_hid *th = hid_get_drvdata(hdev); + unsigned long flags; + int idx, value; + + if (size < TOPPING_FRAME_LEN) + return 0; + if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + return 0; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + return 0; + + idx = topping_index_of(f[5], f[6]); + if (idx < 0) + return 0; + + value = get_unaligned_be32(f + 7); + if (value < topping_m62_ctls[idx].min || + value > topping_m62_ctls[idx].max) + return 0; + + spin_lock_irqsave(&th->lock, flags); + if (th->val[idx] == value) { + spin_unlock_irqrestore(&th->lock, flags); + return 0; + } + th->val[idx] = value; + spin_unlock_irqrestore(&th->lock, flags); + + if (th->kctl[idx]) + snd_ctl_notify(th->card, SNDRV_CTL_EVENT_MASK_VALUE, + &th->kctl[idx]->id); + return 0; +} + +static int topping_ctl_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + int idx = kctl->private_value; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = topping_m62_ctls[idx].min; + uinfo->value.integer.max = topping_m62_ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct topping_hid *th = snd_kcontrol_chip(kctl); + unsigned long flags; + + spin_lock_irqsave(&th->lock, flags); + ucontrol->value.integer.value[0] = th->val[kctl->private_value]; + spin_unlock_irqrestore(&th->lock, flags); + return 0; +} + +static int topping_ctl_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct topping_hid *th = snd_kcontrol_chip(kctl); + int idx = kctl->private_value; + const struct topping_ctl_desc *d = &topping_m62_ctls[idx]; + unsigned long flags; + int value, err; + + value = ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + spin_lock_irqsave(&th->lock, flags); + if (th->val[idx] == value) { + spin_unlock_irqrestore(&th->lock, flags); + return 0; + } + spin_unlock_irqrestore(&th->lock, flags); + + err = topping_send(th, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + err = topping_send(th, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + spin_lock_irqsave(&th->lock, flags); + th->val[idx] = value; + spin_unlock_irqrestore(&th->lock, flags); + return 1; +} + +static int topping_add_ctls(struct topping_hid *th) +{ + struct snd_kcontrol_new tmpl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = topping_ctl_put, + }; + struct snd_kcontrol *kctl; + int i, err; + + for (i = 0; i < TOPPING_NUM_CTLS; i++) { + tmpl.name = topping_m62_ctls[i].name; + tmpl.private_value = i; + tmpl.tlv.p = topping_m62_ctls[i].tlv; + kctl = snd_ctl_new1(&tmpl, th); + if (!kctl) + return -ENOMEM; + err = snd_ctl_add(th->card, kctl); + if (err < 0) + return err; + th->kctl[i] = kctl; + } + return 0; +} + +static int topping_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + struct topping_hid *th; + int err; + + th = devm_kzalloc(&hdev->dev, sizeof(*th), GFP_KERNEL); + if (!th) + return -ENOMEM; + th->hdev = hdev; + spin_lock_init(&th->lock); + hid_set_drvdata(hdev, th); + + err = hid_parse(hdev); + if (err) + return err; + + /* + * No connect mask: the report descriptor describes a Generic + * Desktop application collection with eight unnamed usages, so + * letting hid-generic have it would create an input device for a + * mouse that does not exist. + */ + err = hid_hw_start(hdev, 0); + if (err) + return err; + + err = hid_hw_open(hdev); + if (err) + goto stop; + + /* + * A CARD OF ITS OWN, because the device's real card belongs to + * snd-usb-audio and nothing lets an outside module add an + * element to it. This is the cost of the HID road, and it is + * visible from userspace: two cards for one device. + */ + err = snd_card_new(&hdev->dev, SNDRV_DEFAULT_IDX1, "ToppingCtl", + THIS_MODULE, 0, &th->card); + if (err < 0) + goto close; + + strscpy(th->card->driver, "Topping", sizeof(th->card->driver)); + strscpy(th->card->shortname, "Topping M62 control", + sizeof(th->card->shortname)); + strscpy(th->card->longname, "Topping M62 vendor control channel", + sizeof(th->card->longname)); + + err = topping_add_ctls(th); + if (err < 0) + goto free_card; + + err = snd_card_register(th->card); + if (err < 0) + goto free_card; + + topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1); + return 0; + +free_card: + snd_card_free(th->card); +close: + hid_hw_close(hdev); +stop: + hid_hw_stop(hdev); + return err; +} + +static void topping_remove(struct hid_device *hdev) +{ + struct topping_hid *th = hid_get_drvdata(hdev); + + /* + * The card first: its elements call into this module, and the + * hid device under them is about to stop answering. + */ + if (th->card) + snd_card_free(th->card); + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static const struct hid_device_id topping_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) }, + { } +}; +MODULE_DEVICE_TABLE(hid, topping_devices); + +static struct hid_driver topping_driver = { + .name = "topping", + .id_table = topping_devices, + .probe = topping_probe, + .remove = topping_remove, + .raw_event = topping_raw_event, +}; +module_hid_driver(topping_driver); + +MODULE_DESCRIPTION("HID driver for the Topping M62 vendor control channel"); +MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>"); +MODULE_LICENSE("GPL"); -- 2.43.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* Re: [RFC 0/2] Two ways to reach the Topping M62's analogue gains 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov @ 2026-08-21 11:23 ` Mikhail Gavrilov 2026-08-23 8:50 ` Takashi Iwai 2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 4 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-21 11:23 UTC (permalink / raw) To: tiwai, jikos, bentiss; +Cc: perex, linux-sound, linux-input, linux-kernel On Thu, Aug 20, 2026 at 8:13 PM Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> wrote: > > You asked for PoCs of both roads and a comparison of the actual code > rather than of arguments. Here are both. They are alternatives, not a > series: each is written against mainline 98f21c54f995 on its own, and > either can be applied alone. > > 1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface > 2/2 HID: topping: a HID driver that registers a card of its own > > Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two > CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have > been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for > reading, for unsolicited notification from the front panel, and for > writing. > > What the device is > ================== > > The M62 keeps its two microphone preamp gains, its AUX and Bluetooth > input volumes and its headphone and OTG output volumes behind a vendor > protocol on a HID-class interface, and exposes none 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: a noise-floor ladder > against the card shows the converter's own floor rising with the > signal. So on Linux today the one knob worth setting is the one that > cannot be reached, and a measurement application has to begin by asking > a human to touch the front panel. > > The protocol is fifteen-byte frames -- 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. 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 and a second makes it announce its whole state, after which > every change arrives unasked, including a front panel press. > > The control pipe is not an option: GET_REPORT and SET_REPORT stall with > EPIPE for every report type, so the interrupt endpoints on the HID > interface are the only route. > > What is identical in both > ========================= > > The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...) > is CRC-16/MODBUS, so no private table), and the control table. A knob > is a row of > > { name, target, paired target, property, min, max, TLV } > > so adding one is adding a row. Six rows today. The outputs come in > pairs because the device answers on only one target of each pair and > the other would drift away unheard. > > Where they differ > ================= > > 1/2 claims the HID interface for snd-usb-audio and puts the elements on > the card the device already has. The cost is two-sided: an entry in > hid_ignore_list to keep usbhid off the interface, and one new helper in > sound/usb/card.c, because usb_audio_driver is static there and a quirk > cannot claim an interface without it. That helper is the only change in > 1/2 outside the new file and its dispatch. Nothing is lost by taking > the interface: the report descriptor is a Generic Desktop application > collection with eight unnamed usages, sixteen bytes in and out and no > report ID, so hid-generic can only build an input device for a mouse > that does not exist -- which is what it does today. > > 2/2 binds as a HID driver, and the protocol half is if anything smaller > there: usbhid owns the endpoints, so hid_hw_output_report replaces a > hand-built interrupt URB out, raw_event replaces the one in, and no > interface has to be claimed. It needs nothing in sound/usb. > > But these are mixer controls for an audio device, and the audio > device's card belongs to snd-usb-audio. A HID driver cannot put an > element there. There is no interface for it, and inventing one means > exporting from sound/usb both a lookup from struct usb_device to the > card and an add-element call, and then answering, for a single device, > what happens when the two drivers probe in either order and when either > disconnects first, given that the element would live in one module and > its private data in another. > > So 2/2 does what a HID driver can do alone: it registers a card of its > own. That works, and the cost is visible from userspace rather than > theoretical: > > $ cat /proc/asound/cards > 0 [ToppingCtl ]: Topping - Topping M62 control > ... > 4 [M62 ]: USB-Audio - M62 > > $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33 > amixer: Cannot find the given element from control sysdefault:4 > > One device, two cards; the gains on a card with no PCM beside them; and > anything that looks for a device's mixer next to its streams -- > alsamixer -c, UCM profiles, PipeWire's device model -- does not find > them there. > > Against my own preference, two honest notes. The phantom input device > 2/2 leaves at boot (hid-generic binds first, the specific driver being > a module outside the initramfs) is a packaging artefact, not a property > of that road. And 1/2's claim helper is new API surface in sound/usb, > small as it is. > > Field results > ============= > > With 1/2: the interface belongs to snd-usb-audio while a neighbouring > device's HID interface still belongs to usbhid, so the ignore entry is > precise. Values arrive by themselves -- the headphone volume came up at > 51 while the zero-initialised cache would have said 0. One front panel > press produces exactly one control event. A write reaches the hardware: > the device reports the written value back, and its meters answer. > > With 2/2: the same, on its own card. > > One device fact worth recording: a written gain takes effect at once, > but when the device commits it to non-volatile memory is the firmware's > business, and a value written and then torn off the bus can come back > as the older one. Nothing in either driver depends on that -- neither > treats itself as the source of truth, both ask the device -- but it is > easy to mistake for a driver bug while testing. > > Where I come out > ================ > > The knobs belong on the card the device already has, and 2/2 cannot put > them there without a new cross-subsystem interface built for one > device. 1/2's cost is one static-variable problem solved by one helper > in the file that owns it. So I would take 1/2, which is also your gut > feeling -- but the comparison is what you asked for, and either patch > stands alone if you read it the other way. > > Not covered by either: the OTG input's gain. It has no front panel > control and therefore never announced itself in any capture, so its > property is unknown. It is one row when it is known. > > Mikhail Three things I should have said in the cover letter, one of which argues against the conclusion I drew there. The quirk's hid_ignore_list entry means no hidraw node is created for this device at all. Topping ship a control application for Windows and macOS and not for Linux; there is none today, and the driver should not block one if it appears. The HID road leaves that channel open and the quirk road closes it. I weighed the two roads by where the mixer controls can live and did not weigh this, and it belongs on the scale. The one gap the cover letter named is closed. The OTG input's gain is target 0x27, property 0x04, on the same taper family as Bluetooth -- read out of a capture of the vendor application moving it, with the indices it dwelt on matching the decibels it displayed. Both patches carry the row here; I have not resent them for one line, and it will be in whichever version goes forward. And the protocol turns out to need more than a write path, which bears on the complexity you asked me to compare. The vendor application repeats its subscribe every two seconds, so a driver has to keep that up or the device stops reporting; the device has two memories, and a separate command commits the live state to the one that survives a power cycle; and the source selectors -- which mix or bus each output listens to -- can be written but never read: the device does not announce them, and the vendor application does not ask, because on connect it pushes its whole workspace rather than reading anything. None of that favours either road, since both pay it identically, but it is a fair bit more than a table of gains, and I would rather you saw it before deciding. I have not resent the patches. Both roads work on the hardware; the question is still which one you would rather carry. -- Thanks, Mikhail Gavrilov. ^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [RFC 0/2] Two ways to reach the Topping M62's analogue gains 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov ` (2 preceding siblings ...) 2026-08-21 11:23 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov @ 2026-08-23 8:50 ` Takashi Iwai 2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 4 siblings, 0 replies; 29+ messages in thread From: Takashi Iwai @ 2026-08-23 8:50 UTC (permalink / raw) To: Mikhail Gavrilov Cc: tiwai, jikos, bentiss, perex, linux-sound, linux-input, linux-kernel On Thu, 20 Aug 2026 17:13:27 +0200, Mikhail Gavrilov wrote: > > You asked for PoCs of both roads and a comparison of the actual code > rather than of arguments. Here are both. They are alternatives, not a > series: each is written against mainline 98f21c54f995 on its own, and > either can be applied alone. > > 1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface > 2/2 HID: topping: a HID driver that registers a card of its own > > Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two > CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have > been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for > reading, for unsolicited notification from the front panel, and for > writing. > > What the device is > ================== > > The M62 keeps its two microphone preamp gains, its AUX and Bluetooth > input volumes and its headphone and OTG output volumes behind a vendor > protocol on a HID-class interface, and exposes none 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: a noise-floor ladder > against the card shows the converter's own floor rising with the > signal. So on Linux today the one knob worth setting is the one that > cannot be reached, and a measurement application has to begin by asking > a human to touch the front panel. > > The protocol is fifteen-byte frames -- 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. 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 and a second makes it announce its whole state, after which > every change arrives unasked, including a front panel press. > > The control pipe is not an option: GET_REPORT and SET_REPORT stall with > EPIPE for every report type, so the interrupt endpoints on the HID > interface are the only route. > > What is identical in both > ========================= > > The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...) > is CRC-16/MODBUS, so no private table), and the control table. A knob > is a row of > > { name, target, paired target, property, min, max, TLV } > > so adding one is adding a row. Six rows today. The outputs come in > pairs because the device answers on only one target of each pair and > the other would drift away unheard. > > Where they differ > ================= > > 1/2 claims the HID interface for snd-usb-audio and puts the elements on > the card the device already has. The cost is two-sided: an entry in > hid_ignore_list to keep usbhid off the interface, and one new helper in > sound/usb/card.c, because usb_audio_driver is static there and a quirk > cannot claim an interface without it. That helper is the only change in > 1/2 outside the new file and its dispatch. Nothing is lost by taking > the interface: the report descriptor is a Generic Desktop application > collection with eight unnamed usages, sixteen bytes in and out and no > report ID, so hid-generic can only build an input device for a mouse > that does not exist -- which is what it does today. > > 2/2 binds as a HID driver, and the protocol half is if anything smaller > there: usbhid owns the endpoints, so hid_hw_output_report replaces a > hand-built interrupt URB out, raw_event replaces the one in, and no > interface has to be claimed. It needs nothing in sound/usb. > > But these are mixer controls for an audio device, and the audio > device's card belongs to snd-usb-audio. A HID driver cannot put an > element there. There is no interface for it, and inventing one means > exporting from sound/usb both a lookup from struct usb_device to the > card and an add-element call, and then answering, for a single device, > what happens when the two drivers probe in either order and when either > disconnects first, given that the element would live in one module and > its private data in another. > > So 2/2 does what a HID driver can do alone: it registers a card of its > own. That works, and the cost is visible from userspace rather than > theoretical: > > $ cat /proc/asound/cards > 0 [ToppingCtl ]: Topping - Topping M62 control > ... > 4 [M62 ]: USB-Audio - M62 > > $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33 > amixer: Cannot find the given element from control sysdefault:4 > > One device, two cards; the gains on a card with no PCM beside them; and > anything that looks for a device's mixer next to its streams -- > alsamixer -c, UCM profiles, PipeWire's device model -- does not find > them there. > > Against my own preference, two honest notes. The phantom input device > 2/2 leaves at boot (hid-generic binds first, the specific driver being > a module outside the initramfs) is a packaging artefact, not a property > of that road. And 1/2's claim helper is new API surface in sound/usb, > small as it is. > > Field results > ============= > > With 1/2: the interface belongs to snd-usb-audio while a neighbouring > device's HID interface still belongs to usbhid, so the ignore entry is > precise. Values arrive by themselves -- the headphone volume came up at > 51 while the zero-initialised cache would have said 0. One front panel > press produces exactly one control event. A write reaches the hardware: > the device reports the written value back, and its meters answer. > > With 2/2: the same, on its own card. > > One device fact worth recording: a written gain takes effect at once, > but when the device commits it to non-volatile memory is the firmware's > business, and a value written and then torn off the bus can come back > as the older one. Nothing in either driver depends on that -- neither > treats itself as the source of truth, both ask the device -- but it is > easy to mistake for a driver bug while testing. > > Where I come out > ================ > > The knobs belong on the card the device already has, and 2/2 cannot put > them there without a new cross-subsystem interface built for one > device. 1/2's cost is one static-variable problem solved by one helper > in the file that owns it. So I would take 1/2, which is also your gut > feeling -- but the comparison is what you asked for, and either patch > stands alone if you read it the other way. > > Not covered by either: the OTG input's gain. It has no front panel > control and therefore never announced itself in any capture, so its > property is unknown. It is one row when it is known. Thanks! I didn't expect such complete patches when I asked for PoC, and you've done much better than I thought :) Honestly speaking, both look well acceptable. From the pure kernel POV, the 2nd patch is simpler, but as you pointed out, we'll need another stuff to combining two sound cards. It'd be likely a special UCM profile, but this can be a bit hackish. So, from the usability POV, the first patch would be "easier", and if I have to choose, my gut feeling is to pick the first one. But again, I have no strong opinion, and both look good in general. I'd like to hear from others, too. About the code, there are a few things to be improved. The spinlock could be done better with guard(). A temporary buffer could be handled with __free(), too. thanks, Takashi ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov ` (3 preceding siblings ...) 2026-08-23 8:50 ` Takashi Iwai @ 2026-08-23 14:22 ` Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 4 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 14:22 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v2, and the road is the mixer quirk. The HID driver is dropped from this posting; it is in the RFC thread if anyone wants to argue for it. The cost of this road is accepted knowingly: with the hid_ignore_list entry there is no hidraw node, so a future Linux control application would have no channel of its own to the card. Changes since the RFC: - guard() and __free() as you asked. The spinlock is guard() where a whole function holds it and scoped_guard() elsewhere; one goto that would have jumped out of a guarded scope is gone, replaced by a flag, since the cleanup runs either way but the jump reads like a trap. - The OTG input's gain is in. It was the one gap the RFC cover letter named: it has no front panel control, so it never announced itself. A capture of the vendor application moving it names it target 0x27 on the same taper family as Bluetooth, and the indices it dwelt on match the decibels it displayed. - The subscription is renewed every two seconds. It lapses: a listener that subscribed once got the meters and the identification block and then very little, while one that kept repeating got the gains too, about five seconds in. The vendor application does the same. - New in 2/2: the outputs' source selectors, as enumerated controls. A correction to my own follow-up, which said the HID road leaves a hidraw node open for a future vendor application while the quirk closes it. Half of that is wrong: the HID driver as posted calls hid_hw_start(hdev, 0), which creates no hidraw either. It would take one word to fix there and cannot be fixed on this road at all, so the comparison stands, but the archive should not carry a claim the code did not support. Which raises a form neither posting covered: snd-usb-audio could register the hid_driver itself. usbhid stays the transport, so hidraw survives and no hid_ignore_list entry is needed; the controls still land on the card the device already has, because it is all one module holding the mixer pointer; and the claim helper in card.c goes away. The cost is that snd-usb-audio would depend on the HID core, and I find no precedent for that direction -- the reverse exists, hid-prodikeys registers a card of its own. The probe-order and disconnect questions do not disappear, but they stay inside one module. I mention it rather than implement it: you have picked a road, and I would rather ask whether this is a better one than send a fourth variant unasked. About 2/2 and one thing in it I am not comfortable with. Each output listens to one source chosen inside the card -- a mix, an input, or one playback bus straight from USB -- and the device NEVER reports that choice. Not to this driver, and not to the vendor's own application, which pushes its whole workspace on connect rather than reading anything. So the control can be written but not read, and the item list starts with "Unknown", which is what it shows until a hand has chosen; selecting it is refused. If there is a convention for this that I have missed, I would rather use it. Both patches are on mainline 98f21c54f995 and have been exercised on the hardware: values arrive by themselves after probe, a front panel knob still reaches the driver ten minutes later, and a write reaches the analogue stage -- recording one source at gain 30 and at gain 60 differs by 29.7 dB against the 30.0 dB the taper table predicts, which also confirms the decoded scale. For 2/2, the audible test: point an output away from the bus being played and it goes silent, point it back and the sound returns. Tested on a KASAN and lockdep kernel, including unplug while a stream was running; no reports. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 14 + sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 636 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 3 + 9 files changed, 677 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6 -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-23 14:22 ` Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 14:22 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 14 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 511 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 3 + 9 files changed, 552 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index c31a32a2c748..86447da07a6d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27435,6 +27435,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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/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 := 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..e99b9341086f 100644 --- a/sound/usb/card.c +++ b/sound/usb/card.c @@ -325,6 +325,20 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int return 0; } +/* + * 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); +} + /* * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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..3405f28dec66 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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; + 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; + 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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +static int topping_send(struct topping_mixer *tm, u8 target, u8 prop, + s32 value) +{ + u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_KERNEL); + int err, actual; + + if (!buf) + return -ENOMEM; + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = urb->transfer_buffer; + int idx, value, err; + bool changed; + + if (urb->status) + return; /* resubmitted below only when running */ + if (urb->actual_length < TOPPING_FRAME_LEN) + goto resubmit; + if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = true; + } + } + + if (changed && tm->kctl[idx]) + snd_ctl_notify(tm->mixer->chip->card, + SNDRV_CTL_EVENT_MASK_VALUE, + &tm->kctl[idx]->id); + +resubmit: + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err < 0 && err != -ENODEV && err != -ESHUTDOWN) + 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 = container_of(work, struct topping_mixer, + keepalive.work); + + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, err; + + value = ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] == value) + return 0; + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + scoped_guard(spinlock_irqsave, &tm->lock) + tm->val[elem->control] = value; + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_private_free(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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); + } + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + mixer->private_data = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + + tm->urb = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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->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..4f9770a5db69 100644 --- a/sound/usb/usbaudio.h +++ b/sound/usb/usbaudio.h @@ -80,6 +80,9 @@ struct snd_usb_audio { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +int snd_usb_claim_iface(struct snd_usb_audio *chip, + 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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-23 14:22 ` Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 14:22 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- sound/usb/mixer_topping.c | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c index 3405f28dec66..029f54865e22 100644 --- a/sound/usb/mixer_topping.c +++ b/sound/usb/mixer_topping.c @@ -125,6 +125,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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; @@ -139,6 +178,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 */ }; static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -312,6 +352,58 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= ARRAY_SIZE(topping_sources)) + return -EINVAL; + if (!item) + return -EINVAL; /* "Unknown" is a report, not a choice */ + if (tm->sel[elem->control] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -354,6 +446,34 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + int err; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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); +} + static void topping_private_free(struct usb_mixer_interface *mixer) { struct topping_mixer *tm = mixer->private_data; @@ -479,6 +599,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-23 19:48 ` Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 2 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 19:48 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v3 answers the automated review of v2. Four of its five findings were real, and one of them was a bug a user would have met on every boot. - The enum refused "Unknown", which is the value it reports until a hand has chosen. alsactl stores and restores that value like any other, so the refusal failed a restore of the driver's own report -- observed here as "Cannot write control ... Invalid argument" from alsactl. Writing "Unknown" is now a quiet no-op rather than an error, since it is a report and not a choice either way. - The hardware is reached under snd_usb_lock_shutdown(), the way the rest of this directory reaches it. Without it nothing made the teardown wait for a control callback already in flight. - Suspend and resume are handled rather than survived: the URB does not outlive a system sleep, so notifications stopped for good after the first one. 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 the driver was not. - The claimed interface is released, on the error path and at teardown, so unbind and bind again works instead of failing at the claim. That needed a release helper beside snd_usb_claim_iface(), for the same reason the claim needed one. - A mutex spans each write from the comparison to the cache update. The review called two writers reaching the device in one order and the cache in the other a race, and it is one, though a narrow one. Nothing else changed since v2; the questions in that cover letter about the third form of the driver and about a control that can be written but not read still stand. Tested on the hardware as before: values arrive by themselves after probe, a front panel knob still reaches the driver ten minutes later, a write reaches the analogue stage -- one source recorded at gain 30 and at gain 60 differs by 29.7 dB against the 30.0 dB the taper table predicts -- and for 2/2 the audible test, where pointing an output away from the bus being played silences it and pointing it back returns the sound. On a KASAN and lockdep kernel, including unplug while a stream was running, and now also across a suspend and resume cycle; no reports. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 + sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 709 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 756 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6 -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-23 19:48 ` Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 19:48 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 577 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 624 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index c31a32a2c748..86447da07a6d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27435,6 +27435,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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/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 := 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; } +/* + * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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..2f2365b9b476 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +static int topping_send(struct topping_mixer *tm, u8 target, u8 prop, + s32 value) +{ + u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_KERNEL); + int err, actual; + + if (!buf) + return -ENOMEM; + + /* + * The shutdown lock is what makes a write safe against disconnect: + * the teardown waits for everyone holding it before the card goes + * away, so the device and this mixer are alive for as long as it + * is held. + */ + CLASS(snd_usb_lock, pm)(tm->mixer->chip); + if (pm.err < 0) + return -EIO; + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = urb->transfer_buffer; + int idx, value, err; + bool changed; + + if (urb->status) + return; /* resubmitted below only when running */ + if (urb->actual_length < TOPPING_FRAME_LEN) + goto resubmit; + if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = true; + } + } + + if (changed && tm->kctl[idx]) + snd_ctl_notify(tm->mixer->chip->card, + SNDRV_CTL_EVENT_MASK_VALUE, + &tm->kctl[idx]->id); + +resubmit: + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err < 0 && err != -ENODEV && err != -ESHUTDOWN) + 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 = container_of(work, struct topping_mixer, + keepalive.work); + + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, err; + + value = ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + /* + * 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); + + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] == value) + return 0; + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + scoped_guard(spinlock_irqsave, &tm->lock) + tm->val[elem->control] = value; + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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 = mixer->private_data; + int err; + + if (!tm) + return 0; + + if (tm->urb) { + err = usb_submit_urb(tm->urb, GFP_KERNEL); + if (err < 0) + 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)); + return 0; +} + +static void topping_private_free(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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); + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + mixer->private_data = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed = true; + + tm->urb = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + mixer->private_suspend = topping_suspend; + mixer->private_resume = topping_resume; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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); + 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 { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-23 19:48 ` Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 19:48 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- sound/usb/mixer_topping.c | 132 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c index 2f2365b9b476..8bf5fe483ccf 100644 --- a/sound/usb/mixer_topping.c +++ b/sound/usb/mixer_topping.c @@ -126,6 +126,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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; @@ -142,6 +181,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 */ }; static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -332,6 +372,66 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= ARRAY_SIZE(topping_sources)) + return -EINVAL; + + 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] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -374,6 +474,33 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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); +} + static void topping_suspend(struct usb_mixer_interface *mixer) { struct topping_mixer *tm = mixer->private_data; @@ -543,6 +670,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-23 22:29 ` Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 2 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 22:29 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v4 answers the automated review of v3. One of its three findings led to a change; the other two do not hold, and I would rather say why here than leave the question hanging for a human reader of the thread. The change is in 2/2. The driver marks its elements USB_MIXER_BESPOKEN, which opts them out of the mixer core's restore_mixer_value(), so restoring across a suspend became this driver's own business. For the gains that is covered -- the device announces them and the cache re-syncs by itself -- but a source selector is never reported by the device at all, so nothing covered it: if the card had come up on its own defaults while the host slept, the driver's idea of the selector would have been silently wrong, and writing the remembered value back would have looked like no change and done nothing. The resume path now writes the chosen value again. Said plainly, because it matters for how you weigh it: I did not observe that. This card has a battery, and with the headphone selector on Playback 3/4 and music playing into that bus, a full suspend and wake brought the sound straight back untouched. The change closes a gap this driver opened by opting out of the core's restore, not a fault seen on the hardware. The two findings I am not acting on both concern a control callback using a freed private structure during disconnect. The ALSA core already excludes it. snd_ctl_elem_read() and snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the callback; snd_card_disconnect() ends with snd_power_sync_ref(card), which waits until every such reference is dropped; and in usb-audio's disconnect, snd_card_disconnect() runs before usb_audio_disconnect_components() reaches snd_usb_mixer_disconnect() and hence this driver's private_free(). So no callback can be inside the driver when the structure is freed, and none can enter afterwards -- snd_power_ref_and_wait() sees card->shutdown and returns -ENODEV. The observation that the controls themselves live until snd_card_free() is correct; the access path to them is closed earlier. Nothing else changed since v3. The questions in the v2 cover letter -- about a third form in which snd-usb-audio would register the hid_driver itself, and about a control that can be written but not read -- still stand. Tested on the hardware: values arrive by themselves after probe, a front panel knob still reaches the driver ten minutes later and after a suspend and resume cycle, a write reaches the analogue stage (one source recorded at gain 30 and at gain 60 differs by 30.4 dB against the 30.0 dB the taper table predicts), the audible selector test passes, unbind and bind again works, and alsactl stores and restores these controls without complaint. On a KASAN and lockdep kernel, including unplug while a stream was running; no reports. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 + sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 734 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 781 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6 -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-23 22:29 ` Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 22:29 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 577 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 624 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index c31a32a2c748..86447da07a6d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27435,6 +27435,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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/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 := 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; } +/* + * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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..2f2365b9b476 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +static int topping_send(struct topping_mixer *tm, u8 target, u8 prop, + s32 value) +{ + u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_KERNEL); + int err, actual; + + if (!buf) + return -ENOMEM; + + /* + * The shutdown lock is what makes a write safe against disconnect: + * the teardown waits for everyone holding it before the card goes + * away, so the device and this mixer are alive for as long as it + * is held. + */ + CLASS(snd_usb_lock, pm)(tm->mixer->chip); + if (pm.err < 0) + return -EIO; + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = urb->transfer_buffer; + int idx, value, err; + bool changed; + + if (urb->status) + return; /* resubmitted below only when running */ + if (urb->actual_length < TOPPING_FRAME_LEN) + goto resubmit; + if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = true; + } + } + + if (changed && tm->kctl[idx]) + snd_ctl_notify(tm->mixer->chip->card, + SNDRV_CTL_EVENT_MASK_VALUE, + &tm->kctl[idx]->id); + +resubmit: + err = usb_submit_urb(urb, GFP_ATOMIC); + if (err < 0 && err != -ENODEV && err != -ESHUTDOWN) + 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 = container_of(work, struct topping_mixer, + keepalive.work); + + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, err; + + value = ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + /* + * 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); + + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] == value) + return 0; + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + scoped_guard(spinlock_irqsave, &tm->lock) + tm->val[elem->control] = value; + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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 = mixer->private_data; + int err; + + if (!tm) + return 0; + + if (tm->urb) { + err = usb_submit_urb(tm->urb, GFP_KERNEL); + if (err < 0) + 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)); + return 0; +} + +static void topping_private_free(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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); + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + mixer->private_data = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed = true; + + tm->urb = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + mixer->private_suspend = topping_suspend; + mixer->private_resume = topping_resume; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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); + 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 { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-23 22:29 ` Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-23 22:29 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- sound/usb/mixer_topping.c | 157 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c index 2f2365b9b476..e71ea70ec4b0 100644 --- a/sound/usb/mixer_topping.c +++ b/sound/usb/mixer_topping.c @@ -126,6 +126,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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; @@ -142,6 +181,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 */ }; static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -332,6 +372,66 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= ARRAY_SIZE(topping_sources)) + return -EINVAL; + + 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] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -374,6 +474,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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 = 0; i < TOPPING_NUM_ENUMS; i++) { + if (!tm->sel[i]) + continue; /* nothing was ever chosen */ + d = &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 = mixer->private_data; @@ -407,6 +558,7 @@ static int topping_resume(struct usb_mixer_interface *mixer) */ 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)); return 0; @@ -543,6 +695,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-24 20:13 ` Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 2 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-24 20:13 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v5 takes three more points from the automated review, all in 1/2, and declines two that are a repeat of v3's. Taken: - Allocation on the resume path now asks for no I/O. topping_send() is reached from topping_resume(), where reclaim can wait on a block device that has not woken yet, and the URB submitted there wanted the same treatment. This is what snd_usb_mixer_activate() does a few hundred lines away in mixer.c, so it is the file's own habit rather than a novelty. - A resubmit that fails with -EPERM is no longer reported. That is a URB usb_kill_urb() has already refused, which is the ordinary sound of an unplug rather than a fault. The line has been wrong since v1; nobody noticed until now. Declined, with the same reasoning the v4 cover letter gave, since the finding came back unchanged: a control callback cannot dereference a freed private structure during disconnect. snd_ctl_elem_read() and snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the callback; snd_card_disconnect() ends with snd_power_sync_ref(card), which waits until every such reference is dropped; and in usb-audio's disconnect, snd_card_disconnect() runs before usb_audio_disconnect_components() reaches snd_usb_mixer_disconnect() and hence this driver's private_free(). No callback can be inside the driver when the structure is freed, and none can enter afterwards -- snd_power_ref_and_wait() sees card->shutdown and returns -ENODEV. Nothing else changed since v4. 2/2 is unchanged. The questions from the v2 cover letter still stand: whether snd-usb-audio registering the hid_driver itself would be a better shape than either road posted, and whether there is a convention for a control that can be written but not read. Tested on the hardware as before, including a suspend and resume cycle and unbind and bind again, on a KASAN and lockdep kernel; no reports. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 + sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 744 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 791 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6 -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-24 20:13 ` Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-24 20:13 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 587 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 634 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index c31a32a2c748..86447da07a6d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27435,6 +27435,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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/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 := 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; } +/* + * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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..2f42b2633626 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,587 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +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) = kzalloc(TOPPING_EP_BUF, GFP_NOIO); + int err, actual; + + if (!buf) + return -ENOMEM; + + /* + * The shutdown lock is what makes a write safe against disconnect: + * the teardown waits for everyone holding it before the card goes + * away, so the device and this mixer are alive for as long as it + * is held. + */ + CLASS(snd_usb_lock, pm)(tm->mixer->chip); + if (pm.err < 0) + return -EIO; + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = urb->transfer_buffer; + int idx, value, err; + bool changed; + + if (urb->status) + return; /* resubmitted below only when running */ + if (urb->actual_length < TOPPING_FRAME_LEN) + goto resubmit; + if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = true; + } + } + + if (changed && tm->kctl[idx]) + snd_ctl_notify(tm->mixer->chip->card, + SNDRV_CTL_EVENT_MASK_VALUE, + &tm->kctl[idx]->id); + +resubmit: + err = 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 != -ENODEV && err != -ESHUTDOWN && err != -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 = container_of(work, struct topping_mixer, + keepalive.work); + + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, err; + + value = ucontrol->value.integer.value[0]; + if (value < d->min || value > d->max) + return -EINVAL; + + /* + * 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); + + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] == value) + return 0; + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + scoped_guard(spinlock_irqsave, &tm->lock) + tm->val[elem->control] = value; + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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 = mixer->private_data; + int err; + + if (!tm) + return 0; + + if (tm->urb) { + err = usb_submit_urb(tm->urb, GFP_NOIO); + if (err < 0) + 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)); + return 0; +} + +static void topping_private_free(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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); + kfree(tm->val); + kfree(tm->kctl); + kfree(tm); + mixer->private_data = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed = true; + + tm->urb = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + mixer->private_suspend = topping_suspend; + mixer->private_resume = topping_resume; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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); + 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 { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-24 20:13 ` Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-24 20:13 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- sound/usb/mixer_topping.c | 157 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c index 2f42b2633626..1743b8ee7fac 100644 --- a/sound/usb/mixer_topping.c +++ b/sound/usb/mixer_topping.c @@ -126,6 +126,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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; @@ -142,6 +181,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 */ }; static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -342,6 +382,66 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= ARRAY_SIZE(topping_sources)) + return -EINVAL; + + 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] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -384,6 +484,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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 = 0; i < TOPPING_NUM_ENUMS; i++) { + if (!tm->sel[i]) + continue; /* nothing was ever chosen */ + d = &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 = mixer->private_data; @@ -417,6 +568,7 @@ static int topping_resume(struct usb_mixer_interface *mixer) */ 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)); return 0; @@ -553,6 +705,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-24 22:31 ` Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 2 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-24 22:31 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v6 takes four more points from the automated review and declines two. The first of the four is a deadlock, and it is worth saying how it got there, because neither change that made it was wrong on its own. v3 added a mutex around each write, so two writers could not reach the device in one order and the cache in the other. v4 added a resume-time write of the source selectors, since the device never reports them and nothing else would restore them. Together they close a loop: a write on a runtime-suspended device takes the mutex, calls into the device, and waking it runs this driver's own resume callback on the same thread -- which takes the same mutex, held by the caller. The order is now the other way round: the device is woken outside the lock, so a wake that runs the resume callback finds nothing held. The other three: - A URB that completes with an error is resubmitted unless the error means the URB or the device is gone. Bus noise gives -EPROTO and -EILSEQ, and stopping on those left the card silent until it was replugged. This is what snd_usb_mixer_status_complete() does a few hundred lines away. - The resume path now forbids I/O reclaim for everything under it, not only for the frame buffer: usb_interrupt_msg() allocates a URB of its own with GFP_KERNEL, so a polite flag on our allocation settles nothing by itself. - The claimed interface is held with a reference. Claiming does not keep it alive, and this driver hands the pointer back to the core when the card goes away. Declined, for the third time and with the same reasoning the v4 cover letter gave: a control callback cannot dereference a freed private structure during disconnect. snd_ctl_elem_read() and snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the callback; snd_card_disconnect() ends with snd_power_sync_ref(card), which waits until every such reference is dropped; and in usb-audio's disconnect, snd_card_disconnect() runs before usb_audio_disconnect_components() reaches this driver's private_free(). There is a second reason not to do it anyway: taking the shutdown lock in a get would wake a runtime-suspended device in order to read a number this driver already has in memory. The path was exercised. It needs the card in runtime suspend at the moment a control is written, which does not happen by itself here: the driver's own keepalive writes every two seconds and the default autosuspend delay is also two thousand milliseconds, so the timer never expires. With that delay set to zero the card suspends between keepalives, and a control write then returns at once with the value set. I did not go back to v5 to watch it hang. Tested on the hardware as before: values arrive by themselves after probe, a front panel knob reaches the driver ten minutes later and after a suspend and resume cycle, a write reaches the analogue stage (one source recorded at gain 30 and at gain 60 differs by 30.4 dB against the 30.0 dB the taper table predicts), the audible selector test passes, unbind and bind again works, and alsactl stores and restores these controls without complaint. On a KASAN and lockdep kernel; no reports. The questions from the v2 cover letter still stand: whether snd-usb-audio registering the hid_driver itself would be a better shape than either road posted, and whether there is a convention for a control that can be written but not read. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 + sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 794 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 841 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 47096fc3d064a07c0842f748b99ebf01be120f2b -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-24 22:31 ` Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-24 22:31 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- MAINTAINERS | 6 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-quirks.c | 2 + sound/usb/Makefile | 1 + sound/usb/card.c | 19 ++ sound/usb/mixer_quirks.c | 5 + sound/usb/mixer_topping.c | 632 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 9 files changed, 679 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h diff --git a/MAINTAINERS b/MAINTAINERS index 8d9648737e92..5179f0bd5699 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27469,6 +27469,12 @@ S: Maintained W: https://tomoyo.sourceforge.net/ F: security/tomoyo/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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/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 := 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; } +/* + * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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..f8ddeafb0222 --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,632 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/mutex.h> +#include <linux/sched/mm.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 0x77; +} + +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) = kzalloc(TOPPING_EP_BUF, GFP_NOIO); + int err, actual; + + if (!buf) + return -ENOMEM; + + /* + * The shutdown lock is what makes a write safe against disconnect: + * the teardown waits for everyone holding it before the card goes + * away, so the device and this mixer are alive for as long as it + * is held. + */ + CLASS(snd_usb_lock, pm)(tm->mixer->chip); + if (pm.err < 0) + return -EIO; + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = 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] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = 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 == -ENOENT || urb->status == -ECONNRESET || + urb->status == -ESHUTDOWN) + return; + err = 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 != -ENODEV && err != -ESHUTDOWN && err != -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 = container_of(work, struct topping_mixer, + keepalive.work); + + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, err; + + value = 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. topping_send() takes the same wake lock again, which is + * refcounted and cheap. + */ + 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); + + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] == value) + return 0; + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + scoped_guard(spinlock_irqsave, &tm->lock) + tm->val[elem->control] = value; + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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 = 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 = memalloc_noio_save(); + + if (tm->urb) { + err = 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 = 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 = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed = 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 = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + mixer->private_suspend = topping_suspend; + mixer->private_resume = topping_resume; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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 { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-24 22:31 ` Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-24 22:31 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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 <mikhail.v.gavrilov@gmail.com> --- 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 f8ddeafb0222..df0dbe212c40 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[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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 */ }; static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -365,6 +405,71 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= 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] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -407,6 +512,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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 = 0; i < TOPPING_NUM_ENUMS; i++) { + if (!tm->sel[i]) + continue; /* nothing was ever chosen */ + d = &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 = mixer->private_data; @@ -451,6 +607,7 @@ static int topping_resume(struct usb_mixer_interface *mixer) */ 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); @@ -596,6 +753,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-25 8:56 ` Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 2 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-25 8:56 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v7 takes two more points from the automated review of v6, both in 1/2. The first is a build one: SND_USB_AUDIO now selects CRC16. The frames this driver reads are checked with crc16() and nothing else in the directory pulled it in, so a kernel built with CONFIG_CRC16 off would have failed to link. The second is another deadlock, and again of our own making. The keepalive woke the device before writing; a runtime suspend arriving at the same moment reached this driver's suspend callback, which waits in cancel_delayed_work_sync() for the worker -- while the worker waited in the PM core for that same suspend to finish. Waking is now the caller's business rather than 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. That also fixes something nobody had reported yet: with a write every two seconds and a default autosuspend delay of the same two seconds, the card could never reach runtime suspend at all. It can now. The rest of this letter is v6's, since nothing else changed. v6 took four points from the automated review and declined two. The first of the four is a deadlock, and it is worth saying how it got there, because neither change that made it was wrong on its own. v3 added a mutex around each write, so two writers could not reach the device in one order and the cache in the other. v4 added a resume-time write of the source selectors, since the device never reports them and nothing else would restore them. Together they close a loop: a write on a runtime-suspended device takes the mutex, calls into the device, and waking it runs this driver's own resume callback on the same thread -- which takes the same mutex, held by the caller. The order is now the other way round: the device is woken outside the lock, so a wake that runs the resume callback finds nothing held. The other three: - A URB that completes with an error is resubmitted unless the error means the URB or the device is gone. Bus noise gives -EPROTO and -EILSEQ, and stopping on those left the card silent until it was replugged. This is what snd_usb_mixer_status_complete() does a few hundred lines away. - The resume path now forbids I/O reclaim for everything under it, not only for the frame buffer: usb_interrupt_msg() allocates a URB of its own with GFP_KERNEL, so a polite flag on our allocation settles nothing by itself. - The claimed interface is held with a reference. Claiming does not keep it alive, and this driver hands the pointer back to the core when the card goes away. Declined, for the third time and with the same reasoning the v4 cover letter gave: a control callback cannot dereference a freed private structure during disconnect. snd_ctl_elem_read() and snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the callback; snd_card_disconnect() ends with snd_power_sync_ref(card), which waits until every such reference is dropped; and in usb-audio's disconnect, snd_card_disconnect() runs before usb_audio_disconnect_components() reaches this driver's private_free(). There is a second reason not to do it anyway: taking the shutdown lock in a get would wake a runtime-suspended device in order to read a number this driver already has in memory. The path was exercised. It needs the card in runtime suspend at the moment a control is written, which does not happen by itself here: the driver's own keepalive writes every two seconds and the default autosuspend delay is also two thousand milliseconds, so the timer never expires. With that delay set to zero the card suspends between keepalives, and a control write then returns at once with the value set. I did not go back to v5 to watch it hang. Tested on the hardware as before: values arrive by themselves after probe, a front panel knob reaches the driver ten minutes later and after a suspend and resume cycle, a write reaches the analogue stage (one source recorded at gain 30 and at gain 60 differs by 30.4 dB against the 30.0 dB the taper table predicts), the audible selector test passes, unbind and bind again works, and alsactl stores and restores these controls without complaint. On a KASAN and lockdep kernel; no reports. The questions from the v2 cover letter still stand: whether snd-usb-audio registering the hid_driver itself would be a better shape than either road posted, and whether there is a convention for a control that can be written but not read. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to 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 | 798 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 10 files changed, 846 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 66498c75b4f8017f62d720d9b59675bdf3abce91 -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-25 8:56 ` Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-25 8:56 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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. Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> --- 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 | 636 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 10 files changed, 684 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/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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_SUPPORT=y || MEDIA_SUPPORT=SND_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 := 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; } +/* + * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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..06cea753046a --- /dev/null +++ b/sound/usb/mixer_topping.c @@ -0,0 +1,636 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mixer controls for Topping interfaces behind a vendor HID channel + * + * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/mutex.h> +#include <linux/sched/mm.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 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) = kzalloc(TOPPING_EP_BUF, GFP_NOIO); + int err, actual; + + if (!buf) + return -ENOMEM; + + topping_build(buf, target, prop, value); + err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out, + buf, TOPPING_FRAME_LEN, &actual, 1000); + if (err < 0) + 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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = 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] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = 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 == -ENOENT || urb->status == -ECONNRESET || + urb->status == -ESHUTDOWN) + return; + err = 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 != -ENODEV && err != -ESHUTDOWN && err != -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 = container_of(work, struct topping_mixer, + keepalive.work); + + topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, err; + + value = 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); + + scoped_guard(spinlock_irqsave, &tm->lock) + if (tm->val[elem->control] == value) + return 0; + + err = topping_send(tm, d->target, d->prop, value); + if (err < 0) + return err; + if (d->target_pair) { + /* + * The device announces only one of a pair, so the other + * would drift away unheard. + */ + err = topping_send(tm, d->target_pair, d->prop, value); + if (err < 0) + return err; + } + + scoped_guard(spinlock_irqsave, &tm->lock) + tm->val[elem->control] = value; + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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 = 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 = memalloc_noio_save(); + + if (tm->urb) { + err = 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 = 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 = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed = 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 = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + mixer->private_suspend = topping_suspend; + mixer->private_resume = topping_resume; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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 { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-25 8:56 ` Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-25 8:56 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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 <mikhail.v.gavrilov@gmail.com> --- 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 06cea753046a..79ba1c5fdcdf 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[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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 */ }; static void topping_build(u8 *f, u8 target, u8 prop, s32 value) @@ -369,6 +409,71 @@ static int topping_ctl_put(struct snd_kcontrol *kctl, return 1; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= 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] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -411,6 +516,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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 = 0; i < TOPPING_NUM_ENUMS; i++) { + if (!tm->sel[i]) + continue; /* nothing was ever chosen */ + d = &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 = mixer->private_data; @@ -455,6 +611,7 @@ static int topping_resume(struct usb_mixer_interface *mixer) */ 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); @@ -600,6 +757,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer) if (err < 0) return err; /* private_free cleans up */ } + for (i = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-25 11:12 ` Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov ` (2 more replies) 2 siblings, 3 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-25 11:12 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov v8 takes two more points from the automated review of v7, both in 1/2. The cache now 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 threw that away and left 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 meanwhile. And the keepalive stops renewing a subscription nobody is listening to: a write that fails because the device has gone ends the cycle rather than scheduling another two seconds later, and those two errors are no longer logged, being an unplug rather than a fault. The review asked whether the worker could reschedule forever after an unplug; it could not, since the mixer's private_free() runs during disconnect and cancels it, but the shape it pointed at was worth straightening anyway. The selector control has no such race and is left alone: the device never reports a selector, so nothing but this driver's own writes ever touches that cache, and they are serialised by the mutex. v7 took two points from the review of v6, both in 1/2. The first is a build one: SND_USB_AUDIO now selects CRC16. The frames this driver reads are checked with crc16() and nothing else in the directory pulled it in, so a kernel built with CONFIG_CRC16 off would have failed to link. The second is another deadlock, and again of our own making. The keepalive woke the device before writing; a runtime suspend arriving at the same moment reached this driver's suspend callback, which waits in cancel_delayed_work_sync() for the worker -- while the worker waited in the PM core for that same suspend to finish. Waking is now the caller's business rather than 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. That also fixes something nobody had reported yet: with a write every two seconds and a default autosuspend delay of the same two seconds, the card could never reach runtime suspend at all. It can now. The rest of this letter is v6's, since nothing else changed. v6 took four points from the automated review and declined two. The first of the four is a deadlock, and it is worth saying how it got there, because neither change that made it was wrong on its own. v3 added a mutex around each write, so two writers could not reach the device in one order and the cache in the other. v4 added a resume-time write of the source selectors, since the device never reports them and nothing else would restore them. Together they close a loop: a write on a runtime-suspended device takes the mutex, calls into the device, and waking it runs this driver's own resume callback on the same thread -- which takes the same mutex, held by the caller. The order is now the other way round: the device is woken outside the lock, so a wake that runs the resume callback finds nothing held. The other three: - A URB that completes with an error is resubmitted unless the error means the URB or the device is gone. Bus noise gives -EPROTO and -EILSEQ, and stopping on those left the card silent until it was replugged. This is what snd_usb_mixer_status_complete() does a few hundred lines away. - The resume path now forbids I/O reclaim for everything under it, not only for the frame buffer: usb_interrupt_msg() allocates a URB of its own with GFP_KERNEL, so a polite flag on our allocation settles nothing by itself. - The claimed interface is held with a reference. Claiming does not keep it alive, and this driver hands the pointer back to the core when the card goes away. Declined, for the third time and with the same reasoning the v4 cover letter gave: a control callback cannot dereference a freed private structure during disconnect. snd_ctl_elem_read() and snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the callback; snd_card_disconnect() ends with snd_power_sync_ref(card), which waits until every such reference is dropped; and in usb-audio's disconnect, snd_card_disconnect() runs before usb_audio_disconnect_components() reaches this driver's private_free(). There is a second reason not to do it anyway: taking the shutdown lock in a get would wake a runtime-suspended device in order to read a number this driver already has in memory. The path was exercised. It needs the card in runtime suspend at the moment a control is written, which does not happen by itself here: the driver's own keepalive writes every two seconds and the default autosuspend delay is also two thousand milliseconds, so the timer never expires. With that delay set to zero the card suspends between keepalives, and a control write then returns at once with the value set. I did not go back to v5 to watch it hang. Tested on the hardware as before: values arrive by themselves after probe, a front panel knob reaches the driver ten minutes later and after a suspend and resume cycle, a write reaches the analogue stage (one source recorded at gain 30 and at gain 60 differs by 30.4 dB against the 30.0 dB the taper table predicts), the audible selector test passes, unbind and bind again works, and alsactl stores and restores these controls without complaint. On a KASAN and lockdep kernel; no reports. The questions from the v2 cover letter still stand: whether snd-usb-audio registering the hid_driver itself would be a better shape than either road posted, and whether there is a convention for a control that can be written but not read. Mikhail Gavrilov (2): ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls ALSA: usb-audio: let the M62's outputs say what they listen to 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 | 816 ++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_topping.h | 7 + sound/usb/usbaudio.h | 4 + 10 files changed, 864 insertions(+) create mode 100644 sound/usb/mixer_topping.c create mode 100644 sound/usb/mixer_topping.h base-commit: 66498c75b4f8017f62d720d9b59675bdf3abce91 -- 2.55.0 ^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls 2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov @ 2026-08-25 11:12 ` Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-25 11:12 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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 <mikhail.v.gavrilov@gmail.com> --- 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/ +TOPPING M62 MIXER DRIVER +M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com> +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_topping.* + TOPSTAR LAPTOP EXTRAS DRIVER M: Herton Ronaldo Krzesinski <herton@canonical.com> 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 +#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[] = { { 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_SUPPORT=y || MEDIA_SUPPORT=SND_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 := 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; } +/* + * 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_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(0x152a, 0x875c): /* Topping M62 */ + err = snd_topping_init(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = 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 <mikhail.v.gavrilov@gmail.com> + * + * 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 <linux/crc16.h> +#include <linux/unaligned.h> +#include <linux/init.h> +#include <linux/cleanup.h> +#include <linux/mutex.h> +#include <linux/sched/mm.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/control.h> +#include <sound/core.h> +#include <sound/tlv.h> + +#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[] = { + 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[] = { + 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[] = { + { "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] = 0x22; + f[1] = 0x33; + f[2] = 0x20; + f[3] = 0x01; + f[4] = 0x01; + f[5] = target; + f[6] = prop; + put_unaligned_be32(value, f + 7); + crc = crc16(0xffff, f + 2, 9); + put_unaligned_be16(crc, f + 11); + f[13] = 0x66; + f[14] = 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) = kzalloc(TOPPING_EP_BUF, GFP_NOIO); + int err, actual; + + if (!buf) + return -ENOMEM; + + topping_build(buf, target, prop, value); + err = 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 != -ENODEV && err != -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 = 0; i < tm->num_ctls; i++) + if (tm->ctls[i].target == target && tm->ctls[i].prop == prop) + return i; + return -1; +} + +static void topping_urb_complete(struct urb *urb) +{ + struct topping_mixer *tm = urb->context; + const u8 *f = 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] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77) + goto resubmit; + if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9)) + goto resubmit; + + idx = topping_index_of(tm, f[5], f[6]); + if (idx < 0) + goto resubmit; /* a meter, or something unnamed */ + + value = get_unaligned_be32(f + 7); + if (value < tm->ctls[idx].min || value > tm->ctls[idx].max) + goto resubmit; + + changed = false; + scoped_guard(spinlock_irqsave, &tm->lock) { + if (tm->val[idx] != value) { + tm->val[idx] = value; + changed = 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 == -ENOENT || urb->status == -ECONNRESET || + urb->status == -ESHUTDOWN) + return; + err = 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 != -ENODEV && err != -ESHUTDOWN && err != -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 = container_of(work, struct topping_mixer, + keepalive.work); + int err; + + err = topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1); + if (err == -ENODEV || err == -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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + int idx = elem->control; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = tm->ctls[idx].min; + uinfo->value.integer.max = tm->ctls[idx].max; + uinfo->value.integer.step = 1; + return 0; +} + +static int topping_ctl_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct usb_mixer_elem_info *elem = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(spinlock_irqsave)(&tm->lock); + ucontrol->value.integer.value[0] = 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 = kctl->private_data; + struct usb_mixer_interface *mixer = elem->head.mixer; + struct topping_mixer *tm = mixer->private_data; + const struct topping_ctl_desc *d = &tm->ctls[elem->control]; + int value, prev, err; + + value = 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] == value) + return 0; + prev = tm->val[elem->control]; + tm->val[elem->control] = value; + } + + err = 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 = 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] == value) + tm->val[elem->control] = prev; + return err; + } + + return 1; +} + +static const struct snd_kcontrol_new topping_ctl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, + .info = topping_ctl_info, + .get = topping_ctl_get, + .put = 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 = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_ctl, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = snd_usb_mixer_elem_free; + kctl->tlv.p = tm->ctls[idx].tlv; + strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name)); + + err = snd_usb_mixer_add_control(&elem->head, kctl); + if (err < 0) + return err; + + tm->kctl[idx] = kctl; + return 0; +} + +static void topping_suspend(struct usb_mixer_interface *mixer) +{ + struct topping_mixer *tm = 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 = 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 = memalloc_noio_save(); + + if (tm->urb) { + err = 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 = 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 = 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 = chip->dev; + struct usb_host_interface *alts; + struct usb_interface *iface; + int i, e; + + for (i = 0; i < 256; i++) { + iface = usb_ifnum_to_if(dev, i); + if (!iface) + continue; + alts = &iface->altsetting[0]; + if (alts->desc.bInterfaceClass != USB_CLASS_HID) + continue; + *ep_in = *ep_out = 0; + for (e = 0; e < alts->desc.bNumEndpoints; e++) { + struct usb_endpoint_descriptor *ep; + + ep = &alts->endpoint[e].desc; + if (!usb_endpoint_xfer_int(ep)) + continue; + if (usb_endpoint_dir_in(ep)) { + *ep_in = usb_endpoint_num(ep); + *interval = ep->bInterval; + } else { + *ep_out = 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 = mixer->chip; + struct usb_interface *iface; + struct topping_mixer *tm; + int ep_in = 0, ep_out = 0, interval = 5; + int i, err; + + iface = 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 = kzalloc_obj(*tm); + if (!tm) + return -ENOMEM; + + tm->mixer = mixer; + tm->iface = iface; + tm->ctls = topping_m62_ctls; + tm->num_ctls = ARRAY_SIZE(topping_m62_ctls); + tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in); + tm->pipe_out = usb_sndintpipe(chip->dev, ep_out); + tm->interval = interval; + spin_lock_init(&tm->lock); + mutex_init(&tm->write_lock); + INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive); + + tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL); + tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL); + if (!tm->val || !tm->kctl) { + err = -ENOMEM; + goto fail; + } + + err = snd_usb_claim_iface(chip, iface); + if (err < 0) + goto fail; + tm->claimed = 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 = usb_alloc_urb(0, GFP_KERNEL); + if (!tm->urb) { + err = -ENOMEM; + goto fail; + } + tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL, + &tm->inbuf_dma); + if (!tm->inbuf) { + err = -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 = tm->inbuf_dma; + tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + mixer->private_data = tm; + mixer->private_free = topping_private_free; + mixer->private_suspend = topping_suspend; + mixer->private_resume = topping_resume; + + for (i = 0; i < tm->num_ctls; i++) { + err = topping_add_ctl(tm, i); + if (err < 0) + return err; /* private_free cleans up */ + } + + err = 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 { #define USB_AUDIO_IFACE_UNUSED ((void *)-1L) +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...) \ -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to 2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov @ 2026-08-25 11:12 ` Mikhail Gavrilov 2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-25 11:12 UTC (permalink / raw) To: tiwai Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov 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 <mikhail.v.gavrilov@gmail.com> --- 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[] = { topping_tlv_out_0 }, }; +/* + * 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[] = { + "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[] = { + 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[] = { + { "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 */ }; 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; } +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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + + guard(mutex)(&tm->write_lock); + ucontrol->value.enumerated.item[0] = 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 = kctl->private_data; + struct topping_mixer *tm = elem->head.mixer->private_data; + const struct topping_enum_desc *d; + unsigned int item; + int err; + + item = ucontrol->value.enumerated.item[0]; + if (item >= 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] == item) + return 0; + + d = &topping_m62_enums[elem->control]; + err = topping_send(tm, d->target, d->prop, + topping_source_value[item]); + if (err < 0) + return err; + + tm->sel[elem->control] = item; + return 1; +} + +static const struct snd_kcontrol_new topping_sel = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = topping_sel_info, + .get = topping_sel_get, + .put = topping_sel_put, +}; + static const struct snd_kcontrol_new topping_ctl = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | @@ -429,6 +534,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx) return 0; } +static int topping_add_sel(struct topping_mixer *tm, int idx) +{ + struct usb_mixer_elem_info *elem; + struct snd_kcontrol *kctl; + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = tm->mixer; + elem->head.id = 0; + elem->control = idx; + elem->channels = 1; + elem->val_type = USB_MIXER_BESPOKEN; + + kctl = snd_ctl_new1(&topping_sel, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + kctl->private_free = 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 = 0; i < TOPPING_NUM_ENUMS; i++) { + if (!tm->sel[i]) + continue; /* nothing was ever chosen */ + d = &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 = mixer->private_data; @@ -473,6 +629,7 @@ static int topping_resume(struct usb_mixer_interface *mixer) */ 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 = 0; i < TOPPING_NUM_ENUMS; i++) { + err = topping_add_sel(tm, i); + if (err < 0) + return err; + } err = usb_submit_urb(tm->urb, GFP_KERNEL); if (err < 0) { -- 2.55.0 ^ permalink raw reply related [flat|nested] 29+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls 2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov @ 2026-08-26 18:06 ` Mikhail Gavrilov 2 siblings, 0 replies; 29+ messages in thread From: Mikhail Gavrilov @ 2026-08-26 18:06 UTC (permalink / raw) To: tiwai; +Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel Found on my own bench while chasing something else, and I would rather bring it than have it found for me. The problem, as a user could see it: after unbinding the vendor interface by hand, the driver keeps writing to the card. Reads stop -- the control freezes on its last announced value -- while writes go on reaching the hardware, and the two-second keepalive presumably goes on with them, into an interface the driver no longer owns. # echo 3-1.3:1.4 > /sys/bus/usb/drivers/snd-usb-audio/unbind # amixer -c M62 cget name='Mic-1 Analog Capture Volume' # frozen # amixer -c M62 cset name='Mic-1 Analog Capture Volume' 50 ... and the gain really moves on the card. The sound card itself survives the unbind untouched, which is right and is the good half of the result. My reading of why, and I would be glad to be corrected on it. The quirk claims that interface with usb_driver_claim_interface() and marks it USB_AUDIO_IFACE_UNUSED, so usb_audio_disconnect() returns at its first line and nothing ever tells the quirk to wind down. Meanwhile usb_interrupt_msg() takes a struct usb_device and an endpoint address, not an interface, so losing the claim costs the driver nothing on the write path; the read path dies only because usbcore kills the URBs on the interface being unbound. So the claim is what keeps usbhid away, not what grants the right to write, and the two are easy to conflate -- I had conflated them. How reachable this is: only by hand from sysfs. A plain unplug takes the whole device, and there disconnect runs on the audio interfaces and the quirk is freed with the mixer. I have not found a path that reaches it in ordinary use. How I plan to solve it, and this is where I need your word, because the change is in card.c rather than in my own file. An interface the quirk claimed is marked exactly like one nobody wanted, and those two are different things: the first has a driver behind it that should be told when it goes away. The shapes I can see are (a) let the quirk register a small teardown callback at claim time and have usb_audio_disconnect() run it before the early return, (b) give the claimed-and-used case its own sentinel instead of USB_AUDIO_IFACE_UNUSED, so disconnect can tell them apart, (c) leave card.c alone and have the quirk take a usb_device reference plus its own notifier, which keeps the fix inside sound/usb/mixer_topping.c at the cost of a second path watching the same event. I lean to (b) as the smallest honest change, but this is your file and the sentinel is your convention. Whichever you prefer, I would rather send it as a follow-up once the current series lands than fold it into v9: it is a separate defect, it touches a path shared by every quirk, and stirring it into a series under review would make both harder to read. Mikhail ^ permalink raw reply [flat|nested] 29+ messages in thread
end of thread, other threads:[~2026-08-26 18:06 UTC | newest] Thread overview: 29+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-08-12 17:10 snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) Mikhail Gavrilov 2026-08-13 7:24 ` Takashi Iwai 2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov 2026-08-21 11:23 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov 2026-08-23 8:50 ` Takashi Iwai 2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-23 14:22 ` [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-23 19:48 ` [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-23 22:29 ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-24 20:13 ` [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-24 22:31 ` [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-25 8:56 ` [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov 2026-08-25 11:12 ` [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov 2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox