| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * This is the test which covers PCM middle layer data transferring using |
| * the virtual pcm test driver (snd-pcmtest). |
| * |
| * Copyright 2023 Ivan Orlov <ivan.orlov0322@gmail.com> |
| */ |
| #include <string.h> |
| #include <alsa/asoundlib.h> |
| #include "../kselftest_harness.h" |
| |
| #define CH_NUM 4 |
| |
| struct pattern_buf { |
| char buf[1024]; |
| int len; |
| }; |
| |
| struct pattern_buf patterns[CH_NUM]; |
| |
| struct pcmtest_test_params { |
| unsigned long buffer_size; |
| unsigned long period_size; |
| unsigned long channels; |
| unsigned int rate; |
| snd_pcm_access_t access; |
| size_t sec_buf_len; |
| size_t sample_size; |
| int time; |
| snd_pcm_format_t format; |
| }; |
| |
| static int read_patterns(void) |
| { |
| FILE *fp, *fpl; |
| int i; |
| char pf[64]; |
| char plf[64]; |
| |
| for (i = 0; i < CH_NUM; i++) { |
| sprintf(plf, "/sys/kernel/debug/pcmtest/fill_pattern%d_len", i); |
| fpl = fopen(plf, "r"); |
| if (!fpl) |
| return -1; |
| fscanf(fpl, "%u", &patterns[i].len); |
| fclose(fpl); |
| |
| sprintf(pf, "/sys/kernel/debug/pcmtest/fill_pattern%d", i); |
| fp = fopen(pf, "r"); |
| if (!fp) |
| return -1; |
| fread(patterns[i].buf, 1, patterns[i].len, fp); |
| fclose(fp); |
| } |
| |
| return 0; |
| } |
| |
| static int get_test_results(char *debug_name) |
| { |
| int result; |
| FILE *f; |
| char fname[128]; |
| |
| sprintf(fname, "/sys/kernel/debug/pcmtest/%s", debug_name); |
| |
| f = fopen(fname, "r"); |
| if (!f) { |
| printf("Failed to open file\n"); |
| return -1; |
| } |
| fscanf(f, "%d", &result); |
| fclose(f); |
| |
| return result; |
| } |
| |
| static size_t get_sec_buf_len(unsigned int rate, unsigned long channels, snd_pcm_format_t format) |
| { |
| return rate * channels * snd_pcm_format_physical_width(format) / 8; |
| } |
| |
| static int setup_handle(snd_pcm_t **handle, snd_pcm_sw_params_t *swparams, |
| snd_pcm_hw_params_t *hwparams, struct pcmtest_test_params *params, |
| int card, snd_pcm_stream_t stream) |
| { |
| char pcm_name[32]; |
| int err; |
| |
| sprintf(pcm_name, "hw:%d,0,0", card); |
| err = snd_pcm_open(handle, pcm_name, stream, 0); |
| if (err < 0) |
| return err; |
| snd_pcm_hw_params_any(*handle, hwparams); |
| snd_pcm_hw_params_set_rate_resample(*handle, hwparams, 0); |
| snd_pcm_hw_params_set_access(*handle, hwparams, params->access); |
| snd_pcm_hw_params_set_format(*handle, hwparams, params->format); |
| snd_pcm_hw_params_set_channels(*handle, hwparams, params->channels); |
| snd_pcm_hw_params_set_rate_near(*handle, hwparams, ¶ms->rate, 0); |
| snd_pcm_hw_params_set_period_size_near(*handle, hwparams, ¶ms->period_size, 0); |
| snd_pcm_hw_params_set_buffer_size_near(*handle, hwparams, ¶ms->buffer_size); |
| snd_pcm_hw_params(*handle, hwparams); |
| snd_pcm_sw_params_current(*handle, swparams); |
| |
| snd_pcm_hw_params_set_rate_resample(*handle, hwparams, 0); |
| snd_pcm_sw_params_set_avail_min(*handle, swparams, params->period_size); |
| snd_pcm_hw_params_set_buffer_size_near(*handle, hwparams, ¶ms->buffer_size); |
| snd_pcm_hw_params_set_period_size_near(*handle, hwparams, ¶ms->period_size, 0); |
| snd_pcm_sw_params(*handle, swparams); |
| snd_pcm_hw_params(*handle, hwparams); |
| |
| return 0; |
| } |
| |
| FIXTURE(pcmtest) { |
| int card; |
| snd_pcm_sw_params_t *swparams; |
| snd_pcm_hw_params_t *hwparams; |
| struct pcmtest_test_params params; |
| }; |
| |
| FIXTURE_TEARDOWN(pcmtest) { |
| } |
| |
| FIXTURE_SETUP(pcmtest) { |
| char *card_name; |
| int err; |
| |
| if (geteuid()) |
| SKIP(return, "This test needs root to run!"); |
| |
| err = read_patterns(); |
| if (err) |
| SKIP(return, "Can't read patterns. Probably, module isn't loaded"); |
| |
| card_name = malloc(127); |
| ASSERT_NE(card_name, NULL); |
| self->params.buffer_size = 16384; |
| self->params.period_size = 4096; |
| self->params.channels = CH_NUM; |
| self->params.rate = 8000; |
| self->params.access = SND_PCM_ACCESS_RW_INTERLEAVED; |
| self->params.format = SND_PCM_FORMAT_S16_LE; |
| self->card = -1; |
| self->params.sample_size = snd_pcm_format_physical_width(self->params.format) / 8; |
| |
| self->params.sec_buf_len = get_sec_buf_len(self->params.rate, self->params.channels, |
| self->params.format); |
| self->params.time = 4; |
| |
| while (snd_card_next(&self->card) >= 0) { |
| if (self->card == -1) |
| break; |
| snd_card_get_name(self->card, &card_name); |
| if (!strcmp(card_name, "PCM-Test")) |
| break; |
| } |
| free(card_name); |
| ASSERT_NE(self->card, -1); |
| } |
| |
| /* |
| * Here we are trying to send the looped monotonically increasing sequence of bytes to the driver. |
| * If our data isn't corrupted, the driver will set the content of 'pc_test' debugfs file to '1' |
| */ |
| TEST_F(pcmtest, playback) { |
| snd_pcm_t *handle; |
| unsigned char *it; |
| size_t write_res; |
| int test_results; |
| int i, cur_ch, pos_in_ch; |
| void *samples; |
| struct pcmtest_test_params *params = &self->params; |
| |
| samples = calloc(self->params.sec_buf_len * self->params.time, 1); |
| ASSERT_NE(samples, NULL); |
| |
| snd_pcm_sw_params_alloca(&self->swparams); |
| snd_pcm_hw_params_alloca(&self->hwparams); |
| |
| ASSERT_EQ(setup_handle(&handle, self->swparams, self->hwparams, params, |
| self->card, SND_PCM_STREAM_PLAYBACK), 0); |
| snd_pcm_format_set_silence(params->format, samples, |
| params->rate * params->channels * params->time); |
| it = samples; |
| for (i = 0; i < self->params.sec_buf_len * params->time; i++) { |
| cur_ch = (i / params->sample_size) % CH_NUM; |
| pos_in_ch = i / params->sample_size / CH_NUM * params->sample_size |
| + (i % params->sample_size); |
| it[i] = patterns[cur_ch].buf[pos_in_ch % patterns[cur_ch].len]; |
| } |
| write_res = snd_pcm_writei(handle, samples, params->rate * params->time); |
| ASSERT_GE(write_res, 0); |
| |
| snd_pcm_close(handle); |
| free(samples); |
| test_results = get_test_results("pc_test"); |
| ASSERT_EQ(test_results, 1); |
| } |
| |
| /* |
| * Here we test that the virtual alsa driver returns looped and monotonically increasing sequence |
| * of bytes. In the interleaved mode the buffer will contain samples in the following order: |
| * C0, C1, C2, C3, C0, C1, ... |
| */ |
| TEST_F(pcmtest, capture) { |
| snd_pcm_t *handle; |
| unsigned char *it; |
| size_t read_res; |
| int i, cur_ch, pos_in_ch; |
| void *samples; |
| struct pcmtest_test_params *params = &self->params; |
| |
| samples = calloc(self->params.sec_buf_len * self->params.time, 1); |
| ASSERT_NE(samples, NULL); |
| |
| snd_pcm_sw_params_alloca(&self->swparams); |
| snd_pcm_hw_params_alloca(&self->hwparams); |
| |
| ASSERT_EQ(setup_handle(&handle, self->swparams, self->hwparams, |
| params, self->card, SND_PCM_STREAM_CAPTURE), 0); |
| snd_pcm_format_set_silence(params->format, samples, |
| params->rate * params->channels * params->time); |
| read_res = snd_pcm_readi(handle, samples, params->rate * params->time); |
| ASSERT_GE(read_res, 0); |
| snd_pcm_close(handle); |
| it = (unsigned char *)samples; |
| for (i = 0; i < self->params.sec_buf_len * self->params.time; i++) { |
| cur_ch = (i / params->sample_size) % CH_NUM; |
| pos_in_ch = i / params->sample_size / CH_NUM * params->sample_size |
| + (i % params->sample_size); |
| ASSERT_EQ(it[i], patterns[cur_ch].buf[pos_in_ch % patterns[cur_ch].len]); |
| } |
| free(samples); |
| } |
| |
| // Test capture in the non-interleaved access mode. The are buffers for each recorded channel |
| TEST_F(pcmtest, ni_capture) { |
| snd_pcm_t *handle; |
| struct pcmtest_test_params params = self->params; |
| char **chan_samples; |
| size_t i, j, read_res; |
| |
| chan_samples = calloc(CH_NUM, sizeof(*chan_samples)); |
| ASSERT_NE(chan_samples, NULL); |
| |
| snd_pcm_sw_params_alloca(&self->swparams); |
| snd_pcm_hw_params_alloca(&self->hwparams); |
| |
| params.access = SND_PCM_ACCESS_RW_NONINTERLEAVED; |
| |
| ASSERT_EQ(setup_handle(&handle, self->swparams, self->hwparams, |
| ¶ms, self->card, SND_PCM_STREAM_CAPTURE), 0); |
| |
| for (i = 0; i < CH_NUM; i++) |
| chan_samples[i] = calloc(params.sec_buf_len * params.time, 1); |
| |
| for (i = 0; i < 1; i++) { |
| read_res = snd_pcm_readn(handle, (void **)chan_samples, params.rate * params.time); |
| ASSERT_GE(read_res, 0); |
| } |
| snd_pcm_close(handle); |
| |
| for (i = 0; i < CH_NUM; i++) { |
| for (j = 0; j < params.rate * params.time; j++) |
| ASSERT_EQ(chan_samples[i][j], patterns[i].buf[j % patterns[i].len]); |
| free(chan_samples[i]); |
| } |
| free(chan_samples); |
| } |
| |
| TEST_F(pcmtest, ni_playback) { |
| snd_pcm_t *handle; |
| struct pcmtest_test_params params = self->params; |
| char **chan_samples; |
| size_t i, j, read_res; |
| int test_res; |
| |
| chan_samples = calloc(CH_NUM, sizeof(*chan_samples)); |
| ASSERT_NE(chan_samples, NULL); |
| |
| snd_pcm_sw_params_alloca(&self->swparams); |
| snd_pcm_hw_params_alloca(&self->hwparams); |
| |
| params.access = SND_PCM_ACCESS_RW_NONINTERLEAVED; |
| |
| ASSERT_EQ(setup_handle(&handle, self->swparams, self->hwparams, |
| ¶ms, self->card, SND_PCM_STREAM_PLAYBACK), 0); |
| |
| for (i = 0; i < CH_NUM; i++) { |
| chan_samples[i] = calloc(params.sec_buf_len * params.time, 1); |
| for (j = 0; j < params.sec_buf_len * params.time; j++) |
| chan_samples[i][j] = patterns[i].buf[j % patterns[i].len]; |
| } |
| |
| for (i = 0; i < 1; i++) { |
| read_res = snd_pcm_writen(handle, (void **)chan_samples, params.rate * params.time); |
| ASSERT_GE(read_res, 0); |
| } |
| |
| snd_pcm_close(handle); |
| test_res = get_test_results("pc_test"); |
| ASSERT_EQ(test_res, 1); |
| |
| for (i = 0; i < CH_NUM; i++) |
| free(chan_samples[i]); |
| free(chan_samples); |
| } |
| |
| /* |
| * Here we are testing the custom ioctl definition inside the virtual driver. If it triggers |
| * successfully, the driver sets the content of 'ioctl_test' debugfs file to '1'. |
| */ |
| TEST_F(pcmtest, reset_ioctl) { |
| snd_pcm_t *handle; |
| int test_res; |
| struct pcmtest_test_params *params = &self->params; |
| |
| snd_pcm_sw_params_alloca(&self->swparams); |
| snd_pcm_hw_params_alloca(&self->hwparams); |
| |
| ASSERT_EQ(setup_handle(&handle, self->swparams, self->hwparams, params, |
| self->card, SND_PCM_STREAM_CAPTURE), 0); |
| snd_pcm_reset(handle); |
| test_res = get_test_results("ioctl_test"); |
| ASSERT_EQ(test_res, 1); |
| snd_pcm_close(handle); |
| } |
| |
| TEST_HARNESS_MAIN |