[RFC PATCH] ALSA: pcmtest: Add a volume control with event fault injection

From: HyeongJun An

Date: Mon Nov 02 2026 - 20:10:17 EST


The mixer-test selftest checks that a control sends an event when its
value changes, and only then. No virtual driver has a control that
breaks this rule, so without broken hardware nothing shows that these
checks work.

Add a stereo volume control to pcmtest, and two parameters that make
its put callback report the wrong result. With inject_ctl_no_event it
reports no change, and with inject_ctl_spurious_event it reports a
change every time.

With both off, mixer-test passes on the control. With either one on,
only the matching event check fails.

Signed-off-by: HyeongJun An <sammiee5311@xxxxxxxxx>
---
This is an RFC. I would like to ask whether this is wanted before I go
further.

mixer-test runs eight checks on each control. To see one of them fail
today, you need hardware with the bug or a driver broken by hand. I
broke snd-dummy by hand to test the side effect check. This patch does
the same for the two event checks, with module parameters.

Two questions:

1. Is this useful enough to cover the other six checks too?

2. Should it live in pcmtest, or in a separate driver? pcmtest already
has the inject_* parameters and a selftest, so it seemed the natural
place.

If this goes ahead, the next version will also document the new
parameters in Documentation/sound/cards/pcmtest.rst.

mixer-test results for the new control:

no fault all 8 checks pass
inject_ctl_no_event=1 only event_missing fails
inject_ctl_spurious_event=1 only event_spurious fails

The pcmtest selftest still passes all 10 tests.

Prepared with Claude Code (Claude Opus 5.5). Tested on x86_64 with the
sound/for-next driver, built for a 6.17 host.

sound/drivers/pcmtest.c | 68 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)

diff --git a/sound/drivers/pcmtest.c b/sound/drivers/pcmtest.c
index 66bee8a2ce34..c95bb4717487 100644
--- a/sound/drivers/pcmtest.c
+++ b/sound/drivers/pcmtest.c
@@ -34,6 +34,7 @@
#include <linux/init.h>
#include <sound/pcm.h>
#include <sound/core.h>
+#include <sound/control.h>
#include <linux/dma-mapping.h>
#include <linux/platform_device.h>
#include <linux/string.h>
@@ -57,6 +58,8 @@

#define MAX_PATTERN_LEN 4096

+#define VOLUME_MAX 100
+
static int index = -1;
static char *id = "pcmtest";
static bool enable = true;
@@ -65,6 +68,8 @@ static bool inject_hwpars_err;
static bool inject_prepare_err;
static bool inject_trigger_err;
static bool inject_open_err;
+static bool inject_ctl_no_event;
+static bool inject_ctl_spurious_event;

static short fill_mode = FILL_MODE_PAT;

@@ -109,11 +114,17 @@ module_param(inject_trigger_err, bool, 0600);
MODULE_PARM_DESC(inject_trigger_err, "Inject EINVAL error in the 'trigger' callback");
module_param(inject_open_err, bool, 0600);
MODULE_PARM_DESC(inject_open_err, "Inject EBUSY error in the 'open' callback");
+module_param(inject_ctl_no_event, bool, 0600);
+MODULE_PARM_DESC(inject_ctl_no_event, "Report no change from the control 'put' callback");
+module_param(inject_ctl_spurious_event, bool, 0600);
+MODULE_PARM_DESC(inject_ctl_spurious_event, "Report a change from the control 'put' callback");

struct pcmtst {
struct snd_pcm *pcm;
struct snd_card *card;
struct platform_device *pdev;
+ spinlock_t mixer_lock;
+ long volume[2];
};

struct pcmtst_buf_iter {
@@ -544,6 +555,58 @@ static const struct snd_pcm_ops snd_pcmtst_capture_ops = {
.pointer = snd_pcmtst_pcm_pointer,
};

+static int snd_pcmtst_volume_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 2;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = VOLUME_MAX;
+ return 0;
+}
+
+static int snd_pcmtst_volume_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct pcmtst *pcmtst = snd_kcontrol_chip(kcontrol);
+
+ guard(spinlock_irq)(&pcmtst->mixer_lock);
+ ucontrol->value.integer.value[0] = pcmtst->volume[0];
+ ucontrol->value.integer.value[1] = pcmtst->volume[1];
+ return 0;
+}
+
+static int snd_pcmtst_volume_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct pcmtst *pcmtst = snd_kcontrol_chip(kcontrol);
+ long left = ucontrol->value.integer.value[0];
+ long right = ucontrol->value.integer.value[1];
+ bool changed;
+
+ if (left < 0 || left > VOLUME_MAX || right < 0 || right > VOLUME_MAX)
+ return -EINVAL;
+
+ guard(spinlock_irq)(&pcmtst->mixer_lock);
+ changed = pcmtst->volume[0] != left || pcmtst->volume[1] != right;
+ pcmtst->volume[0] = left;
+ pcmtst->volume[1] = right;
+
+ if (inject_ctl_no_event)
+ return 0;
+ if (inject_ctl_spurious_event)
+ return 1;
+ return changed;
+}
+
+static const struct snd_kcontrol_new snd_pcmtst_volume = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .name = "PCM Playback Volume",
+ .info = snd_pcmtst_volume_info,
+ .get = snd_pcmtst_volume_get,
+ .put = snd_pcmtst_volume_put,
+};
+
static int snd_pcmtst_new_pcm(struct pcmtst *pcmtst)
{
struct snd_pcm *pcm;
@@ -582,11 +645,16 @@ static int pcmtst_probe(struct platform_device *pdev)
pcmtst = card->private_data;
pcmtst->card = card;
pcmtst->pdev = pdev;
+ spin_lock_init(&pcmtst->mixer_lock);

err = snd_pcmtst_new_pcm(pcmtst);
if (err < 0)
return err;

+ err = snd_ctl_add(card, snd_ctl_new1(&snd_pcmtst_volume, pcmtst));
+ if (err < 0)
+ return err;
+
strscpy(card->driver, "PCM-TEST Driver");
strscpy(card->shortname, "PCM-Test");
strscpy(card->longname, "PCM-Test virtual driver");
--
2.43.0