* [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls
@ 2026-10-19 23:01 Will Porter
2026-10-19 23:01 ` [PATCH 1/5] ALSA: usb-audio: Give the Audient iD14 MkI its own mixer map Will Porter
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Will Porter @ 2026-10-19 23:01 UTC (permalink / raw)
To: Takashi Iwai, Jaroslav Kysela
Cc: linux-sound, linux-kernel, Neil Andrews, Christian Ruppert, Will Porter
The Audient iD14 MkI (2708:0002) keeps its monitor section, output
routing, mixer and input options on vendor extension units. It also
uses feature units whose descriptors declare no controls. On Linux a
user cannot change the routing. The device powers up with every output
on DAW Thru, which bypasses the volume knob.
This series adds those controls in a new mixer_audient_id.c, one group
per patch. A MkI-only name map hides the generic controls that each
group replaces. The MkII keeps audient_id14_map.
Scope: only the iD14 MkI is supported, and only its USB ID reaches the
new code. Other Audient iD models keep the generic driver. Their units
may differ, and this series does not cover them. The file follows
mixer_scarlett2.c, which began with one model and added others through
per-model tables. Names with id14_ hold the MkI's unit IDs and tables. A
later iD model can add its own tables to this file.
sound for-next also has mixer_evo.c for the Audient EVO 4 (2708:0006).
EVO and iD are separate Audient product lines. The iD14 MkI uses other
units and a larger control set: a monitor controller, output routing, a
14 x 4 mixer and write-only controls. So this series keeps the iD line
in its own file.
At probe the driver writes only controls that it cannot read back: the
write-only controls (patch 4) and FU 12 (patch 5). They get the launch
values of Audient's iD application. If a probe write fails, the control
shows the fallback value that those patches describe. The driver does
not write the routing or the mix. User space (alsactl, an application)
sets the rest of the iD application's launch state.
There is no resume code. Every write to a write-only control reaches
the device, so alsactl restore also repairs a device that was reset.
The state across a suspend depends on the power source:
- On USB bus power, a system suspend re-enumerates the device.
- With its mains adapter connected, the device keeps its state across a
system suspend. This is also true when the host controller resets the
device and the card stays. The sampled readable controls did not
change.
- A runtime suspend left the readable controls unchanged on both power
sources, with no reset logged. A runtime suspend is possible only
after usbhid releases the HID interface.
Patch 5 replaces the generic "Monitor Mix Playback Volume" (FU 12) with
one of the same name, count and range. FU 12 GET CUR returns stale data
from other units. Its Main L channel ignores writes, but Main R applies
them. audient_id14_map already notes this asymmetry. The new control
holds Main at 0 dB. This is a deliberate change for existing users: a
Main R attenuation set through the generic control no longer applies.
The protocol comes from Audient's iD application (version 4.4.5 for
macOS). USB captures of the application confirmed it.
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
with this series applied
Tests:
- listening tests: routing, dim, talkback, Cue
- a loopback cable: gain boost, polarity, FU 12 per channel
- alsactl store and restore across a replug
- an out-of-list routing value
- a runtime suspend
- three system suspends (S3) with control reads running at about 3000
per second: no read failed after a resume
- the optical power-on mode, read on macOS 27.0.1 with no Audient
software running
The series is based on sound for-next a4ef328ce3b3. Each patch builds
on its own there with make W=1 sound/usb/. The hardware tests used the
complete series on 7.2.9 sources, where snd_usb_mixer_add_list() has no
look_for_empty argument. The series passes true, which keeps that
behaviour.
Will Porter (5):
ALSA: usb-audio: Give the Audient iD14 MkI its own mixer map
ALSA: usb-audio: Add Audient iD14 MkI monitor and routing controls
ALSA: usb-audio: Add Audient iD14 MkI mixer matrix
ALSA: usb-audio: Add Audient iD14 MkI write-only controls
ALSA: usb-audio: Replace the Audient iD14 MkI FU 12 control
sound/usb/Makefile | 1 +
sound/usb/mixer_audient_id.c | 790 +++++++++++++++++++++++++++++++++++
sound/usb/mixer_audient_id.h | 7 +
sound/usb/mixer_maps.c | 10 +-
sound/usb/mixer_quirks.c | 4 +
5 files changed, 811 insertions(+), 1 deletion(-)
create mode 100644 sound/usb/mixer_audient_id.c
create mode 100644 sound/usb/mixer_audient_id.h
base-commit: a4ef328ce3b3ab1bf63c6350e890f13b9b25e3f1
--
2.47.3
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/5] ALSA: usb-audio: Give the Audient iD14 MkI its own mixer map
2026-10-19 23:01 [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls Will Porter
@ 2026-10-19 23:01 ` Will Porter
2026-10-19 23:01 ` [PATCH 2/5] ALSA: usb-audio: Add Audient iD14 MkI monitor and routing controls Will Porter
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Will Porter @ 2026-10-19 23:01 UTC (permalink / raw)
To: Takashi Iwai, Jaroslav Kysela
Cc: linux-sound, linux-kernel, Neil Andrews, Christian Ruppert, Will Porter
The iD14 MkI (2708:0002) and MkII (2708:0008) share audient_id14_map.
Later commits add MkI vendor controls that replace some of the generic
MkI controls. The MkII keeps the generic controls. Give the MkI its own
copy of the map.
No functional change.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Will Porter <mrwillporter@gmail.com>
---
sound/usb/mixer_maps.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/sound/usb/mixer_maps.c b/sound/usb/mixer_maps.c
index 98b414621..7795a5fbe 100644
--- a/sound/usb/mixer_maps.c
+++ b/sound/usb/mixer_maps.c
@@ -526,6 +526,12 @@ 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[] = {
+ { 12, "Monitor Mix Playback" }, /* FU, partial coverage */
+ {}
+};
+
/*
* Audient iD24: feature unit 12 ("Speaker Playback Volume") sits in the
* monitor-mixer branch and does not apply volume to all of its channels;
@@ -638,7 +644,7 @@ static const struct usbmix_ctl_map usbmix_ctl_maps[] = {
{
/* Audient iD14 MkI */
.id = USB_ID(0x2708, 0x0002),
- .map = audient_id14_map,
+ .map = audient_id14_mk1_map,
},
{
/* Audient EVO4 MkI */
--
2.47.3
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 2/5] ALSA: usb-audio: Add Audient iD14 MkI monitor and routing controls
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 ` Will Porter
2026-10-19 23:01 ` [PATCH 3/5] ALSA: usb-audio: Add Audient iD14 MkI mixer matrix Will Porter
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Will Porter @ 2026-10-19 23:01 UTC (permalink / raw)
To: Takashi Iwai, Jaroslav Kysela
Cc: linux-sound, linux-kernel, Neil Andrews, Christian Ruppert, Will Porter
The iD14 MkI keeps three groups of controls where the generic driver
cannot discover them:
- the monitor controller, on Audient extension unit 54
- the headphone level, on feature unit 10, whose descriptor declares
no controls
- the output routing, on extension unit 51
So Linux users cannot change the routing. The MkI powers up with every
output on DAW Thru: the USB stream goes straight to the output and
bypasses the volume knob.
Add mixer_audient_id.c with these controls:
- Monitor Speaker Playback Volume and Switch (speaker level and cut)
- Monitor Dim Playback Switch, Monitor Dim Level Playback Volume
- Monitor Mono Playback Switch, Monitor Mono Mode Playback Enum
- Monitor Polarity Invert Playback Switch
- Monitor Headphone Playback Volume and Switch (FU 10, channels 3-4)
- Talkback Playback Switch
- Main Out L/R Source Playback Enum, Headphone L/R Source Playback Enum:
"Main Mix", "Cue Mix" or "DAW Thru (full scale)"
Every get reads the device, because the volume knob changes the speaker
and headphone levels. The volumes are 0-128 in 1 dB steps: 0 is -128 dB
(mute) and 128 is 0 dB. The driver converts them to the device's s16 in
1/256 dB.
The routing controls are mono enums, as in mixer_scarlett2.c. Thus one
output on DAW Thru cannot hide behind its pair. The "... Playback Enum"
names follow the same driver. A source outside the list reads as an
extra item, "Other". A put rejects "Other", but alsactl store saves it.
A restore of it then fails for that control only, or for the card with
--pedantic. The restore cannot recreate the original source. So after a
replug on bus power, that output stays on its power-on source, DAW Thru.
The file is named for the iD family and keeps model data in per-model
tables, as mixer_scarlett2.c does. Only the iD14 MkI is supported. Other
iD models keep the generic driver.
The driver writes nothing at probe. User space sets the launch state of
Audient's iD application (all outputs on Main Mix).
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:
- routing: listening tests (Main Mix and Cue Mix, left and right) and
read-back
- dim, by ear: host switch and iD key
- talkback, by ear (a later commit adds the talkback source control)
- alsactl store and restore across a replug
- an out-of-list routing byte (0x13) reads as "Other"
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Will Porter <mrwillporter@gmail.com>
---
sound/usb/Makefile | 1 +
sound/usb/mixer_audient_id.c | 407 +++++++++++++++++++++++++++++++++++
sound/usb/mixer_audient_id.h | 7 +
sound/usb/mixer_quirks.c | 4 +
4 files changed, 419 insertions(+)
create mode 100644 sound/usb/mixer_audient_id.c
create mode 100644 sound/usb/mixer_audient_id.h
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index 2e842dcc7..7e4e6553d 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -11,6 +11,7 @@ snd-usb-audio-y := card.o \
helper.o \
implicit.o \
mixer.o \
+ mixer_audient_id.o \
mixer_evo.o \
mixer_quirks.o \
mixer_scarlett.o \
diff --git a/sound/usb/mixer_audient_id.c b/sound/usb/mixer_audient_id.c
new file mode 100644
index 000000000..b4d1e1697
--- /dev/null
+++ b/sound/usb/mixer_audient_id.c
@@ -0,0 +1,407 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Audient iD mixer controls
+ *
+ * Supported models:
+ * - iD14 MkI (2708:0002)
+ *
+ * Other Audient iD models keep the generic driver. Their units may differ,
+ * and this code does not cover them. As in mixer_scarlett2.c, a later
+ * model can add its own control tables to this file. Unit IDs, tables and
+ * functions named id14_* are for the iD14 MkI only. The id_* helpers take
+ * a table.
+ *
+ * The iD14 MkI (2708:0002) has its monitor controller, output routing,
+ * headphone level, input options and a 14 x 4 mixer on Audient extension
+ * units and on feature units whose descriptors declare no controls. The
+ * generic driver cannot discover these. This file adds them.
+ *
+ * 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.
+ *
+ * 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.
+ */
+
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/usb/audio.h>
+#include <linux/usb/audio-v2.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_audient_id.h"
+
+/* Unit IDs */
+#define ID14_FU_OUT 0x0a /* feature unit before the outputs */
+#define ID14_ROUTE 0x33 /* routing XU: CN = destination */
+#define ID14_MON 0x36 /* monitor controller XU */
+
+/* Control selectors */
+#define ID14_ROUTE_SOURCE 0x06 /* u8: source of the destination */
+#define ID14_MON_MONO 0x00
+#define ID14_MON_MONO_MODE 0x01
+#define ID14_MON_POLARITY 0x03
+#define ID14_MON_CUT 0x04
+#define ID14_MON_DIM 0x05
+#define ID14_MON_DIM_LEVEL 0x06 /* s16, 1/256 dB */
+#define ID14_MON_TALKBACK 0x07
+#define ID14_MON_VOLUME 0x12 /* s16, 1/256 dB; the knob also sets it */
+
+/*
+ * Volumes are the device's s16 in 1/256 dB; the knob and the iD application
+ * use 1 dB steps. The controls show the steps: 0 (-128 dB, mute) ... 128 (0 dB).
+ */
+#define ID_DB(db) ((db) * 256)
+#define ID_VOL_MAX 128 /* control value for 0 dB */
+
+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_ENUM, /* item list with device values (struct id_enum) */
+};
+
+struct id_enum {
+ const char *const *texts;
+ const u32 *values;
+ unsigned int items;
+ u8 len; /* bytes on the wire */
+};
+
+struct id_ctl {
+ const char *name;
+ enum id_kind kind;
+ u8 unit;
+ u8 cs;
+ u8 cn; /* first channel number */
+ u8 channels;
+ 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.
+ */
+#define ID_ENUM_TEXTS(name, texts_, len_, ...) \
+ static const u32 name##_values[] = { __VA_ARGS__ }; \
+ static const struct id_enum name = { \
+ .texts = texts_, .values = name##_values, \
+ .items = ARRAY_SIZE(name##_values), .len = len_, \
+ }
+#define ID_ENUM_DEF(name, len_, ...) \
+ ID_ENUM_TEXTS(name, name##_texts, len_, __VA_ARGS__); \
+ static_assert(ARRAY_SIZE(name##_texts) == ARRAY_SIZE(name##_values) + 1)
+
+/* 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);
+
+/*
+ * Routing sources for each destination. DAW Thru plays the USB stream
+ * directly and bypasses the volume knob: full scale. Main Mix is 0x14/0x15
+ * on the main outputs and 0x18/0x19 on the headphones.
+ */
+static const char *const id14_route_texts[] = {
+ "Main Mix", "Cue Mix", "DAW Thru (full scale)", "Other",
+};
+
+ID_ENUM_TEXTS(id14_route_ml, id14_route_texts, 1, 0x14, 0x10, 0x00);
+ID_ENUM_TEXTS(id14_route_mr, id14_route_texts, 1, 0x15, 0x11, 0x01);
+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);
+
+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 },
+ { .name = "Monitor Speaker Playback Switch", .kind = ID_BOOL_INV, .unit = ID14_MON,
+ .cs = ID14_MON_CUT, .channels = 1 },
+ { .name = "Monitor Dim Playback Switch", .kind = ID_BOOL, .unit = ID14_MON,
+ .cs = ID14_MON_DIM, .channels = 1 },
+ { .name = "Monitor Dim Level Playback Volume", .kind = ID_VOL, .unit = ID14_MON,
+ .cs = ID14_MON_DIM_LEVEL, .channels = 1 },
+ { .name = "Monitor Mono Playback Switch", .kind = ID_BOOL, .unit = ID14_MON,
+ .cs = ID14_MON_MONO, .channels = 1 },
+ { .name = "Monitor Mono Mode Playback Enum", .kind = ID_ENUM, .unit = ID14_MON,
+ .cs = ID14_MON_MONO_MODE, .channels = 1, .e = &id14_mono_mode },
+ { .name = "Monitor Polarity Invert Playback Switch", .kind = ID_BOOL, .unit = ID14_MON,
+ .cs = ID14_MON_POLARITY, .channels = 1 },
+ /* The descriptor declares no controls on FU 10; the headphones are CN 3-4. */
+ { .name = "Monitor Headphone Playback Volume", .kind = ID_VOL, .unit = ID14_FU_OUT,
+ .cs = UAC_FU_VOLUME, .cn = 3, .channels = 2 },
+ { .name = "Monitor Headphone Playback Switch", .kind = ID_BOOL_INV, .unit = ID14_FU_OUT,
+ .cs = UAC_FU_MUTE, .cn = 3, .channels = 2 },
+ { .name = "Talkback Playback Switch", .kind = ID_BOOL, .unit = ID14_MON,
+ .cs = ID14_MON_TALKBACK, .channels = 1 },
+ { .name = "Main Out L Source Playback Enum", .kind = ID_ENUM, .unit = ID14_ROUTE,
+ .cs = ID14_ROUTE_SOURCE, .cn = 0, .channels = 1, .e = &id14_route_ml },
+ { .name = "Main Out R Source Playback Enum", .kind = ID_ENUM, .unit = ID14_ROUTE,
+ .cs = ID14_ROUTE_SOURCE, .cn = 1, .channels = 1, .e = &id14_route_mr },
+ { .name = "Headphone L Source Playback Enum", .kind = ID_ENUM, .unit = ID14_ROUTE,
+ .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 },
+ { }
+};
+
+/* Per-control state; the usb_mixer_elem_list must come first (snd_usb_mixer_elem_free). */
+struct id_elem {
+ struct usb_mixer_elem_list list;
+ const struct id_ctl *ctl;
+};
+
+/* 0 = -128 dB (mute), 1 dB per step: ID_VOL_MAX = 0 dB. */
+static const DECLARE_TLV_DB_SCALE(id_vol_tlv, -12800, 100, 1);
+
+static int id_xfer(struct usb_mixer_interface *mixer, bool set,
+ u8 unit, u8 cs, u8 cn, void *buf, int len)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_device *dev = chip->dev;
+ int err;
+
+ CLASS(snd_usb_lock, pm)(chip);
+ if (pm.err < 0)
+ return pm.err;
+ err = snd_usb_ctl_msg(dev,
+ set ? usb_sndctrlpipe(dev, 0) : usb_rcvctrlpipe(dev, 0),
+ UAC2_CS_CUR,
+ USB_TYPE_CLASS | USB_RECIP_INTERFACE |
+ (set ? USB_DIR_OUT : USB_DIR_IN),
+ cs << 8 | cn, unit << 8 | mixer->hostif->desc.bInterfaceNumber,
+ buf, len);
+ if (err < 0)
+ return err;
+ return err == len ? 0 : -EIO; /* a short transfer is not a value */
+}
+
+static int id_len(const struct id_ctl *c)
+{
+ switch (c->kind) {
+ case ID_ENUM:
+ return c->e->len;
+ case ID_VOL:
+ return 2;
+ default:
+ return 1;
+ }
+}
+
+static bool id_valid(const struct id_ctl *c, long val)
+{
+ switch (c->kind) {
+ case ID_BOOL:
+ case ID_BOOL_INV:
+ 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 false;
+}
+
+/* Read channel i. An item outside the list reads as the extra last item. */
+static int id_read(struct usb_mixer_interface *mixer, const struct id_ctl *c,
+ unsigned int i, long *val)
+{
+ u8 buf[4] = { 0 };
+ unsigned int item;
+ s16 raw;
+ u32 dev;
+ int err;
+
+ err = id_xfer(mixer, false, c->unit, c->cs, c->cn + i, buf, id_len(c));
+ if (err < 0)
+ return err;
+ raw = get_unaligned_le16(buf);
+
+ switch (c->kind) {
+ case ID_BOOL:
+ *val = !!buf[0];
+ break;
+ case ID_BOOL_INV:
+ *val = !buf[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,
+ 0, ID_VOL_MAX);
+ break;
+ case ID_ENUM:
+ dev = get_unaligned_le32(buf);
+ for (item = 0; item < c->e->items; item++)
+ if (c->e->values[item] == dev)
+ break;
+ *val = item; /* c->e->items: "Other" */
+ break;
+ }
+ return 0;
+}
+
+static int id_write(struct usb_mixer_interface *mixer, const struct id_ctl *c,
+ unsigned int i, long val)
+{
+ u8 buf[4] = { 0 };
+
+ switch (c->kind) {
+ case ID_BOOL:
+ buf[0] = val;
+ break;
+ case ID_BOOL_INV:
+ buf[0] = !val;
+ break;
+ case ID_VOL:
+ put_unaligned_le16(ID_DB(val - ID_VOL_MAX), buf);
+ break;
+ case ID_ENUM:
+ put_unaligned_le32(c->e->values[val], buf);
+ break;
+ }
+ return id_xfer(mixer, true, c->unit, c->cs, c->cn + i, buf, id_len(c));
+}
+
+static int id_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo)
+{
+ struct id_elem *e = snd_kcontrol_chip(kctl);
+ const struct id_ctl *c = e->ctl;
+
+ switch (c->kind) {
+ case ID_ENUM:
+ return snd_ctl_enum_info(uinfo, c->channels, c->e->items + 1, c->e->texts);
+ case ID_VOL:
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = ID_VOL_MAX;
+ break;
+ default:
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = 1;
+ break;
+ }
+ uinfo->count = c->channels;
+ return 0;
+}
+
+static void id_set_value(const struct id_ctl *c, struct snd_ctl_elem_value *v,
+ unsigned int i, long val)
+{
+ if (c->kind == ID_ENUM)
+ v->value.enumerated.item[i] = val;
+ else
+ v->value.integer.value[i] = val;
+}
+
+static long id_get_value(const struct id_ctl *c, const struct snd_ctl_elem_value *v,
+ unsigned int i)
+{
+ return c->kind == ID_ENUM ? v->value.enumerated.item[i] : v->value.integer.value[i];
+}
+
+static int id_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
+{
+ struct id_elem *e = snd_kcontrol_chip(kctl);
+ const struct id_ctl *c = e->ctl;
+ unsigned int i;
+ long val;
+ int err;
+
+ for (i = 0; i < c->channels; i++) {
+ err = id_read(e->list.mixer, c, i, &val);
+ if (err < 0)
+ return err;
+ id_set_value(c, v, i, val);
+ }
+ return 0;
+}
+
+static int id_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
+{
+ struct id_elem *e = snd_kcontrol_chip(kctl);
+ struct usb_mixer_interface *mixer = e->list.mixer;
+ const struct id_ctl *c = e->ctl;
+ unsigned int i;
+ long cur, val;
+ int err, changed = 0;
+
+ for (i = 0; i < c->channels; i++)
+ if (!id_valid(c, id_get_value(c, v, i)))
+ return -EINVAL;
+
+ CLASS(snd_usb_lock, pm)(mixer->chip);
+ if (pm.err < 0)
+ return pm.err;
+
+ 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;
+ err = id_write(mixer, c, i, val);
+ if (err < 0)
+ return err;
+ }
+ return changed;
+}
+
+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,
+ .info = id_info,
+ .get = id_get,
+ .put = id_put,
+ };
+ struct snd_kcontrol *kctl;
+ struct id_elem *e;
+
+ if (c->kind == ID_VOL) {
+ knew.access |= SNDRV_CTL_ELEM_ACCESS_TLV_READ;
+ knew.tlv.p = id_vol_tlv;
+ }
+
+ e = kzalloc_obj(*e);
+ if (!e)
+ return -ENOMEM;
+ e->list.mixer = mixer;
+ e->list.id = c->unit;
+ e->ctl = c;
+
+ kctl = snd_ctl_new1(&knew, e);
+ if (!kctl) {
+ kfree(e);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ return snd_usb_mixer_add_list(&e->list, kctl, false, true);
+}
+
+/* called from mixer_quirks.c */
+int snd_audient_id14_mk1_controls_create(struct usb_mixer_interface *mixer)
+{
+ const struct id_ctl *c;
+ int err;
+
+ for (c = id14_mk1_ctls; c->name; c++) {
+ err = id_add_ctl(mixer, c);
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
diff --git a/sound/usb/mixer_audient_id.h b/sound/usb/mixer_audient_id.h
new file mode 100644
index 000000000..c456353ed
--- /dev/null
+++ b/sound/usb/mixer_audient_id.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __USB_MIXER_AUDIENT_ID_H
+#define __USB_MIXER_AUDIENT_ID_H
+
+int snd_audient_id14_mk1_controls_create(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_AUDIENT_ID_H */
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index 01210cc3f..478e6152c 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -34,6 +34,7 @@
#include "usbaudio.h"
#include "mixer.h"
#include "mixer_quirks.h"
+#include "mixer_audient_id.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
#include "mixer_us16x08.h"
@@ -4609,6 +4610,9 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
case USB_ID(0x03f0, 0x0269): /* HP TB Dock G2 */
err = hp_dock_mixer_create(mixer);
break;
+ case USB_ID(0x2708, 0x0002): /* Audient iD14 MkI */
+ err = snd_audient_id14_mk1_controls_create(mixer);
+ break;
}
return err;
--
2.47.3
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 3/5] ALSA: usb-audio: Add Audient iD14 MkI mixer matrix
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 ` Will Porter
2026-10-19 23:01 ` [PATCH 4/5] ALSA: usb-audio: Add Audient iD14 MkI write-only controls Will Porter
2026-10-19 23:01 ` [PATCH 5/5] ALSA: usb-audio: Replace the Audient iD14 MkI FU 12 control Will Porter
4 siblings, 0 replies; 6+ messages in thread
From: Will Porter @ 2026-10-19 23:01 UTC (permalink / raw)
To: Takashi Iwai, Jaroslav Kysela
Cc: linux-sound, linux-kernel, Neil Andrews, Christian Ruppert, Will Porter
Mixer unit 60 mixes 14 inputs (Mic 1-2, Digi 1-8, DAW 1-4) into four
outputs (Main L/R, Cue L/R). GET RANGE reports -127 to 0 dB in 1 dB
steps, so the generic driver creates four "Analogue 4 Volume" controls
with values from 0 to 127. The crosspoints take -128 dB (mute) to +6 dB
in 1/256 dB steps. The reported range cannot show the gains above 0 dB
or the fine steps that Audient's iD application uses.
Add one control per crosspoint, "Mix <output> <input> Playback Volume".
The value is the device's s16 in 1/256 dB, from -128 dB (mute) to
+6 dB. The fine steps carry the pan law of Audient's iD application.
GET CUR works, so get reads the device. Hide the generic controls in
the MkI map. The driver writes nothing to the mixer at probe.
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:
- crosspoints read back after writes
- DAW 1/2 reach Main L/R and Cue L/R through them (listening test)
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Will Porter <mrwillporter@gmail.com>
---
sound/usb/mixer_audient_id.c | 124 +++++++++++++++++++++++++++++++++++
sound/usb/mixer_maps.c | 1 +
2 files changed, 125 insertions(+)
diff --git a/sound/usb/mixer_audient_id.c b/sound/usb/mixer_audient_id.c
index b4d1e1697..5b38590ce 100644
--- a/sound/usb/mixer_audient_id.c
+++ b/sound/usb/mixer_audient_id.c
@@ -43,6 +43,7 @@
#define ID14_FU_OUT 0x0a /* feature unit before the outputs */
#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_ROUTE_SOURCE 0x06 /* u8: source of the destination */
@@ -60,6 +61,7 @@
* use 1 dB steps. The controls show the steps: 0 (-128 dB, mute) ... 128 (0 dB).
*/
#define ID_DB(db) ((db) * 256)
+#define ID_DB_MIN ID_DB(-128) /* 0x8000: mute */
#define ID_VOL_MAX 128 /* control value for 0 dB */
enum id_kind {
@@ -392,10 +394,125 @@ static int id_add_ctl(struct usb_mixer_interface *mixer, const struct id_ctl *c)
return snd_usb_mixer_add_list(&e->list, kctl, false, true);
}
+/*
+ * iD14 MkI mixer crosspoints: CS 1, CN = input * 4 + output, s16 in
+ * 1/256 dB. The control value is the raw s16, -128 dB (mute) ... +6 dB;
+ * the iD application's pan law needs the fine steps. GET CUR works; no
+ * probe writes.
+ */
+#define ID14_MIX_OUTPUTS 4
+#define ID14_MIX_MAX ID_DB(6)
+
+static const char *const id14_mix_inputs[] = {
+ "Mic 1", "Mic 2",
+ "Digi 1", "Digi 2", "Digi 3", "Digi 4", "Digi 5", "Digi 6", "Digi 7", "Digi 8",
+ "DAW 1", "DAW 2", "DAW 3", "DAW 4",
+};
+
+static const char *const id14_mix_outputs[ID14_MIX_OUTPUTS] = {
+ "Main L", "Main R", "Cue L", "Cue R",
+};
+
+static const DECLARE_TLV_DB_MINMAX_MUTE(id14_mix_tlv, -12800, 600);
+
+static int id14_mix_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = ID_DB_MIN;
+ uinfo->value.integer.max = ID14_MIX_MAX;
+ return 0;
+}
+
+struct id14_mix {
+ struct usb_mixer_elem_list list; /* first: snd_usb_mixer_elem_free */
+ u8 cn;
+};
+
+static int id14_mix_read(struct id14_mix *m, long *val)
+{
+ u8 buf[2];
+ int err;
+
+ err = id_xfer(m->list.mixer, false, ID14_MIXER, UAC2_MU_MIXER, m->cn, buf, sizeof(buf));
+ if (err < 0)
+ return err;
+ *val = clamp_t(long, (s16)get_unaligned_le16(buf), ID_DB_MIN, ID14_MIX_MAX);
+ return 0;
+}
+
+static int id14_mix_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
+{
+ struct id14_mix *m = snd_kcontrol_chip(kctl);
+ long val;
+ int err;
+
+ err = id14_mix_read(m, &val);
+ if (err < 0)
+ return err;
+ v->value.integer.value[0] = val;
+ return 0;
+}
+
+static int id14_mix_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
+{
+ struct id14_mix *m = snd_kcontrol_chip(kctl);
+ long cur, val = v->value.integer.value[0];
+ u8 buf[2];
+ int err;
+
+ if (val < ID_DB_MIN || val > ID14_MIX_MAX)
+ return -EINVAL;
+ CLASS(snd_usb_lock, pm)(m->list.mixer->chip);
+ if (pm.err < 0)
+ return pm.err;
+ err = id14_mix_read(m, &cur);
+ if (err < 0 || cur == val)
+ return err;
+ put_unaligned_le16(val, buf);
+ err = id_xfer(m->list.mixer, true, ID14_MIXER, UAC2_MU_MIXER, m->cn, buf, sizeof(buf));
+ return err < 0 ? err : 1;
+}
+
+static int id14_mix_add_ctl(struct usb_mixer_interface *mixer, unsigned int in,
+ unsigned int out)
+{
+ struct snd_kcontrol_new knew = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = id14_mix_info,
+ .get = id14_mix_get,
+ .put = id14_mix_put,
+ .tlv.p = id14_mix_tlv,
+ };
+ struct snd_kcontrol *kctl;
+ struct id14_mix *m;
+
+ m = kzalloc_obj(*m);
+ if (!m)
+ return -ENOMEM;
+ m->list.mixer = mixer;
+ m->list.id = ID14_MIXER;
+ m->cn = in * ID14_MIX_OUTPUTS + out;
+
+ kctl = snd_ctl_new1(&knew, m);
+ if (!kctl) {
+ kfree(m);
+ return -ENOMEM;
+ }
+ snprintf(kctl->id.name, sizeof(kctl->id.name), "Mix %s %s Playback Volume",
+ id14_mix_outputs[out], id14_mix_inputs[in]);
+ kctl->private_free = snd_usb_mixer_elem_free;
+ return snd_usb_mixer_add_list(&m->list, kctl, false, true);
+}
+
/* called from mixer_quirks.c */
int snd_audient_id14_mk1_controls_create(struct usb_mixer_interface *mixer)
{
const struct id_ctl *c;
+ unsigned int in, out;
int err;
for (c = id14_mk1_ctls; c->name; c++) {
@@ -403,5 +520,12 @@ int snd_audient_id14_mk1_controls_create(struct usb_mixer_interface *mixer)
if (err < 0)
return err;
}
+ for (in = 0; in < ARRAY_SIZE(id14_mix_inputs); in++) {
+ for (out = 0; out < ID14_MIX_OUTPUTS; out++) {
+ err = id14_mix_add_ctl(mixer, in, out);
+ if (err < 0)
+ return err;
+ }
+ }
return 0;
}
diff --git a/sound/usb/mixer_maps.c b/sound/usb/mixer_maps.c
index 7795a5fbe..e147c1a48 100644
--- a/sound/usb/mixer_maps.c
+++ b/sound/usb/mixer_maps.c
@@ -529,6 +529,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[] = {
{ 12, "Monitor Mix Playback" }, /* FU, partial coverage */
+ { 60, NULL }, /* MU, see mixer_audient_id.c */
{}
};
--
2.47.3
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 4/5] ALSA: usb-audio: Add Audient iD14 MkI write-only controls
2026-10-19 23:01 [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls Will Porter
` (2 preceding siblings ...)
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
2026-10-19 23:01 ` [PATCH 5/5] ALSA: usb-audio: Replace the Audient iD14 MkI FU 12 control Will Porter
4 siblings, 0 replies; 6+ messages in thread
From: Will Porter @ 2026-10-19 23:01 UTC (permalink / raw)
To: Takashi Iwai, Jaroslav Kysela
Cc: linux-sound, linux-kernel, Neil Andrews, Christian Ruppert, Will Porter
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
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 5/5] ALSA: usb-audio: Replace the Audient iD14 MkI FU 12 control
2026-10-19 23:01 [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls Will Porter
` (3 preceding siblings ...)
2026-10-19 23:01 ` [PATCH 4/5] ALSA: usb-audio: Add Audient iD14 MkI write-only controls Will Porter
@ 2026-10-19 23:01 ` Will Porter
4 siblings, 0 replies; 6+ messages in thread
From: Will Porter @ 2026-10-19 23:01 UTC (permalink / raw)
To: Takashi Iwai, Jaroslav Kysela
Cc: linux-sound, linux-kernel, Neil Andrews, Christian Ruppert, Will Porter
Feature unit 12 sets the level of the mixer outputs: channels 1-2 Main,
3-4 Cue. Two firmware faults make the generic control wrong on the
MkI:
- GET CUR does not return the FU 12 value. It returns a reply buffer
that the device shares with FU 10, MU 60 and XU 51. The buffer holds
the data of the last GET or SET to any of them. So the generic
sticky-GET check passes, because GET echoes the SET just sent. The
check then writes the stale value that it read first to channel 1.
- Main L ignores its level, but Main R applies it. So any Main value
other than 0 dB unbalances the main outputs. audient_id14_map already
records this asymmetry. A loopback cable measured it: Main R at
-20 dB, -40 dB and mute changes its output by -20.0, -40.9 and
-56.6 dB. Main L at the same values changes nothing. Cue L and Cue R
both work.
Replace the generic control with one of the same name, count and range.
It is "Monitor Mix Playback Volume", with four values from 0 to 127 in
1 dB steps. 127 is 0 dB. Value 0 is mute (0x8000), the floor that
Audient's iD application writes, and the TLV says so. The four values
keep the Main L/R, Cue L/R layout of existing saved ALSA states, but
Main is now held at 0 dB.
The control holds Main at 0 dB, as the iD application does. (The
application has no Main fader and writes 0 dB at launch.) A put always
sends 0 dB to channels 1-2 and returns that value to the caller. Get
reports it too. For Cue the control shows the last value the device
accepted. At probe the driver writes the iD application's launch values
once per channel: Main 0 dB, Cue muted. A channel whose probe write
fails reads as mute until a put succeeds.
The iD14 MkI profile in alsa-ucm-conf writes 127,127,127,127 to this
control at boot and when its HiFi verb starts. The new control accepts
that write: Main stays at 0 dB and Cue goes to 0 dB, the same result as
with the generic control.
Differences from the generic control:
- Value 0 is now mute. It was -127 dB.
- The control holds Main at 0 dB. A Main R attenuation stored with the
generic control (for example 107, -20 dB) now restores as 0 dB, which
raises Main R by 20 dB. Main L never applied that attenuation, so
Main R comes up to the level that Main L already had. FU 12 comes
before the monitor controller (XU 54) and the headphone level
(FU 10), so the volume knob still sets the listening level.
- Cue starts muted at every probe. An output routed to the Cue Mix
keeps that routing across a module reload or a rebind. On mains
power it also keeps it across a replug. That output is then silent
until alsactl restore runs. On bus power the MkI returns to DAW Thru
when it enumerates, so no output is on the Cue Mix at probe.
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:
- the loopback measurements above
- the Cue master, by ear
- a put of 107,107,90,90 reads back 127,127,90,90
- alsactl restore of the Cue level across a replug
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Will Porter <mrwillporter@gmail.com>
---
sound/usb/mixer_audient_id.c | 147 +++++++++++++++++++++++++++++++++++
sound/usb/mixer_maps.c | 2 +-
2 files changed, 148 insertions(+), 1 deletion(-)
diff --git a/sound/usb/mixer_audient_id.c b/sound/usb/mixer_audient_id.c
index 47e16e572..b729ca0d7 100644
--- a/sound/usb/mixer_audient_id.c
+++ b/sound/usb/mixer_audient_id.c
@@ -45,6 +45,7 @@
#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_FU_MIX 0x0c /* feature unit after the mixer: CN 1-2 Main, 3-4 Cue */
#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 */
@@ -506,6 +507,149 @@ static int id_add_ctl(struct usb_mixer_interface *mixer, const struct id_ctl *c)
return snd_usb_mixer_add_list(&e->list, kctl, false, true);
}
+/*
+ * iD14 MkI FU 12, the mixer output level: CN 1-2 Main, 3-4 Cue. GET RANGE works
+ * (-127 ... 0 dB, 1 dB steps), but GET CUR returns a reply buffer the
+ * device shares with other units, not the FU 12 value. So the generic
+ * control (hidden by the name map) shows stale data, and its sticky-GET
+ * check passes and then writes that stale value to Main L.
+ *
+ * The firmware ignores Main L (CN 1) but applies Main R (CN 2), so any Main
+ * level other than 0 dB unbalances the main outputs (measured with a
+ * loopback; Cue L and Cue R both work). The iD application has no Main
+ * fader and writes 0 dB to Main at launch. This control holds Main at 0 dB:
+ * a put sends 0 dB to CN 1-2 whatever it asks for, and get reports it.
+ *
+ * It keeps the generic control's name and format: four values, 0 ... 127
+ * in 1 dB steps, 127 = 0 dB. Value 0 is mute (0x8000), the floor the iD
+ * application writes. It shows the last value the device accepted, and
+ * every put sends all four channels.
+ */
+#define ID14_FU12_CHANNELS 4
+#define ID14_FU12_MAIN_CHANNELS 2 /* CN 1-2, held at 0 dB */
+#define ID14_FU12_MAX 127
+#define ID14_FU12_MUTE 0
+
+static const DECLARE_TLV_DB_MINMAX_MUTE(id14_fu12_tlv, -12700, 0);
+
+struct id14_fu12 {
+ struct usb_mixer_elem_list list; /* first: snd_usb_mixer_elem_free */
+ long val[ID14_FU12_CHANNELS];
+ u8 valid; /* bit per channel */
+};
+
+static s16 id14_fu12_raw(long val)
+{
+ return val == ID14_FU12_MUTE ? ID_DB_MIN : ID_DB(val - ID14_FU12_MAX);
+}
+
+static int id14_fu12_write(struct id14_fu12 *f, unsigned int ch, long val)
+{
+ u8 buf[2];
+ int err;
+
+ put_unaligned_le16(id14_fu12_raw(val), buf);
+ err = id_xfer(f->list.mixer, true, ID14_FU_MIX, UAC_FU_VOLUME, ch + 1, buf, sizeof(buf));
+ if (err < 0)
+ return err;
+ f->val[ch] = val;
+ f->valid |= BIT(ch);
+ return 0;
+}
+
+static int id14_fu12_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = ID14_FU12_CHANNELS;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = ID14_FU12_MAX;
+ return 0;
+}
+
+static int id14_fu12_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
+{
+ struct id14_fu12 *f = snd_kcontrol_chip(kctl);
+ unsigned int ch;
+
+ /* A channel that was never written reads as mute. */
+ for (ch = 0; ch < ID14_FU12_CHANNELS; ch++)
+ v->value.integer.value[ch] = f->valid & BIT(ch) ? f->val[ch] : ID14_FU12_MUTE;
+ return 0;
+}
+
+static int id14_fu12_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *v)
+{
+ struct id14_fu12 *f = snd_kcontrol_chip(kctl);
+ unsigned int ch;
+ long val;
+ int err, changed = 0;
+
+ for (ch = 0; ch < ID14_FU12_CHANNELS; ch++)
+ if (v->value.integer.value[ch] < 0 || v->value.integer.value[ch] > ID14_FU12_MAX)
+ return -EINVAL;
+
+ CLASS(snd_usb_lock, pm)(f->list.mixer->chip);
+ if (pm.err < 0)
+ return pm.err;
+
+ /* Main is held at 0 dB: report that back to the caller. */
+ for (ch = 0; ch < ID14_FU12_MAIN_CHANNELS; ch++)
+ v->value.integer.value[ch] = ID14_FU12_MAX;
+
+ for (ch = 0; ch < ID14_FU12_CHANNELS; ch++) {
+ val = v->value.integer.value[ch];
+ if (!(f->valid & BIT(ch)) || f->val[ch] != val)
+ changed = 1;
+ err = id14_fu12_write(f, ch, val);
+ if (err < 0)
+ return err;
+ }
+ return changed;
+}
+
+/*
+ * At probe, write the iD application's launch values: Main at 0 dB (the
+ * iD application has no Main fader) and Cue muted.
+ */
+static int id14_fu12_create(struct usb_mixer_interface *mixer)
+{
+ struct snd_kcontrol_new knew = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "Monitor Mix Playback Volume",
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = id14_fu12_info,
+ .get = id14_fu12_get,
+ .put = id14_fu12_put,
+ .tlv.p = id14_fu12_tlv,
+ };
+ struct snd_kcontrol *kctl;
+ struct id14_fu12 *f;
+ unsigned int ch;
+ int err;
+
+ f = kzalloc_obj(*f);
+ if (!f)
+ return -ENOMEM;
+ f->list.mixer = mixer;
+ f->list.id = ID14_FU_MIX;
+
+ for (ch = 0; ch < ID14_FU12_CHANNELS; ch++) {
+ err = id14_fu12_write(f, ch, ch < ID14_FU12_MAIN_CHANNELS ?
+ ID14_FU12_MAX : ID14_FU12_MUTE);
+ if (err < 0) /* the channel stays unknown and reads as mute */
+ usb_audio_warn(mixer->chip, "iD14: cannot set FU 12 channel %u (%d)\n",
+ ch + 1, err);
+ }
+
+ kctl = snd_ctl_new1(&knew, f);
+ if (!kctl) {
+ kfree(f);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ return snd_usb_mixer_add_list(&f->list, kctl, false, true);
+}
+
/*
* iD14 MkI mixer crosspoints: CS 1, CN = input * 4 + output, s16 in
* 1/256 dB. The control value is the raw s16, -128 dB (mute) ... +6 dB;
@@ -632,6 +776,9 @@ int snd_audient_id14_mk1_controls_create(struct usb_mixer_interface *mixer)
if (err < 0)
return err;
}
+ err = id14_fu12_create(mixer);
+ if (err < 0)
+ return err;
for (in = 0; in < ARRAY_SIZE(id14_mix_inputs); in++) {
for (out = 0; out < ID14_MIX_OUTPUTS; out++) {
err = id14_mix_add_ctl(mixer, in, out);
diff --git a/sound/usb/mixer_maps.c b/sound/usb/mixer_maps.c
index 63a77f3eb..2228fd34f 100644
--- a/sound/usb/mixer_maps.c
+++ b/sound/usb/mixer_maps.c
@@ -529,7 +529,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 */
+ { 12, NULL }, /* FU, see mixer_audient_id.c */
{ 60, NULL }, /* MU, see mixer_audient_id.c */
{}
};
--
2.47.3
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-10-19 23:02 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH 4/5] ALSA: usb-audio: Add Audient iD14 MkI write-only controls Will Porter
2026-10-19 23:01 ` [PATCH 5/5] ALSA: usb-audio: Replace the Audient iD14 MkI FU 12 control Will Porter
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®