All of lore.kernel.org
 help / color / mirror / Atom feed
From: Will Porter <mrwillporter@gmail.com>
To: Takashi Iwai <tiwai@suse.com>, Jaroslav Kysela <perex@perex.cz>
Cc: linux-sound@vger.kernel.org, linux-kernel@vger.kernel.org,
	Neil Andrews <neil@androos.io>, Christian Ruppert <arc@gmx.li>,
	Will Porter <mrwillporter@gmail.com>
Subject: [PATCH 4/5] ALSA: usb-audio: Add Audient iD14 MkI write-only controls
Date: Mon, 19 Oct 2026 18:01:58 -0500	[thread overview]
Message-ID: <20261019230159.834954-5-mrwillporter@gmail.com> (raw)
In-Reply-To: <20261019230159.834954-1-mrwillporter@gmail.com>

Some MkI controls cannot be read back:
- GET stalls for the input gain boost and polarity (feature unit 11),
  the iD key function and the digital input mode.
- GET of the talkback source answers 0, whatever value a SET wrote.

For FU 11 the generic driver creates "Input Gain Control" and "Phase
Inverter Control" and logs "cannot get min/max values".

Add:
- Mic 1/2 Boost Capture Switch (+10 dB)
- Mic 1/2 and Digi 1/2 Polarity Invert Capture Switch
- iD Key Function Playback Enum (ScrollControl, Mono, Mono + Polarity,
  Dim, Talkback)
- Talkback Source Capture Enum (None, Mic 1-2, Digi 1-8)
- Digital In Mode Capture Enum (ADAT, S/PDIF)

and hide the generic FU 11 controls in the MkI map. The named polarity
switches replace the ten values of "Phase Inverter Control". The
controls follow Audient's iD application. It offers gain boost on the
Mic inputs only, and polarity on the Mic and Digi inputs. The device
refuses polarity on Digi 3-8 with EPIPE, in ADAT and in S/PDIF mode
(no ADAT signal connected). So Digi 3-8 have no polarity control.

These controls show the last value the device accepted. Every put sends
its value, also an unchanged one, so that a restore reaches a device
that was reset. At probe the driver sets each control once, so that the
value it shows is true:
- boost and polarity off, iD key ScrollControl, talkback source None:
  the defaults of the iD application and the manual
- S/PDIF: the MkI's measured power-on mode

If that write fails, the control reads "Unknown" (or off) until a put
succeeds. alsactl store saves that value.

The probe write replaces any value the device kept. The device keeps
its values across a module reload or a rebind. On mains power it also
keeps them across a replug. In each of these cases the controls return
to the probe values (for example from ADAT to S/PDIF) until alsactl
restore runs.

Test setup:
- iD14 MkI, firmware 1.1.1
- Bazzite 44, kernel 7.2.9-ogc3.1.fc44
- snd-usb-audio built out of tree from the v7.2.9 sound/usb sources
  and the complete driver (this commit and the later ones)

Results:
- a loopback cable measured the gain boost (+10.0 dB) and the Mic
  polarity
- listening tests: the iD key set to Dim, talkback from Mic 2
- alsactl restored the controls across a replug
- the optical power-on mode, read on macOS 27.0.1 with no Audient
  software running: S/PDIF
- the device accepts the Digi 1/2 polarity request. Its effect was not
  measured.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Will Porter <mrwillporter@gmail.com>
---
 sound/usb/mixer_audient_id.c | 144 +++++++++++++++++++++++++++++++----
 sound/usb/mixer_maps.c       |   1 +
 2 files changed, 129 insertions(+), 16 deletions(-)

diff --git a/sound/usb/mixer_audient_id.c b/sound/usb/mixer_audient_id.c
index 5b38590ce..47e16e572 100644
--- a/sound/usb/mixer_audient_id.c
+++ b/sound/usb/mixer_audient_id.c
@@ -19,8 +19,10 @@
  * The protocol comes from Audient's iD application (version 4.4.5 for
  * macOS). USB captures of the application confirmed it.
  *
- * The controls read the device on every get: the front panel changes
- * several of them.
+ * Most controls read the device on every get. Controls whose GET cannot
+ * report the current state show the last value the device accepted.
+ * Every put to these controls sends the value, even when unchanged, so
+ * that alsactl restore also reaches a device that was reset.
  *
  * Get and put need no lock of their own: the control core runs put with
  * card->controls_rwsem held for writing and get with it held for reading.
@@ -40,12 +42,17 @@
 #include "mixer_audient_id.h"
 
 /* Unit IDs */
+#define ID14_IT_DIGITAL		0x01	/* input terminal: optical and coaxial inputs */
 #define ID14_FU_OUT		0x0a	/* feature unit before the outputs */
+#define ID14_FU_IN		0x0b	/* feature unit on the inputs: CN 1-2 Mic, 3-10 Digi */
 #define ID14_ROUTE		0x33	/* routing XU: CN = destination */
 #define ID14_MON		0x36	/* monitor controller XU */
 #define ID14_MIXER		0x3c	/* mixer unit: 14 inputs x 4 outputs */
 
 /* Control selectors */
+#define ID14_IT_OPTICAL_MODE	0x00	/* u32: 0 ADAT, 1 S/PDIF */
+#define ID14_IN_BOOST		0x0b	/* s16: 0x0a00 = +10 dB, 0 = off */
+#define ID14_IN_POLARITY	0x0d
 #define ID14_ROUTE_SOURCE	0x06	/* u8: source of the destination */
 #define ID14_MON_MONO		0x00
 #define ID14_MON_MONO_MODE	0x01
@@ -54,6 +61,8 @@
 #define ID14_MON_DIM		0x05
 #define ID14_MON_DIM_LEVEL	0x06	/* s16, 1/256 dB */
 #define ID14_MON_TALKBACK	0x07
+#define ID14_MON_TALKBACK_SRC	0x08	/* GET always returns 0 */
+#define ID14_MON_ID_KEY		0x10	/* u16: the selector the iD key toggles */
 #define ID14_MON_VOLUME		0x12	/* s16, 1/256 dB; the knob also sets it */
 
 /*
@@ -68,6 +77,7 @@ enum id_kind {
 	ID_BOOL,	/* u8: 1 = on */
 	ID_BOOL_INV,	/* u8: 1 = cut or mute, shown as a switch that is off */
 	ID_VOL,		/* s16 in 1/256 dB; control value 0 ... ID_VOL_MAX, 1 dB steps */
+	ID_BOOST,	/* s16: on = +10 dB */
 	ID_ENUM,	/* item list with device values (struct id_enum) */
 };
 
@@ -78,6 +88,8 @@ struct id_enum {
 	u8 len;		/* bytes on the wire */
 };
 
+#define ID_WO		BIT(0)	/* write-only: GET stalls or does not report writes */
+
 struct id_ctl {
 	const char *name;
 	enum id_kind kind;
@@ -85,12 +97,15 @@ struct id_ctl {
 	u8 cs;
 	u8 cn;		/* first channel number */
 	u8 channels;
+	u8 flags;
+	int init;	/* ID_WO: value written at probe */
 	const struct id_enum *e;
 };
 
 /*
  * The texts have one item more than the values. A put cannot select the last
- * item, "Other": it shows a device value outside the list.
+ * item: a readable control shows it for a device value outside the list
+ * ("Other"), a write-only control while its value is not known ("Unknown").
  */
 #define ID_ENUM_TEXTS(name, texts_, len_, ...)					\
 	static const u32 name##_values[] = { __VA_ARGS__ };			\
@@ -102,11 +117,35 @@ struct id_ctl {
 	ID_ENUM_TEXTS(name, name##_texts, len_, __VA_ARGS__);			\
 	static_assert(ARRAY_SIZE(name##_texts) == ARRAY_SIZE(name##_values) + 1)
 
+/*
+ * iD key function: the value is the monitor-controller selector the key
+ * toggles. ScrollControl (0xffff) toggles nothing: the knob then scrolls
+ * through the HID interface.
+ */
+static const char *const id14_id_key_texts[] = {
+	"ScrollControl", "Mono", "Mono + Polarity", "Dim", "Talkback", "Unknown",
+};
+
+ID_ENUM_DEF(id14_id_key, 2, 0xffff, ID14_MON_MONO, ID14_MON_POLARITY, ID14_MON_DIM,
+	    ID14_MON_TALKBACK);
+
 /* Mono mode: one-speaker sum (Left, Right) or phantom centre. */
 static const char *const id14_mono_mode_texts[] = { "Left", "Centre", "Right", "Other" };
 
 ID_ENUM_DEF(id14_mono_mode, 1, 1, 0, 2);
 
+/* Talkback source: the routing source of the input; none = 0xff. */
+static const char *const id14_talkback_texts[] = {
+	"None", "Mic 1", "Mic 2", "Digi 1", "Digi 2", "Digi 3", "Digi 4", "Digi 5", "Digi 6",
+	"Digi 7", "Digi 8", "Unknown",
+};
+
+ID_ENUM_DEF(id14_talkback, 1, 0xff, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
+
+static const char *const id14_optical_texts[] = { "ADAT", "S/PDIF", "Unknown" };
+
+ID_ENUM_DEF(id14_optical, 4, 0, 1);
+
 /*
  * Routing sources for each destination. DAW Thru plays the USB stream
  * directly and bypasses the volume knob: full scale. Main Mix is 0x14/0x15
@@ -122,6 +161,16 @@ ID_ENUM_TEXTS(id14_route_hl, id14_route_texts, 1, 0x18, 0x10, 0x02);
 ID_ENUM_TEXTS(id14_route_hr, id14_route_texts, 1, 0x19, 0x11, 0x03);
 static_assert(ARRAY_SIZE(id14_route_texts) == ARRAY_SIZE(id14_route_ml_values) + 1);
 
+/*
+ * The iD application offers polarity on the Mic and Digi strips, not on
+ * DAW. The MkI accepts it on Digi 1-2 only: CN 5-10 stall in both optical modes
+ * (tested without an ADAT signal).
+ */
+#define ID14_DIGI_POLARITY(n)							\
+	{ .name = "Digi " #n " Polarity Invert Capture Switch", .kind = ID_BOOL,	\
+	  .unit = ID14_FU_IN, .cs = ID14_IN_POLARITY, .cn = 2 + (n), .channels = 1,	\
+	  .flags = ID_WO, .init = 0 }
+
 static const struct id_ctl id14_mk1_ctls[] = {
 	{ .name = "Monitor Speaker Playback Volume", .kind = ID_VOL, .unit = ID14_MON,
 	  .cs = ID14_MON_VOLUME, .channels = 1 },
@@ -152,6 +201,26 @@ static const struct id_ctl id14_mk1_ctls[] = {
 	  .cs = ID14_ROUTE_SOURCE, .cn = 2, .channels = 1, .e = &id14_route_hl },
 	{ .name = "Headphone R Source Playback Enum", .kind = ID_ENUM, .unit = ID14_ROUTE,
 	  .cs = ID14_ROUTE_SOURCE, .cn = 3, .channels = 1, .e = &id14_route_hr },
+	/* Write-only: GET stalls. The probe writes the iD application defaults. */
+	{ .name = "Mic 1 Boost Capture Switch", .kind = ID_BOOST, .unit = ID14_FU_IN,
+	  .cs = ID14_IN_BOOST, .cn = 1, .channels = 1, .flags = ID_WO, .init = 0 },
+	{ .name = "Mic 2 Boost Capture Switch", .kind = ID_BOOST, .unit = ID14_FU_IN,
+	  .cs = ID14_IN_BOOST, .cn = 2, .channels = 1, .flags = ID_WO, .init = 0 },
+	{ .name = "Mic 1 Polarity Invert Capture Switch", .kind = ID_BOOL, .unit = ID14_FU_IN,
+	  .cs = ID14_IN_POLARITY, .cn = 1, .channels = 1, .flags = ID_WO, .init = 0 },
+	{ .name = "Mic 2 Polarity Invert Capture Switch", .kind = ID_BOOL, .unit = ID14_FU_IN,
+	  .cs = ID14_IN_POLARITY, .cn = 2, .channels = 1, .flags = ID_WO, .init = 0 },
+	ID14_DIGI_POLARITY(1), ID14_DIGI_POLARITY(2),
+	{ .name = "iD Key Function Playback Enum", .kind = ID_ENUM, .unit = ID14_MON,
+	  .cs = ID14_MON_ID_KEY, .channels = 1, .flags = ID_WO, .init = 0, .e = &id14_id_key },
+	/* Write-only: GET returns 0 whatever was written. */
+	{ .name = "Talkback Source Capture Enum", .kind = ID_ENUM, .unit = ID14_MON,
+	  .cs = ID14_MON_TALKBACK_SRC, .channels = 1, .flags = ID_WO, .init = 0,
+	  .e = &id14_talkback },
+	/* Write-only: GET stalls. The probe writes the power-on mode, S/PDIF. */
+	{ .name = "Digital In Mode Capture Enum", .kind = ID_ENUM, .unit = ID14_IT_DIGITAL,
+	  .cs = ID14_IT_OPTICAL_MODE, .channels = 1, .flags = ID_WO, .init = 1,
+	  .e = &id14_optical },
 	{ }
 };
 
@@ -159,6 +228,8 @@ static const struct id_ctl id14_mk1_ctls[] = {
 struct id_elem {
 	struct usb_mixer_elem_list list;
 	const struct id_ctl *ctl;
+	long val[2];	/* ID_WO: the last value the device accepted */
+	u8 valid;	/* ID_WO: bit per channel, set when val[] is that value */
 };
 
 /* 0 = -128 dB (mute), 1 dB per step: ID_VOL_MAX = 0 dB. */
@@ -192,6 +263,7 @@ static int id_len(const struct id_ctl *c)
 	case ID_ENUM:
 		return c->e->len;
 	case ID_VOL:
+	case ID_BOOST:
 		return 2;
 	default:
 		return 1;
@@ -203,11 +275,12 @@ static bool id_valid(const struct id_ctl *c, long val)
 	switch (c->kind) {
 	case ID_BOOL:
 	case ID_BOOL_INV:
+	case ID_BOOST:
 		return val == 0 || val == 1;
 	case ID_VOL:
 		return val >= 0 && val <= ID_VOL_MAX;
 	case ID_ENUM:
-		return val >= 0 && val < c->e->items;	/* never "Other" */
+		return val >= 0 && val < c->e->items;	/* never "Other" or "Unknown" */
 	}
 	return false;
 }
@@ -234,6 +307,9 @@ static int id_read(struct usb_mixer_interface *mixer, const struct id_ctl *c,
 	case ID_BOOL_INV:
 		*val = !buf[0];
 		break;
+	case ID_BOOST:
+		*val = raw != 0;
+		break;
 	case ID_VOL:
 		/* the knob moves in whole dB; round anything else to the nearest step */
 		*val = clamp_t(long, DIV_ROUND_CLOSEST(raw, ID_DB(1)) + ID_VOL_MAX,
@@ -262,6 +338,9 @@ static int id_write(struct usb_mixer_interface *mixer, const struct id_ctl *c,
 	case ID_BOOL_INV:
 		buf[0] = !val;
 		break;
+	case ID_BOOST:
+		put_unaligned_le16(val ? ID_DB(10) : 0, buf);
+		break;
 	case ID_VOL:
 		put_unaligned_le16(ID_DB(val - ID_VOL_MAX), buf);
 		break;
@@ -319,9 +398,14 @@ static int id_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
 	int err;
 
 	for (i = 0; i < c->channels; i++) {
-		err = id_read(e->list.mixer, c, i, &val);
-		if (err < 0)
-			return err;
+		if (c->flags & ID_WO) {
+			/* No last value (the probe write failed): "Unknown", or off. */
+			val = e->valid & BIT(i) ? e->val[i] : c->kind == ID_ENUM ? c->e->items : 0;
+		} else {
+			err = id_read(e->list.mixer, c, i, &val);
+			if (err < 0)
+				return err;
+		}
 		id_set_value(c, v, i, val);
 	}
 	return 0;
@@ -346,15 +430,25 @@ static int id_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
 
 	for (i = 0; i < c->channels; i++) {
 		val = id_get_value(c, v, i);
-		err = id_read(mixer, c, i, &cur);
-		if (err < 0)
-			return err;
-		if (cur == val)
-			continue;
-		changed = 1;
+		if (c->flags & ID_WO) {
+			/* Always send: the device may have been reset since the last write. */
+			if (!(e->valid & BIT(i)) || e->val[i] != val)
+				changed = 1;
+		} else {
+			err = id_read(mixer, c, i, &cur);
+			if (err < 0)
+				return err;
+			if (cur == val)
+				continue;
+			changed = 1;
+		}
 		err = id_write(mixer, c, i, val);
 		if (err < 0)
-			return err;
+			return err;	/* a write-only channel keeps its last accepted value */
+		if (c->flags & ID_WO) {
+			e->val[i] = val;
+			e->valid |= BIT(i);
+		}
 	}
 	return changed;
 }
@@ -364,15 +458,19 @@ static int id_add_ctl(struct usb_mixer_interface *mixer, const struct id_ctl *c)
 	struct snd_kcontrol_new knew = {
 		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
 		.name = c->name,
-		/* The front panel changes values: user space must not cache them. */
-		.access = SNDRV_CTL_ELEM_ACCESS_READWRITE | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
 		.info = id_info,
 		.get = id_get,
 		.put = id_put,
 	};
 	struct snd_kcontrol *kctl;
 	struct id_elem *e;
+	unsigned int i;
+	int err;
 
+	/* The front panel changes readable values: user space must not cache them. */
+	if (!(c->flags & ID_WO))
+		knew.access |= SNDRV_CTL_ELEM_ACCESS_VOLATILE;
 	if (c->kind == ID_VOL) {
 		knew.access |= SNDRV_CTL_ELEM_ACCESS_TLV_READ;
 		knew.tlv.p = id_vol_tlv;
@@ -385,6 +483,20 @@ static int id_add_ctl(struct usb_mixer_interface *mixer, const struct id_ctl *c)
 	e->list.id = c->unit;
 	e->ctl = c;
 
+	/* A write-only control needs a known value. On failure it reads "Unknown" (or off). */
+	if (c->flags & ID_WO) {
+		for (i = 0; i < c->channels; i++) {
+			err = id_write(mixer, c, i, c->init);
+			if (err < 0) {
+				usb_audio_warn(mixer->chip, "iD14: cannot set '%s' (%d)\n",
+					       c->name, err);
+				continue;
+			}
+			e->val[i] = c->init;
+			e->valid |= BIT(i);
+		}
+	}
+
 	kctl = snd_ctl_new1(&knew, e);
 	if (!kctl) {
 		kfree(e);
diff --git a/sound/usb/mixer_maps.c b/sound/usb/mixer_maps.c
index e147c1a48..63a77f3eb 100644
--- a/sound/usb/mixer_maps.c
+++ b/sound/usb/mixer_maps.c
@@ -528,6 +528,7 @@ static const struct usbmix_name_map audient_id14_map[] = {
 
 /* Audient iD14 MkI: model-specific control map. */
 static const struct usbmix_name_map audient_id14_mk1_map[] = {
+	{ 11, NULL },			/* FU, see mixer_audient_id.c */
 	{ 12, "Monitor Mix Playback" },	/* FU, partial coverage */
 	{ 60, NULL },			/* MU, see mixer_audient_id.c */
 	{}
-- 
2.47.3


  parent reply	other threads:[~2026-10-19 23:02 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 23:01 [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls Will Porter
2026-10-19 23:01 ` [PATCH 1/5] ALSA: usb-audio: Give the Audient iD14 MkI its own mixer map Will Porter
2026-10-19 23:01 ` [PATCH 2/5] ALSA: usb-audio: Add Audient iD14 MkI monitor and routing controls Will Porter
2026-10-19 23:01 ` [PATCH 3/5] ALSA: usb-audio: Add Audient iD14 MkI mixer matrix Will Porter
2026-10-19 23:01 ` Will Porter [this message]
2026-10-19 23:01 ` [PATCH 5/5] ALSA: usb-audio: Replace the Audient iD14 MkI FU 12 control Will Porter
2026-10-20 13:10 ` [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls Takashi Iwai

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=20261019230159.834954-5-mrwillporter@gmail.com \
    --to=mrwillporter@gmail.com \
    --cc=arc@gmx.li \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-sound@vger.kernel.org \
    --cc=neil@androos.io \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.