* [PATCH 0/2] ALSA: aloop: Fix notify mode, add a selftest
@ 2026-10-03 13:26 Henrik Enquist via B4 Relay
2026-10-03 13:26 ` [PATCH 1/2] ALSA: aloop: Don't constrain playback params in notify mode Henrik Enquist via B4 Relay
2026-10-03 13:26 ` [PATCH 2/2] selftests/alsa: Add a test for snd-aloop " Henrik Enquist via B4 Relay
0 siblings, 2 replies; 3+ messages in thread
From: Henrik Enquist via B4 Relay @ 2026-10-03 13:26 UTC (permalink / raw)
To: Jaroslav Kysela, Takashi Iwai, Mark Brown, Shuah Khan
Cc: linux-sound, linux-kernel, linux-kselftest, Pavel Hofman, stable,
Henrik Enquist
Hi,
snd-aloop has a notify mode (the pcm_notify module parameter, or the
"PCM Notify" control per cable) where the playback side may switch
format, rate and channels while a capture is running. The driver then
stops the capture and updates the "PCM Slave" controls, so the capture
application can reopen with the new parameters. alsaloop enables it in
its slave mode, and I'd like to use it in CamillaDSP, which captures
from a loopback and needs to follow whatever the player outputs.
This hasn't worked since 2018. Commit 898dfe4687f4 ("ALSA: aloop: Fix
racy hw constraints adjustment") moved the hw rules over to the shared
cable->hw. That fixed a real race, but it also pins the playback side
to the capture's parameters in notify mode. Pavel reported it on
alsa-devel in 2020 [1], and Jaroslav reproduced it and pointed at the
same commit:
It seems that 898dfe4687f4 from Takashi broke this functionality
(tied the cable parameters more strictly, so the playback cannot set
freely own parameters for the pcm_notify=1 case). We need to find
another way to detach capture stream in this case.
The thread ended there, without a patch.
The breakage is quiet, which probably explains why it lasted. A player
that probes the playback device sees only the capture's rate and
format, and resamples to them. aplay prints a warning, most players
don't. There's no rate change, no stopped capture and no event.
Patch 1 skips the hw rules for the playback side when notify is on,
which gives it back the freedom loopback_open() still intends it to
have. That makes the capture stop in loopback_check_format() reachable
again. That path was hardened this year by 826af7fa62e3 ("ALSA: aloop:
Fix racy access at PCM trigger") and e5c33cdc6f40 ("ALSA: aloop: Fix
peer runtime UAF during format-change stop"), and with those in place
I'm comfortable turning it back on. They need to go first in any
stable backport.
The period bytes rule is skipped as well. It came later, for the sound
timer source, but without skipping it a playback that switches rate
keeps the old capture's period size. The stream then runs with a period
that doesn't match the timer, and every switch fills dmesg with "Period
size ... not corresponding to timer resolution".
Patch 2 adds a selftest, so this doesn't break silently again. It
doesn't need to go to stable, so it can go via for-next if that's
easier.
Testing: for-linus at b4e7fc36e31f, arm64 VM, kernel with KASAN,
lockdep, DEBUG_ATOMIC_SLEEP and DEBUG_LIST, comparing the patched
driver with the unpatched one built from the same tree. With the
jiffies, hrtimer and sound timer (via snd-dummy) sources:
- With notify on, rate, format and channel changes on the playback
side now go through, stop the capture and update the controls. With
notify off they're refused as before.
- A capture that reopens at "PCM Slave Rate" after each stop follows a
player alternating between 44.1k and 96k files, 20 of 20 switches,
with both RW and mmap access. Unpatched: 10 of 20, the playback stays
pinned to the capture's rate.
- A capture opened second is still pinned to the playback.
- 2 minutes of random open/start/close on both ends per source gave no
KASAN or lockdep reports. The sound timer source logs an occasional
"snd_timer_stop ... failed with -16", at the same rate unpatched.
- The new selftest fails 2 of 4 tests unpatched, and passes 4 of 4
patched, 50 runs in a row.
Not tested: real hardware, other architectures, pause/resume, more
than two substreams.
One question, following up on Takashi's point in that thread about
telling user space that a stream got invalidated. After the stop, the
capture drains and then reads return -EBADFD. alsa-lib now documents
-ENODATA as meaning the PCM must be completely restarted, which looks
like the right signal here. Would you want aloop to report that
instead? I left it out since it changes what user space sees, but I'm
happy to do it as a follow-up.
AI disclosure: I used Claude Code (Claude Opus 5.5) for this. Starting
from my description of the problem and the 2020 thread, it did the
root cause analysis from the driver source and git history, set up the
test kernels and VM, wrote the test scripts, ran the measurements, and
wrote both patches and their changelogs. I reviewed all of it, and both
patches carry an Assisted-by tag.
[1] https://lore.kernel.org/all/b4af9071-f8d7-5b47-4d7a-c5743bd67394@ivitera.com/
Henrik
---
Henrik Enquist (2):
ALSA: aloop: Don't constrain playback params in notify mode
selftests/alsa: Add a test for snd-aloop notify mode
sound/drivers/aloop.c | 20 ++
tools/testing/selftests/alsa/.gitignore | 1 +
tools/testing/selftests/alsa/Makefile | 2 +-
tools/testing/selftests/alsa/aloop-test.c | 345 ++++++++++++++++++++++++++++++
4 files changed, 367 insertions(+), 1 deletion(-)
---
base-commit: 13dffca9043500ed6033d770b491a043fb952bc5
change-id: 20261003-aloop-notify-fix-4f7beb408ab8
Best regards,
--
Henrik Enquist <henrik.enquist@gmail.com>
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 1/2] ALSA: aloop: Don't constrain playback params in notify mode
2026-10-03 13:26 [PATCH 0/2] ALSA: aloop: Fix notify mode, add a selftest Henrik Enquist via B4 Relay
@ 2026-10-03 13:26 ` Henrik Enquist via B4 Relay
2026-10-03 13:26 ` [PATCH 2/2] selftests/alsa: Add a test for snd-aloop " Henrik Enquist via B4 Relay
1 sibling, 0 replies; 3+ messages in thread
From: Henrik Enquist via B4 Relay @ 2026-10-03 13:26 UTC (permalink / raw)
To: Jaroslav Kysela, Takashi Iwai, Mark Brown, Shuah Khan
Cc: linux-sound, linux-kernel, linux-kselftest, Pavel Hofman, stable,
Henrik Enquist
From: Henrik Enquist <henrik.enquist@gmail.com>
With notify enabled (the pcm_notify module parameter or the "PCM
Notify" control), the playback side of a cable may change format, rate
and channels while a capture is running. loopback_check_format() then
stops the capture and notifies the "PCM Slave" controls, so that the
capture application can reopen with the new parameters. For this
reason loopback_open() still gives a notify mode playback the
unconstrained loopback_pcm_hardware.
Since commit 898dfe4687f4 ("ALSA: aloop: Fix racy hw constraints
adjustment") the hw rules refine against the shared cable->hw instead
of the substream's own runtime->hw. This pins the playback side to the
parameters of an open capture whatever the notify setting is. A player
probing the device sees only the capture's rate, format and channels,
and the format change path in loopback_check_format() can't be reached
by a new playback.
Skip the format, rate and channels rules for the playback substream
when notify is enabled. Skip the period bytes rule as well: with a
sound timer source, params_change() already takes the period size from
a notify mode playback, and keeping the old capture's period bytes
leaves a playback at another rate with a period that doesn't match the
timer. The capture side stays constrained to the cable parameters.
The capture stop this makes reachable again was hardened by commit
826af7fa62e3 ("ALSA: aloop: Fix racy access at PCM trigger") and
commit e5c33cdc6f40 ("ALSA: aloop: Fix peer runtime UAF during
format-change stop"), which need to be applied first when backporting.
Reproducer, with notify enabled on the cable:
arecord -D hw:Loopback,1,0 -r 44100 -f S32_LE -c 2 /dev/null &
aplay -D hw:Loopback,0,0 96k.wav
Before: aplay is forced to 44100 Hz and the capture keeps running.
After: aplay runs at 96000 Hz, the capture is stopped and "PCM Slave
Rate" reports 96000.
Fixes: 898dfe4687f4 ("ALSA: aloop: Fix racy hw constraints adjustment")
Reported-by: Pavel Hofman <pavel.hofman@ivitera.com>
Closes: https://lore.kernel.org/all/b4af9071-f8d7-5b47-4d7a-c5743bd67394@ivitera.com/
Cc: stable@vger.kernel.org
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Henrik Enquist <henrik.enquist@gmail.com>
---
sound/drivers/aloop.c | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/sound/drivers/aloop.c b/sound/drivers/aloop.c
index 81cb1f59f7..5f0e2dc967 100644
--- a/sound/drivers/aloop.c
+++ b/sound/drivers/aloop.c
@@ -1073,6 +1073,18 @@ static unsigned int get_cable_index(struct snd_pcm_substream *substream)
return !substream->stream;
}
+/*
+ * In notify mode the playback side may change format, rate, channels and,
+ * with a sound timer, the period size. A running capture that no longer
+ * matches is stopped by loopback_check_format().
+ * call in loopback->cable_lock
+ */
+static bool loopback_playback_unconstrained(struct loopback_pcm *dpcm)
+{
+ return dpcm->substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
+ get_notify(dpcm);
+}
+
static int rule_format(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
@@ -1082,6 +1094,8 @@ static int rule_format(struct snd_pcm_hw_params *params,
snd_mask_none(&m);
scoped_guard(mutex, &dpcm->loopback->cable_lock) {
+ if (loopback_playback_unconstrained(dpcm))
+ return 0;
m.bits[0] = (u_int32_t)cable->hw.formats;
m.bits[1] = (u_int32_t)(cable->hw.formats >> 32);
}
@@ -1096,6 +1110,8 @@ static int rule_rate(struct snd_pcm_hw_params *params,
struct snd_interval t;
scoped_guard(mutex, &dpcm->loopback->cable_lock) {
+ if (loopback_playback_unconstrained(dpcm))
+ return 0;
t.min = cable->hw.rate_min;
t.max = cable->hw.rate_max;
}
@@ -1112,6 +1128,8 @@ static int rule_channels(struct snd_pcm_hw_params *params,
struct snd_interval t;
scoped_guard(mutex, &dpcm->loopback->cable_lock) {
+ if (loopback_playback_unconstrained(dpcm))
+ return 0;
t.min = cable->hw.channels_min;
t.max = cable->hw.channels_max;
}
@@ -1128,6 +1146,8 @@ static int rule_period_bytes(struct snd_pcm_hw_params *params,
struct snd_interval t;
scoped_guard(mutex, &dpcm->loopback->cable_lock) {
+ if (loopback_playback_unconstrained(dpcm))
+ return 0;
t.min = cable->hw.period_bytes_min;
t.max = cable->hw.period_bytes_max;
}
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 2/2] selftests/alsa: Add a test for snd-aloop notify mode
2026-10-03 13:26 [PATCH 0/2] ALSA: aloop: Fix notify mode, add a selftest Henrik Enquist via B4 Relay
2026-10-03 13:26 ` [PATCH 1/2] ALSA: aloop: Don't constrain playback params in notify mode Henrik Enquist via B4 Relay
@ 2026-10-03 13:26 ` Henrik Enquist via B4 Relay
1 sibling, 0 replies; 3+ messages in thread
From: Henrik Enquist via B4 Relay @ 2026-10-03 13:26 UTC (permalink / raw)
To: Jaroslav Kysela, Takashi Iwai, Mark Brown, Shuah Khan
Cc: linux-sound, linux-kernel, linux-kselftest, Henrik Enquist
From: Henrik Enquist <henrik.enquist@gmail.com>
Add a test for the hw constraints between the playback and capture
devices of an snd-aloop cable, and for the "PCM Slave" controls that
report the playback side's parameters to the capture application.
With notify disabled, a playback opened while a capture is set up must
be pinned to the capture's parameters. With notify enabled, it may pick
others. Starting it stops the running capture, and the "PCM Slave
Format", "Rate", "Channels" and "Access Mode" controls report the new
parameters, each with a value event. A capture opened second is pinned
to the playback either way.
The tests only check hw_refine results, stream states and control
values, so they don't depend on timing. They skip if snd-aloop isn't
loaded or the devices are in use.
Without "ALSA: aloop: Don't constrain playback params in notify mode",
the two notify mode tests fail.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Henrik Enquist <henrik.enquist@gmail.com>
---
tools/testing/selftests/alsa/.gitignore | 1 +
tools/testing/selftests/alsa/Makefile | 2 +-
tools/testing/selftests/alsa/aloop-test.c | 345 ++++++++++++++++++++++++++++++
3 files changed, 347 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/alsa/.gitignore b/tools/testing/selftests/alsa/.gitignore
index 3dd8e1176b..7b0e1e9ebf 100644
--- a/tools/testing/selftests/alsa/.gitignore
+++ b/tools/testing/selftests/alsa/.gitignore
@@ -1,3 +1,4 @@
+aloop-test
global-timer
mixer-test
pcm-test
diff --git a/tools/testing/selftests/alsa/Makefile b/tools/testing/selftests/alsa/Makefile
index 8dab90ad22..afd64a679d 100644
--- a/tools/testing/selftests/alsa/Makefile
+++ b/tools/testing/selftests/alsa/Makefile
@@ -16,7 +16,7 @@ LDLIBS+=-lpthread
OVERRIDE_TARGETS = 1
-TEST_GEN_PROGS := mixer-test pcm-test test-pcmtest-driver utimer-test
+TEST_GEN_PROGS := aloop-test mixer-test pcm-test test-pcmtest-driver utimer-test
TEST_GEN_PROGS_EXTENDED := libatest.so global-timer
diff --git a/tools/testing/selftests/alsa/aloop-test.c b/tools/testing/selftests/alsa/aloop-test.c
new file mode 100644
index 0000000000..a58b9fdbdc
--- /dev/null
+++ b/tools/testing/selftests/alsa/aloop-test.c
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for the hw constraints between the two ends of an snd-aloop cable,
+ * with and without the "PCM Notify" control, and for the "PCM Slave"
+ * controls that report the playback side's parameters.
+ *
+ * Needs snd-aloop loaded with the default card id "Loopback". The tests use
+ * the cable between hw:Loopback,0,0 (playback) and hw:Loopback,1,0 (capture).
+ */
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <alsa/asoundlib.h>
+#include "kselftest_harness.h"
+
+#define FRAMES 1024
+#define MAX_CHANNELS 4
+
+struct stream_params {
+ snd_pcm_access_t access;
+ snd_pcm_format_t format;
+ unsigned int channels;
+ unsigned int rate;
+};
+
+/* The playback params differ from the capture params in every field. */
+static const struct stream_params capture_params = {
+ SND_PCM_ACCESS_RW_INTERLEAVED, SND_PCM_FORMAT_S16_LE, 2, 44100
+};
+
+static const struct stream_params playback_params = {
+ SND_PCM_ACCESS_RW_NONINTERLEAVED, SND_PCM_FORMAT_S32_LE, 4, 96000
+};
+
+struct probe_result {
+ unsigned int rate_min, rate_max;
+ unsigned int channels_min, channels_max;
+ bool format_ok;
+};
+
+/* Value events seen on the cable's controls */
+enum {
+ EV_ACTIVE = 1 << 0,
+ EV_FORMAT = 1 << 1,
+ EV_RATE = 1 << 2,
+ EV_CHANNELS = 1 << 3,
+ EV_ACCESS = 1 << 4,
+};
+
+FIXTURE(aloop) {
+ char play_name[32];
+ char capt_name[32];
+ snd_ctl_t *ctl;
+ bool saved_notify;
+ bool notify_saved;
+};
+
+/* The cable's controls are on the capture side device. */
+static void cable_ctl_id(snd_ctl_elem_value_t *value, const char *name)
+{
+ snd_ctl_elem_value_set_interface(value, SND_CTL_ELEM_IFACE_PCM);
+ snd_ctl_elem_value_set_name(value, name);
+ snd_ctl_elem_value_set_device(value, 1);
+ snd_ctl_elem_value_set_subdevice(value, 0);
+}
+
+static long cable_ctl_get(snd_ctl_t *ctl, const char *name)
+{
+ snd_ctl_elem_value_t *value;
+
+ snd_ctl_elem_value_alloca(&value);
+ cable_ctl_id(value, name);
+ if (snd_ctl_elem_read(ctl, value) < 0)
+ return -1;
+ if (!strcmp(name, "PCM Slave Access Mode"))
+ return snd_ctl_elem_value_get_enumerated(value, 0);
+ return snd_ctl_elem_value_get_integer(value, 0);
+}
+
+static int set_notify(snd_ctl_t *ctl, bool on)
+{
+ snd_ctl_elem_value_t *value;
+
+ snd_ctl_elem_value_alloca(&value);
+ cable_ctl_id(value, "PCM Notify");
+ snd_ctl_elem_value_set_boolean(value, 0, on);
+ return snd_ctl_elem_write(ctl, value);
+}
+
+/* Read all pending control events and return the EV_* bits for the cable's controls. */
+static unsigned int read_events(snd_ctl_t *ctl)
+{
+ static const struct {
+ const char *name;
+ unsigned int bit;
+ } names[] = {
+ { "PCM Slave Active", EV_ACTIVE },
+ { "PCM Slave Format", EV_FORMAT },
+ { "PCM Slave Rate", EV_RATE },
+ { "PCM Slave Channels", EV_CHANNELS },
+ { "PCM Slave Access Mode", EV_ACCESS },
+ };
+ snd_ctl_event_t *event;
+ unsigned int seen = 0;
+ int i;
+
+ snd_ctl_event_alloca(&event);
+ while (snd_ctl_read(ctl, event) > 0) {
+ if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM ||
+ !(snd_ctl_event_elem_get_mask(event) & SND_CTL_EVENT_MASK_VALUE) ||
+ snd_ctl_event_elem_get_device(event) != 1 ||
+ snd_ctl_event_elem_get_subdevice(event) != 0)
+ continue;
+ for (i = 0; i < ARRAY_SIZE(names); i++)
+ if (!strcmp(snd_ctl_event_elem_get_name(event), names[i].name))
+ seen |= names[i].bit;
+ }
+ return seen;
+}
+
+/* Open and configure a stream. snd_pcm_hw_params() also prepares it. */
+static int open_pcm(snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream,
+ const struct stream_params *p)
+{
+ unsigned int buffer_time = 100000;
+ snd_pcm_hw_params_t *hw;
+ int err;
+
+ snd_pcm_hw_params_alloca(&hw);
+ err = snd_pcm_open(pcm, name, stream, 0);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_params_any(*pcm, hw);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_access(*pcm, hw, p->access);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_format(*pcm, hw, p->format);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_channels(*pcm, hw, p->channels);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_rate(*pcm, hw, p->rate, 0);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_buffer_time_near(*pcm, hw, &buffer_time, NULL);
+ if (err >= 0)
+ err = snd_pcm_hw_params(*pcm, hw);
+ if (err < 0) {
+ snd_pcm_close(*pcm);
+ *pcm = NULL;
+ }
+ return err;
+}
+
+/* What a client probing the device sees, and whether it may use the given format */
+static int probe_pcm(const char *name, snd_pcm_stream_t stream, snd_pcm_format_t format,
+ struct probe_result *res)
+{
+ snd_pcm_hw_params_t *hw;
+ snd_pcm_t *pcm;
+ int err;
+
+ snd_pcm_hw_params_alloca(&hw);
+ err = snd_pcm_open(&pcm, name, stream, 0);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_params_any(pcm, hw);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_rate_min(hw, &res->rate_min, NULL);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_rate_max(hw, &res->rate_max, NULL);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_channels_min(hw, &res->channels_min);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_channels_max(hw, &res->channels_max);
+ if (err >= 0)
+ res->format_ok = !snd_pcm_hw_params_test_format(pcm, hw, format);
+ snd_pcm_close(pcm);
+ return err;
+}
+
+static int start_playback(snd_pcm_t *pcm, const struct stream_params *p)
+{
+ static char silence[FRAMES * MAX_CHANNELS * 4];
+ void *bufs[MAX_CHANNELS];
+ snd_pcm_sframes_t written;
+ unsigned int i;
+
+ if (p->access == SND_PCM_ACCESS_RW_NONINTERLEAVED) {
+ for (i = 0; i < p->channels; i++)
+ bufs[i] = silence + i * FRAMES * 4;
+ written = snd_pcm_writen(pcm, bufs, FRAMES);
+ } else {
+ written = snd_pcm_writei(pcm, silence, FRAMES);
+ }
+ if (written < 0)
+ return written;
+ if (snd_pcm_state(pcm) == SND_PCM_STATE_PREPARED)
+ return snd_pcm_start(pcm);
+ return 0;
+}
+
+FIXTURE_SETUP(aloop) {
+ char ctl_name[32];
+ snd_pcm_t *pcm;
+ int card, err;
+
+ card = snd_card_get_index("Loopback");
+ if (card < 0)
+ SKIP(return, "No Loopback card, snd-aloop is probably not loaded");
+
+ sprintf(ctl_name, "hw:%d", card);
+ sprintf(self->play_name, "hw:%d,0,0", card);
+ sprintf(self->capt_name, "hw:%d,1,0", card);
+
+ err = snd_pcm_open(&pcm, self->capt_name, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
+ if (err == -EBUSY)
+ SKIP(return, "%s is in use", self->capt_name);
+ ASSERT_EQ(err, 0);
+ snd_pcm_close(pcm);
+ err = snd_pcm_open(&pcm, self->play_name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
+ if (err == -EBUSY)
+ SKIP(return, "%s is in use", self->play_name);
+ ASSERT_EQ(err, 0);
+ snd_pcm_close(pcm);
+
+ ASSERT_EQ(snd_ctl_open(&self->ctl, ctl_name, SND_CTL_NONBLOCK), 0);
+ ASSERT_EQ(snd_ctl_subscribe_events(self->ctl, 1), 0);
+ self->saved_notify = cable_ctl_get(self->ctl, "PCM Notify");
+ self->notify_saved = true;
+}
+
+FIXTURE_TEARDOWN(aloop) {
+ if (self->notify_saved)
+ set_notify(self->ctl, self->saved_notify);
+ if (self->ctl)
+ snd_ctl_close(self->ctl);
+}
+
+/* Without notify, a playback opened while a capture is set up is pinned to its parameters. */
+TEST_F(aloop, playback_constrained_without_notify) {
+ struct probe_result res;
+ snd_pcm_t *capt;
+
+ ASSERT_EQ(set_notify(self->ctl, false), 0);
+ ASSERT_EQ(open_pcm(&capt, self->capt_name, SND_PCM_STREAM_CAPTURE, &capture_params), 0);
+
+ ASSERT_EQ(probe_pcm(self->play_name, SND_PCM_STREAM_PLAYBACK, playback_params.format,
+ &res), 0);
+ EXPECT_EQ(res.rate_min, capture_params.rate);
+ EXPECT_EQ(res.rate_max, capture_params.rate);
+ EXPECT_EQ(res.channels_min, capture_params.channels);
+ EXPECT_EQ(res.channels_max, capture_params.channels);
+ EXPECT_FALSE(res.format_ok);
+
+ snd_pcm_close(capt);
+}
+
+/* With notify, the playback side is free to pick other parameters. */
+TEST_F(aloop, playback_unconstrained_with_notify) {
+ struct probe_result res;
+ snd_pcm_t *capt;
+
+ ASSERT_EQ(set_notify(self->ctl, true), 0);
+ ASSERT_EQ(open_pcm(&capt, self->capt_name, SND_PCM_STREAM_CAPTURE, &capture_params), 0);
+
+ ASSERT_EQ(probe_pcm(self->play_name, SND_PCM_STREAM_PLAYBACK, playback_params.format,
+ &res), 0);
+ EXPECT_LE(res.rate_min, capture_params.rate);
+ EXPECT_GE(res.rate_max, playback_params.rate);
+ EXPECT_LE(res.channels_min, capture_params.channels);
+ EXPECT_GE(res.channels_max, playback_params.channels);
+ EXPECT_TRUE(res.format_ok);
+
+ snd_pcm_close(capt);
+}
+
+/*
+ * With notify, starting a playback with different parameters stops the
+ * running capture, and the "PCM Slave" controls report the new parameters
+ * with a value event for each one that changed.
+ */
+TEST_F(aloop, params_change_stops_capture_with_notify) {
+ snd_pcm_t *capt, *play;
+ unsigned int events;
+
+ /*
+ * The controls keep the last playback's parameters, and only notify on
+ * a change. Start a playback with the capture's parameters while no
+ * capture is open, so that every control changes below.
+ */
+ ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &capture_params), 0);
+ ASSERT_EQ(start_playback(play, &capture_params), 0);
+ snd_pcm_close(play);
+
+ ASSERT_EQ(set_notify(self->ctl, true), 0);
+ ASSERT_EQ(open_pcm(&capt, self->capt_name, SND_PCM_STREAM_CAPTURE, &capture_params), 0);
+ ASSERT_EQ(snd_pcm_start(capt), 0);
+ ASSERT_EQ(snd_pcm_state(capt), SND_PCM_STATE_RUNNING);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Active"), 0);
+ read_events(self->ctl);
+
+ ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &playback_params), 0)
+ TH_LOG("Playback refused other parameters while the capture is running");
+ ASSERT_EQ(start_playback(play, &playback_params), 0);
+
+ /* loopback_check_format() stops the capture from the playback's start trigger. */
+ EXPECT_NE(snd_pcm_state(capt), SND_PCM_STATE_RUNNING);
+
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Active"), 1);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Format"), playback_params.format);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Rate"), playback_params.rate);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Channels"), playback_params.channels);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Access Mode"), 1);
+
+ events = read_events(self->ctl);
+ EXPECT_TRUE(events & EV_ACTIVE);
+ EXPECT_TRUE(events & EV_FORMAT);
+ EXPECT_TRUE(events & EV_RATE);
+ EXPECT_TRUE(events & EV_CHANNELS);
+ EXPECT_TRUE(events & EV_ACCESS);
+
+ snd_pcm_close(play);
+ snd_pcm_close(capt);
+}
+
+/* With notify, a capture opened second is still pinned to the playback's parameters. */
+TEST_F(aloop, capture_constrained_with_notify) {
+ struct probe_result res;
+ snd_pcm_t *play;
+
+ ASSERT_EQ(set_notify(self->ctl, true), 0);
+ ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &playback_params), 0);
+
+ ASSERT_EQ(probe_pcm(self->capt_name, SND_PCM_STREAM_CAPTURE, capture_params.format,
+ &res), 0);
+ EXPECT_EQ(res.rate_min, playback_params.rate);
+ EXPECT_EQ(res.rate_max, playback_params.rate);
+ EXPECT_EQ(res.channels_min, playback_params.channels);
+ EXPECT_EQ(res.channels_max, playback_params.channels);
+ EXPECT_FALSE(res.format_ok);
+
+ snd_pcm_close(play);
+}
+
+TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-03 13:26 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-03 13:26 [PATCH 0/2] ALSA: aloop: Fix notify mode, add a selftest Henrik Enquist via B4 Relay
2026-10-03 13:26 ` [PATCH 1/2] ALSA: aloop: Don't constrain playback params in notify mode Henrik Enquist via B4 Relay
2026-10-03 13:26 ` [PATCH 2/2] selftests/alsa: Add a test for snd-aloop " Henrik Enquist via B4 Relay
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®