diff options
author | Linus Torvalds <torvalds@linux-foundation.org> | 2022-12-13 22:27:26 +0300 |
---|---|---|
committer | Linus Torvalds <torvalds@linux-foundation.org> | 2022-12-13 22:27:26 +0300 |
commit | 102f9d3d455870844c47b82322c2dfc0a35eb745 (patch) | |
tree | 31e2c500794f0827d1319fa28c71a0616c598971 /tools | |
parent | 8715c6d3100fc7c6edddf29af4a399a1c12d028c (diff) | |
parent | 8ec2d95f50c06f5cf2a2b94bcdf47f494f91ad55 (diff) | |
download | linux-102f9d3d455870844c47b82322c2dfc0a35eb745.tar.xz |
Merge tag 'sound-6.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound
Pull sound updates from Takashi Iwai:
"This looks like a relatively calm development cycle; there have been
only few changes in ALSA and ASoC core sides while we get lots of
device-specific fixes and updates as usual. Most of commits are about
ASoC, including Intel SOF/AVS and many device tree updates.
Below are some highlights:
Core:
- Improvement in memalloc helper for fallback allocations
- More cleanups of ASoC DAPM code
ASoC:
- Factoring out of mapping hw_params onto SoundWire configuration
- The ever ongoing overhauls of the Intel DSP code continue,
including support for loading libraries and probes with IPC4 on
SOF.
- Support for more sample formats on JZ4740
- Lots of device tree conversions and fixups
- Support for Allwinner D1, a range of AMD and Intel systems,
Mediatek systems with multiple DMICs, Nuvoton NAU8318, NXP
fsl_rpmsg and i.MX93, Qualcomm AudioReach Enable, MFC and SAL,
RealTek RT1318 and Rockchip RK3588
ALSA:
- Addition of PCM kselftest; still minimalistic but can be extended
in future
- Fixes for corner-case XRUNs with USB-audio implicit feedback mode
- Usual device-specific quirk updates for USB- and HD-audio
- FireWire DICE updates
This also contains a few cross-tree updates:
- Some OMAP board file updates for removal of relevant OMAP platforms
- A new I2C API update for I2C probe API adaption
- A DRM update for the further hdmi-codec updates"
* tag 'sound-6.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound: (417 commits)
ALSA: mts64: fix possible null-ptr-defer in snd_mts64_interrupt
ALSA: patch_realtek: Fix Dell Inspiron Plus 16
ALSA: hda/cirrus: Add extra 10 ms delay to allow PLL settle and lock.
ASoC: dt-bindings: Correct Alexandre Belloni email
ASoC: dt-bindings: maxim,max98504: Convert to DT schema
ASoC: dt-bindings: maxim,max98357a: Convert to DT schema
ASoC: dt-bindings: Reference common DAI properties
ASoC: dt-bindings: Extend name-prefix.yaml into common DAI properties
ASoC: rt715: Make read-only arrays capture_reg_H and capture_reg_L static const
ASoC: uniphier: aio-core: Make some read-only arrays static const
ASoC: wcd938x: Make read-only array minCode_param static const
ASoC: qcom: lpass-sc7280: Add maybe_unused tag for system PM ops
ASoC : SOF: amd: Add support for IPC and DSP dumps
ASoC: SOF: amd: Use poll function instead to read ACP_SHA_DSP_FW_QUALIFIER
ALSA: usb-audio: Workaround for XRUN at prepare
ALSA: pcm: Handle XRUN at trigger START
ALSA: pcm: Set missing stop_operating flag at undoing trigger start
drm: tda99x: Don't advertise non-existent capture support
ASoC: hdmi-codec: Allow playback and capture to be disabled
kselftest/alsa: Add more coverage of sample rates and channel counts
...
Diffstat (limited to 'tools')
-rw-r--r-- | tools/testing/selftests/alsa/.gitignore | 1 | ||||
-rw-r--r-- | tools/testing/selftests/alsa/Makefile | 15 | ||||
-rw-r--r-- | tools/testing/selftests/alsa/alsa-local.h | 24 | ||||
-rw-r--r-- | tools/testing/selftests/alsa/conf.c | 448 | ||||
-rw-r--r-- | tools/testing/selftests/alsa/conf.d/Lenovo_ThinkPad_P1_Gen2.conf | 79 | ||||
-rw-r--r-- | tools/testing/selftests/alsa/mixer-test.c | 53 | ||||
-rw-r--r-- | tools/testing/selftests/alsa/pcm-test.c | 489 |
7 files changed, 1057 insertions, 52 deletions
diff --git a/tools/testing/selftests/alsa/.gitignore b/tools/testing/selftests/alsa/.gitignore index 3bb7c41266a8..2b0d11797f25 100644 --- a/tools/testing/selftests/alsa/.gitignore +++ b/tools/testing/selftests/alsa/.gitignore @@ -1 +1,2 @@ mixer-test +pcm-test diff --git a/tools/testing/selftests/alsa/Makefile b/tools/testing/selftests/alsa/Makefile index fd8ddce2b1a6..a8c0383878d3 100644 --- a/tools/testing/selftests/alsa/Makefile +++ b/tools/testing/selftests/alsa/Makefile @@ -6,7 +6,20 @@ LDLIBS += $(shell pkg-config --libs alsa) ifeq ($(LDLIBS),) LDLIBS += -lasound endif +CFLAGS += -L$(OUTPUT) -Wl,-rpath=./ -TEST_GEN_PROGS := mixer-test +OVERRIDE_TARGETS = 1 + +TEST_GEN_PROGS := mixer-test pcm-test + +TEST_GEN_PROGS_EXTENDED := libatest.so + +TEST_FILES := conf.d include ../lib.mk + +$(OUTPUT)/libatest.so: conf.c alsa-local.h + $(CC) $(CFLAGS) -shared -fPIC $< $(LDLIBS) -o $@ + +$(OUTPUT)/%: %.c $(TEST_GEN_PROGS_EXTENDED) alsa-local.h + $(CC) $(CFLAGS) $< $(LDLIBS) -latest -o $@ diff --git a/tools/testing/selftests/alsa/alsa-local.h b/tools/testing/selftests/alsa/alsa-local.h new file mode 100644 index 000000000000..65f197ea9773 --- /dev/null +++ b/tools/testing/selftests/alsa/alsa-local.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// kselftest configuration helpers for the hw specific configuration +// +// Original author: Jaroslav Kysela <perex@perex.cz> +// Copyright (c) 2022 Red Hat Inc. + +#ifndef __ALSA_LOCAL_H +#define __ALSA_LOCAL_H + +#include <alsa/asoundlib.h> + +snd_config_t *get_alsalib_config(void); + +void conf_load(void); +void conf_free(void); +snd_config_t *conf_by_card(int card); +snd_config_t *conf_get_subtree(snd_config_t *root, const char *key1, const char *key2); +int conf_get_count(snd_config_t *root, const char *key1, const char *key2); +const char *conf_get_string(snd_config_t *root, const char *key1, const char *key2, const char *def); +long conf_get_long(snd_config_t *root, const char *key1, const char *key2, long def); +int conf_get_bool(snd_config_t *root, const char *key1, const char *key2, int def); + +#endif /* __ALSA_LOCAL_H */ diff --git a/tools/testing/selftests/alsa/conf.c b/tools/testing/selftests/alsa/conf.c new file mode 100644 index 000000000000..c7ffc8f04195 --- /dev/null +++ b/tools/testing/selftests/alsa/conf.c @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// kselftest configuration helpers for the hw specific configuration +// +// Original author: Jaroslav Kysela <perex@perex.cz> +// Copyright (c) 2022 Red Hat Inc. + +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <errno.h> +#include <assert.h> +#include <dirent.h> +#include <regex.h> +#include <sys/stat.h> + +#include "../kselftest.h" +#include "alsa-local.h" + +#define SYSFS_ROOT "/sys" + +struct card_data { + int card; + snd_config_t *config; + const char *filename; + struct card_data *next; +}; + +static struct card_data *conf_cards; + +static const char *alsa_config = +"ctl.hw {\n" +" @args [ CARD ]\n" +" @args.CARD.type string\n" +" type hw\n" +" card $CARD\n" +"}\n" +"pcm.hw {\n" +" @args [ CARD DEV SUBDEV ]\n" +" @args.CARD.type string\n" +" @args.DEV.type integer\n" +" @args.SUBDEV.type integer\n" +" type hw\n" +" card $CARD\n" +" device $DEV\n" +" subdevice $SUBDEV\n" +"}\n" +; + +#ifdef SND_LIB_VER +#if SND_LIB_VERSION >= SND_LIB_VER(1, 2, 6) +#define LIB_HAS_LOAD_STRING +#endif +#endif + +#ifndef LIB_HAS_LOAD_STRING +static int snd_config_load_string(snd_config_t **config, const char *s, + size_t size) +{ + snd_input_t *input; + snd_config_t *dst; + int err; + + assert(config && s); + if (size == 0) + size = strlen(s); + err = snd_input_buffer_open(&input, s, size); + if (err < 0) + return err; + err = snd_config_top(&dst); + if (err < 0) { + snd_input_close(input); + return err; + } + err = snd_config_load(dst, input); + snd_input_close(input); + if (err < 0) { + snd_config_delete(dst); + return err; + } + *config = dst; + return 0; +} +#endif + +snd_config_t *get_alsalib_config(void) +{ + snd_config_t *config; + int err; + + err = snd_config_load_string(&config, alsa_config, strlen(alsa_config)); + if (err < 0) { + ksft_print_msg("Unable to parse custom alsa-lib configuration: %s\n", + snd_strerror(err)); + ksft_exit_fail(); + } + return config; +} + +static struct card_data *conf_data_by_card(int card, bool msg) +{ + struct card_data *conf; + + for (conf = conf_cards; conf; conf = conf->next) { + if (conf->card == card) { + if (msg) + ksft_print_msg("using hw card config %s for card %d\n", + conf->filename, card); + return conf; + } + } + return NULL; +} + +static int dump_config_tree(snd_config_t *top) +{ + snd_output_t *out; + int err; + + err = snd_output_stdio_attach(&out, stdout, 0); + if (err < 0) + ksft_exit_fail_msg("stdout attach\n"); + if (snd_config_save(top, out)) + ksft_exit_fail_msg("config save\n"); + snd_output_close(out); +} + +static snd_config_t *load(const char *filename) +{ + snd_config_t *dst; + snd_input_t *input; + int err; + + err = snd_input_stdio_open(&input, filename, "r"); + if (err < 0) + ksft_exit_fail_msg("Unable to parse filename %s\n", filename); + err = snd_config_top(&dst); + if (err < 0) + ksft_exit_fail_msg("Out of memory\n"); + err = snd_config_load(dst, input); + snd_input_close(input); + if (err < 0) + ksft_exit_fail_msg("Unable to parse filename %s\n", filename); + return dst; +} + +static char *sysfs_get(const char *sysfs_root, const char *id) +{ + char path[PATH_MAX], link[PATH_MAX + 1]; + struct stat sb; + ssize_t len; + char *e; + int fd; + + if (id[0] == '/') + id++; + snprintf(path, sizeof(path), "%s/%s", sysfs_root, id); + if (lstat(path, &sb) != 0) + return NULL; + if (S_ISLNK(sb.st_mode)) { + len = readlink(path, link, sizeof(link) - 1); + if (len <= 0) { + ksft_exit_fail_msg("sysfs: cannot read link '%s': %s\n", + path, strerror(errno)); + return NULL; + } + link[len] = '\0'; + e = strrchr(link, '/'); + if (e) + return strdup(e + 1); + return NULL; + } + if (S_ISDIR(sb.st_mode)) + return NULL; + if ((sb.st_mode & S_IRUSR) == 0) + return NULL; + + fd = open(path, O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) + return NULL; + ksft_exit_fail_msg("sysfs: open failed for '%s': %s\n", + path, strerror(errno)); + } + len = read(fd, path, sizeof(path)-1); + close(fd); + if (len < 0) + ksft_exit_fail_msg("sysfs: unable to read value '%s': %s\n", + path, errno); + while (len > 0 && path[len-1] == '\n') + len--; + path[len] = '\0'; + e = strdup(path); + if (e == NULL) + ksft_exit_fail_msg("Out of memory\n"); + return e; +} + +static bool sysfs_match(const char *sysfs_root, snd_config_t *config) +{ + snd_config_t *node, *path_config, *regex_config; + snd_config_iterator_t i, next; + const char *path_string, *regex_string, *v; + regex_t re; + regmatch_t match[1]; + int iter = 0, ret; + + snd_config_for_each(i, next, config) { + node = snd_config_iterator_entry(i); + if (snd_config_search(node, "path", &path_config)) + ksft_exit_fail_msg("Missing path field in the sysfs block\n"); + if (snd_config_search(node, "regex", ®ex_config)) + ksft_exit_fail_msg("Missing regex field in the sysfs block\n"); + if (snd_config_get_string(path_config, &path_string)) + ksft_exit_fail_msg("Path field in the sysfs block is not a string\n"); + if (snd_config_get_string(regex_config, ®ex_string)) + ksft_exit_fail_msg("Regex field in the sysfs block is not a string\n"); + iter++; + v = sysfs_get(sysfs_root, path_string); + if (!v) + return false; + if (regcomp(&re, regex_string, REG_EXTENDED)) + ksft_exit_fail_msg("Wrong regex '%s'\n", regex_string); + ret = regexec(&re, v, 1, match, 0); + regfree(&re); + if (ret) + return false; + } + return iter > 0; +} + +static bool test_filename1(int card, const char *filename, const char *sysfs_card_root) +{ + struct card_data *data, *data2; + snd_config_t *config, *sysfs_config, *card_config, *sysfs_card_config, *node; + snd_config_iterator_t i, next; + + config = load(filename); + if (snd_config_search(config, "sysfs", &sysfs_config) || + snd_config_get_type(sysfs_config) != SND_CONFIG_TYPE_COMPOUND) + ksft_exit_fail_msg("Missing global sysfs block in filename %s\n", filename); + if (snd_config_search(config, "card", &card_config) || + snd_config_get_type(card_config) != SND_CONFIG_TYPE_COMPOUND) + ksft_exit_fail_msg("Missing global card block in filename %s\n", filename); + if (!sysfs_match(SYSFS_ROOT, sysfs_config)) + return false; + snd_config_for_each(i, next, card_config) { + node = snd_config_iterator_entry(i); + if (snd_config_search(node, "sysfs", &sysfs_card_config) || + snd_config_get_type(sysfs_card_config) != SND_CONFIG_TYPE_COMPOUND) + ksft_exit_fail_msg("Missing card sysfs block in filename %s\n", filename); + if (!sysfs_match(sysfs_card_root, sysfs_card_config)) + continue; + data = malloc(sizeof(*data)); + if (!data) + ksft_exit_fail_msg("Out of memory\n"); + data2 = conf_data_by_card(card, false); + if (data2) + ksft_exit_fail_msg("Duplicate card '%s' <-> '%s'\n", filename, data2->filename); + data->card = card; + data->filename = filename; + data->config = node; + data->next = conf_cards; + conf_cards = data; + return true; + } + return false; +} + +static bool test_filename(const char *filename) +{ + char fn[128]; + int card; + + for (card = 0; card < 32; card++) { + snprintf(fn, sizeof(fn), "%s/class/sound/card%d", SYSFS_ROOT, card); + if (access(fn, R_OK) == 0 && test_filename1(card, filename, fn)) + return true; + } + return false; +} + +static int filename_filter(const struct dirent *dirent) +{ + size_t flen; + + if (dirent == NULL) + return 0; + if (dirent->d_type == DT_DIR) + return 0; + flen = strlen(dirent->d_name); + if (flen <= 5) + return 0; + if (strncmp(&dirent->d_name[flen-5], ".conf", 5) == 0) + return 1; + return 0; +} + +void conf_load(void) +{ + const char *fn = "conf.d"; + struct dirent **namelist; + int n, j; + + n = scandir(fn, &namelist, filename_filter, alphasort); + if (n < 0) + ksft_exit_fail_msg("scandir: %s\n", strerror(errno)); + for (j = 0; j < n; j++) { + size_t sl = strlen(fn) + strlen(namelist[j]->d_name) + 2; + char *filename = malloc(sl); + if (filename == NULL) + ksft_exit_fail_msg("Out of memory\n"); + sprintf(filename, "%s/%s", fn, namelist[j]->d_name); + if (test_filename(filename)) + filename = NULL; + free(filename); + free(namelist[j]); + } + free(namelist); +} + +void conf_free(void) +{ + struct card_data *conf; + + while (conf_cards) { + conf = conf_cards; + conf_cards = conf->next; + snd_config_delete(conf->config); + } +} + +snd_config_t *conf_by_card(int card) +{ + struct card_data *conf; + + conf = conf_data_by_card(card, true); + if (conf) + return conf->config; + return NULL; +} + +static int conf_get_by_keys(snd_config_t *root, const char *key1, + const char *key2, snd_config_t **result) +{ + int ret; + + if (key1) { + ret = snd_config_search(root, key1, &root); + if (ret != -ENOENT && ret < 0) + return ret; + } + if (key2) + ret = snd_config_search(root, key2, &root); + if (ret >= 0) + *result = root; + return ret; +} + +snd_config_t *conf_get_subtree(snd_config_t *root, const char *key1, const char *key2) +{ + int ret; + + if (!root) + return NULL; + ret = conf_get_by_keys(root, key1, key2, &root); + if (ret == -ENOENT) + return NULL; + if (ret < 0) + ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret)); + return root; +} + +int conf_get_count(snd_config_t *root, const char *key1, const char *key2) +{ + snd_config_t *cfg; + snd_config_iterator_t i, next; + int count, ret; + + if (!root) + return -1; + ret = conf_get_by_keys(root, key1, key2, &cfg); + if (ret == -ENOENT) + return -1; + if (ret < 0) + ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret)); + if (snd_config_get_type(cfg) != SND_CONFIG_TYPE_COMPOUND) + ksft_exit_fail_msg("key '%s'.'%s' is not a compound\n", key1, key2); + count = 0; + snd_config_for_each(i, next, cfg) + count++; + return count; +} + +const char *conf_get_string(snd_config_t *root, const char *key1, const char *key2, const char *def) +{ + snd_config_t *cfg; + const char *s; + int ret; + + if (!root) + return def; + ret = conf_get_by_keys(root, key1, key2, &cfg); + if (ret == -ENOENT) + return def; + if (ret < 0) + ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret)); + if (snd_config_get_string(cfg, &s)) + ksft_exit_fail_msg("key '%s'.'%s' is not a string\n", key1, key2); + return s; +} + +long conf_get_long(snd_config_t *root, const char *key1, const char *key2, long def) +{ + snd_config_t *cfg; + long l; + int ret; + + if (!root) + return def; + ret = conf_get_by_keys(root, key1, key2, &cfg); + if (ret == -ENOENT) + return def; + if (ret < 0) + ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret)); + if (snd_config_get_integer(cfg, &l)) + ksft_exit_fail_msg("key '%s'.'%s' is not an integer\n", key1, key2); + return l; +} + +int conf_get_bool(snd_config_t *root, const char *key1, const char *key2, int def) +{ + snd_config_t *cfg; + long l; + int ret; + + if (!root) + return def; + ret = conf_get_by_keys(root, key1, key2, &cfg); + if (ret == -ENOENT) + return def; + if (ret < 0) + ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret)); + ret = snd_config_get_bool(cfg); + if (ret < 0) + ksft_exit_fail_msg("key '%s'.'%s' is not an bool\n", key1, key2); + return !!ret; +} diff --git a/tools/testing/selftests/alsa/conf.d/Lenovo_ThinkPad_P1_Gen2.conf b/tools/testing/selftests/alsa/conf.d/Lenovo_ThinkPad_P1_Gen2.conf new file mode 100644 index 000000000000..9eca985e0c08 --- /dev/null +++ b/tools/testing/selftests/alsa/conf.d/Lenovo_ThinkPad_P1_Gen2.conf @@ -0,0 +1,79 @@ +# +# Example configuration for Lenovo ThinkPad P1 Gen2 +# + +# +# Use regex match for the string read from the given sysfs path +# +# The sysfs root directory (/sys) is hardwired in the test code +# (may be changed on demand). +# +# All strings must match. +# +sysfs [ + { + path "class/dmi/id/product_sku" + regex "LENOVO_MT_20QU_BU_Think_FM_ThinkPad P1 Gen 2" + } +] + +card.hda { + # + # Use regex match for the /sys/class/sound/card*/ tree (relative) + # + sysfs [ + { + path "device/subsystem_device" + regex "0x229e" + } + { + path "device/subsystem_vendor" + regex "0x17aa" + } + ] + + # + # PCM configuration + # + # pcm.0.0 - device 0 subdevice 0 + # + pcm.0.0 { + PLAYBACK { + # + # Uncomment to override values for specific tests + # + #test_name1 { + # access RW_INTERLEAVED + # format S16_LE + # rate 48000 + # channels 2 + # period_size 512 + # buffer_size 4096 + #} + #test_name2 { + # access RW_INTERLEAVED + # format S16_LE + # rate 48000 + # channels 2 + # period_size 24000 + # buffer_size 192000 + #} + } + CAPTURE { + # use default tests, check for the presence + } + } + # + # uncomment to force the missing device checks + # + #pcm.0.2 { + # PLAYBACK { + # # check for the presence + # } + #} + #pcm.0.3 { + # CAPTURE { + # # check for the presence + # } + #} +} diff --git a/tools/testing/selftests/alsa/mixer-test.c b/tools/testing/selftests/alsa/mixer-test.c index a38b89c28030..05f1749ae19d 100644 --- a/tools/testing/selftests/alsa/mixer-test.c +++ b/tools/testing/selftests/alsa/mixer-test.c @@ -26,6 +26,7 @@ #include <stdint.h> #include "../kselftest.h" +#include "alsa-local.h" #define TESTS_PER_CONTROL 7 @@ -50,56 +51,11 @@ struct ctl_data { struct ctl_data *next; }; -static const char *alsa_config = -"ctl.hw {\n" -" @args [ CARD ]\n" -" @args.CARD.type string\n" -" type hw\n" -" card $CARD\n" -"}\n" -; - int num_cards = 0; int num_controls = 0; struct card_data *card_list = NULL; struct ctl_data *ctl_list = NULL; -#ifdef SND_LIB_VER -#if SND_LIB_VERSION >= SND_LIB_VER(1, 2, 6) -#define LIB_HAS_LOAD_STRING -#endif -#endif - -#ifndef LIB_HAS_LOAD_STRING -static int snd_config_load_string(snd_config_t **config, const char *s, - size_t size) -{ - snd_input_t *input; - snd_config_t *dst; - int err; - - assert(config && s); - if (size == 0) - size = strlen(s); - err = snd_input_buffer_open(&input, s, size); - if (err < 0) - return err; - err = snd_config_top(&dst); - if (err < 0) { - snd_input_close(input); - return err; - } - err = snd_config_load(dst, input); - snd_input_close(input); - if (err < 0) { - snd_config_delete(dst); - return err; - } - *config = dst; - return 0; -} -#endif - static void find_controls(void) { char name[32]; @@ -112,12 +68,7 @@ static void find_controls(void) if (snd_card_next(&card) < 0 || card < 0) return; - err = snd_config_load_string(&config, alsa_config, strlen(alsa_config)); - if (err < 0) { - ksft_print_msg("Unable to parse custom alsa-lib configuration: %s\n", - snd_strerror(err)); - ksft_exit_fail(); - } + config = get_alsalib_config(); while (card >= 0) { sprintf(name, "hw:%d", card); diff --git a/tools/testing/selftests/alsa/pcm-test.c b/tools/testing/selftests/alsa/pcm-test.c new file mode 100644 index 000000000000..f293c7d81009 --- /dev/null +++ b/tools/testing/selftests/alsa/pcm-test.c @@ -0,0 +1,489 @@ +// 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; +} |