From: Will Porter <mrwillporter@gmail.com>
To: Christian Ruppert <arc@gmx.li>
Cc: Takashi Iwai <tiwai@suse.com>, Jaroslav Kysela <perex@perex.cz>,
linux-sound@vger.kernel.org, linux-kernel@vger.kernel.org,
Neil Andrews <neil@androos.io>
Subject: [RFC PATCH 16/16] ALSA: mixer_audient: Replace the Audient iD14 MkI FU 12 control
Date: Fri, 6 Nov 2026 10:31:29 -0600 [thread overview]
Message-ID: <20261106163129.1006907-17-mrwillporter@gmail.com> (raw)
In-Reply-To: <20261106163129.1006907-1-mrwillporter@gmail.com>
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 and -40 dB changes its output by -20.0 and -40.9 dB. Main L
at the same values changes nothing. Cue L and Cue R both work: Cue L
follows the set value within about 1 dB down to at least -70 dB; the
headphone residual hides lower levels.
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 writes 0x8000, the floor that
Audient's iD application writes, and the TLV declares it as mute. On
Cue L, value 0 gives no output above the fixed headphone residual,
about 76 dB below 0 dB. The four values keep the Main L/R, Cue L/R
layout of existing saved ALSA states.
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. If the probe writes succeed,
that output is then muted until user space sets the Cue level
(alsactl restore or the UCM boot write). On bus power the MkI returns
to DAW Thru when it enumerates, so no output is on the Cue Mix at
probe.
The control does not fit the generic types, so the device table gains
a create hook for controls the tables cannot describe, called after
the tables.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Will Porter <mrwillporter@gmail.com>
---
sound/usb/mixer_audient.c | 167 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_maps.c | 2 +-
2 files changed, 168 insertions(+), 1 deletion(-)
diff --git a/sound/usb/mixer_audient.c b/sound/usb/mixer_audient.c
index ad39d17c0b5e..e65c62e1eac4 100644
--- a/sound/usb/mixer_audient.c
+++ b/sound/usb/mixer_audient.c
@@ -491,11 +491,15 @@ static const char *const snd_audient_id14_mk1_matrix[] = {
NULL
};
+static int snd_audient_id14_mk1_create(struct usb_mixer_interface *mixer);
+
static const struct audient_devinfo {
u32 usb_id;
const struct snd_audient_ctrl *controls;
const char *const *const matrix_names;
const unsigned int matrix_channels;
+ /* controls the tables cannot describe; called after the tables */
+ int (*create)(struct usb_mixer_interface *mixer);
} audient_devinfo[] = {
{
.usb_id = USB_ID(USB_AUDIENT_VID, USB_EVO4_PID),
@@ -508,6 +512,7 @@ static const struct audient_devinfo {
.controls = snd_audient_id14_mk1_controls,
.matrix_names = snd_audient_id14_mk1_matrix,
.matrix_channels = 2,
+ .create = snd_audient_id14_mk1_create,
},
{ 0 } /* sentinel */
};
@@ -1132,6 +1137,165 @@ static int snd_audient_add_ctrl(struct usb_mixer_interface *mixer,
}
+/*
+ * 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 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 (Cue L and Cue R both
+ * work). 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, 0 = mute (0x8000). 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(snd_audient_tlv_id14_fu12, -12700, 0);
+
+static const struct snd_audient_ctrl_type snd_audient_id14_fu12_type = {
+ .len = 2,
+ .unit = ID14_FU_MIX,
+ .sel = UAC_FU_VOLUME,
+};
+
+struct snd_audient_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 int snd_audient_id14_fu12_write(struct snd_audient_id14_fu12 *f,
+ unsigned int ch, long val)
+{
+ __le16 buf = cpu_to_le16(val == ID14_FU12_MUTE ? -128 * 256
+ : (val - ID14_FU12_MAX) * 256);
+ int ret;
+
+ ret = snd_audient_set_cur(f->list.mixer, &snd_audient_id14_fu12_type,
+ ch + 1, &buf);
+ if (ret < 0)
+ return ret;
+
+ f->val[ch] = val;
+ f->valid |= BIT(ch);
+
+ return 0;
+}
+
+static int snd_ctl_audient_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 snd_ctl_audient_id14_fu12_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ctl_val)
+{
+ struct snd_audient_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++)
+ ctl_val->value.integer.value[ch] = (f->valid & BIT(ch))
+ ? f->val[ch]
+ : ID14_FU12_MUTE;
+
+ return 0;
+}
+
+static int snd_ctl_audient_id14_fu12_set(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ctl_val)
+{
+ struct snd_audient_id14_fu12 *f = snd_kcontrol_chip(kctl);
+ long *const v = ctl_val->value.integer.value;
+ unsigned int ch;
+ int changed = 0;
+
+ for (ch = 0; ch < ID14_FU12_CHANNELS; ch++)
+ if (v[ch] < 0 || v[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[ch] = ID14_FU12_MAX;
+
+ for (ch = 0; ch < ID14_FU12_CHANNELS; ch++) {
+ int ret;
+
+ if (!(f->valid & BIT(ch)) || f->val[ch] != v[ch])
+ changed = 1;
+
+ ret = snd_audient_id14_fu12_write(f, ch, v[ch]);
+ if (ret < 0)
+ return ret;
+ }
+
+ return changed;
+}
+
+/* At probe, set Main to 0 dB and mute Cue. */
+static int snd_audient_id14_mk1_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 = snd_ctl_audient_id14_fu12_info,
+ .get = snd_ctl_audient_id14_fu12_get,
+ .put = snd_ctl_audient_id14_fu12_set,
+ .tlv.p = snd_audient_tlv_id14_fu12,
+ };
+ struct snd_audient_id14_fu12 *f;
+ struct snd_kcontrol *kctl;
+ unsigned int ch;
+
+ 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++) {
+ const int ret = snd_audient_id14_fu12_write(f, ch,
+ ch < ID14_FU12_MAIN_CHANNELS ? ID14_FU12_MAX
+ : ID14_FU12_MUTE);
+
+ if (ret < 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, ret);
+ }
+
+ 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);
+}
+
/* called from mixer_quirks.c */
int snd_audient_controls_create(struct usb_mixer_interface *mixer)
{
@@ -1171,5 +1335,8 @@ int snd_audient_controls_create(struct usb_mixer_interface *mixer)
}
}
+ if (info->create)
+ return info->create(mixer);
+
return 0;
}
diff --git a/sound/usb/mixer_maps.c b/sound/usb/mixer_maps.c
index 30673bbbf762..d719aa6e4c97 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.c */
- { 12, "Monitor Mix Playback" }, /* FU, partial coverage */
+ { 12, NULL }, /* FU, see mixer_audient.c */
{ 60, NULL }, /* MU, see mixer_audient.c */
{}
};
--
2.47.3
prev parent reply other threads:[~2026-11-06 16:32 UTC|newest]
Thread overview: 56+ 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 ` [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
2026-10-20 13:10 ` [PATCH 0/5] ALSA: usb-audio: Audient iD14 MkI vendor controls Takashi Iwai
2026-10-26 18:36 ` Christian Ruppert
2026-10-26 21:44 ` Will Porter
2026-10-27 6:35 ` Takashi Iwai
2026-10-27 23:21 ` Christian Ruppert
2026-10-28 2:36 ` Will Porter
2026-11-01 0:00 ` [RFC PATCH 00/11] " Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 01/11] ALSA: rename mixer_evo to mixer_audient Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 02/11] ALSA: mixer_audient: explicitly define full control names Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 03/11] ALSA: mixer_audient: clarify USB control message definition Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 04/11] ALSA: mixer_audient: Add length check to USB transfer Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 05/11] ALSA: mixer_audient: make USB control transfer size configurable Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 06/11] ALSA: mixer_audient: Allow arbitrary boolean control values Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 07/11] ALSA: mixer_audient: Add integer range checks Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 08/11] ALSA: mixer_audient: Add control initialization at probe Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 09/11] ALSA: mixer_audient: Add readback-free parameter set Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 10/11] ALSA: mixer_audient: add control value caching Christian Ruppert
2026-11-01 0:00 ` [RFC PATCH 11/11] ALSA: mixer_audient: Create matrix programmatically Christian Ruppert
2026-11-01 17:35 ` [RFC PATCH 00/11] Re: ALSA: usb-audio: Audient iD14 MkI vendor controls Will Porter
2026-11-02 0:32 ` [RFC V2 PATCH 00/11] Re: ALSA: usb-audio: Audient iD14 MkI vendor Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 01/11] ALSA: rename mixer_evo to mixer_audient Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 02/11] ALSA: mixer_audient: explicitly define full control names Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 03/11] ALSA: mixer_audient: clarify USB control message definition Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 04/11] ALSA: mixer_audient: Add length check to USB transfer Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 05/11] ALSA: mixer_audient: make USB control transfer size configurable Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 06/11] ALSA: mixer_audient: Allow arbitrary boolean control values Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 07/11] ALSA: mixer_audient: Add integer range checks Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 08/11] ALSA: mixer_audient: Add control initialization at probe Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 09/11] ALSA: mixer_audient: Add readback-free parameter set Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 10/11] ALSA: mixer_audient: add control value caching Christian Ruppert
2026-11-02 0:32 ` [RFC V2 PATCH 11/11] ALSA: mixer_audient: Create matrix programmatically Christian Ruppert
2026-11-03 2:45 ` [RFC V2 PATCH 00/11] Re: ALSA: usb-audio: Audient iD14 MkI vendor Will Porter
2026-11-03 23:01 ` Christian Ruppert
2026-11-06 16:31 ` [RFC PATCH 00/16] ALSA: usb-audio: Audient driver and iD14 MkI support Will Porter
2026-11-06 16:31 ` [RFC PATCH 01/16] ALSA: rename mixer_evo to mixer_audient Will Porter
2026-11-06 16:31 ` [RFC PATCH 02/16] ALSA: mixer_audient: explicitly define full control names Will Porter
2026-11-06 16:31 ` [RFC PATCH 03/16] ALSA: mixer_audient: clarify USB control message definition Will Porter
2026-11-06 16:31 ` [RFC PATCH 04/16] ALSA: mixer_audient: Add length check to USB transfer Will Porter
2026-11-06 16:31 ` [RFC PATCH 05/16] ALSA: mixer_audient: make USB control transfer size configurable Will Porter
2026-11-06 16:31 ` [RFC PATCH 06/16] ALSA: mixer_audient: Allow arbitrary boolean control values Will Porter
2026-11-06 16:31 ` [RFC PATCH 07/16] ALSA: mixer_audient: Add integer range checks Will Porter
2026-11-06 16:31 ` [RFC PATCH 08/16] ALSA: mixer_audient: Add control initialization at probe Will Porter
2026-11-06 16:31 ` [RFC PATCH 09/16] ALSA: mixer_audient: Add readback-free parameter set Will Porter
2026-11-06 16:31 ` [RFC PATCH 10/16] ALSA: mixer_audient: add control value caching Will Porter
2026-11-06 16:31 ` [RFC PATCH 11/16] ALSA: mixer_audient: Create matrix programmatically Will Porter
2026-11-06 16:31 ` [RFC PATCH 12/16] ALSA: mixer_audient: Add a step size to integer controls Will Porter
2026-11-06 16:31 ` [RFC PATCH 13/16] ALSA: mixer_audient: Add enumerated controls Will Porter
2026-11-06 16:31 ` [RFC PATCH 14/16] ALSA: usb-audio: Give the Audient iD14 MkI its own mixer map Will Porter
2026-11-06 16:31 ` [RFC PATCH 15/16] ALSA: mixer_audient: Add Audient iD14 MkI controls Will Porter
2026-11-06 16:31 ` Will Porter [this message]
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=20261106163129.1006907-17-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 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®