linux/tools/testing/selftests/alsa/pcm-test.c
Mark Brown 7d721baea1 kselftest/alsa: Add more coverage of sample rates and channel counts
Now that we can skip unsupported configurations add some more test cases
using that, cover 8kHz, 44.1kHz and 96kHz plus 8kHz mono and 48kHz 6
channel.

44.1kHz is a different clock base to the existing 48kHz tests and may
therefore show problems with the clock configuration if only 8kHz based
rates are really available (or help diagnose if bad clocking is due to
only 44.1kHz based rates being supported). 8kHz mono and 48Hz 6 channel
are real world formats and should show if clocking does not account for
channel count properly.

Signed-off-by: Mark Brown <broonie@kernel.org>
Link: https://lore.kernel.org/r/20221201170745.1111236-7-broonie@kernel.org
Signed-off-by: Takashi Iwai <tiwai@suse.de>
2022-12-01 20:02:14 +01:00

490 lines
14 KiB
C

// SPDX-License-Identifier: GPL-2.0
//
// kselftest for the ALSA PCM API
//
// Original author: Jaroslav Kysela <perex@perex.cz>
// Copyright (c) 2022 Red Hat Inc.
// This test will iterate over all cards detected in the system, exercising
// every PCM device it can find. This may conflict with other system
// software if there is audio activity so is best run on a system with a
// minimal active userspace.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <assert.h>
#include "../kselftest.h"
#include "alsa-local.h"
typedef struct timespec timestamp_t;
struct pcm_data {
snd_pcm_t *handle;
int card;
int device;
int subdevice;
snd_pcm_stream_t stream;
snd_config_t *pcm_config;
struct pcm_data *next;
};
int num_pcms = 0;
struct pcm_data *pcm_list = NULL;
int num_missing = 0;
struct pcm_data *pcm_missing = NULL;
struct time_test_def {
const char *cfg_prefix;
const char *format;
long rate;
long channels;
long period_size;
long buffer_size;
};
void timestamp_now(timestamp_t *tstamp)
{
if (clock_gettime(CLOCK_MONOTONIC_RAW, tstamp))
ksft_exit_fail_msg("clock_get_time\n");
}
long long timestamp_diff_ms(timestamp_t *tstamp)
{
timestamp_t now, diff;
timestamp_now(&now);
if (tstamp->tv_nsec > now.tv_nsec) {
diff.tv_sec = now.tv_sec - tstamp->tv_sec - 1;
diff.tv_nsec = (now.tv_nsec + 1000000000L) - tstamp->tv_nsec;
} else {
diff.tv_sec = now.tv_sec - tstamp->tv_sec;
diff.tv_nsec = now.tv_nsec - tstamp->tv_nsec;
}
return (diff.tv_sec * 1000) + ((diff.tv_nsec + 500000L) / 1000000L);
}
static long device_from_id(snd_config_t *node)
{
const char *id;
char *end;
long v;
if (snd_config_get_id(node, &id))
ksft_exit_fail_msg("snd_config_get_id\n");
errno = 0;
v = strtol(id, &end, 10);
if (errno || *end)
return -1;
return v;
}
static void missing_device(int card, int device, int subdevice, snd_pcm_stream_t stream)
{
struct pcm_data *pcm_data;
for (pcm_data = pcm_list; pcm_data != NULL; pcm_data = pcm_data->next) {
if (pcm_data->card != card)
continue;
if (pcm_data->device != device)
continue;
if (pcm_data->subdevice != subdevice)
continue;
if (pcm_data->stream != stream)
continue;
return;
}
pcm_data = calloc(1, sizeof(*pcm_data));
if (!pcm_data)
ksft_exit_fail_msg("Out of memory\n");
pcm_data->card = card;
pcm_data->device = device;
pcm_data->subdevice = subdevice;
pcm_data->stream = stream;
pcm_data->next = pcm_missing;
pcm_missing = pcm_data;
num_missing++;
}
static void missing_devices(int card, snd_config_t *card_config)
{
snd_config_t *pcm_config, *node1, *node2;
snd_config_iterator_t i1, i2, next1, next2;
int device, subdevice;
pcm_config = conf_get_subtree(card_config, "pcm", NULL);
if (!pcm_config)
return;
snd_config_for_each(i1, next1, pcm_config) {
node1 = snd_config_iterator_entry(i1);
device = device_from_id(node1);
if (device < 0)
continue;
if (snd_config_get_type(node1) != SND_CONFIG_TYPE_COMPOUND)
continue;
snd_config_for_each(i2, next2, node1) {
node2 = snd_config_iterator_entry(i2);
subdevice = device_from_id(node2);
if (subdevice < 0)
continue;
if (conf_get_subtree(node2, "PLAYBACK", NULL))
missing_device(card, device, subdevice, SND_PCM_STREAM_PLAYBACK);
if (conf_get_subtree(node2, "CAPTURE", NULL))
missing_device(card, device, subdevice, SND_PCM_STREAM_CAPTURE);
}
}
}
static void find_pcms(void)
{
char name[32], key[64];
int card, dev, subdev, count, direction, err;
snd_pcm_stream_t stream;
struct pcm_data *pcm_data;
snd_ctl_t *handle;
snd_pcm_info_t *pcm_info;
snd_config_t *config, *card_config, *pcm_config;
snd_pcm_info_alloca(&pcm_info);
card = -1;
if (snd_card_next(&card) < 0 || card < 0)
return;
config = get_alsalib_config();
while (card >= 0) {
sprintf(name, "hw:%d", card);
err = snd_ctl_open_lconf(&handle, name, 0, config);
if (err < 0) {
ksft_print_msg("Failed to get hctl for card %d: %s\n",
card, snd_strerror(err));
goto next_card;
}
card_config = conf_by_card(card);
dev = -1;
while (1) {
if (snd_ctl_pcm_next_device(handle, &dev) < 0)
ksft_exit_fail_msg("snd_ctl_pcm_next_device\n");
if (dev < 0)
break;
for (direction = 0; direction < 2; direction++) {
stream = direction ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK;
sprintf(key, "pcm.%d.%s", dev, snd_pcm_stream_name(stream));
pcm_config = conf_get_subtree(card_config, key, NULL);
if (conf_get_bool(card_config, key, "skip", false)) {
ksft_print_msg("skipping pcm %d.%d.%s\n", card, dev, snd_pcm_stream_name(stream));
continue;
}
snd_pcm_info_set_device(pcm_info, dev);
snd_pcm_info_set_subdevice(pcm_info, 0);
snd_pcm_info_set_stream(pcm_info, stream);
err = snd_ctl_pcm_info(handle, pcm_info);
if (err == -ENOENT)
continue;
if (err < 0)
ksft_exit_fail_msg("snd_ctl_pcm_info: %d:%d:%d\n",
dev, 0, stream);
count = snd_pcm_info_get_subdevices_count(pcm_info);
for (subdev = 0; subdev < count; subdev++) {
sprintf(key, "pcm.%d.%d.%s", dev, subdev, snd_pcm_stream_name(stream));
if (conf_get_bool(card_config, key, "skip", false)) {
ksft_print_msg("skipping pcm %d.%d.%d.%s\n", card, dev,
subdev, snd_pcm_stream_name(stream));
continue;
}
pcm_data = calloc(1, sizeof(*pcm_data));
if (!pcm_data)
ksft_exit_fail_msg("Out of memory\n");
pcm_data->card = card;
pcm_data->device = dev;
pcm_data->subdevice = subdev;
pcm_data->stream = stream;
pcm_data->pcm_config = conf_get_subtree(card_config, key, NULL);
pcm_data->next = pcm_list;
pcm_list = pcm_data;
num_pcms++;
}
}
}
/* check for missing devices */
missing_devices(card, card_config);
next_card:
snd_ctl_close(handle);
if (snd_card_next(&card) < 0) {
ksft_print_msg("snd_card_next");
break;
}
}
snd_config_delete(config);
}
static void test_pcm_time1(struct pcm_data *data,
const struct time_test_def *test)
{
char name[64], key[128], msg[256];
const char *cs;
int i, err;
snd_pcm_t *handle = NULL;
snd_pcm_access_t access = SND_PCM_ACCESS_RW_INTERLEAVED;
snd_pcm_format_t format;
unsigned char *samples = NULL;
snd_pcm_sframes_t frames;
long long ms;
long rate, channels, period_size, buffer_size;
unsigned int rchannels;
unsigned int rrate;
snd_pcm_uframes_t rperiod_size, rbuffer_size, start_threshold;
timestamp_t tstamp;
bool pass = false, automatic = true;
snd_pcm_hw_params_t *hw_params;
snd_pcm_sw_params_t *sw_params;
bool skip = false;
snd_pcm_hw_params_alloca(&hw_params);
snd_pcm_sw_params_alloca(&sw_params);
cs = conf_get_string(data->pcm_config, test->cfg_prefix, "format", test->format);
format = snd_pcm_format_value(cs);
if (format == SND_PCM_FORMAT_UNKNOWN)
ksft_exit_fail_msg("Wrong format '%s'\n", cs);
rate = conf_get_long(data->pcm_config, test->cfg_prefix, "rate", test->rate);
channels = conf_get_long(data->pcm_config, test->cfg_prefix, "channels", test->channels);
period_size = conf_get_long(data->pcm_config, test->cfg_prefix, "period_size", test->period_size);
buffer_size = conf_get_long(data->pcm_config, test->cfg_prefix, "buffer_size", test->buffer_size);
automatic = strcmp(test->format, snd_pcm_format_name(format)) == 0 &&
test->rate == rate &&
test->channels == channels &&
test->period_size == period_size &&
test->buffer_size == buffer_size;
samples = malloc((rate * channels * snd_pcm_format_physical_width(format)) / 8);
if (!samples)
ksft_exit_fail_msg("Out of memory\n");
snd_pcm_format_set_silence(format, samples, rate * channels);
sprintf(name, "hw:%d,%d,%d", data->card, data->device, data->subdevice);
err = snd_pcm_open(&handle, name, data->stream, 0);
if (err < 0) {
snprintf(msg, sizeof(msg), "Failed to get pcm handle: %s", snd_strerror(err));
goto __close;
}
err = snd_pcm_hw_params_any(handle, hw_params);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_any: %s", snd_strerror(err));
goto __close;
}
err = snd_pcm_hw_params_set_rate_resample(handle, hw_params, 0);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_rate_resample: %s", snd_strerror(err));
goto __close;
}
err = snd_pcm_hw_params_set_access(handle, hw_params, access);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_access %s: %s",
snd_pcm_access_name(access), snd_strerror(err));
goto __close;
}
__format:
err = snd_pcm_hw_params_set_format(handle, hw_params, format);
if (err < 0) {
if (automatic && format == SND_PCM_FORMAT_S16_LE) {
format = SND_PCM_FORMAT_S32_LE;
ksft_print_msg("%s.%d.%d.%d.%s.%s format S16_LE -> S32_LE\n",
test->cfg_prefix,
data->card, data->device, data->subdevice,
snd_pcm_stream_name(data->stream),
snd_pcm_access_name(access));
}
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_format %s: %s",
snd_pcm_format_name(format), snd_strerror(err));
goto __close;
}
rchannels = channels;
err = snd_pcm_hw_params_set_channels_near(handle, hw_params, &rchannels);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_channels %ld: %s", channels, snd_strerror(err));
goto __close;
}
if (rchannels != channels) {
snprintf(msg, sizeof(msg), "channels unsupported %ld != %ld", channels, rchannels);
skip = true;
goto __close;
}
rrate = rate;
err = snd_pcm_hw_params_set_rate_near(handle, hw_params, &rrate, 0);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_rate %ld: %s", rate, snd_strerror(err));
goto __close;
}
if (rrate != rate) {
snprintf(msg, sizeof(msg), "rate unsupported %ld != %ld", rate, rrate);
skip = true;
goto __close;
}
rperiod_size = period_size;
err = snd_pcm_hw_params_set_period_size_near(handle, hw_params, &rperiod_size, 0);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_period_size %ld: %s", period_size, snd_strerror(err));
goto __close;
}
rbuffer_size = buffer_size;
err = snd_pcm_hw_params_set_buffer_size_near(handle, hw_params, &rbuffer_size);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params_set_buffer_size %ld: %s", buffer_size, snd_strerror(err));
goto __close;
}
err = snd_pcm_hw_params(handle, hw_params);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_hw_params: %s", snd_strerror(err));
goto __close;
}
err = snd_pcm_sw_params_current(handle, sw_params);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_sw_params_current: %s", snd_strerror(err));
goto __close;
}
if (data->stream == SND_PCM_STREAM_PLAYBACK) {
start_threshold = (rbuffer_size / rperiod_size) * rperiod_size;
} else {
start_threshold = rperiod_size;
}
err = snd_pcm_sw_params_set_start_threshold(handle, sw_params, start_threshold);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_sw_params_set_start_threshold %ld: %s", (long)start_threshold, snd_strerror(err));
goto __close;
}
err = snd_pcm_sw_params_set_avail_min(handle, sw_params, rperiod_size);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_sw_params_set_avail_min %ld: %s", (long)rperiod_size, snd_strerror(err));
goto __close;
}
err = snd_pcm_sw_params(handle, sw_params);
if (err < 0) {
snprintf(msg, sizeof(msg), "snd_pcm_sw_params: %s", snd_strerror(err));
goto __close;
}
ksft_print_msg("%s.%d.%d.%d.%s hw_params.%s.%s.%ld.%ld.%ld.%ld sw_params.%ld\n",
test->cfg_prefix,
data->card, data->device, data->subdevice,
snd_pcm_stream_name(data->stream),
snd_pcm_access_name(access),
snd_pcm_format_name(format),
(long)rate, (long)channels,
(long)rperiod_size, (long)rbuffer_size,
(long)start_threshold);
timestamp_now(&tstamp);
for (i = 0; i < 4; i++) {
if (data->stream == SND_PCM_STREAM_PLAYBACK) {
frames = snd_pcm_writei(handle, samples, rate);
if (frames < 0) {
snprintf(msg, sizeof(msg),
"Write failed: expected %d, wrote %li", rate, frames);
goto __close;
}
if (frames < rate) {
snprintf(msg, sizeof(msg),
"expected %d, wrote %li", rate, frames);
goto __close;
}
} else {
frames = snd_pcm_readi(handle, samples, rate);
if (frames < 0) {
snprintf(msg, sizeof(msg),
"expected %d, wrote %li", rate, frames);
goto __close;
}
if (frames < rate) {
snprintf(msg, sizeof(msg),
"expected %d, wrote %li", rate, frames);
goto __close;
}
}
}
snd_pcm_drain(handle);
ms = timestamp_diff_ms(&tstamp);
if (ms < 3900 || ms > 4100) {
snprintf(msg, sizeof(msg), "time mismatch: expected 4000ms got %lld", ms);
goto __close;
}
msg[0] = '\0';
pass = true;
__close:
if (!skip) {
ksft_test_result(pass, "%s.%d.%d.%d.%s%s%s\n",
test->cfg_prefix,
data->card, data->device, data->subdevice,
snd_pcm_stream_name(data->stream),
msg[0] ? " " : "", msg);
} else {
ksft_test_result_skip("%s.%d.%d.%d.%s%s%s\n",
test->cfg_prefix,
data->card, data->device,
data->subdevice,
snd_pcm_stream_name(data->stream),
msg[0] ? " " : "", msg);
}
free(samples);
if (handle)
snd_pcm_close(handle);
}
static const struct time_test_def time_tests[] = {
/* name format rate chan period buffer */
{ "8k.1.big", "S16_LE", 8000, 2, 8000, 32000 },
{ "8k.2.big", "S16_LE", 8000, 2, 8000, 32000 },
{ "44k1.2.big", "S16_LE", 44100, 2, 22050, 192000 },
{ "48k.2.small", "S16_LE", 48000, 2, 512, 4096 },
{ "48k.2.big", "S16_LE", 48000, 2, 24000, 192000 },
{ "48k.6.big", "S16_LE", 48000, 6, 48000, 576000 },
{ "96k.2.big", "S16_LE", 96000, 2, 48000, 192000 },
};
int main(void)
{
struct pcm_data *pcm;
int i;
ksft_print_header();
conf_load();
find_pcms();
ksft_set_plan(num_missing + num_pcms * ARRAY_SIZE(time_tests));
for (pcm = pcm_missing; pcm != NULL; pcm = pcm->next) {
ksft_test_result(false, "test.missing.%d.%d.%d.%s\n",
pcm->card, pcm->device, pcm->subdevice,
snd_pcm_stream_name(pcm->stream));
}
for (pcm = pcm_list; pcm != NULL; pcm = pcm->next) {
for (i = 0; i < ARRAY_SIZE(time_tests); i++) {
test_pcm_time1(pcm, &time_tests[i]);
}
}
conf_free();
ksft_exit_pass();
return 0;
}