linux-input.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
To: tiwai@suse.com
Cc: perex@perex.cz, jikos@kernel.org, bentiss@kernel.org,
	linux-sound@vger.kernel.org, linux-input@vger.kernel.org,
	linux-kernel@vger.kernel.org,
	Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
Subject: [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
Date: Mon, 24 Aug 2026 03:29:46 +0500	[thread overview]
Message-ID: <20260823222946.171345-3-mikhail.v.gavrilov@gmail.com> (raw)
In-Reply-To: <20260823222946.171345-1-mikhail.v.gavrilov@gmail.com>

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


  parent reply	other threads:[~2026-08-23 22:29 UTC|newest]

Thread overview: 40+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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:38         ` sashiko-bot
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 14:38         ` sashiko-bot
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 20:07           ` sashiko-bot
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 20:03           ` sashiko-bot
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:46             ` sashiko-bot
2026-08-23 22:29           ` Mikhail Gavrilov [this message]
2026-08-23 22:46             ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to sashiko-bot
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:40               ` sashiko-bot
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 20:25               ` sashiko-bot
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:47                 ` sashiko-bot
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-24 22:58                 ` sashiko-bot
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  9:12                   ` sashiko-bot
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260823222946.171345-3-mikhail.v.gavrilov@gmail.com \
    --to=mikhail.v.gavrilov@gmail.com \
    --cc=bentiss@kernel.org \
    --cc=jikos@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-sound@vger.kernel.org \
    --cc=perex@perex.cz \
    --cc=tiwai@suse.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).