* [PATCH RFC] ALSA: pci: add analog support for ESI MAYA44e
@ 2026-11-11 6:58 Nordschamane via B4 Relay
2026-11-11 7:32 ` sashiko-bot
0 siblings, 1 reply; 3+ messages in thread
From: Nordschamane via B4 Relay @ 2026-11-11 6:58 UTC (permalink / raw)
To: linux-sound; +Cc: Jaroslav Kysela, Takashi Iwai, linux-kernel
From: Nordschamane <nordschamane1@proton.me>
Add four-channel analog playback and capture for the ESI MAYA44e PCIe
card (19fe:7000, subsystem 0e51:0001). Playback and capture share the
sample clock and can use independent period sizes. Expose analog source,
gain, direct monitoring, DAC and crossmix controls.
The ES7010L register sequence comes from observed PCI transactions and
analysis of the ESI Windows driver. The existing ALSA PCI MAYA44 driver
was a reference for the WM8776 command format.
OpenAI Codex assisted with analysis of PCI traces and vendor driver
behavior, implementation of the C driver and its ALSA integration, and
generation of test tooling and the patch description. The development
requests covered first enabling analog playback, then adding capture,
mixer controls and suspend/resume, and reducing the driver to the
analog paths for this RFC.
Built in the Linux 7.0 tree on x86-64 and x86-32. The x86-64 module
build passed with W=1 and CONFIG_PM_SLEEP enabled. The x86-32 build
also linked the driver into vmlinux. The patch was refreshed on the
ALSA for-next base below with the same driver source.
Both stereo output pairs were audible on a physical MAYA44e running
Kubuntu 7.0.0-43-generic. Four-channel capture and simultaneous playback
and capture completed without XRUNs at 96 kHz, S32_LE and a 512-frame
period. Capture used an undriven input. External analog capture and
suspend/resume remain untested on this revision.
Feedback is welcome on the PCM constraints, mixer control names, and
the amount of hardware initialization detail appropriate for this driver.
Signed-off-by: Nordschamane <nordschamane1@proton.me>
---
sound/pci/Kconfig | 10 +
sound/pci/Makefile | 2 +
sound/pci/maya44e.c | 1742 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1754 insertions(+)
diff --git a/sound/pci/Kconfig b/sound/pci/Kconfig
index 6366f72..cb69fb9 100644
--- a/sound/pci/Kconfig
+++ b/sound/pci/Kconfig
@@ -746,6 +746,16 @@ config SND_MAESTRO3_INPUT
If you say N the buttons will directly control the master volume.
It is recommended to say Y.
+config SND_MAYA44E
+ tristate "ESI MAYA44e"
+ select SND_PCM
+ help
+ Enable four-channel analog playback and capture on the ESI MAYA44e
+ PCIe audio interface.
+
+ To compile this driver as a module, choose M here. The module
+ will be called snd-maya44e.
+
config SND_MIXART
tristate "Digigram miXart"
select FW_LOADER
diff --git a/sound/pci/Makefile b/sound/pci/Makefile
index 9d5e8e1..aea2fef 100644
--- a/sound/pci/Makefile
+++ b/sound/pci/Makefile
@@ -22,6 +22,7 @@ snd-fm801-y := fm801.o
snd-intel8x0-y := intel8x0.o
snd-intel8x0m-y := intel8x0m.o
snd-maestro3-y := maestro3.o
+snd-maya44e-y := maya44e.o
snd-rme32-y := rme32.o
snd-rme96-y := rme96.o
snd-sis7019-y := sis7019.o
@@ -48,6 +49,7 @@ obj-$(CONFIG_SND_FM801) += snd-fm801.o
obj-$(CONFIG_SND_INTEL8X0) += snd-intel8x0.o
obj-$(CONFIG_SND_INTEL8X0M) += snd-intel8x0m.o
obj-$(CONFIG_SND_MAESTRO3) += snd-maestro3.o
+obj-$(CONFIG_SND_MAYA44E) += snd-maya44e.o
obj-$(CONFIG_SND_RME32) += snd-rme32.o
obj-$(CONFIG_SND_RME96) += snd-rme96.o
obj-$(CONFIG_SND_SIS7019) += snd-sis7019.o
diff --git a/sound/pci/maya44e.c b/sound/pci/maya44e.c
new file mode 100644
index 0000000..d30f257
--- /dev/null
+++ b/sound/pci/maya44e.c
@@ -0,0 +1,1742 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ESI MAYA44e PCIe analog audio driver
+ *
+ * The controller has one DMA engine per stereo pair and direction.
+ * ALSA exposes four interleaved channels with a shared sample clock.
+ * Register behavior was reconstructed from observed transactions and
+ * analysis of the vendor Windows driver.
+ */
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <linux/pm.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/tlv.h>
+
+#define MAYA_MIN_PERIOD 128
+#define MAYA_DEFAULT_PERIOD 256
+#define MAYA_MAX_PERIOD 1024
+#define MAYA_MAX_FRAMES (MAYA_MAX_PERIOD * 2)
+#define MAYA_PAIR_BYTES_MAX (MAYA_MAX_FRAMES * 8)
+#define MAYA_PCM_BYTES_MAX (MAYA_MAX_FRAMES * 16)
+/* Preserve the original driver's full descriptor/service allocation span. */
+#define MAYA_DESC_BYTES 0x10100
+#define MAYA_POLL_LIMIT 100
+
+static const unsigned int maya_period_sizes[] = {128, 256, 512, 1024};
+static const struct snd_pcm_hw_constraint_list maya_period_constraints = {
+ .count = ARRAY_SIZE(maya_period_sizes),
+ .list = maya_period_sizes,
+};
+
+static bool maya_valid_period(snd_pcm_uframes_t period)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(maya_period_sizes); ++i)
+ if (maya_period_sizes[i] == period)
+ return true;
+ return false;
+}
+
+struct maya_rate_config {
+ unsigned int rate;
+ u16 word;
+ u8 nibble, gpio;
+};
+
+/* Recovered from Windows 0x14610. No 12/64/176.4/192 kHz branch exists there. */
+static const struct maya_rate_config maya_rates[] = {
+ {8000, 0x0531, 9, 0},
+ {11025, 0x4331, 5, 0},
+ {16000, 0x0231, 7, 0},
+ {22050, 0x4131, 4, 0},
+ {24000, 0x0131, 6, 0},
+ {32000, 0x0a31, 3, 0},
+ {44100, 0x4031, 0, 0},
+ {48000, 0x0031, 2, 0},
+ {88200, 0x4831, 10, 5},
+ {96000, 0x0831, 10, 5},
+};
+
+static const struct maya_rate_config *maya_find_rate(unsigned int rate)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(maya_rates); ++i)
+ if (maya_rates[i].rate == rate)
+ return &maya_rates[i];
+ return NULL;
+}
+
+static unsigned int maya_sample_bytes(snd_pcm_format_t format)
+{
+ switch (format) {
+ case SNDRV_PCM_FORMAT_S16_LE:
+ return 2;
+ case SNDRV_PCM_FORMAT_S24_3LE:
+ return 3;
+ case SNDRV_PCM_FORMAT_S24_LE:
+ case SNDRV_PCM_FORMAT_S32_LE:
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+/* The controller always uses left-aligned 24-bit samples in LE32 words.
+ * S24_LE instead stores its 24 significant bits in the low three bytes.
+ * Unsigned shifts avoid signed overflow; widening adds zeros, not precision.
+ * S32_LE retains the established DMA path; only its top 24 bits reach I2S.
+ */
+static inline u32 maya_unpack_sample(const u8 *source, snd_pcm_format_t format)
+{
+ switch (format) {
+ case SNDRV_PCM_FORMAT_S16_LE:
+ return (u32)get_unaligned_le16(source) << 16;
+ case SNDRV_PCM_FORMAT_S24_3LE:
+ return get_unaligned_le24(source) << 8;
+ case SNDRV_PCM_FORMAT_S24_LE:
+ return get_unaligned_le32(source) << 8;
+ case SNDRV_PCM_FORMAT_S32_LE:
+ return get_unaligned_le32(source);
+ default:
+ return 0;
+ }
+}
+
+/* Narrowing discards low bits without dither. Applications can dither before
+ * submitting playback or request 24-bit capture and dither in userspace.
+ */
+static inline void maya_pack_sample(u8 *target, u32 sample, snd_pcm_format_t format)
+{
+ switch (format) {
+ case SNDRV_PCM_FORMAT_S16_LE:
+ put_unaligned_le16(sample >> 16, target);
+ break;
+ case SNDRV_PCM_FORMAT_S24_3LE:
+ put_unaligned_le24(sample >> 8, target);
+ break;
+ case SNDRV_PCM_FORMAT_S24_LE:
+ /* Sign-extend the nominal 24-bit value into its 32-bit container. */
+ put_unaligned_le32((s32)sample >> 8, target);
+ break;
+ case SNDRV_PCM_FORMAT_S32_LE:
+ put_unaligned_le32(sample, target);
+ break;
+ default:
+ break;
+ }
+}
+
+struct maya_desc {
+ __le32 address, address_high, bytes, reserved;
+};
+
+struct maya_capture {
+ struct snd_pcm_substream *substream;
+ void *ring[2];
+ dma_addr_t dma[2];
+ bool running;
+ unsigned int period, frames, position;
+};
+
+struct maya {
+ struct pci_dev *pci;
+ struct snd_pcm *pcm;
+ struct snd_pcm_substream *substream;
+ struct maya_capture capture;
+ unsigned int reserved_rate[2];
+ u8 input_volume[2][2], input_gain[2], input_source[2];
+ bool input_enabled[2], monitor[2];
+ struct work_struct fault_work;
+ void __iomem *io;
+ /* Protects IRQ/DMA state; may be nested inside codec_lock. */
+ spinlock_t lock;
+ /* Serializes codec/control operations; never taken under lock. */
+ struct mutex codec_lock;
+ void *ring[2], *desc_allocation, *table;
+ dma_addr_t ring_dma[2], desc_dma, table_dma;
+ bool enabled, regions, irq_requested, running, fault, removing, suspended;
+ unsigned int suspend_rate;
+ unsigned int rate, period_frames, buffer_frames;
+ snd_pcm_uframes_t last_appl;
+ u8 dac[2][2], mix[2];
+ bool mix_enabled[2], dac_enabled[2];
+ unsigned long irq_window;
+ u32 irq_window_handled, irq_unexpected;
+};
+
+static u8 mr8(struct maya *m, unsigned int reg)
+{
+ udelay(1);
+ return readb(m->io + reg);
+}
+
+static u32 mr32(struct maya *m, unsigned int reg)
+{
+ udelay(1);
+ return readl(m->io + reg);
+}
+
+static void mw8(struct maya *m, unsigned int reg, u8 value)
+{
+ writeb(value, m->io + reg);
+ udelay(1);
+}
+
+static void mw16(struct maya *m, unsigned int reg, u16 value)
+{
+ writew(value, m->io + reg);
+ udelay(1);
+}
+
+static void mw32(struct maya *m, unsigned int reg, u32 value)
+{
+ writel(value, m->io + reg);
+ udelay(1);
+}
+
+/* Caller holds m->lock, or probe/free has exclusive ownership. */
+static void maya_quiesce(struct maya *m)
+{
+ unsigned int slot;
+
+ m->running = false;
+ m->capture.running = false;
+ pci_intx(m->pci, 0);
+ pci_clear_master(m->pci);
+ if (m->io && !m->suspended) {
+ for (slot = 0; slot < 4; ++slot) {
+ mw8(m, 0x192 + slot * 4, 2);
+ mw8(m, 0x80 + slot * 0x20, 0);
+ }
+ }
+}
+
+static void maya_fatal(struct maya *m)
+{
+ bool notify = !m->fault && !m->removing && !m->suspended && m->pcm;
+
+ m->fault = true;
+ maya_quiesce(m);
+ /* Fatal callers can hold stream/device locks. Notify ALSA elsewhere. */
+ if (notify)
+ schedule_work(&m->fault_work);
+}
+
+/* Each close clears its pointer under lock, then flushes this work before
+ * its runtime can disappear. Flushing rather than cancelling also preserves
+ * notification of a still-open peer. No driver lock is held while calling ALSA.
+ */
+static void maya_fault_work(struct work_struct *work)
+{
+ struct maya *m = container_of(work, struct maya, fault_work);
+ struct snd_pcm_substream *playback, *capture;
+ unsigned long flags;
+
+ spin_lock_irqsave(&m->lock, flags);
+ playback = m->substream;
+ capture = m->capture.substream;
+ spin_unlock_irqrestore(&m->lock, flags);
+ if (playback)
+ snd_pcm_stop_xrun(playback);
+ if (capture)
+ snd_pcm_stop_xrun(capture);
+}
+
+/* DMA must already be stopped, or probe must have exclusive ownership with
+ * bus mastering disabled. Allocation sizes remain constant; only the active
+ * ring changes. Both stereo pairs always use the same period and ring size.
+ */
+static void maya_configure_buffer(struct maya *m, unsigned int period)
+{
+ unsigned int pair, half;
+
+ m->period_frames = period;
+ m->buffer_frames = period * 2;
+ for (pair = 0; pair < 2; ++pair) {
+ struct maya_desc *descriptors = m->table + 0x200 + pair * 0x100;
+
+ for (half = 0; half < 2; ++half) {
+ descriptors[half].address =
+ cpu_to_le32(lower_32_bits(m->ring_dma[pair] + half * period * 8));
+ descriptors[half].address_high = 0;
+ descriptors[half].bytes = cpu_to_le32(period * 8);
+ descriptors[half].reserved = 0;
+ }
+ }
+ dma_wmb();
+}
+
+/* This is the original reset/rearm sequence, with an explicit timeout error.
+ * It never starts DMA. A failed reset cuts off bus mastering before any free.
+ */
+static int maya_rearm(struct maya *m, unsigned int pair)
+{
+ unsigned int b = 0xc0 + pair * 0x20;
+ unsigned int irq = 0x198 + pair * 4;
+ unsigned int i;
+
+ mw32(m, b, 0xff000000);
+ mw8(m, irq + 2, 2);
+ mw8(m, b, 1);
+ for (i = 0; i < MAYA_POLL_LIMIT; ++i) {
+ if (mr32(m, b) & 1)
+ break;
+ /* Reset callers may hold m->lock, so this wait must not sleep. */
+ udelay(10);
+ }
+ if (i == MAYA_POLL_LIMIT) {
+ maya_fatal(m);
+ return -ETIMEDOUT;
+ }
+ mw8(m, b, 0);
+ mw32(m, b + 0x18, lower_32_bits(m->table_dma + 0x200 + pair * 0x100));
+ mw32(m, b + 0x1c, 0);
+ mw16(m, b + 0x10, 0xbf);
+ mw16(m, b + 0x0e, 4);
+ mw8(m, b + 0x0c, 1);
+ mw32(m, b + 8, m->buffer_frames * 8);
+ mw16(m, irq, m->period_frames);
+ return 0;
+}
+
+static void maya_start_pair(struct maya *m, unsigned int pair)
+{
+ unsigned int b = 0xc0 + pair * 0x20;
+
+ mw8(m, b + 3, 0xff);
+ mw8(m, 0x19a + pair * 4, pair ? 3 : 7);
+ if (!(mr32(m, b) & 2))
+ mw8(m, b, 2);
+}
+
+static int maya_stop(struct maya *m)
+{
+ unsigned int pair;
+
+ m->running = false;
+ if (m->fault)
+ return -EIO;
+ /* Preserve a running capture peer: reset is deferred to prepare. */
+ for (pair = 0; pair < 2; ++pair) {
+ mw8(m, 0x19a + pair * 4, 2);
+ mw8(m, 0xc0 + pair * 0x20, 0);
+ }
+ return 0;
+}
+
+/* The codec command interface may sleep and is serialized by codec_lock. */
+static int maya_codec_ready(struct maya *m)
+{
+ unsigned int i;
+
+ for (i = 0; i < 200; ++i) {
+ if (!(mr32(m, 0x4c4) & 0x80))
+ return 0;
+ usleep_range(100, 200);
+ }
+
+ return -ETIMEDOUT;
+}
+
+static int maya_codec_write(struct maya *m, unsigned int codec,
+ unsigned int reg, u16 value)
+{
+ unsigned int command = (reg << 1) | ((value >> 8) & 1);
+ int err = maya_codec_ready(m);
+
+ if (err)
+ return err;
+ udelay(1);
+ mw8(m, 0x4c0, codec ? 0x36 : 0x34);
+ udelay(1);
+ mw8(m, 0x4c1, command);
+ udelay(1);
+ mw8(m, 0x4c2, command >> 8);
+ udelay(1);
+ mw8(m, 0x4c3, 1);
+ mw32(m, 0x4c4, 0xe0000010);
+ mw8(m, 0x4d0, value);
+ mw8(m, 0x4c4, 0x90);
+ /* Unlike the original, propagate a timeout from the last command too. */
+ return maya_codec_ready(m);
+}
+
+static int maya_codec_init(struct maya *m)
+{
+ static const struct { u8 reg; u16 value; } init[] = {
+ {0, 0x1f9}, {1, 0x1f9}, {2, 0x100},
+ {3, 0x100}, {4, 0x100}, {5, 0x100},
+ {6, 0}, {7, 0x95}, {8, 0}, {9, 0}, {10, 0x22},
+ {11, 0x22}, {12, 0x22}, {13, 0}, {14, 0x1cf},
+ {15, 0x1cf}, {16, 0x7a}, {17, 0}, {18, 0x32},
+ {19, 0}, {20, 0xbc}, {21, 2}, {22, 1},
+ };
+ unsigned int pass, codec, i;
+ int err;
+
+ mw8(m, 0x480, 0xff);
+ mw8(m, 0x481, 0xaa);
+ usleep_range(1000, 1500);
+ for (pass = 0; pass < 5; ++pass) {
+ for (codec = 0; codec < 2; ++codec) {
+ for (i = 0; i < ARRAY_SIZE(init); ++i) {
+ err = maya_codec_write(m, codec, init[i].reg,
+ init[i].value);
+ if (err)
+ return err;
+ udelay(2);
+ }
+ }
+ usleep_range(1000, 1500);
+ }
+ mw8(m, 0x481, 0x2a);
+ for (codec = 0; codec < 2; ++codec) {
+ /* R5 is master: set it before restoring independently cached L/R. */
+ err = maya_codec_write(m, codec, 5, 0x1c3);
+ if (err)
+ return err;
+ for (i = 0; i < 2; ++i) {
+ err = maya_codec_write(m, codec, 3 + i, 0x100 | m->dac[codec][i]);
+ if (err)
+ return err;
+ }
+ err = maya_codec_write(m, codec, 2, 0x100);
+ if (err)
+ return err;
+ }
+ return 0;
+}
+
+/* Caller holds codec_lock then lock, or probe has exclusive ownership.
+ * This only touches clock-selection registers, never mixer levels/routes or
+ * DMA start bits. Keep the Windows access order, widths and bit-preserving masks.
+ */
+static int maya_set_rate(struct maya *m, unsigned int rate)
+{
+ const struct maya_rate_config *config = maya_find_rate(rate);
+ u32 value;
+ u8 old481;
+ unsigned int i;
+
+ if (!config)
+ return -EINVAL;
+ if (m->fault || m->removing)
+ return -EIO;
+ if (m->rate == rate)
+ return 0;
+ if (m->running || m->capture.running)
+ return -EBUSY;
+ old481 = mr8(m, 0x481);
+ for (i = 0; i < 4; ++i)
+ mw16(m, 0x92 + i * 0x20, config->word);
+ udelay(1);
+ mw8(m, 0x481, (old481 & 0xfa) | config->gpio);
+ value = mr8(m, 0x18a);
+ if (value & 15)
+ mw8(m, 0x18a, value & 0xf0);
+ value = mr32(m, 0x1a0);
+ mw32(m, 0x1a0, (value & 0xffff0fff) | (config->nibble << 12));
+ WRITE_ONCE(m->rate, rate);
+ return 0;
+}
+
+static int maya_controller_init(struct maya *m)
+{
+ u32 value;
+ unsigned int i;
+ int err;
+
+ value = mr32(m, 8) & ~BIT(31);
+ mw32(m, 8, value);
+ msleep(200);
+ mw32(m, 8, value | BIT(31));
+ msleep(200);
+ mw32(m, 0x4e4, 0x38000);
+ value = mr32(m, 0x4e0);
+ for (i = 0; i < 20; ++i) {
+ mw32(m, 0x4e0, value | BIT(22));
+ mw32(m, 0x4e0, value & ~BIT(22));
+ }
+ mw8(m, 0x188, mr8(m, 0x188) & 0xf8);
+ mw8(m, 0x189, mr8(m, 0x189) & 0x98);
+ /* Disable digital output and select the external receive path. */
+ mw32(m, 0x1a0, 0);
+ mw32(m, 0x1b0, BIT(31));
+ /* Preserve the Windows initial 44.1 kHz-family setting during codec init. */
+ for (i = 0; i < 4; ++i)
+ mw16(m, 0x92 + i * 0x20, 0x4031);
+ err = maya_codec_init(m);
+ if (err)
+ return err;
+ /* Use 48 kHz until a PCM stream selects its clock. */
+ err = maya_set_rate(m, 48000);
+ if (err)
+ return err;
+ mw32(m, 0x70, lower_32_bits(m->table_dma + 0xa00));
+ err = maya_rearm(m, 0);
+ return err ? err : maya_rearm(m, 1);
+}
+
+/* Input DMA has the same two-descriptor layout as output, but format 0x77.
+ * Only the input engines are reset here; active playback is never touched.
+ */
+static int maya_capture_rearm(struct maya *m, unsigned int pair)
+{
+ unsigned int b = 0x80 + pair * 0x20, ir = 0x190 + pair * 4, i;
+
+ mw32(m, b, 0xff000000);
+ mw8(m, ir + 2, 2);
+ mw8(m, b, 1);
+ for (i = 0; i < MAYA_POLL_LIMIT; ++i) {
+ if (mr32(m, b) & 1)
+ break;
+ /* Reset callers may hold m->lock, so this wait must not sleep. */
+ udelay(10);
+ }
+ if (i == MAYA_POLL_LIMIT) {
+ maya_fatal(m);
+ return -ETIMEDOUT;
+ }
+ mw8(m, b, 0);
+ mw32(m, b + 0x18, lower_32_bits(m->table_dma + pair * 0x100));
+ mw32(m, b + 0x1c, 0);
+ mw16(m, b + 0x10, 0x77);
+ mw16(m, b + 0x0e, 4);
+ mw8(m, b + 0x0c, 1);
+ mw32(m, b + 8, m->capture.frames * 8);
+ mw16(m, ir, m->capture.period);
+ return 0;
+}
+
+static int maya_capture_stop(struct maya *m)
+{
+ unsigned int pair;
+
+ m->capture.running = false;
+ if (m->fault)
+ return -EIO;
+ /* Stop promptly. Full reset belongs to prepare, not the IRQ-disabled
+ * STOP path: resetting both engines here can starve the playback peer.
+ */
+ for (pair = 0; pair < 2; ++pair) {
+ mw8(m, 0x192 + pair * 4, 2);
+ mw8(m, 0x80 + pair * 0x20, 0);
+ }
+ return 0;
+}
+
+/* The 3/4 engine starts first. At a 1/2 boundary both completed stereo
+ * halves can be combined. Expose only the half copied to ALSA, never the
+ * still-being-written half. Coarse period pointers are intentional.
+ */
+static void maya_capture_period(struct maya *m)
+{
+ struct maya_capture *c = &m->capture;
+ struct snd_pcm_runtime *runtime = c->substream->runtime;
+ unsigned int sample_bytes = maya_sample_bytes(runtime->format);
+ u8 *dst = runtime->dma_area;
+ __le32 *left = c->ring[0], *right = c->ring[1];
+ unsigned int pos = (mr32(m, 0x84) >> 3) % c->frames;
+ unsigned int start = (pos / c->period ^ 1) * c->period, i;
+ bool pass[4];
+
+ /* R21 mutes the analogue PGA input; ADC noise/settling can remain.
+ * Guarantee exact digital silence for Capture Switch off and -infinity.
+ * Snapshot once per completed period, preserving independent L/R.
+ */
+ for (i = 0; i < 4; ++i)
+ pass[i] = READ_ONCE(m->input_enabled[i / 2]) &&
+ READ_ONCE(m->input_volume[i / 2][i % 2]) != 0;
+
+ dma_rmb();
+ for (i = start; i < start + c->period; ++i) {
+ u8 *frame = dst + i * sample_bytes * 4;
+
+ maya_pack_sample(frame, pass[0] ? le32_to_cpu(left[i * 2]) : 0,
+ runtime->format);
+ maya_pack_sample(frame + sample_bytes,
+ pass[1] ? le32_to_cpu(left[i * 2 + 1]) : 0,
+ runtime->format);
+ maya_pack_sample(frame + sample_bytes * 2,
+ pass[2] ? le32_to_cpu(right[i * 2]) : 0,
+ runtime->format);
+ maya_pack_sample(frame + sample_bytes * 3,
+ pass[3] ? le32_to_cpu(right[i * 2 + 1]) : 0,
+ runtime->format);
+ }
+ c->position = (start + c->period) % c->frames;
+}
+
+static irqreturn_t maya_irq(int irq, void *opaque)
+{
+ struct maya *m = opaque;
+ struct snd_pcm_substream *substream = NULL, *capture = NULL;
+ unsigned long flags;
+ u32 summary, extra;
+ u8 status;
+ unsigned int pair;
+ bool tick = false, capture_tick = false, failed = false;
+
+ spin_lock_irqsave(&m->lock, flags);
+
+ if (m->fault || m->removing || m->suspended) {
+ spin_unlock_irqrestore(&m->lock, flags);
+ return IRQ_NONE;
+ }
+ summary = mr32(m, 0x4b0);
+
+ if (!summary) {
+ spin_unlock_irqrestore(&m->lock, flags);
+ return IRQ_NONE;
+ }
+
+ if (time_after(jiffies, m->irq_window + HZ)) {
+ m->irq_window = jiffies;
+ m->irq_window_handled = 0;
+ }
+ /* Count only this device's asserted sources, not shared IRQ_NONE calls. */
+ if (++m->irq_window_handled > 8192) {
+ failed = true;
+ goto fatal;
+ }
+ for (pair = 0; pair < 4; ++pair) {
+ unsigned int b = 0x80 + pair * 0x20;
+ unsigned int ir = 0x190 + pair * 4;
+ bool pending;
+
+ if (!(summary & BIT(pair)))
+ continue;
+ status = mr8(m, b + 3);
+ extra = mr32(m, ir);
+ pending = (extra & 0x20000) || (status & 4);
+ if (extra & 0x20000)
+ mw8(m, ir + 2, extra >> 16);
+ if (status & 4)
+ mw8(m, b + 3, status);
+ if (pair == 2 && pending)
+ tick = true;
+ if (pair == 0 && pending)
+ capture_tick = true;
+ }
+ if (summary & ~0x0f) {
+ m->irq_unexpected++;
+ if (m->irq_unexpected >= 16) {
+ failed = true;
+ goto fatal;
+ }
+ }
+ if (capture_tick && m->capture.running && m->capture.substream) {
+ maya_capture_period(m);
+ capture = m->capture.substream;
+ }
+ if (tick && m->running)
+ substream = m->substream;
+ goto unlock;
+fatal:
+ maya_fatal(m);
+unlock:
+ spin_unlock_irqrestore(&m->lock, flags);
+ if (failed) {
+ dev_err_ratelimited(&m->pci->dev,
+ "interrupt fault: summary=%#x; bus mastering and INTx disabled\n",
+ summary);
+ } else {
+ if (substream)
+ snd_pcm_period_elapsed(substream);
+ if (capture)
+ snd_pcm_period_elapsed(capture);
+ }
+ return IRQ_HANDLED;
+}
+
+static const struct snd_pcm_hardware maya_pcm_hardware = {
+ .info = SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER |
+ SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
+ SNDRV_PCM_INFO_SYNC_APPLPTR | SNDRV_PCM_INFO_NO_REWINDS,
+ .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_3LE |
+ SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
+ .rates = SNDRV_PCM_RATE_8000_48000 | SNDRV_PCM_RATE_24000 |
+ SNDRV_PCM_RATE_88200 | SNDRV_PCM_RATE_96000,
+ .rate_min = 8000, .rate_max = 96000,
+ .channels_min = 4, .channels_max = 4,
+ .buffer_bytes_max = MAYA_PCM_BYTES_MAX,
+ .period_bytes_min = MAYA_MIN_PERIOD * 8,
+ .period_bytes_max = MAYA_MAX_PERIOD * 16,
+ .periods_min = 2, .periods_max = 2,
+};
+
+/* Full duplex shares one clock, but each direction may use its own period.
+ * Reserve at hw_params, not just while running, to protect prepared peers.
+ */
+static int maya_hw_params(struct snd_pcm_substream *s, struct snd_pcm_hw_params *p)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned int direction = s->stream;
+ int err = 0;
+
+ mutex_lock(&m->codec_lock);
+ if (m->reserved_rate[1 - direction] &&
+ m->reserved_rate[1 - direction] != params_rate(p))
+ err = -EBUSY;
+ else
+ m->reserved_rate[direction] = params_rate(p);
+ mutex_unlock(&m->codec_lock);
+ return err;
+}
+
+static int maya_hw_free(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+
+ mutex_lock(&m->codec_lock);
+ m->reserved_rate[s->stream] = 0;
+ mutex_unlock(&m->codec_lock);
+ return 0;
+}
+
+static int maya_open(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+ int err;
+
+ if (READ_ONCE(m->fault) || READ_ONCE(m->removing) || READ_ONCE(m->suspended))
+ return -EIO;
+ s->runtime->hw = maya_pcm_hardware;
+ err = snd_pcm_hw_constraint_integer(s->runtime, SNDRV_PCM_HW_PARAM_PERIODS);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_constraint_list(s->runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+ &maya_period_constraints);
+ if (err < 0)
+ return err;
+ /* S32_LE carries 24 significant MSBs; S16/S24 retain their nominal width. */
+ err = snd_pcm_hw_constraint_msbits(s->runtime, 0, 32, 24);
+ if (err < 0)
+ return err;
+ spin_lock_irqsave(&m->lock, flags);
+ m->substream = s;
+ spin_unlock_irqrestore(&m->lock, flags);
+ return 0;
+}
+
+static int maya_close(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+
+ spin_lock_irqsave(&m->lock, flags);
+ if (m->running)
+ maya_stop(m);
+ m->substream = NULL;
+ spin_unlock_irqrestore(&m->lock, flags);
+ flush_work(&m->fault_work);
+ synchronize_irq(m->pci->irq);
+ return 0;
+}
+
+static int maya_pcm_ioctl(struct snd_pcm_substream *s, unsigned int cmd, void *arg)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ int err = snd_pcm_lib_ioctl(s, cmd, arg);
+
+ if (!err && cmd == SNDRV_PCM_IOCTL1_RESET)
+ m->last_appl = s->runtime->status->hw_ptr;
+ return err;
+}
+
+static int maya_prepare(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+ int err;
+
+ if (!maya_sample_bytes(s->runtime->format) ||
+ !maya_valid_period(s->runtime->period_size) ||
+ s->runtime->buffer_size != s->runtime->period_size * 2 ||
+ !maya_find_rate(s->runtime->rate))
+ return -EINVAL;
+ mutex_lock(&m->codec_lock);
+ spin_lock_irqsave(&m->lock, flags);
+ if (m->fault || m->removing || m->suspended)
+ err = -EIO;
+ else if (m->running)
+ err = -EBUSY;
+ else
+ err = maya_stop(m);
+ spin_unlock_irqrestore(&m->lock, flags);
+ if (!err) {
+ unsigned int pair, pass;
+
+ /* Reset the old layout first: a failed reset must not replace
+ * descriptors or the cached geometry. Each reset releases IRQs.
+ */
+ for (pass = 0; pass < 2 && !err; ++pass) {
+ if (pass) {
+ if (m->period_frames == s->runtime->period_size)
+ break;
+ maya_configure_buffer(m, s->runtime->period_size);
+ }
+ for (pair = 0; pair < 2; ++pair) {
+ spin_lock_irqsave(&m->lock, flags);
+ err = m->fault || m->removing || m->suspended ?
+ -EIO : maya_rearm(m, pair);
+ spin_unlock_irqrestore(&m->lock, flags);
+ if (err)
+ break;
+ }
+ }
+ if (!err) {
+ spin_lock_irqsave(&m->lock, flags);
+ err = maya_set_rate(m, s->runtime->rate);
+ spin_unlock_irqrestore(&m->lock, flags);
+ }
+ }
+ mutex_unlock(&m->codec_lock);
+ if (err)
+ return err;
+ memset(m->ring[0], 0, m->buffer_frames * 8);
+ memset(m->ring[1], 0, m->buffer_frames * 8);
+ memset(s->runtime->dma_area, 0,
+ s->runtime->buffer_size * maya_sample_bytes(s->runtime->format) * 4);
+ m->last_appl = 0;
+ dma_wmb();
+ return 0;
+}
+
+static int maya_trigger(struct snd_pcm_substream *s, int command)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+ int err = 0;
+
+ spin_lock_irqsave(&m->lock, flags);
+ if (m->fault || m->removing || m->suspended) {
+ spin_unlock_irqrestore(&m->lock, flags);
+ return -EIO;
+ }
+ switch (command) {
+ case SNDRV_PCM_TRIGGER_START:
+ dma_wmb();
+ maya_start_pair(m, 1);
+ maya_start_pair(m, 0);
+ m->running = true;
+ break;
+ case SNDRV_PCM_TRIGGER_STOP:
+ case SNDRV_PCM_TRIGGER_SUSPEND:
+ err = maya_stop(m);
+ break;
+ default:
+ err = -EINVAL;
+ }
+ /* Flush posted PCI writes before returning to the PCM core. */
+ if (!err)
+ mr32(m, 0xc0);
+ spin_unlock_irqrestore(&m->lock, flags);
+ return err;
+}
+
+static int maya_sync_stop(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+
+ synchronize_irq(m->pci->irq);
+ return 0;
+}
+
+static snd_pcm_uframes_t maya_pointer(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+ u32 position, frames;
+
+ spin_lock_irqsave(&m->lock, flags);
+ position = m->fault || m->removing || m->suspended ? 0 : mr32(m, 0xc4);
+ frames = m->buffer_frames;
+ spin_unlock_irqrestore(&m->lock, flags);
+ return (position >> 3) % frames;
+}
+
+/* ALSA calls ack after committing appl_ptr, under its stream lock. Explicit
+ * appl_ptr synchronization is mandatory because the mmap staging buffer is
+ * not the controller's pair-interleaved DMA layout. Rewinds are disabled.
+ */
+static int maya_ack(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ struct snd_pcm_runtime *r = s->runtime;
+ snd_pcm_uframes_t appl = READ_ONCE(r->control->appl_ptr);
+ snd_pcm_sframes_t count = appl - m->last_appl;
+ snd_pcm_uframes_t frame;
+ unsigned int sample_bytes = maya_sample_bytes(r->format);
+ __le32 *out12 = m->ring[0], *out34 = m->ring[1];
+
+ if (READ_ONCE(m->fault) || READ_ONCE(m->removing) || READ_ONCE(m->suspended))
+ return -EIO;
+ if (!sample_bytes)
+ return -EINVAL;
+ if (count < 0)
+ count += r->boundary;
+ if (count < 0 || count > (snd_pcm_sframes_t)r->buffer_size) {
+ dev_err_ratelimited(&m->pci->dev,
+ "invalid appl_ptr delta=%ld old=%lu new=%lu boundary=%lu\n",
+ count, m->last_appl, appl, r->boundary);
+ return -EINVAL;
+ }
+ for (frame = 0; frame < (snd_pcm_uframes_t)count; ++frame) {
+ unsigned int slot = (m->last_appl + frame) % r->buffer_size;
+ const u8 *source = r->dma_area + slot * sample_bytes * 4;
+
+ out12[slot * 2] = cpu_to_le32(maya_unpack_sample(source, r->format));
+ out12[slot * 2 + 1] =
+ cpu_to_le32(maya_unpack_sample(source + sample_bytes, r->format));
+ out34[slot * 2] =
+ cpu_to_le32(maya_unpack_sample(source + sample_bytes * 2, r->format));
+ out34[slot * 2 + 1] =
+ cpu_to_le32(maya_unpack_sample(source + sample_bytes * 3, r->format));
+ }
+ dma_wmb();
+ m->last_appl = appl;
+ return 0;
+}
+
+static int maya_fill_silence(struct snd_pcm_substream *s, int channel,
+ unsigned long position, unsigned long bytes)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned int frame_bytes = maya_sample_bytes(s->runtime->format) * 4;
+ unsigned long ring_bytes = s->runtime->buffer_size * frame_bytes;
+ unsigned long dma_position, dma_bytes;
+
+ (void)channel; /* Only four-channel interleaved PCM is exposed. */
+ /* The ALSA core splits the request at the logical ring end. */
+ if (!frame_bytes || position % frame_bytes || bytes % frame_bytes ||
+ position > ring_bytes ||
+ bytes > ring_bytes - position)
+ return -EINVAL;
+ dma_position = position / frame_bytes * 8;
+ dma_bytes = bytes / frame_bytes * 8;
+ memset(s->runtime->dma_area + position, 0, bytes);
+ memset(m->ring[0] + dma_position, 0, dma_bytes);
+ memset(m->ring[1] + dma_position, 0, dma_bytes);
+ dma_wmb();
+
+ return 0;
+}
+
+static const struct snd_pcm_ops maya_pcm_ops = {
+ .open = maya_open,
+ .close = maya_close,
+ .ioctl = maya_pcm_ioctl,
+ .hw_params = maya_hw_params,
+ .hw_free = maya_hw_free,
+ .prepare = maya_prepare,
+ .trigger = maya_trigger,
+ .sync_stop = maya_sync_stop,
+ .pointer = maya_pointer,
+ .ack = maya_ack,
+ .fill_silence = maya_fill_silence,
+};
+
+static int maya_capture_open(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+ int err;
+
+ if (READ_ONCE(m->fault) || READ_ONCE(m->removing) || READ_ONCE(m->suspended))
+ return -EIO;
+ s->runtime->hw = maya_pcm_hardware;
+ /* ADC datasheet rates start at 32 kHz; lower controller rates are not
+ * yet validated for capture. Do not advertise unsupported ADC clocks.
+ */
+ s->runtime->hw.rates = SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 |
+ SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_88200 | SNDRV_PCM_RATE_96000;
+ s->runtime->hw.rate_min = 32000;
+ err = snd_pcm_hw_constraint_integer(s->runtime, SNDRV_PCM_HW_PARAM_PERIODS);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_constraint_list(s->runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+ &maya_period_constraints);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_constraint_msbits(s->runtime, 0, 32, 24);
+ if (err < 0)
+ return err;
+ spin_lock_irqsave(&m->lock, flags);
+ m->capture.substream = s;
+ spin_unlock_irqrestore(&m->lock, flags);
+ return 0;
+}
+
+static int maya_capture_close(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+
+ spin_lock_irqsave(&m->lock, flags);
+ if (m->capture.running)
+ maya_capture_stop(m);
+ m->capture.substream = NULL;
+ spin_unlock_irqrestore(&m->lock, flags);
+ flush_work(&m->fault_work);
+ synchronize_irq(m->pci->irq);
+ return 0;
+}
+
+static int maya_capture_prepare(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ struct maya_capture *c = &m->capture;
+ unsigned long flags;
+ unsigned int pair, half, period = s->runtime->period_size;
+ int err = 0;
+
+ if (!maya_sample_bytes(s->runtime->format) ||
+ !maya_valid_period(period) || s->runtime->buffer_size != period * 2 ||
+ s->runtime->rate < 32000 || !maya_find_rate(s->runtime->rate))
+ return -EINVAL;
+ mutex_lock(&m->codec_lock);
+ spin_lock_irqsave(&m->lock, flags);
+ if (m->fault || m->removing || m->suspended)
+ err = -EIO;
+ else if (c->running)
+ err = -EBUSY;
+ else
+ err = maya_set_rate(m, s->runtime->rate);
+ if (!err)
+ err = maya_capture_stop(m);
+ spin_unlock_irqrestore(&m->lock, flags);
+ if (!err) {
+ c->period = period;
+ c->frames = period * 2;
+ c->position = 0;
+ for (pair = 0; pair < 2; ++pair) {
+ struct maya_desc *descriptors = m->table + pair * 0x100;
+
+ for (half = 0; half < 2; ++half) {
+ descriptors[half].address =
+ cpu_to_le32(lower_32_bits(c->dma[pair] +
+ half * period * 8));
+ descriptors[half].address_high = 0;
+ descriptors[half].bytes = cpu_to_le32(period * 8);
+ descriptors[half].reserved = 0;
+ }
+ memset(c->ring[pair], 0, c->frames * 8);
+ }
+ memset(s->runtime->dma_area, 0,
+ c->frames * maya_sample_bytes(s->runtime->format) * 4);
+ dma_wmb();
+ /* Give playback IRQs a scheduling point between input resets. */
+ for (pair = 0; pair < 2; ++pair) {
+ spin_lock_irqsave(&m->lock, flags);
+ err = m->fault || m->removing || m->suspended ?
+ -EIO : maya_capture_rearm(m, pair);
+ spin_unlock_irqrestore(&m->lock, flags);
+ if (err)
+ break;
+ }
+ }
+ mutex_unlock(&m->codec_lock);
+ return err;
+}
+
+static int maya_capture_trigger(struct snd_pcm_substream *s, int cmd)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+ unsigned long flags;
+ int err = 0, pair;
+
+ spin_lock_irqsave(&m->lock, flags);
+ if (m->fault || m->removing || m->suspended) {
+ err = -EIO;
+ goto out;
+ }
+ switch (cmd) {
+ case SNDRV_PCM_TRIGGER_START:
+ for (pair = 1; pair >= 0; --pair) {
+ unsigned int b = 0x80 + pair * 0x20;
+
+ mw8(m, b + 3, 0xff);
+ /* Independent capture needs its own period IRQ source. */
+ mw8(m, 0x192 + pair * 4, pair ? 3 : 7);
+ if (!(mr32(m, b) & 2))
+ mw8(m, b, 2);
+ }
+ m->capture.running = true;
+
+ break;
+ case SNDRV_PCM_TRIGGER_STOP:
+ case SNDRV_PCM_TRIGGER_SUSPEND:
+ err = maya_capture_stop(m);
+ break;
+ default:
+ err = -EINVAL;
+ }
+out:
+ spin_unlock_irqrestore(&m->lock, flags);
+ return err;
+}
+
+static snd_pcm_uframes_t maya_capture_pointer(struct snd_pcm_substream *s)
+{
+ struct maya *m = snd_pcm_substream_chip(s);
+
+ return READ_ONCE(m->capture.position);
+}
+
+static const struct snd_pcm_ops maya_capture_ops = {
+ .open = maya_capture_open,
+ .close = maya_capture_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .hw_params = maya_hw_params,
+ .hw_free = maya_hw_free,
+ .prepare = maya_capture_prepare,
+ .trigger = maya_capture_trigger,
+ .sync_stop = maya_sync_stop,
+ .pointer = maya_capture_pointer,
+};
+
+enum { CTL_DAC, CTL_MIX_VOLUME, CTL_MIX_SWITCH, CTL_DAC_SWITCH };
+
+static int maya_ctl_info(struct snd_kcontrol *k, struct snd_ctl_elem_info *info)
+{
+ unsigned int kind = k->private_value >> 8;
+
+ if (kind == CTL_MIX_SWITCH || kind == CTL_DAC_SWITCH)
+ return snd_ctl_boolean_mono_info(k, info);
+ info->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ info->count = kind == CTL_DAC ? 2 : 1;
+ info->value.integer.min = 0;
+ info->value.integer.max = kind == CTL_DAC ? 255 : 48;
+ return 0;
+}
+
+static int maya_ctl_get(struct snd_kcontrol *k, struct snd_ctl_elem_value *v)
+{
+ struct maya *m = snd_kcontrol_chip(k);
+ unsigned int pair = k->private_value & 1;
+ unsigned int kind = k->private_value >> 8;
+
+ mutex_lock(&m->codec_lock);
+ if (kind == CTL_DAC) {
+ v->value.integer.value[0] = m->dac[pair][0];
+ v->value.integer.value[1] = m->dac[pair][1];
+ } else if (kind == CTL_DAC_SWITCH) {
+ v->value.integer.value[0] = m->dac_enabled[pair];
+ } else {
+ v->value.integer.value[0] = kind == CTL_MIX_VOLUME ?
+ m->mix[pair] : m->mix_enabled[pair];
+ }
+ mutex_unlock(&m->codec_lock);
+ return 0;
+}
+
+static int maya_ctl_put(struct snd_kcontrol *k, struct snd_ctl_elem_value *v)
+{
+ struct maya *m = snd_kcontrol_chip(k);
+ unsigned int pair = k->private_value & 1;
+ unsigned int kind = k->private_value >> 8;
+ long a = v->value.integer.value[0], b = v->value.integer.value[1];
+ unsigned int level;
+ bool enabled;
+ int err = 0, changed = 0;
+
+ if (a < 0 || a > (kind == CTL_DAC ? 255 : kind == CTL_MIX_VOLUME ? 48 : 1) ||
+ (kind == CTL_DAC && (b < 0 || b > 255)))
+ return -EINVAL;
+ mutex_lock(&m->codec_lock);
+ if (READ_ONCE(m->fault) || READ_ONCE(m->removing) || READ_ONCE(m->suspended)) {
+ err = -EIO;
+ goto out;
+ }
+ if (kind == CTL_DAC) {
+ if (m->dac[pair][0] != a) {
+ err = maya_codec_write(m, pair, 3, 0x100 | a);
+ if (err)
+ goto out;
+ m->dac[pair][0] = a;
+ changed = 1;
+ }
+ if (m->dac[pair][1] != b) {
+ err = maya_codec_write(m, pair, 4, 0x100 | b);
+ if (err)
+ goto out;
+ m->dac[pair][1] = b;
+ changed = 1;
+ }
+ } else if (kind == CTL_DAC_SWITCH) {
+ changed = m->dac_enabled[pair] != a;
+ if (changed) {
+ /* R22 gates this DAC's direct and outgoing crossmix paths.
+ * Preserve the separately controlled analogue input monitor path.
+ */
+ err = maya_codec_write(m, pair, 22, (a ? 1 : 0) |
+ (m->monitor[pair] &&
+ m->input_enabled[pair] ? 4 : 0));
+ if (err)
+ goto out;
+ m->dac_enabled[pair] = a;
+ }
+ } else {
+ level = kind == CTL_MIX_VOLUME ? a : m->mix[pair];
+ enabled = kind == CTL_MIX_SWITCH ? a : m->mix_enabled[pair];
+ changed = level != m->mix[pair] || enabled != m->mix_enabled[pair];
+ if (changed) {
+ err = maya_codec_write(m, 1 - pair, 2,
+ 0x100 | (enabled ? level + 0x49 : 0));
+ if (err)
+ goto out;
+ m->mix[pair] = level;
+ m->mix_enabled[pair] = enabled;
+ }
+ }
+out:
+ mutex_unlock(&m->codec_lock);
+ return err ? err : changed;
+}
+
+static const DECLARE_TLV_DB_SCALE(dac_db, -12750, 50, 1);
+static const DECLARE_TLV_DB_SCALE(mix_db, -4800, 100, 0);
+
+#define MAYA_CONTROL(label, kind, pair, scale) { \
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = label, \
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | \
+ ((kind) == CTL_MIX_SWITCH || (kind) == CTL_DAC_SWITCH ? \
+ 0 : SNDRV_CTL_ELEM_ACCESS_TLV_READ), \
+ .info = maya_ctl_info, .get = maya_ctl_get, .put = maya_ctl_put, \
+ .private_value = ((kind) << 8) | (pair), .tlv.p = scale }
+
+static const struct snd_kcontrol_new maya_controls[] = {
+ MAYA_CONTROL("Output 1-2 Playback Volume", CTL_DAC, 0, dac_db),
+ MAYA_CONTROL("Output 3-4 Playback Volume", CTL_DAC, 1, dac_db),
+ MAYA_CONTROL("DAC 1-2 Playback Switch", CTL_DAC_SWITCH, 0, NULL),
+ MAYA_CONTROL("DAC 3-4 Playback Switch", CTL_DAC_SWITCH, 1, NULL),
+ MAYA_CONTROL("Output 1-2 Crossmix Playback Volume", CTL_MIX_VOLUME, 0, mix_db),
+ MAYA_CONTROL("Output 1-2 Crossmix Playback Switch", CTL_MIX_SWITCH, 0, NULL),
+ MAYA_CONTROL("Output 3-4 Crossmix Playback Volume", CTL_MIX_VOLUME, 1, mix_db),
+ MAYA_CONTROL("Output 3-4 Crossmix Playback Switch", CTL_MIX_SWITCH, 1, NULL),
+};
+
+/* Input register facts checked against original 0x144d0/0x14580/0x15c00
+ * and WM8776 rev4.1. No mono/LRBOTH (R21 bit8), ALC or AUX routing.
+ */
+enum { INPUT_VOLUME, INPUT_GAIN, INPUT_SWITCH, INPUT_SOURCE, INPUT_MONITOR };
+
+static u16 maya_input_mux(struct maya *m, unsigned int pair)
+{
+ u16 source = m->input_source[pair] ? 8 : 2;
+
+ return source | (m->input_enabled[pair] ? 0 : 0xc0);
+}
+
+static int maya_input_levels(struct maya *m, unsigned int pair)
+{
+ unsigned int ch;
+ int err;
+
+ for (ch = 0; ch < 2; ++ch) {
+ unsigned int volume = m->input_volume[pair][ch];
+ unsigned int raw = 62 + volume + m->input_gain[pair];
+
+ /* -infinity is a digital gate in maya_capture_period. Keep the
+ * last PGA setting there: code 0 causes a bias/settling transient
+ * on this board when returning to normal gain.
+ */
+ if (!volume)
+ continue;
+ err = maya_codec_write(m, pair, 14 + ch, 0x100 | raw);
+ if (err)
+ return err;
+ }
+ return 0;
+}
+
+static int maya_input_info(struct snd_kcontrol *k, struct snd_ctl_elem_info *info)
+{
+ unsigned int kind = k->private_value >> 8, pair = k->private_value & 1;
+ static const char * const sources12[] = {"Line", "Mic"};
+ static const char * const sources34[] = {"Line", "Hi-Z"};
+
+ if (kind == INPUT_SOURCE)
+ return snd_ctl_enum_info(info, 1, 2, pair ? sources34 : sources12);
+ if (kind >= INPUT_SWITCH && kind != INPUT_SOURCE)
+ return snd_ctl_boolean_mono_info(k, info);
+ info->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ info->count = kind == INPUT_VOLUME ? 2 : 1;
+ info->value.integer.min = 0;
+ info->value.integer.max = kind == INPUT_VOLUME ? 145 : 48;
+ return 0;
+}
+
+static int maya_input_get(struct snd_kcontrol *k, struct snd_ctl_elem_value *v)
+{
+ struct maya *m = snd_kcontrol_chip(k);
+ unsigned int pair = k->private_value & 1, kind = k->private_value >> 8;
+
+ mutex_lock(&m->codec_lock);
+ switch (kind) {
+ case INPUT_VOLUME:
+ v->value.integer.value[0] = m->input_volume[pair][0];
+ v->value.integer.value[1] = m->input_volume[pair][1];
+ break;
+ case INPUT_GAIN:
+ v->value.integer.value[0] = m->input_gain[pair];
+ break;
+ case INPUT_SOURCE:
+ v->value.enumerated.item[0] = m->input_source[pair];
+ break;
+ case INPUT_SWITCH:
+ v->value.integer.value[0] = m->input_enabled[pair];
+ break;
+ case INPUT_MONITOR:
+ v->value.integer.value[0] = m->monitor[pair];
+ break;
+ }
+ mutex_unlock(&m->codec_lock);
+ return 0;
+}
+
+static int maya_input_put(struct snd_kcontrol *k, struct snd_ctl_elem_value *v)
+{
+ struct maya *m = snd_kcontrol_chip(k);
+ unsigned int pair = k->private_value & 1, kind = k->private_value >> 8;
+ long a = kind == INPUT_SOURCE ? v->value.enumerated.item[0] : v->value.integer.value[0];
+ long b = v->value.integer.value[1];
+ unsigned long flags;
+ u8 gpio;
+ int err = 0, changed = 0;
+
+ if (a < 0 || a > (kind == INPUT_VOLUME ? 145 : kind == INPUT_GAIN ? 48 : 1) ||
+ (kind == INPUT_VOLUME && (b < 0 || b > 145)))
+ return -EINVAL;
+ mutex_lock(&m->codec_lock);
+ if (READ_ONCE(m->fault) || READ_ONCE(m->removing) || READ_ONCE(m->suspended)) {
+ err = -EIO;
+ goto out;
+ }
+ switch (kind) {
+ case INPUT_VOLUME:
+ changed = m->input_volume[pair][0] != a || m->input_volume[pair][1] != b;
+ if (changed) {
+ m->input_volume[pair][0] = a;
+ m->input_volume[pair][1] = b;
+ err = maya_input_levels(m, pair);
+ }
+ break;
+ case INPUT_GAIN:
+ changed = m->input_gain[pair] != a;
+ if (changed) {
+ m->input_gain[pair] = a;
+ err = maya_input_levels(m, pair);
+ }
+ break;
+ case INPUT_SWITCH:
+ changed = m->input_enabled[pair] != a;
+ if (changed) {
+ m->input_enabled[pair] = a;
+ err = maya_codec_write(m, pair, 21, maya_input_mux(m, pair));
+ if (!err)
+ err = maya_codec_write(m, pair, 22, (m->dac_enabled[pair] ? 1 : 0) |
+ (m->monitor[pair] && a ? 4 : 0));
+ }
+ break;
+ case INPUT_MONITOR:
+ changed = m->monitor[pair] != a;
+ if (changed) {
+ err = maya_codec_write(m, pair, 22,
+ (m->dac_enabled[pair] ? 1 : 0) |
+ (a && m->input_enabled[pair] ? 4 : 0));
+ if (!err)
+ m->monitor[pair] = a;
+ }
+ break;
+ case INPUT_SOURCE:
+ changed = m->input_source[pair] != a;
+ if (!changed)
+ break;
+ /* Disconnect bypass during the switch and keep phantom off. */
+ err = maya_codec_write(m, pair, 22, m->dac_enabled[pair] ? 1 : 0);
+ if (err)
+ break;
+ gpio = mr8(m, 0x481);
+ if (!pair)
+ gpio = (gpio & ~0x50) | (a ? 0x10 : 0);
+ else
+ gpio = (gpio & ~0x20) | (a ? 0 : 0x20);
+ mw8(m, 0x481, gpio);
+ m->input_source[pair] = a;
+ err = maya_codec_write(m, pair, 21, maya_input_mux(m, pair));
+ if (!err)
+ err = maya_codec_write(m, pair, 22,
+ (m->dac_enabled[pair] ? 1 : 0) |
+ (m->monitor[pair] &&
+ m->input_enabled[pair] ? 4 : 0));
+ break;
+ }
+ if (err) {
+ /* A partial codec transaction leaves the actual state uncertain.
+ * Fail closed: phantom off, all DMA stopped, require module reload.
+ */
+ mw8(m, 0x481, mr8(m, 0x481) & 0xbf);
+ spin_lock_irqsave(&m->lock, flags);
+ maya_fatal(m);
+ spin_unlock_irqrestore(&m->lock, flags);
+ }
+out:
+ mutex_unlock(&m->codec_lock);
+ return err ? err : changed;
+}
+
+static const DECLARE_TLV_DB_SCALE(input_volume_db, -7250, 50, 1);
+static const DECLARE_TLV_DB_SCALE(input_gain_db, 0, 50, 0);
+#define MAYA_INPUT(label, kind, pair, scale) { \
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = label, \
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | \
+ ((kind) <= INPUT_GAIN ? SNDRV_CTL_ELEM_ACCESS_TLV_READ : 0), \
+ .info = maya_input_info, .get = maya_input_get, .put = maya_input_put, \
+ .private_value = ((kind) << 8) | (pair), .tlv.p = scale }
+static const struct snd_kcontrol_new maya_input_controls[] = {
+ MAYA_INPUT("Input 1-2 Capture Volume", INPUT_VOLUME, 0, input_volume_db),
+ MAYA_INPUT("Input 3-4 Capture Volume", INPUT_VOLUME, 1, input_volume_db),
+ MAYA_INPUT("Input 1-2 Capture Gain", INPUT_GAIN, 0, input_gain_db),
+ MAYA_INPUT("Input 3-4 Capture Gain", INPUT_GAIN, 1, input_gain_db),
+ MAYA_INPUT("Input 1-2 Capture Switch", INPUT_SWITCH, 0, NULL),
+ MAYA_INPUT("Input 3-4 Capture Switch", INPUT_SWITCH, 1, NULL),
+ MAYA_INPUT("Input 1-2 Capture Source", INPUT_SOURCE, 0, NULL),
+ MAYA_INPUT("Input 3-4 Capture Source", INPUT_SOURCE, 1, NULL),
+ MAYA_INPUT("Input 1-2 Monitor Playback Switch", INPUT_MONITOR, 0, NULL),
+ MAYA_INPUT("Input 3-4 Monitor Playback Switch", INPUT_MONITOR, 1, NULL),
+};
+
+static void maya_free(struct snd_card *card)
+{
+ struct maya *m = card->private_data;
+ unsigned long flags;
+ unsigned int pair;
+
+ spin_lock_irqsave(&m->lock, flags);
+ m->removing = true;
+ if (m->enabled) {
+ if (m->io && !m->suspended)
+ mw8(m, 0x481, mr8(m, 0x481) & 0xbf);
+ maya_fatal(m);
+ }
+ spin_unlock_irqrestore(&m->lock, flags);
+ cancel_work_sync(&m->fault_work);
+ if (m->irq_requested)
+ free_irq(m->pci->irq, m);
+ if (m->enabled)
+ pci_wait_for_pending_transaction(m->pci);
+ for (pair = 0; pair < 2; ++pair)
+ if (m->capture.ring[pair])
+ dma_free_coherent(&m->pci->dev, MAYA_PAIR_BYTES_MAX,
+ m->capture.ring[pair], m->capture.dma[pair]);
+ for (pair = 0; pair < 2; ++pair)
+ if (m->ring[pair])
+ dma_free_coherent(&m->pci->dev, MAYA_PAIR_BYTES_MAX,
+ m->ring[pair], m->ring_dma[pair]);
+ if (m->desc_allocation)
+ dma_free_coherent(&m->pci->dev, MAYA_DESC_BYTES,
+ m->desc_allocation, m->desc_dma);
+ if (m->io)
+ pci_iounmap(m->pci, m->io);
+ if (m->regions)
+ pci_release_regions(m->pci);
+ if (m->enabled)
+ pci_disable_device(m->pci);
+}
+
+static int maya_probe(struct pci_dev *pci, const struct pci_device_id *id)
+{
+ struct snd_card *card;
+ struct maya *m;
+ unsigned int pair, offset;
+ int err;
+
+ err = snd_card_new(&pci->dev, -1, "MAYA44e", THIS_MODULE, sizeof(*m), &card);
+ if (err)
+ return err;
+ m = card->private_data;
+ m->pci = pci;
+ m->period_frames = MAYA_DEFAULT_PERIOD;
+ m->buffer_frames = MAYA_DEFAULT_PERIOD * 2;
+ INIT_WORK(&m->fault_work, maya_fault_work);
+ spin_lock_init(&m->lock);
+ mutex_init(&m->codec_lock);
+ card->private_free = maya_free;
+ err = pci_enable_device_mem(pci);
+ if (err)
+ goto fail;
+ m->enabled = true;
+ pci_clear_master(pci);
+ pci_intx(pci, 0);
+ err = pci_request_regions(pci, "snd-maya44e");
+ if (err)
+ goto fail;
+ m->regions = true;
+ if (!(pci_resource_flags(pci, 0) & IORESOURCE_MEM) || pci_resource_len(pci, 0) < 0x500) {
+ err = -ENODEV;
+ goto fail;
+ }
+ err = dma_set_mask_and_coherent(&pci->dev, DMA_BIT_MASK(32));
+ if (err)
+ goto fail;
+ m->io = pci_iomap(pci, 0, 0);
+ if (!m->io) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ m->desc_allocation = dma_alloc_coherent(&pci->dev, MAYA_DESC_BYTES,
+ &m->desc_dma, GFP_KERNEL);
+ if (!m->desc_allocation) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ memset(m->desc_allocation, 0, MAYA_DESC_BYTES);
+ m->table_dma = (m->desc_dma + 0x100) & ~((dma_addr_t)0xff);
+ offset = m->table_dma - m->desc_dma;
+ m->table = m->desc_allocation + offset;
+ for (pair = 0; pair < 2; ++pair) {
+ m->ring[pair] = dma_alloc_coherent(&pci->dev, MAYA_PAIR_BYTES_MAX,
+ &m->ring_dma[pair], GFP_KERNEL);
+ if (!m->ring[pair]) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ memset(m->ring[pair], 0, MAYA_PAIR_BYTES_MAX);
+ m->dac[pair][1] = 0xc3;
+ m->dac[pair][0] = 0xc3;
+ m->dac_enabled[pair] = true; /* Matches the initial R22=1. */
+ m->capture.ring[pair] = dma_alloc_coherent(&pci->dev, MAYA_PAIR_BYTES_MAX,
+ &m->capture.dma[pair], GFP_KERNEL);
+ if (!m->capture.ring[pair]) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ memset(m->capture.ring[pair], 0, MAYA_PAIR_BYTES_MAX);
+ m->input_volume[pair][1] = 145;
+ m->input_volume[pair][0] = 145;
+ m->input_enabled[pair] = true;
+ m->mix[pair] = 18; /* -30 dB when explicitly enabled later. */
+ }
+ maya_configure_buffer(m, MAYA_DEFAULT_PERIOD);
+ err = maya_controller_init(m);
+ if (err) {
+ dev_err(&pci->dev, "controller initialization failed: %d\n", err);
+ goto fail;
+ }
+ err = request_irq(pci->irq, maya_irq, IRQF_SHARED, "snd-maya44e", m);
+ if (err)
+ goto fail;
+ m->irq_requested = true;
+ err = snd_pcm_new(card, "MAYA44e", 0, 1, 1, &m->pcm);
+ if (err)
+ goto fail;
+ m->pcm->private_data = m;
+ strscpy(m->pcm->name, "MAYA44e Analog");
+ snd_pcm_set_ops(m->pcm, SNDRV_PCM_STREAM_PLAYBACK, &maya_pcm_ops);
+ snd_pcm_set_ops(m->pcm, SNDRV_PCM_STREAM_CAPTURE, &maya_capture_ops);
+ err = snd_pcm_set_managed_buffer_all(m->pcm, SNDRV_DMA_TYPE_VMALLOC,
+ NULL, MAYA_PCM_BYTES_MAX, MAYA_PCM_BYTES_MAX);
+ if (err)
+ goto fail;
+ for (pair = 0; pair < ARRAY_SIZE(maya_controls); ++pair) {
+ err = snd_ctl_add(card, snd_ctl_new1(&maya_controls[pair], m));
+ if (err)
+ goto fail;
+ }
+ for (pair = 0; pair < ARRAY_SIZE(maya_input_controls); ++pair) {
+ err = snd_ctl_add(card, snd_ctl_new1(&maya_input_controls[pair], m));
+ if (err)
+ goto fail;
+ }
+ strscpy(card->driver, "MAYA44e");
+ strscpy(card->shortname, "ESI MAYA44e");
+ snprintf(card->longname, sizeof(card->longname),
+ "ESI MAYA44e at %s irq %d", pci_name(pci), pci->irq);
+ pci_set_drvdata(pci, card);
+ m->irq_window = jiffies;
+ pci_set_master(pci);
+ pci_intx(pci, 1);
+ err = snd_card_register(card);
+ if (err)
+ goto fail;
+ return 0;
+fail:
+ pci_set_drvdata(pci, NULL);
+ snd_card_free(card);
+ return err;
+}
+
+static void maya_remove(struct pci_dev *pci)
+{
+ struct snd_card *card = pci_get_drvdata(pci);
+
+ if (card) {
+ snd_card_disconnect(card);
+ snd_card_free(card);
+ pci_set_drvdata(pci, NULL);
+ }
+}
+
+/* codec_lock held; IRQ and all PCM/controls/MMIO readers remain gated by
+ * suspended until the complete board state is restored. PCI core restores
+ * power/config space before calling resume; DMA stays disabled here.
+ */
+static int maya_restore_after_sleep(struct maya *m)
+{
+ unsigned int pair;
+ u8 gpio;
+ int err;
+
+ memset(m->ring[0], 0, MAYA_PAIR_BYTES_MAX);
+ memset(m->ring[1], 0, MAYA_PAIR_BYTES_MAX);
+ memset(m->capture.ring[0], 0, MAYA_PAIR_BYTES_MAX);
+ memset(m->capture.ring[1], 0, MAYA_PAIR_BYTES_MAX);
+ maya_configure_buffer(m, m->period_frames);
+ m->last_appl = 0;
+ m->capture.position = 0;
+
+ /* A retained rate cache must not suppress clock programming after S3. */
+ m->rate = 0;
+ err = maya_controller_init(m);
+ if (err)
+ return err;
+ err = maya_set_rate(m, m->suspend_rate);
+ if (err)
+ return err;
+ gpio = mr8(m, 0x481) & ~0x70;
+ gpio |= (m->input_source[0] == 1 ? 0x10 : 0) |
+ (m->input_source[1] ? 0 : 0x20);
+ mw8(m, 0x481, gpio);
+ for (pair = 0; pair < 2; ++pair) {
+ err = maya_input_levels(m, pair);
+ if (err)
+ return err;
+ err = maya_codec_write(m, pair, 21, maya_input_mux(m, pair));
+ if (err)
+ return err;
+ err = maya_codec_write(m, pair, 22, (m->dac_enabled[pair] ? 1 : 0) |
+ (m->monitor[pair] &&
+ m->input_enabled[pair] ? 4 : 0));
+ if (err)
+ return err;
+ err = maya_codec_write(m, 1 - pair, 2,
+ 0x100 | (m->mix_enabled[pair] ? m->mix[pair] + 0x49 : 0));
+ if (err)
+ return err;
+ }
+ if (maya_valid_period(m->capture.period)) {
+ for (pair = 0; pair < 2; ++pair) {
+ err = maya_capture_rearm(m, pair);
+ if (err)
+ return err;
+ }
+ }
+ dma_wmb();
+ return 0;
+}
+
+static int maya_suspend(struct device *dev)
+{
+ struct snd_card *card = dev_get_drvdata(dev);
+ struct maya *m = card->private_data;
+ unsigned long flags;
+
+ snd_power_change_state(card, SNDRV_CTL_POWER_D3hot);
+ /* ALSA stream locks must be acquired before our register/codec locks. */
+ snd_pcm_suspend_all(m->pcm);
+ mutex_lock(&m->codec_lock);
+ spin_lock_irqsave(&m->lock, flags);
+ m->suspend_rate = m->rate;
+ if (!m->suspended) {
+ mw8(m, 0x481, mr8(m, 0x481) & ~0x40);
+ maya_quiesce(m);
+ m->suspended = true;
+ }
+ spin_unlock_irqrestore(&m->lock, flags);
+ synchronize_irq(m->pci->irq);
+ mutex_unlock(&m->codec_lock);
+ /* The gate prevents new work; complete any pending peer notification. */
+ flush_work(&m->fault_work);
+ return 0;
+}
+
+static int maya_resume(struct device *dev)
+{
+ struct snd_card *card = dev_get_drvdata(dev);
+ struct maya *m = card->private_data;
+ unsigned long flags;
+ int err;
+
+ mutex_lock(&m->codec_lock);
+ if (!m->suspended) {
+ mutex_unlock(&m->codec_lock);
+ return 0;
+ }
+ if (m->fault || m->removing) {
+ err = -EIO;
+ goto fail;
+ }
+ pci_intx(m->pci, 0);
+ pci_clear_master(m->pci);
+ err = maya_restore_after_sleep(m);
+ if (err)
+ goto fail;
+ spin_lock_irqsave(&m->lock, flags);
+ m->irq_window = jiffies;
+ m->irq_window_handled = 0;
+
+ m->suspended = false;
+ pci_set_master(m->pci);
+ pci_intx(m->pci, 1);
+ spin_unlock_irqrestore(&m->lock, flags);
+ mutex_unlock(&m->codec_lock);
+ snd_power_change_state(card, SNDRV_CTL_POWER_D0);
+ return 0;
+fail:
+ spin_lock_irqsave(&m->lock, flags);
+
+ maya_fatal(m);
+ spin_unlock_irqrestore(&m->lock, flags);
+ mutex_unlock(&m->codec_lock);
+ dev_err(dev, "resume failed: %d; DMA and interrupts disabled\n", err);
+ return err;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(maya_pm_ops, maya_suspend, maya_resume);
+
+static const struct pci_device_id maya_ids[] = {
+ { PCI_DEVICE_SUB(0x19fe, 0x7000, 0x0e51, 0x0001) },
+ { }
+};
+MODULE_DEVICE_TABLE(pci, maya_ids);
+
+static struct pci_driver maya_driver = {
+ .name = "snd-maya44e",
+ .id_table = maya_ids,
+ .probe = maya_probe,
+ .remove = maya_remove,
+ .driver.pm = pm_sleep_ptr(&maya_pm_ops),
+};
+module_pci_driver(maya_driver);
+
+MODULE_AUTHOR("MAYA44e Linux reverse-engineering project");
+MODULE_DESCRIPTION("ESI MAYA44e analog audio driver");
+MODULE_LICENSE("GPL");
---
base-commit: ec925d3740159f4b4bd3b7640be5d0d2b4664106
change-id: 20261111-codex-alsa-maya44e-2c41718770fb
Best regards,
--
Nordschamane <nordschamane1@proton.me>
^ permalink raw reply related [flat|nested] 3+ messages in thread* Re: [PATCH RFC] ALSA: pci: add analog support for ESI MAYA44e
2026-11-11 6:58 [PATCH RFC] ALSA: pci: add analog support for ESI MAYA44e Nordschamane via B4 Relay
@ 2026-11-11 7:32 ` sashiko-bot
2026-11-11 9:25 ` Nordschamane
0 siblings, 1 reply; 3+ messages in thread
From: sashiko-bot @ 2026-11-11 7:32 UTC (permalink / raw)
To: Nordschamane, linux-sound
Thank you for your contribution! Sashiko AI review found 6 potential
issues to consider:
- [High] ALSA: maya44e: pseudonymous Signed-off-by trailer and
MODULE_AUTHOR
- [High] ALSA: maya44e: unrestored D0 power state in maya_resume() error
path
- [High] ALSA: maya44e: NULL runtime->dma_area dereference in maya_ack()
after hw_free
- [Medium] ALSA: maya44e: missing joint-duplex rate constraint in
maya_open()
- [Medium] ALSA: maya44e: skipped PGA updates at zero volume in
maya_input_levels()
- [Medium] ALSA: maya44e: unlocked last_appl updates in maya_pcm_ioctl()
and maya_prepare()
--
commit 06d75b60c918848008af6c5fc9d3ea134cf8c6cd
Author: Nordschamane <nordschamane1@proton.me>
ALSA: pci: add analog support for ESI MAYA44e
This commit adds an ALSA PCI driver for the ESI MAYA44e PCIe audio
interface supporting four-channel analog playback and capture, mixer
controls, and power management.
> diff --git a/sound/pci/maya44e.c b/sound/pci/maya44e.c
> new file mode 100644
> index 0000000000000..d30f25742df48
> --- /dev/null
> +++ b/sound/pci/maya44e.c
[ ... ]
> +static const struct snd_pcm_hardware maya_pcm_hardware = {
> + .info = SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER |
> + SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
> + SNDRV_PCM_INFO_SYNC_APPLPTR | SNDRV_PCM_INFO_NO_REWINDS,
> + .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_3LE |
> + SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
> + .rates = SNDRV_PCM_RATE_8000_48000 | SNDRV_PCM_RATE_24000 |
> + SNDRV_PCM_RATE_88200 | SNDRV_PCM_RATE_96000,
> + .rate_min = 8000, .rate_max = 96000,
> + .channels_min = 4, .channels_max = 4,
> + .buffer_bytes_max = MAYA_PCM_BYTES_MAX,
> + .period_bytes_min = MAYA_MIN_PERIOD * 8,
> + .period_bytes_max = MAYA_MAX_PERIOD * 16,
> + .periods_min = 2, .periods_max = 2,
> +};
> +
> +/* Full duplex shares one clock, but each direction may use its own period.
> + * Reserve at hw_params, not just while running, to protect prepared peers.
> + */
> +static int maya_hw_params(struct snd_pcm_substream *s, struct snd_pcm_hw_params *p)
> +{
> + struct maya *m = snd_pcm_substream_chip(s);
> + unsigned int direction = s->stream;
> + int err = 0;
> +
> + mutex_lock(&m->codec_lock);
> + if (m->reserved_rate[1 - direction] &&
> + m->reserved_rate[1 - direction] != params_rate(p))
> + err = -EBUSY;
> + else
> + m->reserved_rate[direction] = params_rate(p);
> + mutex_unlock(&m->codec_lock);
> + return err;
> +}
[Severity: Medium]
Should maya_pcm_hardware include SNDRV_PCM_INFO_JOINT_DUPLEX and constrain
SNDRV_PCM_HW_PARAM_RATE when the opposite stream has already reserved a
rate?
During snd_pcm_hw_params(), snd_pcm_hw_params_choose() picks the minimum
rate in the refined interval before calling maya_hw_params(). When one
stream has reserved a rate above the minimum (such as 48000 Hz) and a
second stream passes a rate range containing 48000 Hz, ALSA selects 8000
Hz or 32000 Hz and maya_hw_params() rejects it with -EBUSY. In addition,
because playback allows 8000..24000 Hz while capture starts at 32000 Hz,
reserving a sub-32 kHz playback rate prevents capture from configuring
hw_params at all.
[ ... ]
> +static int maya_pcm_ioctl(struct snd_pcm_substream *s, unsigned int cmd, void *arg)
> +{
> + struct maya *m = snd_pcm_substream_chip(s);
> + int err = snd_pcm_lib_ioctl(s, cmd, arg);
> +
> + if (!err && cmd == SNDRV_PCM_IOCTL1_RESET)
> + m->last_appl = s->runtime->status->hw_ptr;
> + return err;
> +}
> +
> +static int maya_prepare(struct snd_pcm_substream *s)
> +{
[ ... ]
> + memset(m->ring[0], 0, m->buffer_frames * 8);
> + memset(m->ring[1], 0, m->buffer_frames * 8);
> + memset(s->runtime->dma_area, 0,
> + s->runtime->buffer_size * maya_sample_bytes(s->runtime->format) * 4);
> + m->last_appl = 0;
> + dma_wmb();
> + return 0;
> +}
[Severity: Medium]
Can updating m->last_appl without holding the PCM stream lock desynchronize
it from runtime->control->appl_ptr?
In snd_pcm_reset(), snd_pcm_do_reset() -> snd_pcm_lib_ioctl_reset() drops
the PCM stream lock before maya_pcm_ioctl() reads runtime->status->hw_ptr,
and snd_pcm_post_reset() re-acquires the lock afterward to copy hw_ptr into
runtime->control->appl_ptr:
CPU0 CPU1
snd_pcm_reset()
snd_pcm_do_reset()
maya_pcm_ioctl()
snd_pcm_lib_ioctl_reset()
m->last_appl = ...->hw_ptr;
maya_irq()
snd_pcm_period_elapsed()
/* advances hw_ptr */
snd_pcm_post_reset()
appl_ptr = ...->hw_ptr; <-- appl_ptr != m->last_appl
When a period interrupt lands in that window on a running playback stream,
m->last_appl lags behind appl_ptr by a period, causing a subsequent write
of more than one period in maya_ack() to compute count > r->buffer_size and
fail with -EINVAL. Similarly, snd_pcm_sync_ptr() calls maya_ack() under
only pcm_stream_lock_irq() without holding buffer_mutex, so unlocked writes
to m->last_appl in maya_prepare() and maya_pcm_ioctl() race with concurrent
SNDRV_PCM_IOCTL_SYNC_PTR calls.
[ ... ]
> +static int maya_ack(struct snd_pcm_substream *s)
> +{
> + struct maya *m = snd_pcm_substream_chip(s);
> + struct snd_pcm_runtime *r = s->runtime;
> + snd_pcm_uframes_t appl = READ_ONCE(r->control->appl_ptr);
> + snd_pcm_sframes_t count = appl - m->last_appl;
> + snd_pcm_uframes_t frame;
> + unsigned int sample_bytes = maya_sample_bytes(r->format);
> + __le32 *out12 = m->ring[0], *out34 = m->ring[1];
> +
> + if (READ_ONCE(m->fault) || READ_ONCE(m->removing) || READ_ONCE(m->suspended))
> + return -EIO;
> + if (!sample_bytes)
> + return -EINVAL;
> + if (count < 0)
> + count += r->boundary;
> + if (count < 0 || count > (snd_pcm_sframes_t)r->buffer_size) {
> + dev_err_ratelimited(&m->pci->dev,
> + "invalid appl_ptr delta=%ld old=%lu new=%lu boundary=%lu\n",
> + count, m->last_appl, appl, r->boundary);
> + return -EINVAL;
> + }
> + for (frame = 0; frame < (snd_pcm_uframes_t)count; ++frame) {
> + unsigned int slot = (m->last_appl + frame) % r->buffer_size;
> + const u8 *source = r->dma_area + slot * sample_bytes * 4;
> +
> + out12[slot * 2] = cpu_to_le32(maya_unpack_sample(source, r->format));
[Severity: High]
Can maya_ack() dereference a NULL or concurrently freed r->dma_area if
SNDRV_PCM_IOCTL_SYNC_PTR is called after SNDRV_PCM_IOCTL_HW_FREE?
When SNDRV_PCM_IOCTL_HW_FREE runs after SNDRV_PCM_IOCTL_HW_PARAMS,
do_hw_free() -> snd_pcm_lib_free_pages() frees the managed vmalloc buffer
and sets r->dma_area to NULL under r->buffer_mutex, while leaving
r->format, r->buffer_size and r->boundary populated. Because
snd_pcm_sync_ptr() only takes pcm_stream_lock_irq() and calls
pcm_lib_apply_appl_ptr() -> maya_ack() without checking r->state or
r->dma_area:
sound/core/pcm_native.c:snd_pcm_sync_ptr() {
...
scoped_guard(pcm_stream_lock_irq, substream) {
if (!(sflags & SNDRV_PCM_SYNC_PTR_APPL)) {
err = pcm_lib_apply_appl_ptr(substream, scontrol.appl_ptr);
...
}
calling SNDRV_PCM_IOCTL_SYNC_PTR with flags = 0 and a non-zero appl_ptr
passes the bounds checks in maya_ack() and dereferences NULL r->dma_area in
maya_unpack_sample() (or races with snd_pcm_lib_free_pages() freeing the
buffer).
[ ... ]
> +static int maya_input_levels(struct maya *m, unsigned int pair)
> +{
> + unsigned int ch;
> + int err;
> +
> + for (ch = 0; ch < 2; ++ch) {
> + unsigned int volume = m->input_volume[pair][ch];
> + unsigned int raw = 62 + volume + m->input_gain[pair];
> +
> + /* -infinity is a digital gate in maya_capture_period. Keep the
> + * last PGA setting there: code 0 causes a bias/settling transient
> + * on this board when returning to normal gain.
> + */
> + if (!volume)
> + continue;
> + err = maya_codec_write(m, pair, 14 + ch, 0x100 | raw);
> + if (err)
> + return err;
> + }
> + return 0;
> +}
[Severity: Medium]
Does skipping the WM8776 R14/R15 write when volume is 0 leave the analog
PGA unmuted on the hardware direct-monitoring path and after resume?
Input 1-2/3-4 Capture Volume advertises 0 as mute in input_volume_db, and
maya_capture_period() gates captured samples in software. However, WM8776
R14/R15 also feeds the analog monitor bypass enabled by Input 1-2/3-4
Monitor Playback Switch, so setting volume to 0 leaves the monitored input
audible at its previous gain and ignores Input Capture Gain updates made
while volume is 0. Furthermore, on S3 resume maya_codec_init() resets R14
and R15 to 0x1cf (0 dB), and maya_restore_after_sleep() ->
maya_input_levels() then skips any channel whose cached volume is 0.
[ ... ]
> +static int maya_suspend(struct device *dev)
> +{
> + struct snd_card *card = dev_get_drvdata(dev);
> + struct maya *m = card->private_data;
> + unsigned long flags;
> +
> + snd_power_change_state(card, SNDRV_CTL_POWER_D3hot);
> + /* ALSA stream locks must be acquired before our register/codec locks. */
> + snd_pcm_suspend_all(m->pcm);
> + mutex_lock(&m->codec_lock);
> + spin_lock_irqsave(&m->lock, flags);
> + m->suspend_rate = m->rate;
> + if (!m->suspended) {
> + mw8(m, 0x481, mr8(m, 0x481) & ~0x40);
> + maya_quiesce(m);
> + m->suspended = true;
> + }
> + spin_unlock_irqrestore(&m->lock, flags);
> + synchronize_irq(m->pci->irq);
> + mutex_unlock(&m->codec_lock);
> + /* The gate prevents new work; complete any pending peer notification. */
> + flush_work(&m->fault_work);
> + return 0;
> +}
> +
> +static int maya_resume(struct device *dev)
> +{
> + struct snd_card *card = dev_get_drvdata(dev);
> + struct maya *m = card->private_data;
> + unsigned long flags;
> + int err;
> +
> + mutex_lock(&m->codec_lock);
> + if (!m->suspended) {
> + mutex_unlock(&m->codec_lock);
> + return 0;
> + }
> + if (m->fault || m->removing) {
> + err = -EIO;
> + goto fail;
> + }
> + pci_intx(m->pci, 0);
> + pci_clear_master(m->pci);
> + err = maya_restore_after_sleep(m);
> + if (err)
> + goto fail;
> + spin_lock_irqsave(&m->lock, flags);
> + m->irq_window = jiffies;
> + m->irq_window_handled = 0;
> +
> + m->suspended = false;
> + pci_set_master(m->pci);
> + pci_intx(m->pci, 1);
> + spin_unlock_irqrestore(&m->lock, flags);
> + mutex_unlock(&m->codec_lock);
> + snd_power_change_state(card, SNDRV_CTL_POWER_D0);
> + return 0;
> +fail:
> + spin_lock_irqsave(&m->lock, flags);
> +
> + maya_fatal(m);
> + spin_unlock_irqrestore(&m->lock, flags);
> + mutex_unlock(&m->codec_lock);
> + dev_err(dev, "resume failed: %d; DMA and interrupts disabled\n", err);
> + return err;
> +}
[Severity: High]
Does the fail path in maya_resume() leave the card stuck in
SNDRV_CTL_POWER_D3hot with m->suspended still set?
If m->fault is set before suspend or maya_restore_after_sleep() fails,
maya_resume() returns without calling snd_power_change_state(card,
SNDRV_CTL_POWER_D0) or clearing m->suspended. Any process that later
closes or accesses a PCM substream or mixer control sleeps forever in
TASK_UNINTERRUPTIBLE inside snd_power_ref_and_wait() / snd_power_wait().
Keeping m->suspended true also causes maya_fatal() -> maya_quiesce() and
maya_free() to skip MMIO register shutdown and fault_work scheduling even
though the PCI device is in D0, and a subsequent maya_suspend() will
overwrite m->suspend_rate with 0 if maya_restore_after_sleep() failed after
clearing m->rate.
[ ... ]
> +MODULE_AUTHOR("MAYA44e Linux reverse-engineering project");
> +MODULE_DESCRIPTION("ESI MAYA44e analog audio driver");
> +MODULE_LICENSE("GPL");
[Severity: High]
Under Documentation/process/submitting-patches.rst, Signed-off-by and
author attribution must use a known real full name rather than a single-word
handle ("Nordschamane <nordschamane1@proton.me>") or a generic project
string in MODULE_AUTHOR. Also, the transient notes in the commit message
about the tree base and RFC feedback requests belong below the "---"
separator line so they are not recorded in the permanent git history.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261111-codex-alsa-maya44e-v1-1-df60af443e93@proton.me?part=1
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH RFC] ALSA: pci: add analog support for ESI MAYA44e
2026-11-11 7:32 ` sashiko-bot
@ 2026-11-11 9:25 ` Nordschamane
0 siblings, 0 replies; 3+ messages in thread
From: Nordschamane @ 2026-11-11 9:25 UTC (permalink / raw)
To: sashiko-reviews; +Cc: linux-sound
Thanks for the review. Here is how I addressed each point in RFC v2.
> [High] ALSA: maya44e: pseudonymous Signed-off-by trailer and
> MODULE_AUTHOR
Nordschamane is my established public identity for this project, and
this address is how to reach me. The current kernel DCO guidance asks
for a known identity. I kept that identity in Signed-off-by.
MODULE_AUTHOR identifies the project. I moved the revision notes, test
results, and base information below the patch separator.
> [High] ALSA: maya44e: unrestored D0 power state in maya_resume() error
> path
On permanent resume failure, v2 disables bus mastering and INTx. It
stops all four DMA slots and disables phantom power when the device is
accessible in PCI D0. It then disconnects the ALSA card so operations
waiting for D0 can return.
> [High] ALSA: maya44e: NULL runtime->dma_area dereference in maya_ack()
> after hw_free
maya_ack() now pins managed PCM buffer access before inspecting or
copying the buffer. It rejects an unconfigured or unavailable buffer.
> [Medium] ALSA: maya44e: missing joint-duplex rate constraint in
> maya_open()
v2 advertises SNDRV_PCM_INFO_JOINT_DUPLEX and refines the rate against
the opposite stream's reservation in either direction. The hw_params
reservation check also handles concurrent negotiations.
> [Medium] ALSA: maya44e: skipped PGA updates at zero volume in
> maya_input_levels()
v2 uses the WM8776 per-channel PGA mute to silence analog monitoring
at zero capture volume. It preserves the last nonzero PGA setting and
reapplies it along with the current gain after codec reset.
> [Medium] ALSA: maya44e: unlocked last_appl updates in maya_pcm_ioctl()
> and maya_prepare()
The playback PCM reset callback now rebases last_appl under the same
ALSA stream lock that protects the appl_ptr update. The v2 patch lists
Joonho Lee's PCM reset callback patch (v25 03/16) as a prerequisite.
The final v2 passed a build and host tests. The hardware results in
the changelog are from an earlier build, as noted there.
Thanks,
Nordschamane
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-11 9:25 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-11 6:58 [PATCH RFC] ALSA: pci: add analog support for ESI MAYA44e Nordschamane via B4 Relay
2026-11-11 7:32 ` sashiko-bot
2026-11-11 9:25 ` Nordschamane
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox