From 29b99fd46d2714d40522f383e9f2a8bfd154e4c6 Mon Sep 17 00:00:00 2001
From: Tobiah <toby@tobiah.org>
Date: Sun, 6 Sep 2026 07:44:12 -0700
Subject: [PATCH] ALSA: usb-audio: add mixer support for PreSonus Studio 1810

The PreSonus Studio 1810 USB 2.0 interface (194f:0106) uses the same
sample-rate and alternate-setting handling as the Studio 1810c, but its
internal DSP mixer is not exposed by the existing driver.

Add the device ID to the 1810c quirk paths and expose the Studio 1810
hardware mixer for its three analog output pairs. Each destination
provides controls for analog, DAW, S/PDIF and ADAT sources, with hardware
mute and a -96.0 dB to +10.0 dB range in 0.1 dB steps.

Initialize the represented routes to a deterministic state, preserve
direct monitoring for analog inputs 1 and 2, and establish one-to-one
DAW playback for the three analog output pairs.

The analog input monitoring, DAW playback routing, analog output stages
and mixer level controls were tested on Studio 1810 hardware. S/PDIF and
ADAT routes follow the reverse-engineered protocol but have not been
physically tested.

Signed-off-by: Tobiah <toby@tobiah.org>
---
 sound/usb/format.c       |   5 +-
 sound/usb/mixer_quirks.c |   1 +
 sound/usb/mixer_s1810c.c | 357 +++++++++++++++++++++++++++++++++++++++
 sound/usb/quirks.c       |   5 +-
 4 files changed, 364 insertions(+), 4 deletions(-)

diff --git a/sound/usb/format.c b/sound/usb/format.c
index 0fa2f3f..134a235 100644
--- a/sound/usb/format.c
+++ b/sound/usb/format.c
@@ -447,8 +447,9 @@ static int parse_uac2_sample_rate_range(struct snd_usb_audio *chip,
 
 		for (rate = min; rate <= max; rate += res) {
 
-			/* Filter out invalid rates on Presonus Studio 1810c */
-			if (chip->usb_id == USB_ID(0x194f, 0x010c) &&
+			/* Filter out invalid rates on Presonus Studio 1810 and 1810c. */
+			if ((chip->usb_id == USB_ID(0x194f, 0x0106) ||
+			     chip->usb_id == USB_ID(0x194f, 0x010c)) &&
 			    !s1810c_valid_sample_rate(fp, rate))
 				goto skip_rate;
 			/* Filter out invalid rates on Presonus Studio 1824c */
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index 50c42a4..27a02f9 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -4473,6 +4473,7 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
 		err = snd_rme_controls_create(mixer);
 		break;
 
+	case USB_ID(0x194f, 0x0106): /* Presonus Studio 1810 */
 	case USB_ID(0x194f, 0x010c): /* Presonus Studio 1810c */
 		err = snd_sc1810_init_mixer(mixer);
 		break;
diff --git a/sound/usb/mixer_s1810c.c b/sound/usb/mixer_s1810c.c
index 7eac7d1..0800604 100644
--- a/sound/usb/mixer_s1810c.c
+++ b/sound/usb/mixer_s1810c.c
@@ -16,8 +16,10 @@
 #include <linux/usb.h>
 #include <linux/usb/audio-v2.h>
 #include <linux/slab.h>
+#include <linux/int_log.h>
 #include <sound/core.h>
 #include <sound/control.h>
+#include <sound/tlv.h>
 
 #include "usbaudio.h"
 #include "mixer.h"
@@ -142,8 +144,73 @@ struct s1810c_ctl_packet {
  */
 #define MIXER_LEVEL_MUTE 0
 #define MIXER_LEVEL_N99DB 0xbc
+#define MIXER_LEVEL_N96DB 0x109
 #define MIXER_LEVEL_N3DB 0xb53bf0
 #define MIXER_LEVEL_0DB 0x1000000
+#define MIXER_LEVEL_P10DB 0x3298b08
+
+#define S1810_MIXER_DB10_MIN (-960)
+#define S1810_MIXER_DB10_MAX 100
+#define S1810_MIXER_CTL_MIN 0
+#define S1810_MIXER_CTL_MAX \
+	(S1810_MIXER_DB10_MAX - S1810_MIXER_DB10_MIN + 1)
+
+/*
+ * ALSA value 0 is hardware mute.  Values 1..1061 represent -96.0dB
+ * through +10.0dB in 0.1dB steps.
+ */
+static const DECLARE_TLV_DB_SCALE(s1810_mixer_db_scale, -9610, 10, 1);
+
+static int s1810_raw_to_db10(u32 raw)
+{
+	s64 x;
+
+	if (!raw)
+		return S1810_MIXER_DB10_MIN;
+
+	x = (s64)intlog10(raw) - intlog10(MIXER_LEVEL_0DB);
+	x *= 200;
+	x += (s64)1 << 23;
+
+	return (int)(x >> 24);
+}
+
+static u32 s1810_db10_to_raw(int db10)
+{
+	s64 target, delta_lo, delta_prev;
+	u32 lo = 1;
+	u32 hi = MIXER_LEVEL_P10DB;
+
+	/* Preserve protocol values already established from USB captures. */
+	if (db10 <= S1810_MIXER_DB10_MIN)
+		return MIXER_LEVEL_N96DB;
+	if (db10 == -30)
+		return MIXER_LEVEL_N3DB;
+	if (!db10)
+		return MIXER_LEVEL_0DB;
+	if (db10 >= S1810_MIXER_DB10_MAX)
+		return MIXER_LEVEL_P10DB;
+
+	target = intlog10(MIXER_LEVEL_0DB);
+	target += DIV_ROUND_CLOSEST((s64)db10 * (1 << 24), 200);
+
+	/* Find the raw fixed-point gain nearest to the requested dB value. */
+	while (lo < hi) {
+		u32 mid = lo + (hi - lo) / 2;
+
+		if (intlog10(mid) < target)
+			lo = mid + 1;
+		else
+			hi = mid;
+	}
+
+	if (lo == 1)
+		return lo;
+
+	delta_lo = (s64)intlog10(lo) - target;
+	delta_prev = target - (s64)intlog10(lo - 1);
+	return delta_prev <= delta_lo ? lo - 1 : lo;
+}
 
 /*
  * This packet includes mixer volumes and
@@ -170,6 +237,7 @@ struct s1810_mixer_state {
 	uint16_t seqnum;
 	struct mutex usb_mutex;
 	struct mutex data_mutex;
+	u32 mixer_cache[4][36][2];
 };
 
 static int
@@ -237,6 +305,7 @@ snd_sc1810c_get_status_field(struct usb_device *dev,
 	}
 
 	(*field) = __le32_to_cpu(pkt_in.fields[field_idx]);
+
 	(*seqnum)++;
 	return 0;
 }
@@ -527,6 +596,153 @@ snd_s1810c_switch_set(struct snd_kcontrol *kctl,
 	return (ret < 0) ? 0 : 1;
 }
 
+#define S1810_MIXER_PVAL_STEREO_PAIR BIT(20)
+
+static int snd_s1810c_mixer_get(struct snd_kcontrol *kctl,
+				struct snd_ctl_elem_value *ctl_elem)
+{
+	struct usb_mixer_elem_list *list = snd_kcontrol_chip(kctl);
+	struct usb_mixer_interface *mixer = list->mixer;
+	struct s1810_mixer_state *private = mixer->private_data;
+	u32 pval = (u32)kctl->private_value;
+	u32 b = (pval >> 8) & 0x3f;
+	u32 c = (pval >> 16) & 0x3;
+	bool stereo_pair = pval & S1810_MIXER_PVAL_STEREO_PAIR;
+	u32 d;
+
+	guard(mutex)(&private->data_mutex);
+	for (d = 0; d < 2; d++) {
+		u32 route_b = b + (stereo_pair ? d : 0);
+		u32 raw = private->mixer_cache[c][route_b][d];
+
+		if (raw == MIXER_LEVEL_MUTE) {
+			ctl_elem->value.integer.value[d] =
+				S1810_MIXER_CTL_MIN;
+		} else {
+			int db10 = clamp(s1810_raw_to_db10(raw),
+					 S1810_MIXER_DB10_MIN,
+					 S1810_MIXER_DB10_MAX);
+
+			ctl_elem->value.integer.value[d] =
+				db10 - S1810_MIXER_DB10_MIN + 1;
+		}
+	}
+
+	return 0;
+}
+
+static int snd_s1810c_mixer_put(struct snd_kcontrol *kctl,
+				struct snd_ctl_elem_value *ctl_elem)
+{
+	struct usb_mixer_elem_list *list = snd_kcontrol_chip(kctl);
+	struct usb_mixer_interface *mixer = list->mixer;
+	struct snd_usb_audio *chip = mixer->chip;
+	struct s1810_mixer_state *private = mixer->private_data;
+	u32 pval = (u32)kctl->private_value;
+	u32 b = (pval >> 8) & 0x3f;
+	u32 c = (pval >> 16) & 0x3;
+	bool stereo_pair = pval & S1810_MIXER_PVAL_STEREO_PAIR;
+	u32 raw[2];
+	u32 d;
+	int ret;
+	bool changed = false;
+
+	for (d = 0; d < 2; d++) {
+		long value = ctl_elem->value.integer.value[d];
+
+		if (value < S1810_MIXER_CTL_MIN ||
+		    value > S1810_MIXER_CTL_MAX)
+			return -EINVAL;
+
+		raw[d] = value == S1810_MIXER_CTL_MIN ?
+			MIXER_LEVEL_MUTE :
+			s1810_db10_to_raw(value + S1810_MIXER_DB10_MIN - 1);
+	}
+
+	guard(mutex)(&private->data_mutex);
+	for (d = 0; d < 2; d++) {
+		u32 route_b = b + (stereo_pair ? d : 0);
+
+		if (private->mixer_cache[c][route_b][d] != raw[d]) {
+			changed = true;
+			break;
+		}
+	}
+	if (!changed)
+		return 0;
+
+	changed = false;
+	guard(mutex)(&private->usb_mutex);
+	for (d = 0; d < 2; d++) {
+		u32 route_b = b + (stereo_pair ? d : 0);
+
+		if (private->mixer_cache[c][route_b][d] == raw[d])
+			continue;
+
+		ret = snd_s1810c_send_ctl_packet(chip->dev,
+						 SC1810C_SEL_MIXER,
+						 route_b, c, d, raw[d]);
+		if (ret < 0)
+			return ret;
+
+		private->mixer_cache[c][route_b][d] = raw[d];
+		changed = true;
+	}
+
+	return changed;
+}
+
+static int snd_s1810c_mixer_info(struct snd_kcontrol *kctl,
+				 struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 2;
+	uinfo->value.integer.min = S1810_MIXER_CTL_MIN;
+	uinfo->value.integer.max = S1810_MIXER_CTL_MAX;
+	uinfo->value.integer.step = 1;
+
+	return 0;
+}
+
+static int snd_s1810c_mixer_init(struct usb_mixer_interface *mixer,
+				 const char *name, u32 b, u32 c, bool stereo_pair)
+{
+	struct snd_kcontrol_new new_kctl = {
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.access = (SNDRV_CTL_ELEM_ACCESS_READWRITE |
+			   SNDRV_CTL_ELEM_ACCESS_TLV_READ),
+		.name = name,
+		.info = snd_s1810c_mixer_info,
+		.get = snd_s1810c_mixer_get,
+		.put = snd_s1810c_mixer_put,
+		.tlv = { .p = s1810_mixer_db_scale },
+		.private_value = (b << 8) | (c << 16) |
+				 (stereo_pair ?
+				  S1810_MIXER_PVAL_STEREO_PAIR : 0),
+	};
+	struct snd_kcontrol *kctl;
+	struct usb_mixer_elem_info *elem;
+
+	elem = kzalloc_obj(struct usb_mixer_elem_info);
+	if (!elem)
+		return -ENOMEM;
+
+	elem->head.mixer = mixer;
+	elem->control = 0;
+	elem->head.id = 0;
+	elem->channels = 3;
+
+	kctl = snd_ctl_new1(&new_kctl, elem);
+	if (!kctl) {
+		kfree(elem);
+		return -ENOMEM;
+	}
+
+	kctl->private_free = snd_usb_mixer_elem_free;
+
+	return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
 static int
 snd_s1810c_switch_init(struct usb_mixer_interface *mixer,
 		       const struct snd_kcontrol_new *new_kctl)
@@ -629,12 +845,78 @@ static void snd_sc1810_mixer_state_free(struct usb_mixer_interface *mixer)
 	mixer->private_data = NULL;
 }
 
+struct s1810_mixer_route {
+	const char *name;
+	u8 b;
+	u8 c;
+	bool stereo_pair;
+};
+
+#define S1810_ROUTE(_dest, _source, _b, _c, _stereo_pair) \
+	{ .name = _dest " " _source " Level Playback Volume", \
+	  .b = (_b), .c = (_c), .stereo_pair = (_stereo_pair) }
+
+static const struct s1810_mixer_route s1810_analog_output_routes[] = {
+	S1810_ROUTE("1 Main Out", "01 Input 1", 0, 0, false),
+	S1810_ROUTE("1 Main Out", "02 Input 2", 1, 0, false),
+	S1810_ROUTE("1 Main Out", "03 Input 3", 2, 0, false),
+	S1810_ROUTE("1 Main Out", "04 Input 4", 3, 0, false),
+	S1810_ROUTE("1 Main Out", "05 Input 5", 4, 0, false),
+	S1810_ROUTE("1 Main Out", "06 Input 6", 5, 0, false),
+	S1810_ROUTE("1 Main Out", "07 Input 7", 6, 0, false),
+	S1810_ROUTE("1 Main Out", "08 Input 8", 7, 0, false),
+	S1810_ROUTE("1 Main Out", "09 DAW 1/2", 18, 0, true),
+	S1810_ROUTE("1 Main Out", "10 DAW 3/4", 20, 0, true),
+	S1810_ROUTE("1 Main Out", "11 DAW 5/6", 22, 0, true),
+	S1810_ROUTE("1 Main Out", "12 DAW 7/8", 24, 0, true),
+	S1810_ROUTE("1 Main Out", "13 S/PDIF", 8, 0, true),
+	S1810_ROUTE("1 Main Out", "14 ADAT1/2", 10, 0, true),
+	S1810_ROUTE("1 Main Out", "15 ADAT3/4", 12, 0, true),
+	S1810_ROUTE("1 Main Out", "16 ADAT5/6", 14, 0, true),
+	S1810_ROUTE("1 Main Out", "17 ADAT7/8", 16, 0, true),
+	S1810_ROUTE("2 Out3/4", "01 Input 1", 0, 1, false),
+	S1810_ROUTE("2 Out3/4", "02 Input 2", 1, 1, false),
+	S1810_ROUTE("2 Out3/4", "03 Input 3", 2, 1, false),
+	S1810_ROUTE("2 Out3/4", "04 Input 4", 3, 1, false),
+	S1810_ROUTE("2 Out3/4", "05 Input 5", 4, 1, false),
+	S1810_ROUTE("2 Out3/4", "06 Input 6", 5, 1, false),
+	S1810_ROUTE("2 Out3/4", "07 Input 7", 6, 1, false),
+	S1810_ROUTE("2 Out3/4", "08 Input 8", 7, 1, false),
+	S1810_ROUTE("2 Out3/4", "09 DAW 1/2", 18, 1, true),
+	S1810_ROUTE("2 Out3/4", "10 DAW 3/4", 20, 1, true),
+	S1810_ROUTE("2 Out3/4", "11 DAW 5/6", 22, 1, true),
+	S1810_ROUTE("2 Out3/4", "12 DAW 7/8", 24, 1, true),
+	S1810_ROUTE("2 Out3/4", "13 S/PDIF", 8, 1, true),
+	S1810_ROUTE("2 Out3/4", "14 ADAT1/2", 10, 1, true),
+	S1810_ROUTE("2 Out3/4", "15 ADAT3/4", 12, 1, true),
+	S1810_ROUTE("2 Out3/4", "16 ADAT5/6", 14, 1, true),
+	S1810_ROUTE("2 Out3/4", "17 ADAT7/8", 16, 1, true),
+	S1810_ROUTE("3 Out5/6", "01 Input 1", 0, 2, false),
+	S1810_ROUTE("3 Out5/6", "02 Input 2", 1, 2, false),
+	S1810_ROUTE("3 Out5/6", "03 Input 3", 2, 2, false),
+	S1810_ROUTE("3 Out5/6", "04 Input 4", 3, 2, false),
+	S1810_ROUTE("3 Out5/6", "05 Input 5", 4, 2, false),
+	S1810_ROUTE("3 Out5/6", "06 Input 6", 5, 2, false),
+	S1810_ROUTE("3 Out5/6", "07 Input 7", 6, 2, false),
+	S1810_ROUTE("3 Out5/6", "08 Input 8", 7, 2, false),
+	S1810_ROUTE("3 Out5/6", "09 DAW 1/2", 18, 2, true),
+	S1810_ROUTE("3 Out5/6", "10 DAW 3/4", 20, 2, true),
+	S1810_ROUTE("3 Out5/6", "11 DAW 5/6", 22, 2, true),
+	S1810_ROUTE("3 Out5/6", "12 DAW 7/8", 24, 2, true),
+	S1810_ROUTE("3 Out5/6", "13 S/PDIF", 8, 2, true),
+	S1810_ROUTE("3 Out5/6", "14 ADAT1/2", 10, 2, true),
+	S1810_ROUTE("3 Out5/6", "15 ADAT3/4", 12, 2, true),
+	S1810_ROUTE("3 Out5/6", "16 ADAT5/6", 14, 2, true),
+	S1810_ROUTE("3 Out5/6", "17 ADAT7/8", 16, 2, true),
+};
+
 /* Entry point, called from mixer_quirks.c */
 int snd_sc1810_init_mixer(struct usb_mixer_interface *mixer)
 {
 	struct s1810_mixer_state *private = NULL;
 	struct snd_usb_audio *chip = mixer->chip;
 	struct usb_device *dev = chip->dev;
+	u32 b, c, d;
 	int ret = 0;
 
 	/* Run this only once */
@@ -645,6 +927,57 @@ int snd_sc1810_init_mixer(struct usb_mixer_interface *mixer)
 	if (ret < 0)
 		return ret;
 
+	if (chip->usb_id == USB_ID(0x194f, 0x0106)) {
+		/* Mute every source route represented by the controls below. */
+		for (c = 0; c < 3; c++) {
+			for (b = 0; b < 26; b++) {
+				for (d = 0; d < 2; d++) {
+					ret = snd_s1810c_send_ctl_packet(
+						dev, SC1810C_SEL_MIXER, b, c, d,
+						MIXER_LEVEL_MUTE);
+					if (ret < 0)
+						return ret;
+				}
+			}
+		}
+
+		/* Default analog monitoring: Input 1 left, Input 2 right. */
+		ret = snd_s1810c_send_ctl_packet(dev, SC1810C_SEL_MIXER,
+						 0, 0, 0, MIXER_LEVEL_0DB);
+		if (ret < 0)
+			return ret;
+		ret = snd_s1810c_send_ctl_packet(dev, SC1810C_SEL_MIXER,
+						 1, 0, 1, MIXER_LEVEL_0DB);
+		if (ret < 0)
+			return ret;
+
+		/* Default one-to-one host playback for the analog outputs. */
+		for (c = 0; c < 3; c++) {
+			u32 daw_b = 18 + c * 2;
+
+			for (d = 0; d < 2; d++) {
+				ret = snd_s1810c_send_ctl_packet(
+					dev, SC1810C_SEL_MIXER, daw_b + d, c, d,
+					MIXER_LEVEL_0DB);
+				if (ret < 0)
+					return ret;
+			}
+		}
+
+		/* Enable all three physically verified analog output stages. */
+		for (b = 0; b < 3; b++) {
+			for (d = 0; d < 2; d++) {
+				ret = snd_s1810c_send_ctl_packet(
+					dev, SC1810C_SEL_OUTPUT, b, 0, d,
+					MIXER_LEVEL_0DB);
+				if (ret < 0)
+					return ret;
+			}
+		}
+
+		dev_info(&dev->dev,
+			 "initialized complete analog-output monitor matrix\n");
+	}
 	private = kzalloc_obj(struct s1810_mixer_state);
 	if (!private)
 		return -ENOMEM;
@@ -657,6 +990,17 @@ int snd_sc1810_init_mixer(struct usb_mixer_interface *mixer)
 
 	private->seqnum = 1;
 
+	if (chip->usb_id == USB_ID(0x194f, 0x0106)) {
+		private->mixer_cache[0][0][0] = MIXER_LEVEL_0DB;
+		private->mixer_cache[0][1][1] = MIXER_LEVEL_0DB;
+		private->mixer_cache[0][18][0] = MIXER_LEVEL_0DB;
+		private->mixer_cache[0][19][1] = MIXER_LEVEL_0DB;
+		private->mixer_cache[1][20][0] = MIXER_LEVEL_0DB;
+		private->mixer_cache[1][21][1] = MIXER_LEVEL_0DB;
+		private->mixer_cache[2][22][0] = MIXER_LEVEL_0DB;
+		private->mixer_cache[2][23][1] = MIXER_LEVEL_0DB;
+	}
+
 	ret = snd_s1810c_switch_init(mixer, &snd_s1810c_line_sw);
 	if (ret < 0)
 		return ret;
@@ -693,5 +1037,18 @@ int snd_sc1810_init_mixer(struct usb_mixer_interface *mixer)
 		break;
 	}
 
+	if (chip->usb_id == USB_ID(0x194f, 0x0106)) {
+		for (b = 0; b < ARRAY_SIZE(s1810_analog_output_routes); b++) {
+			const struct s1810_mixer_route *route =
+				&s1810_analog_output_routes[b];
+
+			ret = snd_s1810c_mixer_init(mixer, route->name,
+						    route->b, route->c,
+						    route->stereo_pair);
+			if (ret < 0)
+				return ret;
+		}
+	}
+
 	return ret;
 }
diff --git a/sound/usb/quirks.c b/sound/usb/quirks.c
index e8ae346..1a483cb 100644
--- a/sound/usb/quirks.c
+++ b/sound/usb/quirks.c
@@ -1595,8 +1595,9 @@ int snd_usb_apply_interface_quirk(struct snd_usb_audio *chip,
 	/* fasttrackpro usb: skip altsets incompatible with device_setup */
 	if (chip->usb_id == USB_ID(0x0763, 0x2012))
 		return fasttrackpro_skip_setting_quirk(chip, iface, altno);
-	/* presonus studio 1810c: skip altsets incompatible with device_setup */
-	if (chip->usb_id == USB_ID(0x194f, 0x010c))
+	/* Presonus Studio 1810/1810c: filter altsets by device_setup. */
+	if (chip->usb_id == USB_ID(0x194f, 0x0106) ||
+	    chip->usb_id == USB_ID(0x194f, 0x010c))
 		return s1810c_skip_setting_quirk(chip, iface, altno);
 
 	return 0;
-- 
2.53.0

