[PATCH v10 8/8] ALSA: usb-audio: add the Topping E2x2 OTG's input mutes

From: Mikhail Gavrilov

Date: Sun Oct 18 2026 - 17:01:14 EST


TOPPING Professional Control Center shows a MUTE beside each input of
the E2x2 OTG, and on Linux an input could not be muted on the card: the
gain controls start at +0 dB, as the application's knobs do, and its
MUTE is a value below them.

The card keeps no MUTE of its own. The application writes the input's
gain, property 05, to 0, writes 0 again at every step of the knob while
the MUTE is lit, and on release writes back the gain it kept; nothing
else is written either way, and the Mobile input's MUTE writes 0x22
alone, as its gain does, which silences both of its channels. The card
acts on the 0 at once: an input's meter, which follows the gain,
read -96.0 within 82 ms of the MUTE and came back within 62 ms of the
release. The gain itself never reaches 0, so 0 is the MUTE and anything
above it the gain.

Offer each input's MUTE as a switch named after its gain, so that
alsa-lib's simple mixer joins the two and a sound server mutes through
the card, as on the M62. The switch and the gain write that one
property: 0 while the switch is off, the gain otherwise, and a gain set
while the switch is off is only kept, as the application keeps it. The
first bind writes the switches on, so a MUTE another host left on the
card is released there, as writing the gains already released it.

An off step at the bottom of each gain would have been simpler, but the
step above it is +0 dB: the input would go from silence to its full
level in one step, and a sound server that follows the scale in
decibels would find nothing between the two.

The faders get no switch. Their MUTE, on Mobile OUT, S/PDIF OUT and
each loopback, writes the same 0 to both targets of the pair, which is
already a fader's bottom step, and the steps above it start at -80 dB.
Output 1+2 has no MUTE in the application at all, only its three output
buttons, which are switches here already.

Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@xxxxxxxxx>
---
sound/usb/mixer_topping.c | 43 +++++++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)

diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 5fd14b3..6e1f60d 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -117,6 +117,7 @@ struct topping_ctl_desc {
const s32 *map; /* what each step writes, or NULL: the step */
bool sw; /* an on/off switch, not a level */
bool mute; /* a MUTE, 1 when on: the switch is its inverse */
+ bool zero_mute; /* the MUTE of the level above, as its 0 */
u8 resend; /* written again after this one if on, or 0 */
u8 modes; /* the modes that have it, or 0 for all */
};
@@ -404,12 +405,28 @@ static_assert(ARRAY_SIZE(topping_e2x2_fader) == 82);
static const DECLARE_TLV_DB_SCALE(topping_tlv_fader, -8100, 100, 1);

static const struct topping_ctl_desc topping_e2x2_ctls[] = {
+ /*
+ * An input's MUTE has no property of its own. The vendor
+ * application writes the gain to 0, writes 0 again at every step of
+ * the knob while the MUTE is lit, and on release writes back the
+ * gain it kept; the gain itself never reaches 0. So each gain has
+ * its MUTE after it, as a switch of the same name, and the two write
+ * that one property: 0 while the switch is off, the gain otherwise,
+ * and a gain set while the switch is off is only kept. A fader's
+ * MUTE is the same 0, which is already the fader's bottom step.
+ */
{ "Mic1 Digital Capture Volume", 0x21, 0, 0x05, 0, 20,
0, topping_tlv_gain, topping_e2x2_gain },
+ { "Mic1 Digital Capture Switch", 0x21, 0, 0x05, 0, 1, 1,
+ .sw = true, .zero_mute = true },
{ "Mic2 Digital Capture Volume", 0x23, 0, 0x05, 0, 20,
0, topping_tlv_gain, topping_e2x2_gain },
+ { "Mic2 Digital Capture Switch", 0x23, 0, 0x05, 0, 1, 1,
+ .sw = true, .zero_mute = true },
{ "OTG Capture Volume", 0x22, 0, 0x05, 0, 20,
0, topping_tlv_gain, topping_e2x2_gain },
+ { "OTG Capture Switch", 0x22, 0, 0x05, 0, 1, 1,
+ .sw = true, .zero_mute = true },
/*
* Output 1+2's fader as "Master": the sound server's stock
* profile takes a control by that name for the volume of the
@@ -630,10 +647,17 @@ static bool topping_muted(const struct topping_model *m, int idx)
return idx + 1 < m->num_ctls && m->ctls[idx + 1].mute;
}

+/* the same, for a MUTE written as the level's 0 */
+static bool topping_zero_muted(const struct topping_model *m, int idx)
+{
+ return idx + 1 < m->num_ctls && m->ctls[idx + 1].zero_mute;
+}
+
static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
{
const struct topping_model *m = tm->model;
const struct topping_ctl_desc *d = &m->ctls[idx];
+ const struct topping_ctl_desc *lvl;
s32 wire = d->mute ? !value : d->map ? d->map[value] : value;
bool kept;
int err, other, on;
@@ -663,6 +687,23 @@ static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
}
}

+ /*
+ * A zero_mute switch and the level above it write one property: 0
+ * while the switch is off, the level otherwise. A level set while
+ * its switch is off is only kept here as well.
+ */
+ if (d->zero_mute) {
+ lvl = &m->ctls[idx - 1];
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ other = tm->val[idx - 1];
+ wire = !value ? 0 : lvl->map ? lvl->map[other] : other;
+ } else if (topping_zero_muted(m, idx)) {
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ on = tm->val[idx + 1];
+ if (!on)
+ return 0;
+ }
+
err = topping_write(tm, d->target, d->prop, wire);
if (!err && d->target_pair) {
/*
@@ -715,6 +756,8 @@ static void topping_sync(struct topping_mixer *tm)
for (i = 0; i < m->num_ctls; i++) {
if (!topping_here(tm, m->ctls[i].modes))
continue;
+ if (topping_zero_muted(m, i))
+ continue; /* its switch writes it */
scoped_guard(spinlock_irqsave, &tm->val_lock)
value = tm->val[i];
topping_write_ctl(tm, i, value);
--
2.43.0