| // 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; |
| } |