From 025877a3833d12aff023ce9e89d75f4d6fd61ddb Mon Sep 17 00:00:00 2001 From: Christian Ruppert Date: Sat, 19 Sep 2026 17:18:39 +0200 Subject: [PATCH] Add Audient EVO4 mixer quirks The controls enumerated by the Audient EVO4 are incomplete. Add missing controls for * channel muting * the cross-fader * the recording mixer * phantom power The driver is structured so that it should be trivial to extend it to the EVO8 (and potentially other Audient devices) but we need a kind soul who has access to that hardware to correctly map the control names and test everything. Signed-off-by: Christian Ruppert Signed-off-by: Takashi Iwai Link: https://patch.msgid.link/20260919151840.24371-3-arc@gmx.li --- MAINTAINERS | 7 + sound/usb/Makefile | 1 + sound/usb/mixer_evo.c | 505 +++++++++++++++++++++++++++++++++++++++ sound/usb/mixer_evo.h | 12 + sound/usb/mixer_quirks.c | 5 + 5 files changed, 530 insertions(+) create mode 100644 sound/usb/mixer_evo.c create mode 100644 sound/usb/mixer_evo.h diff --git a/MAINTAINERS b/MAINTAINERS index c2414447892c..656721c04713 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4346,6 +4346,13 @@ F: drivers/net/ieee802154/at86rf230.h F: drivers/net/ieee802154/atusb.c F: drivers/net/ieee802154/atusb.h +AUDIENT EVO MIXER DRIVER +M: Christian Ruppert +L: linux-sound@vger.kernel.org +S: Maintained +F: sound/usb/mixer_evo.c +F: sound/usb/mixer_evo.h + AUDIT SUBSYSTEM M: Paul Moore M: Eric Paris diff --git a/sound/usb/Makefile b/sound/usb/Makefile index e62794a87e73..2e842dcc73de 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_evo.o \ mixer_quirks.o \ mixer_scarlett.o \ mixer_scarlett2.o \ diff --git a/sound/usb/mixer_evo.c b/sound/usb/mixer_evo.c new file mode 100644 index 000000000000..c327b82c8d2d --- /dev/null +++ b/sound/usb/mixer_evo.c @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Audient EVO driver for ALSA + * Copyright (C) 2026 Christian Ruppert + * + * Based on the work of Ivan Hazucha for audient-evo-py. + * https://github.com/vanzaho/audient-evo-py/ + */ + + +#include +#include +#include +#include +#include +#include + +#include "usbaudio.h" +#include "mixer.h" +#include "mixer_quirks.h" +#include "helper.h" +#include "mixer_evo.h" + + +enum snd_evo_type { + SND_EVO_TYPE_MONITOR = 0, + SND_EVO_TYPE_PHANTOM, + SND_EVO_TYPE_MUTE, + SND_EVO_TYPE_MUTE_OUT, + SND_EVO_TYPE_MIXER, + SND_EVO_TYPE_NCTYPES +}; + +static const struct snd_evo_ctrl_type { + snd_ctl_elem_type_t type; + bool invert; + union { + int integer; + } min; + union { + int integer; + } max; + u16 wValue; + u16 wIndex; +} snd_evo_ctypes[SND_EVO_TYPE_NCTYPES] = { + [SND_EVO_TYPE_MONITOR] = { + .type = SNDRV_CTL_ELEM_TYPE_INTEGER, + .min.integer = 0, + .max.integer = 127, + .wValue = 0x0000, + .wIndex = 0x3800, + }, + [SND_EVO_TYPE_PHANTOM] = { + .type = SNDRV_CTL_ELEM_TYPE_BOOLEAN, + .invert = false, + .min.integer = 0, + .max.integer = 1, + .wValue = 0x0000, + .wIndex = 0x3A00, + }, + [SND_EVO_TYPE_MUTE] = { + .type = SNDRV_CTL_ELEM_TYPE_BOOLEAN, + .invert = true, + .min.integer = 0, + .max.integer = 1, + .wValue = 0x0200, + .wIndex = 0x3A00, + }, + [SND_EVO_TYPE_MUTE_OUT] = { + .type = SNDRV_CTL_ELEM_TYPE_BOOLEAN, + .invert = true, + .min.integer = 0, + .max.integer = 1, + .wValue = 0x0100, + .wIndex = 0x3B00, + }, + [SND_EVO_TYPE_MIXER] = { + .type = SNDRV_CTL_ELEM_TYPE_INTEGER, + .min.integer = -128 * 256, + .max.integer = 6 * 256, + .wValue = 0x0100, + .wIndex = 0x3C00, + }, +}; + +#define SND_EVO_FLAGS_DIRECTION (1L<<0) +#define SND_EVO_FLAGS_CAPTURE (1L<<0) +#define SND_EVO_FLAGS_PLAYBACK (0L<<0) + +struct snd_evo_ctrl { + char *basename; + enum snd_evo_type type; + unsigned int channels; + unsigned int hwchannels; + unsigned int val_offs; + unsigned long flags; +}; + +static const struct snd_evo_ctrl evo4_controls[] = { + { + .basename = "Master", + .type = SND_EVO_TYPE_MUTE_OUT, + .channels = 1, + .hwchannels = 1, + .val_offs = 0, + .flags = SND_EVO_FLAGS_PLAYBACK, + }, + { + .basename = "Mic", + .type = SND_EVO_TYPE_MUTE, + .channels = 4, + .hwchannels = 2, + .val_offs = 0, + .flags = SND_EVO_FLAGS_PLAYBACK, + }, + { + .basename = "Monitor", + .type = SND_EVO_TYPE_MONITOR, + .channels = 1, + .hwchannels = 1, + .val_offs = 0, + .flags = SND_EVO_FLAGS_PLAYBACK, + }, + { + .basename = "Mic 1", + .type = SND_EVO_TYPE_MIXER, + .channels = 2, + .hwchannels = 2, + .val_offs = 0, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Mic 2", + .type = SND_EVO_TYPE_MIXER, + .channels = 2, + .hwchannels = 2, + .val_offs = 2, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Master Left", + .type = SND_EVO_TYPE_MIXER, + .channels = 2, + .hwchannels = 2, + .val_offs = 4, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Master Right", + .type = SND_EVO_TYPE_MIXER, + .channels = 2, + .hwchannels = 2, + .val_offs = 6, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Loopback Left", + .type = SND_EVO_TYPE_MIXER, + .channels = 2, + .hwchannels = 2, + .val_offs = 8, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Loopback Right", + .type = SND_EVO_TYPE_MIXER, + .channels = 2, + .hwchannels = 2, + .val_offs = 10, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Mic 1 Phantom", + .type = SND_EVO_TYPE_PHANTOM, + .channels = 1, + .hwchannels = 1, + .val_offs = 0, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { + .basename = "Mic 2 Phantom", + .type = SND_EVO_TYPE_PHANTOM, + .channels = 1, + .hwchannels = 1, + .val_offs = 1, + .flags = SND_EVO_FLAGS_CAPTURE, + }, + { 0 }, /* sentinel */ +}; + +static const struct evo_devinfo { + u32 usb_id; + const struct snd_evo_ctrl *controls; +} evo_devinfo[] = { + { + .usb_id = USB_ID(USB_AUDIENT_VID, USB_EVO4_PID), + .controls = evo4_controls, + }, + { + .usb_id = USB_ID(USB_AUDIENT_VID, USB_EVO4_PID), + .controls = NULL, + }, + { 0 } /* sentinel */ +}; + + +static inline unsigned long snd_evo_kctl_priv(enum snd_evo_type type, + u8 valoffs, u8 hwchannels) +{ + unsigned long type_l = type & 0xFF; + unsigned long valoffs_l = valoffs & 0xFF; + unsigned long hwchannels_l = hwchannels & 0xFF; + + return (hwchannels_l << 16) | (valoffs_l << 8) | type_l; +} + +static __always_inline +const struct snd_evo_ctrl_type *snd_evo_kctl_type(struct snd_kcontrol *kctl) +{ + return &snd_evo_ctypes[kctl->private_value & 0xFF]; +} + +static __always_inline u16 snd_evo_kctl_valoffs(struct snd_kcontrol *kctl) +{ + return (kctl->private_value >> 8) & 0xFF; +} + +static __always_inline u16 snd_evo_kctl_hwchannels(struct snd_kcontrol *kctl) +{ + return (kctl->private_value >> 16) & 0xFF; +} + +static inline u16 snd_evo_kctl_channels(struct snd_kcontrol *kctl) +{ + struct usb_mixer_elem_info *info = kctl->private_data; + + return info->channels; +} + + +#define EVO_REQ_CUR (0x01) +#define EVO_REQTYPE_SET (USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE) +#define EVO_REQTYPE_GET (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE) + +static inline int snd_evo_get_cur(struct usb_mixer_interface *mixer, + u16 value, u16 index, + void *buf, size_t len) +{ + struct usb_device *const dev = mixer->chip->dev; + + return snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0), + EVO_REQ_CUR, EVO_REQTYPE_GET, + value, index, buf, len); +} + +static inline int snd_evo_set_cur(struct usb_mixer_interface *mixer, + u16 value, u16 index, + void *buf, size_t len) +{ + struct usb_device *const dev = mixer->chip->dev; + + return snd_usb_ctl_msg(dev, usb_sndctrlpipe(dev, 0), + EVO_REQ_CUR, EVO_REQTYPE_SET, + value, index, buf, len); +} + + +static int snd_ctl_evo_boolean_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ctl_val) +{ + struct usb_mixer_elem_list *const list = snd_kcontrol_chip(kctl); + struct usb_mixer_interface *const mixer = list->mixer; + + const unsigned int nchan = snd_evo_kctl_channels(kctl); + const unsigned int hwchan = snd_evo_kctl_hwchannels(kctl); + const struct snd_evo_ctrl_type *const ctype = snd_evo_kctl_type(kctl); + const u16 value = ctype->wValue + snd_evo_kctl_valoffs(kctl); + const u16 index = ctype->wIndex; + + int i; + + for (i = 0; i < hwchan; i++) { + u32 buf; + int ret; + + ret = snd_evo_get_cur(mixer, value + i, index, + &buf, sizeof(buf)); + if (ret < 0) + return ret; + + ctl_val->value.integer.value[i] = le32_to_cpu(buf) ? 1 : 0; + + if (ctype->invert) + ctl_val->value.integer.value[i] ^= 0x1; + } + + for (; i < nchan; i++) + ctl_val->value.integer.value[i] = ctype->invert + ? ctype->max.integer + : ctype->min.integer; + + return 0; +} + +static int snd_ctl_evo_boolean_set(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ctl_val) +{ + struct usb_mixer_elem_list *list = snd_kcontrol_chip(kctl); + struct usb_mixer_interface *mixer = list->mixer; + + const unsigned int hwchan = snd_evo_kctl_hwchannels(kctl); + const struct snd_evo_ctrl_type *const ctype = snd_evo_kctl_type(kctl); + const u16 value = ctype->wValue + snd_evo_kctl_valoffs(kctl); + const u16 index = ctype->wIndex; + + int changed = 0; + int i; + + for (i = 0; i < hwchan; i++) { + u32 rbuf; + u32 buf = cpu_to_le32(ctl_val->value.integer.value[i] ? 1 : 0); + int ret; + + if (ctype->invert) + buf ^= 0x1; + + ret = snd_evo_get_cur(mixer, value + i, index, + &rbuf, sizeof(rbuf)); + if (ret < 0) + return ret; + + if (rbuf != buf) { + changed = 1; + + ret = snd_evo_set_cur(mixer, value + i, index, + &buf, sizeof(buf)); + if (ret < 0) + return ret; + } + } + + return changed; +} + +static int snd_ctl_evo_integer_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ctl_val) +{ + struct usb_mixer_elem_list *const list = snd_kcontrol_chip(kctl); + struct usb_mixer_interface *const mixer = list->mixer; + + const unsigned int nchan = snd_evo_kctl_channels(kctl); + const unsigned int hwchan = snd_evo_kctl_hwchannels(kctl); + const struct snd_evo_ctrl_type *const ctype = snd_evo_kctl_type(kctl); + const u16 value = ctype->wValue + snd_evo_kctl_valoffs(kctl); + const u16 index = ctype->wIndex; + + int i; + + for (i = 0; i < hwchan; i++) { + u16 buf; + s16 val; + int ret; + + ret = snd_evo_get_cur(mixer, value + i, index, + &buf, sizeof(buf)); + if (ret < 0) + return ret; + + val = le16_to_cpu(buf); + ctl_val->value.integer.value[i] = val; + } + + for (; i < nchan; i++) + ctl_val->value.integer.value[i] = ctype->min.integer; + + return 0; +} + +static int snd_ctl_evo_integer_set(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ctl_val) +{ + struct usb_mixer_elem_list *list = snd_kcontrol_chip(kctl); + struct usb_mixer_interface *mixer = list->mixer; + + const unsigned int hwchan = snd_evo_kctl_hwchannels(kctl); + const struct snd_evo_ctrl_type *const ctype = snd_evo_kctl_type(kctl); + const u16 value = ctype->wValue + snd_evo_kctl_valoffs(kctl); + const u16 index = ctype->wIndex; + + int changed = 0; + int i; + + for (i = 0; i < hwchan; i++) { + s16 rbuf; + s16 buf = cpu_to_le16(ctl_val->value.integer.value[i]); + int ret; + + ret = snd_evo_get_cur(mixer, value + i, index, + &rbuf, sizeof(rbuf)); + if (ret < 0) + return ret; + + if (rbuf != buf) { + changed = 1; + + ret = snd_evo_set_cur(mixer, value + i, index, + &buf, sizeof(buf)); + if (ret < 0) + return ret; + } + } + + return changed; +} + +static int snd_ctl_evo_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + const struct snd_evo_ctrl_type *const ctype = snd_evo_kctl_type(kctl); + + uinfo->type = ctype->type; + uinfo->access = SNDRV_CTL_ELEM_ACCESS_READWRITE + | SNDRV_CTL_ELEM_ACCESS_VOLATILE; + uinfo->count = snd_evo_kctl_channels(kctl); + uinfo->value.integer.min = ctype->min.integer; + uinfo->value.integer.max = ctype->max.integer; + return 0; +} + + +static int snd_evo_add_ctrl(struct usb_mixer_interface *mixer, + const struct snd_evo_ctrl *ctrl) +{ + const struct snd_evo_ctrl_type *ctype = &snd_evo_ctypes[ctrl->type]; + char name[SNDRV_CTL_ELEM_ID_NAME_MAXLEN]; + struct snd_kcontrol_new knew = { 0 }; + struct snd_kcontrol *kctl; + struct usb_mixer_elem_info *elem; + + knew.iface = SNDRV_CTL_ELEM_IFACE_MIXER; + knew.name = name; + knew.info = snd_ctl_evo_info; + knew.private_value = snd_evo_kctl_priv(ctrl->type, ctrl->val_offs, + ctrl->hwchannels); + + switch (ctype->type) { + case SNDRV_CTL_ELEM_TYPE_INTEGER: + knew.get = snd_ctl_evo_integer_get; + knew.put = snd_ctl_evo_integer_set; + break; + case SNDRV_CTL_ELEM_TYPE_BOOLEAN: + knew.get = snd_ctl_evo_boolean_get; + knew.put = snd_ctl_evo_boolean_set; + break; + default: + return -EINVAL; + } + + snprintf(name, sizeof(name), "%s %s %s", ctrl->basename, + ((ctrl->flags & SND_EVO_FLAGS_DIRECTION) + == SND_EVO_FLAGS_CAPTURE) ? "Capture" : "Playback", + (ctype->type == SNDRV_CTL_ELEM_TYPE_BOOLEAN) + ? "Switch" : "Volume"); + + elem = kzalloc_obj(*elem); + if (!elem) + return -ENOMEM; + + elem->head.mixer = mixer; + elem->channels = ctrl->channels; + + kctl = snd_ctl_new1(&knew, elem); + if (!kctl) { + kfree(elem); + return -ENOMEM; + } + + kctl->private_free = snd_usb_mixer_elem_free; + + return snd_usb_mixer_add_control(&elem->head, kctl); +} + + +/* called from mixer_quirks.c */ +int snd_evo_controls_create(struct usb_mixer_interface *mixer) +{ + const struct evo_devinfo *info; + const struct snd_evo_ctrl *ctrl; + + for (info = evo_devinfo; info->controls; info++) + if (info->usb_id == mixer->chip->usb_id) + break; + + if (!info->controls) + return -ENODEV; + + for (ctrl = info->controls; ctrl->basename; ctrl++) { + const int ret = snd_evo_add_ctrl(mixer, ctrl); + + if (ret < 0) + return ret; + } + + return 0; +} diff --git a/sound/usb/mixer_evo.h b/sound/usb/mixer_evo.h new file mode 100644 index 000000000000..7fe7e197185c --- /dev/null +++ b/sound/usb/mixer_evo.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __USB_MIXER_EVO_H +#define __USB_MIXER_EVO_H + +#include "mixer.h" + +#define USB_AUDIENT_VID (0x2708) +#define USB_EVO4_PID (0x0006) + +int snd_evo_controls_create(struct usb_mixer_interface *mixer); + +#endif /* __USB_MIXER_EVO_H */ diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c index fc622eb95dc5..1e266fdbbb8e 100644 --- a/sound/usb/mixer_quirks.c +++ b/sound/usb/mixer_quirks.c @@ -38,6 +38,7 @@ #include "mixer_scarlett2.h" #include "mixer_us16x08.h" #include "mixer_s1810c.h" +#include "mixer_evo.h" #include "helper.h" #include "fcp.h" @@ -4539,6 +4540,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer) err = snd_fcp_init(mixer); break; + case USB_ID(USB_AUDIENT_VID, USB_EVO4_PID): /* Audient EVO 4 */ + err = snd_evo_controls_create(mixer); + break; + case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */ err = snd_soundblaster_e1_switch_create(mixer); break;