1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
|
// 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);
}
/*
* A playback that joins a running capture with the same parameters doesn't
* stop it, and the "PCM Slave" controls still report the playback's
* parameters rather than those of an earlier stream.
*/
TEST_F(aloop, matching_playback_updates_controls) {
snd_pcm_t *capt, *play;
unsigned int events;
/* Leave the controls at other parameters than the ones used below. */
ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &playback_params), 0);
ASSERT_EQ(start_playback(play, &playback_params), 0);
snd_pcm_close(play);
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(snd_pcm_start(capt), 0);
read_events(self->ctl);
ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &capture_params), 0);
ASSERT_EQ(start_playback(play, &capture_params), 0);
EXPECT_EQ(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"), capture_params.format);
EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Rate"), capture_params.rate);
EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Channels"), capture_params.channels);
EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Access Mode"), 0);
events = read_events(self->ctl);
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
|