diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/igt.h | 1 | ||||
-rw-r--r-- | lib/igt_alsa.c | 42 | ||||
-rw-r--r-- | lib/igt_alsa.h | 1 | ||||
-rw-r--r-- | lib/igt_audio.c | 313 | ||||
-rw-r--r-- | lib/igt_audio.h | 12 | ||||
-rw-r--r-- | lib/igt_aux.c | 31 | ||||
-rw-r--r-- | lib/igt_aux.h | 1 | ||||
-rw-r--r-- | lib/igt_chamelium.c | 101 | ||||
-rw-r--r-- | lib/igt_chamelium.h | 11 | ||||
-rw-r--r-- | lib/igt_chamelium_stream.c | 589 | ||||
-rw-r--r-- | lib/igt_chamelium_stream.h | 52 | ||||
-rw-r--r-- | lib/meson.build | 5 |
12 files changed, 1064 insertions, 95 deletions
@@ -43,6 +43,7 @@ #include "igt_stats.h" #ifdef HAVE_CHAMELIUM #include "igt_chamelium.h" +#include "igt_chamelium_stream.h" #endif #include "instdone.h" #include "intel_batchbuffer.h" diff --git a/lib/igt_alsa.c b/lib/igt_alsa.c index bb6682cc..22089881 100644 --- a/lib/igt_alsa.c +++ b/lib/igt_alsa.c @@ -26,9 +26,11 @@ #include "config.h" +#include <limits.h> #include <alsa/asoundlib.h> #include "igt_alsa.h" +#include "igt_aux.h" #include "igt_core.h" #define HANDLES_MAX 8 @@ -61,6 +63,26 @@ struct alsa { int input_samples_trigger; }; +/** + * alsa_has_exclusive_access: + * Check whether ALSA has exclusive access to audio devices. Fails if + * PulseAudio is running. + */ +bool alsa_has_exclusive_access(void) +{ + if (igt_is_process_running("pulseaudio")) { + igt_warn("alsa doesn't have exclusive access to audio devices\n"); + igt_warn("It seems that PulseAudio is running. Audio tests " + "need direct access to audio devices, so PulseAudio " + "needs to be stopped. You can do so by running " + "`pulseaudio --kill`. Also make sure to add " + "autospawn=no to /etc/pulse/client.conf\n"); + return false; + } + + return true; +} + static void alsa_error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...) { @@ -78,6 +100,10 @@ struct alsa *alsa_init(void) { struct alsa *alsa; + if (!alsa_has_exclusive_access()) { + return NULL; + } + alsa = malloc(sizeof(struct alsa)); memset(alsa, 0, sizeof(struct alsa)); @@ -553,16 +579,20 @@ int alsa_run(struct alsa *alsa, int duration_ms) if (ret < 0) { ret = snd_pcm_recover(handle, ret, 0); - if (ret < 0) + if (ret < 0) { + igt_debug("snd_pcm_recover after snd_pcm_writei failed"); goto complete; + } } output_counts[i] += ret; } else if (output_counts[i] < output_trigger && ret < 0) { ret = snd_pcm_recover(handle, ret, 0); - if (ret < 0) + if (ret < 0) { + igt_debug("snd_pcm_recover failed"); goto complete; + } } } @@ -609,16 +639,20 @@ int alsa_run(struct alsa *alsa, int duration_ms) ret = 0; } else if (ret < 0) { ret = snd_pcm_recover(handle, ret, 0); - if (ret < 0) + if (ret < 0) { + igt_debug("snd_pcm_recover after snd_pcm_readi failed"); goto complete; + } } input_count += ret; input_total += ret; } else if (input_count < input_trigger && ret < 0) { ret = snd_pcm_recover(handle, ret, 0); - if (ret < 0) + if (ret < 0) { + igt_debug("snd_pcm_recover failed"); goto complete; + } } } } while (!reached); diff --git a/lib/igt_alsa.h b/lib/igt_alsa.h index 50795130..5c804b46 100644 --- a/lib/igt_alsa.h +++ b/lib/igt_alsa.h @@ -33,6 +33,7 @@ struct alsa; +bool alsa_has_exclusive_access(void); struct alsa *alsa_init(void); int alsa_open_output(struct alsa *alsa, const char *device_name); int alsa_open_input(struct alsa *alsa, const char *device_name); diff --git a/lib/igt_audio.c b/lib/igt_audio.c index a0592d53..7624f565 100644 --- a/lib/igt_audio.c +++ b/lib/igt_audio.c @@ -26,8 +26,11 @@ #include "config.h" -#include <math.h> +#include <errno.h> +#include <fcntl.h> #include <gsl/gsl_fft_real.h> +#include <math.h> +#include <unistd.h> #include "igt_audio.h" #include "igt_core.h" @@ -128,7 +131,7 @@ int audio_signal_add_frequency(struct audio_signal *signal, int frequency) */ void audio_signal_synthesize(struct audio_signal *signal) { - short *period; + int16_t *period; double value; int frames; int freq; @@ -145,9 +148,9 @@ void audio_signal_synthesize(struct audio_signal *signal) for (j = 0; j < frames; j++) { value = 2.0 * M_PI * freq / signal->sampling_rate * j; - value = sin(value) * SHRT_MAX / signal->freqs_count; + value = sin(value) * INT16_MAX / signal->freqs_count; - period[j] = (short) value; + period[j] = (int16_t) value; } signal->freqs[i].period = period; @@ -186,17 +189,16 @@ void audio_signal_clean(struct audio_signal *signal) * signal data (in interleaved S16_LE format), at the requested sampling rate * and number of channels. */ -void audio_signal_fill(struct audio_signal *signal, short *buffer, int frames) +void audio_signal_fill(struct audio_signal *signal, int16_t *buffer, int frames) { - short *destination; - short *source; + int16_t *destination, *source; int total; int freq_frames; int freq_offset; int count; int i, j, k; - memset(buffer, 0, sizeof(short) * signal->channels * frames); + memset(buffer, 0, sizeof(int16_t) * signal->channels * frames); for (i = 0; i < signal->freqs_count; i++) { total = 0; @@ -229,97 +231,236 @@ void audio_signal_fill(struct audio_signal *signal, short *buffer, int frames) } /** - * audio_signal_detect: - * @signal: The target signal structure - * @channels: The input data's number of channels - * @sampling_rate: The input data's sampling rate - * @buffer: The input data's buffer - * @frames: The input data's number of frames - * - * Detect that the frequencies specified in @signal, and only those, are - * present in the input data. The input data's format is required to be S16_LE. + * Checks that frequencies specified in signal, and only those, are included + * in the input data. * - * Returns: A boolean indicating whether the detection was successful + * sampling_rate is given in Hz. data_len is the number of elements in data. */ -bool audio_signal_detect(struct audio_signal *signal, int channels, - int sampling_rate, short *buffer, int frames) +bool audio_signal_detect(struct audio_signal *signal, int sampling_rate, + double *data, size_t data_len) { - double data[frames]; - int amplitude[frames / 2]; + size_t bin_power_len = data_len / 2 + 1; + double bin_power[bin_power_len]; bool detected[signal->freqs_count]; - int threshold; - bool above; - int error; - int freq = 0; - int max; - int c, i, j; + int ret, freq_accuracy, freq, local_max_freq; + double max, local_max, threshold; + size_t i, j; + bool above, success; + + /* Allowed error in Hz due to FFT step */ + freq_accuracy = sampling_rate / data_len; + igt_debug("Allowed freq. error: %d Hz\n", freq_accuracy); + + ret = gsl_fft_real_radix2_transform(data, 1, data_len); + igt_assert(ret == 0); + + /* Compute the power received by every bin of the FFT, and record the + * maximum power received as a way to normalize all the others. + * + * For i < data_len / 2, the real part of the i-th term is stored at + * data[i] and its imaginary part is stored at data[data_len - i]. + * i = 0 and i = data_len / 2 are special cases, they are purely real + * so their imaginary part isn't stored. + * + * The power is encoded as the magnitude of the complex number and the + * phase is encoded as its angle. + */ + max = 0; + bin_power[0] = data[0]; + for (i = 1; i < bin_power_len - 1; i++) { + bin_power[i] = hypot(data[i], data[data_len - i]); + if (bin_power[i] > max) + max = bin_power[i]; + } + bin_power[bin_power_len - 1] = data[data_len / 2]; + + for (i = 0; i < signal->freqs_count; i++) + detected[i] = false; + + /* Do a linear search through the FFT bins' power to find the the local + * maximums that exceed half of the absolute maximum that we previously + * calculated. + * + * Since the frequencies might not be perfectly aligned with the bins of + * the FFT, we need to find the local maximum across some consecutive + * bins. Once the power returns under the power threshold, we compare + * the frequency of the bin that received the maximum power to the + * expected frequencies. If found, we mark this frequency as such, + * otherwise we warn that an unexpected frequency was found. + */ + threshold = max / 2; + success = true; + above = false; + local_max = 0; + local_max_freq = -1; + for (i = 0; i < bin_power_len; i++) { + freq = sampling_rate * i / data_len; + + if (bin_power[i] > threshold) + above = true; + + if (!above) { + continue; + } - /* Allowed error in Hz due to FFT step. */ - error = sampling_rate / frames; + /* If we were above the threshold and we're not anymore, it's + * time to decide whether the peak frequency is correct or + * invalid. */ + if (bin_power[i] < threshold) { + for (j = 0; j < signal->freqs_count; j++) { + if (signal->freqs[j].freq > + local_max_freq - freq_accuracy && + signal->freqs[j].freq < + local_max_freq + freq_accuracy) { + detected[j] = true; + igt_debug("Frequency %d detected\n", + local_max_freq); + break; + } + } - for (c = 0; c < channels; c++) { - for (i = 0; i < frames; i++) - data[i] = (double) buffer[i * channels + c]; + /* We haven't generated this frequency, but we detected + * it. */ + if (j == signal->freqs_count) { + igt_debug("Detected additional frequency: %d\n", + local_max_freq); + success = false; + } - gsl_fft_real_radix2_transform(data, 1, frames); + above = false; + local_max = 0; + local_max_freq = -1; + } - max = 0; + if (bin_power[i] > local_max) { + local_max = bin_power[i]; + local_max_freq = freq; + } + } - for (i = 0; i < frames / 2; i++) { - amplitude[i] = hypot(data[i], data[frames - i]); - if (amplitude[i] > max) - max = amplitude[i]; + /* Check that all frequencies we generated have been detected. */ + for (i = 0; i < signal->freqs_count; i++) { + if (!detected[i]) { + igt_debug("Missing frequency: %d\n", + signal->freqs[i].freq); + success = false; } + } - for (i = 0; i < signal->freqs_count; i++) - detected[i] = false; - - threshold = max / 2; - above = false; - max = 0; - - for (i = 0; i < frames / 2; i++) { - if (amplitude[i] > threshold) - above = true; - - if (above) { - if (amplitude[i] < threshold) { - above = false; - max = 0; - - for (j = 0; j < signal->freqs_count; j++) { - if (signal->freqs[j].freq > - freq - error && - signal->freqs[j].freq < - freq + error) { - detected[j] = true; - break; - } - } - - /* Detected frequency was not generated. */ - if (j == signal->freqs_count) { - igt_debug("Detected additional frequency: %d\n", - freq); - return false; - } - } + return success; +} - if (amplitude[i] > max) { - max = amplitude[i]; - freq = sampling_rate * i / frames; - } - } - } +/** + * Extracts a single channel from a multi-channel S32_LE input buffer. + */ +size_t audio_extract_channel_s32_le(double *dst, size_t dst_cap, + int32_t *src, size_t src_len, + int n_channels, int channel) +{ + size_t dst_len, i; - for (i = 0; i < signal->freqs_count; i++) { - if (!detected[i]) { - igt_debug("Missing frequency: %d\n", - signal->freqs[i].freq); - return false; - } - } + igt_assert(channel < n_channels); + igt_assert(src_len % n_channels == 0); + dst_len = src_len / n_channels; + igt_assert(dst_len <= dst_cap); + for (i = 0; i < dst_len; i++) + dst[i] = (double) src[i * n_channels + channel]; + + return dst_len; +} + +#define RIFF_TAG "RIFF" +#define WAVE_TAG "WAVE" +#define FMT_TAG "fmt " +#define DATA_TAG "data" + +static void +append_to_buffer(char *dst, size_t *i, const void *src, size_t src_size) +{ + memcpy(&dst[*i], src, src_size); + *i += src_size; +} + +/** + * audio_create_wav_file_s32_le: + * @qualifier: the basename of the file (the test name will be prepended, and + * the file extension will be appended) + * @sample_rate: the sample rate in Hz + * @channels: the number of channels + * @path: if non-NULL, will be set to a pointer to the new file path (the + * caller is responsible for free-ing it) + * + * Creates a new WAV file. + * + * After calling this function, the caller is expected to write S32_LE PCM data + * to the returned file descriptor. + * + * See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html for + * a WAV file format specification. + * + * Returns: a file descriptor to the newly created file, or -1 on error. + */ +int audio_create_wav_file_s32_le(const char *qualifier, uint32_t sample_rate, + uint16_t channels, char **path) +{ + char _path[PATH_MAX]; + const char *test_name, *subtest_name; + int fd; + char header[44]; + size_t i = 0; + uint32_t file_size, chunk_size, byte_rate; + uint16_t format, block_align, bits_per_sample; + + test_name = igt_test_name(); + subtest_name = igt_subtest_name(); + + igt_assert(igt_frame_dump_path); + snprintf(_path, sizeof(_path), "%s/audio-%s-%s-%s.wav", + igt_frame_dump_path, test_name, subtest_name, qualifier); + + if (path) + *path = strdup(_path); + + igt_debug("Dumping %s audio to %s\n", qualifier, _path); + fd = open(_path, O_WRONLY | O_CREAT | O_TRUNC); + if (fd < 0) { + igt_warn("open failed: %s\n", strerror(errno)); + return -1; + } + + /* File header */ + file_size = UINT32_MAX; /* unknown file size */ + append_to_buffer(header, &i, RIFF_TAG, strlen(RIFF_TAG)); + append_to_buffer(header, &i, &file_size, sizeof(file_size)); + append_to_buffer(header, &i, WAVE_TAG, strlen(WAVE_TAG)); + + /* Format chunk */ + chunk_size = 16; + format = 1; /* PCM */ + bits_per_sample = 32; /* S32_LE */ + byte_rate = sample_rate * channels * bits_per_sample / 8; + block_align = channels * bits_per_sample / 8; + append_to_buffer(header, &i, FMT_TAG, strlen(FMT_TAG)); + append_to_buffer(header, &i, &chunk_size, sizeof(chunk_size)); + append_to_buffer(header, &i, &format, sizeof(format)); + append_to_buffer(header, &i, &channels, sizeof(channels)); + append_to_buffer(header, &i, &sample_rate, sizeof(sample_rate)); + append_to_buffer(header, &i, &byte_rate, sizeof(byte_rate)); + append_to_buffer(header, &i, &block_align, sizeof(block_align)); + append_to_buffer(header, &i, &bits_per_sample, sizeof(bits_per_sample)); + + /* Data chunk */ + chunk_size = UINT32_MAX; /* unknown chunk size */ + append_to_buffer(header, &i, DATA_TAG, strlen(DATA_TAG)); + append_to_buffer(header, &i, &chunk_size, sizeof(chunk_size)); + + igt_assert(i == sizeof(header)); + + if (write(fd, header, sizeof(header)) != sizeof(header)) { + igt_warn("write failed: %s'n", strerror(errno)); + close(fd); + return -1; } - return true; + return fd; } diff --git a/lib/igt_audio.h b/lib/igt_audio.h index b3b658a4..4aa43e69 100644 --- a/lib/igt_audio.h +++ b/lib/igt_audio.h @@ -30,6 +30,7 @@ #include "config.h" #include <stdbool.h> +#include <stdint.h> struct audio_signal; @@ -37,8 +38,13 @@ struct audio_signal *audio_signal_init(int channels, int sampling_rate); int audio_signal_add_frequency(struct audio_signal *signal, int frequency); void audio_signal_synthesize(struct audio_signal *signal); void audio_signal_clean(struct audio_signal *signal); -void audio_signal_fill(struct audio_signal *signal, short *buffer, int frames); -bool audio_signal_detect(struct audio_signal *signal, int channels, - int sampling_rate, short *buffer, int frames); +void audio_signal_fill(struct audio_signal *signal, int16_t *buffer, int frames); +bool audio_signal_detect(struct audio_signal *signal, int sampling_rate, + double *data, size_t data_len); +size_t audio_extract_channel_s32_le(double *dst, size_t dst_cap, + int32_t *src, size_t src_len, + int n_channels, int channel); +int audio_create_wav_file_s32_le(const char *qualifier, uint32_t sample_rate, + uint16_t channels, char **path); #endif diff --git a/lib/igt_aux.c b/lib/igt_aux.c index 05528352..3dd68d95 100644 --- a/lib/igt_aux.c +++ b/lib/igt_aux.c @@ -1260,6 +1260,37 @@ void igt_set_module_param_int(const char *name, int val) } /** + * igt_is_process_running: + * @comm: Name of process in the form found in /proc/pid/comm (limited to 15 + * chars) + * + * Returns: true in case the process has been found, false otherwise. + * + * This function checks in the process table for an entry with the name @comm. + */ +int igt_is_process_running(const char *comm) +{ + PROCTAB *proc; + proc_t *proc_info; + bool found = false; + + proc = openproc(PROC_FILLCOM | PROC_FILLSTAT); + igt_assert(proc != NULL); + + while ((proc_info = readproc(proc, NULL))) { + if (!strncasecmp(proc_info->cmd, comm, sizeof(proc_info->cmd))) { + freeproc(proc_info); + found = true; + break; + } + freeproc(proc_info); + } + + closeproc(proc); + return found; +} + +/** * igt_terminate_process: * @sig: Signal to send * @comm: Name of process in the form found in /proc/pid/comm (limited to 15 diff --git a/lib/igt_aux.h b/lib/igt_aux.h index 55392790..dbd88b67 100644 --- a/lib/igt_aux.h +++ b/lib/igt_aux.h @@ -279,6 +279,7 @@ bool igt_allow_unlimited_files(void); void igt_set_module_param(const char *name, const char *val); void igt_set_module_param_int(const char *name, int val); +int igt_is_process_running(const char *comm); int igt_terminate_process(int sig, const char *comm); void igt_lsof(const char *dpath); diff --git a/lib/igt_chamelium.c b/lib/igt_chamelium.c index 02cc9b2c..7c9030d1 100644 --- a/lib/igt_chamelium.c +++ b/lib/igt_chamelium.c @@ -218,6 +218,12 @@ void chamelium_destroy_frame_dump(struct chamelium_frame_dump *dump) free(dump); } +void chamelium_destroy_audio_file(struct chamelium_audio_file *audio_file) +{ + free(audio_file->path); + free(audio_file); +} + struct fsm_monitor_args { struct chamelium *chamelium; struct chamelium_port *port; @@ -924,6 +930,101 @@ int chamelium_get_captured_frame_count(struct chamelium *chamelium) return ret; } +/** + * chamelium_start_capturing_audio: + * @chamelium: the Chamelium instance + * @port: the port to capture audio from (it must support audio) + * @save_to_file: whether the captured audio data should be saved to a file on + * the Chamelium device + * + * Starts capturing audio from a Chamelium port. To stop the capture, use + * #chamelium_stop_capturing_audio. To retrieve the audio data, either use the + * stream server or enable @save_to_file (the latter is mainly useful for + * debugging purposes). + * + * It isn't possible to capture audio from multiple ports at the same time. + */ +void chamelium_start_capturing_audio(struct chamelium *chamelium, + struct chamelium_port *port, + bool save_to_file) +{ + xmlrpc_value *res; + + res = chamelium_rpc(chamelium, port, "StartCapturingAudio", "(ib)", + port->id, save_to_file); + xmlrpc_DECREF(res); +} + +static void audio_format_from_xml(struct chamelium *chamelium, + xmlrpc_value *res, int *rate, int *channels) +{ + xmlrpc_value *res_type, *res_rate, *res_sample_format, *res_channel; + char *type, *sample_format; + + xmlrpc_struct_find_value(&chamelium->env, res, "file_type", &res_type); + xmlrpc_struct_find_value(&chamelium->env, res, "rate", &res_rate); + xmlrpc_struct_find_value(&chamelium->env, res, "sample_format", &res_sample_format); + xmlrpc_struct_find_value(&chamelium->env, res, "channel", &res_channel); + + xmlrpc_read_string(&chamelium->env, res_type, (const char **) &type); + igt_assert(strcmp(type, "raw") == 0); + free(type); + + xmlrpc_read_string(&chamelium->env, res_sample_format, (const char **) &sample_format); + igt_assert(strcmp(sample_format, "S32_LE") == 0); + free(sample_format); + + xmlrpc_read_int(&chamelium->env, res_rate, rate); + xmlrpc_read_int(&chamelium->env, res_channel, channels); + + xmlrpc_DECREF(res_channel); + xmlrpc_DECREF(res_sample_format); + xmlrpc_DECREF(res_rate); + xmlrpc_DECREF(res_type); +} + +/** + * chamelium_stop_capturing_audio: + * @chamelium: the Chamelium instance + * @port: the port from which audio is being captured + * + * Stops capturing audio from a Chamelium port. If + * #chamelium_start_capturing_audio has been called with @save_to_file enabled, + * this function will return a #chamelium_audio_file struct containing details + * about the audio file. Once the caller is done with the struct, they should + * release it with #chamelium_destroy_audio_file. + */ +struct chamelium_audio_file *chamelium_stop_capturing_audio(struct chamelium *chamelium, + struct chamelium_port *port) +{ + xmlrpc_value *res, *res_path, *res_props; + struct chamelium_audio_file *file = NULL; + char *path; + + res = chamelium_rpc(chamelium, NULL, "StopCapturingAudio", "(i)", + port->id); + xmlrpc_array_read_item(&chamelium->env, res, 0, &res_path); + xmlrpc_array_read_item(&chamelium->env, res, 1, &res_props); + + xmlrpc_read_string(&chamelium->env, res_path, (const char **) &path); + + if (strlen(path) > 0) { + file = calloc(1, sizeof(*file)); + file->path = path; + + audio_format_from_xml(chamelium, res_props, + &file->rate, &file->channels); + } else { + free(path); + } + + xmlrpc_DECREF(res_props); + xmlrpc_DECREF(res_path); + xmlrpc_DECREF(res); + + return file; +} + static pixman_image_t *convert_frame_format(pixman_image_t *src, int format) { diff --git a/lib/igt_chamelium.h b/lib/igt_chamelium.h index 233ead85..047f8c5d 100644 --- a/lib/igt_chamelium.h +++ b/lib/igt_chamelium.h @@ -53,6 +53,12 @@ enum chamelium_check { CHAMELIUM_CHECK_CRC, }; +struct chamelium_audio_file { + char *path; + int rate; /* Hz */ + int channels; +}; + struct chamelium *chamelium_init(int drm_fd); void chamelium_deinit(struct chamelium *chamelium); void chamelium_reset(struct chamelium *chamelium); @@ -100,6 +106,10 @@ void chamelium_start_capture(struct chamelium *chamelium, void chamelium_stop_capture(struct chamelium *chamelium, int frame_count); void chamelium_capture(struct chamelium *chamelium, struct chamelium_port *port, int x, int y, int w, int h, int frame_count); +void chamelium_start_capturing_audio(struct chamelium *chamelium, + struct chamelium_port *port, bool save_to_file); +struct chamelium_audio_file *chamelium_stop_capturing_audio(struct chamelium *chamelium, + struct chamelium_port *port); igt_crc_t *chamelium_read_captured_crcs(struct chamelium *chamelium, int *frame_count); struct chamelium_frame_dump *chamelium_read_captured_frame(struct chamelium *chamelium, @@ -131,5 +141,6 @@ void chamelium_assert_frame_match_or_dump(struct chamelium *chamelium, void chamelium_crop_analog_frame(struct chamelium_frame_dump *dump, int width, int height); void chamelium_destroy_frame_dump(struct chamelium_frame_dump *dump); +void chamelium_destroy_audio_file(struct chamelium_audio_file *audio_file); #endif /* IGT_CHAMELIUM_H */ diff --git a/lib/igt_chamelium_stream.c b/lib/igt_chamelium_stream.c new file mode 100644 index 00000000..68ddb217 --- /dev/null +++ b/lib/igt_chamelium_stream.c @@ -0,0 +1,589 @@ +/* + * Copyright © 2019 Intel Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Authors: Simon Ser <simon.ser@intel.com> + */ + +#include "config.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netdb.h> +#include <stdbool.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include "igt_chamelium_stream.h" +#include "igt_core.h" +#include "igt_rc.h" + +#define STREAM_PORT 9994 +#define STREAM_VERSION_MAJOR 1 +#define STREAM_VERSION_MINOR 0 + +enum stream_error { + STREAM_ERROR_NONE = 0, + STREAM_ERROR_COMMAND = 1, + STREAM_ERROR_ARGUMENT = 2, + STREAM_ERROR_EXISTS = 3, + STREAM_ERROR_VIDEO_MEM_OVERFLOW_STOP = 4, + STREAM_ERROR_VIDEO_MEM_OVERFLOW_DROP = 5, + STREAM_ERROR_AUDIO_MEM_OVERFLOW_STOP = 6, + STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP = 7, + STREAM_ERROR_NO_MEM = 8, +}; + +enum stream_message_kind { + STREAM_MESSAGE_REQUEST = 0, + STREAM_MESSAGE_RESPONSE = 1, + STREAM_MESSAGE_DATA = 2, +}; + +enum stream_message_type { + STREAM_MESSAGE_RESET = 0, + STREAM_MESSAGE_GET_VERSION = 1, + STREAM_MESSAGE_VIDEO_STREAM = 2, + STREAM_MESSAGE_SHRINK_VIDEO = 3, + STREAM_MESSAGE_VIDEO_FRAME = 4, + STREAM_MESSAGE_DUMP_REALTIME_VIDEO = 5, + STREAM_MESSAGE_STOP_DUMP_VIDEO = 6, + STREAM_MESSAGE_DUMP_REALTIME_AUDIO = 7, + STREAM_MESSAGE_STOP_DUMP_AUDIO = 8, +}; + +struct chamelium_stream { + char *host; + unsigned int port; + + int fd; +}; + +static const char *stream_error_str(enum stream_error err) +{ + switch (err) { + case STREAM_ERROR_NONE: + return "no error"; + case STREAM_ERROR_COMMAND: + return "invalid command"; + case STREAM_ERROR_ARGUMENT: + return "invalid arguments"; + case STREAM_ERROR_EXISTS: + return "dump already started"; + case STREAM_ERROR_VIDEO_MEM_OVERFLOW_STOP: + return "video dump stopped after overflow"; + case STREAM_ERROR_VIDEO_MEM_OVERFLOW_DROP: + return "video frame dropped after overflow"; + case STREAM_ERROR_AUDIO_MEM_OVERFLOW_STOP: + return "audio dump stoppred after overflow"; + case STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP: + return "audio page dropped after overflow"; + case STREAM_ERROR_NO_MEM: + return "out of memory"; + } + return "unknown error"; +} + +/** + * The Chamelium URL is specified in the configuration file. We need to extract + * the host to connect to the stream server. + */ +static char *parse_url_host(const char *url) +{ + static const char prefix[] = "http://"; + char *colon; + + if (strstr(url, prefix) != url) + return NULL; + url += strlen(prefix); + + colon = strchr(url, ':'); + if (!colon) + return NULL; + + return strndup(url, colon - url); +} + +static bool chamelium_stream_read_config(struct chamelium_stream *client) +{ + GError *error = NULL; + gchar *chamelium_url; + + if (!igt_key_file) { + igt_warn("No configuration file available for chamelium\n"); + return false; + } + + chamelium_url = g_key_file_get_string(igt_key_file, "Chamelium", "URL", + &error); + if (!chamelium_url) { + igt_warn("Couldn't read Chamelium URL from config file: %s\n", + error->message); + return false; + } + + client->host = parse_url_host(chamelium_url); + if (!client->host) { + igt_warn("Invalid Chamelium URL in config file: %s\n", + chamelium_url); + return false; + } + client->port = STREAM_PORT; + + return true; +} + +static bool chamelium_stream_connect(struct chamelium_stream *client) +{ + int ret; + char port_str[16]; + struct addrinfo hints = {}; + struct addrinfo *results, *ai; + struct timeval tv = {}; + + igt_debug("Connecting to Chamelium stream server: tcp://%s:%u\n", + client->host, client->port); + + snprintf(port_str, sizeof(port_str), "%u", client->port); + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + ret = getaddrinfo(client->host, port_str, &hints, &results); + if (ret != 0) { + igt_warn("getaddrinfo failed: %s\n", gai_strerror(ret)); + return false; + } + + client->fd = -1; + for (ai = results; ai != NULL; ai = ai->ai_next) { + client->fd = socket(ai->ai_family, ai->ai_socktype, + ai->ai_protocol); + if (client->fd == -1) + continue; + + if (connect(client->fd, ai->ai_addr, ai->ai_addrlen) == -1) { + close(client->fd); + client->fd = -1; + continue; + } + + break; + } + + freeaddrinfo(results); + + if (client->fd < 0) { + igt_warn("Failed to connect to Chamelium stream server\n"); + return false; + } + + /* Set a read and write timeout of 5 seconds. */ + tv.tv_sec = 5; + setsockopt(client->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(client->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + return true; +} + +static bool read_whole(int fd, void *buf, size_t buf_len) +{ + ssize_t ret; + size_t n = 0; + char *ptr; + + while (n < buf_len) { + ptr = (char *) buf + n; + ret = read(fd, ptr, buf_len - n); + if (ret < 0) { + igt_warn("read failed: %s\n", strerror(errno)); + return false; + } else if (ret == 0) { + igt_warn("short read\n"); + return false; + } + n += ret; + } + + return true; +} + +static bool write_whole(int fd, void *buf, size_t buf_len) +{ + ssize_t ret; + size_t n = 0; + char *ptr; + + while (n < buf_len) { + ptr = (char *) buf + n; + ret = write(fd, ptr, buf_len - n); + if (ret < 0) { + igt_warn("write failed: %s\n", strerror(errno)); + return false; + } else if (ret == 0) { + igt_warn("short write\n"); + return false; + } + n += ret; + } + + return true; +} + +static bool read_and_discard(int fd, size_t len) +{ + char buf[1024]; + size_t n; + + while (len > 0) { + n = len; + if (n > sizeof(buf)) + n = sizeof(buf); + + if (!read_whole(fd, buf, n)) + return false; + + len -= n; + } + + return true; +} + +/** Read a message header from the socket. + * + * The header is laid out as follows: + * - u16: message type + * - u16: error code + * - u32: message length + */ +static bool chamelium_stream_read_header(struct chamelium_stream *client, + enum stream_message_kind *kind, + enum stream_message_type *type, + enum stream_error *err, + size_t *len) +{ + uint16_t _type; + char buf[8]; + + if (!read_whole(client->fd, buf, sizeof(buf))) + return false; + + _type = ntohs(*(uint16_t *) &buf[0]); + *type = _type & 0xFF; + *kind = _type >> 8; + *err = ntohs(*(uint16_t *) &buf[2]); + *len = ntohl(*(uint32_t *) &buf[4]); + + return true; +} + +static bool chamelium_stream_write_header(struct chamelium_stream *client, + enum stream_message_type type, + enum stream_error err, + size_t len) +{ + char buf[8]; + uint16_t _type; + + _type = type | (STREAM_MESSAGE_REQUEST << 8); + + *(uint16_t *) &buf[0] = htons(_type); + *(uint16_t *) &buf[2] = htons(err); + *(uint32_t *) &buf[4] = htonl(len); + + return write_whole(client->fd, buf, sizeof(buf)); +} + +static bool chamelium_stream_read_response(struct chamelium_stream *client, + enum stream_message_type type, + void *buf, size_t buf_len) +{ + enum stream_message_kind read_kind; + enum stream_message_type read_type; + enum stream_error read_err; + size_t read_len; + + if (!chamelium_stream_read_header(client, &read_kind, &read_type, + &read_err, &read_len)) + return false; + + if (read_kind != STREAM_MESSAGE_RESPONSE) { + igt_warn("Expected a response, got kind %d\n", read_kind); + return false; + } + if (read_type != type) { + igt_warn("Expected message type %d, got %d\n", + type, read_type); + return false; + } + if (read_err != STREAM_ERROR_NONE) { + igt_warn("Received error: %s (%d)\n", + stream_error_str(read_err), read_err); + return false; + } + if (buf_len != read_len) { + igt_warn("Received invalid message body size " + "(got %zu bytes, want %zu bytes)\n", + read_len, buf_len); + return false; + } + + return read_whole(client->fd, buf, buf_len); +} + +static bool chamelium_stream_write_request(struct chamelium_stream *client, + enum stream_message_type type, + void *buf, size_t buf_len) +{ + if (!chamelium_stream_write_header(client, type, STREAM_ERROR_NONE, + buf_len)) + return false; + + if (buf_len == 0) + return true; + + return write_whole(client->fd, buf, buf_len); +} + +static bool chamelium_stream_call(struct chamelium_stream *client, + enum stream_message_type type, + void *req_buf, size_t req_len, + void *resp_buf, size_t resp_len) +{ + if (!chamelium_stream_write_request(client, type, req_buf, req_len)) + return false; + + return chamelium_stream_read_response(client, type, resp_buf, resp_len); +} + +static bool chamelium_stream_check_version(struct chamelium_stream *client) +{ + char resp[2]; + uint8_t major, minor; + + if (!chamelium_stream_call(client, STREAM_MESSAGE_GET_VERSION, + NULL, 0, resp, sizeof(resp))) + return false; + + major = resp[0]; + minor = resp[1]; + if (major != STREAM_VERSION_MAJOR || minor < STREAM_VERSION_MINOR) { + igt_warn("Version mismatch (want %d.%d, got %d.%d)\n", + STREAM_VERSION_MAJOR, STREAM_VERSION_MINOR, + major, minor); + return false; + } + + return true; +} + +/** + * chamelium_stream_dump_realtime_audio: + * + * Starts audio capture. The caller can then call + * #chamelium_stream_receive_realtime_audio to receive audio pages. + */ +bool chamelium_stream_dump_realtime_audio(struct chamelium_stream *client, + enum chamelium_stream_realtime_mode mode) +{ + char req[1]; + + igt_debug("Starting real-time audio capture\n"); + + req[0] = mode; + return chamelium_stream_call(client, STREAM_MESSAGE_DUMP_REALTIME_AUDIO, + req, sizeof(req), NULL, 0); +} + +/** + * chamelium_stream_receive_realtime_audio: + * @page_count: if non-NULL, will be set to the dumped page number + * @buf: must either point to a dynamically allocated memory region or NULL + * @buf_len: number of elements of *@buf, for zero if @buf is NULL + * + * Receives one audio page from the streaming server. + * + * In "best effort" mode, some pages can be dropped. This can be detected via + * the page count. + * + * buf_len will be set to the size of the page. The caller is responsible for + * calling free(3) on *buf. + */ +bool chamelium_stream_receive_realtime_audio(struct chamelium_stream *client, + size_t *page_count, + int32_t **buf, size_t *buf_len) +{ + enum stream_message_kind kind; + enum stream_message_type type; + enum stream_error err; + size_t body_len; + char page_count_buf[4]; + int32_t *ptr; + + while (true) { + if (!chamelium_stream_read_header(client, &kind, &type, + &err, &body_len)) + return false; + + if (kind != STREAM_MESSAGE_DATA) { + igt_warn("Expected a data message, got kind %d\n", kind); + return false; + } + if (type != STREAM_MESSAGE_DUMP_REALTIME_AUDIO) { + igt_warn("Expected real-time audio dump message, " + "got type %d\n", type); + return false; + } + + if (err == STREAM_ERROR_NONE) + break; + else if (err != STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP) { + igt_warn("Received error: %s (%d)\n", + stream_error_str(err), err); + return false; + } + + igt_debug("Dropped an audio page because of an overflow\n"); + igt_assert(body_len == 0); + } + + igt_assert(body_len >= sizeof(page_count_buf)); + + if (!read_whole(client->fd, page_count_buf, sizeof(page_count_buf))) + return false; + if (page_count) + *page_count = ntohl(*(uint32_t *) &page_count_buf[0]); + body_len -= sizeof(page_count_buf); + + igt_assert(body_len % sizeof(int32_t) == 0); + if (*buf_len * sizeof(int32_t) != body_len) { + ptr = realloc(*buf, body_len); + if (!ptr) { + igt_warn("realloc failed: %s\n", strerror(errno)); + return false; + } + *buf = ptr; + *buf_len = body_len / sizeof(int32_t); + } + + return read_whole(client->fd, *buf, body_len); +} + +/** + * chamelium_stream_stop_realtime_audio: + * + * Stops real-time audio capture. This also drops any buffered audio pages. + * The caller shouldn't call #chamelium_stream_receive_realtime_audio after + * stopping audio capture. + */ +bool chamelium_stream_stop_realtime_audio(struct chamelium_stream *client) +{ + enum stream_message_kind kind; + enum stream_message_type type; + enum stream_error err; + size_t len; + + igt_debug("Stopping real-time audio capture\n"); + + if (!chamelium_stream_write_request(client, + STREAM_MESSAGE_STOP_DUMP_AUDIO, + NULL, 0)) + return false; + + while (true) { + if (!chamelium_stream_read_header(client, &kind, &type, + &err, &len)) + return false; + + if (kind == STREAM_MESSAGE_RESPONSE) + break; + + if (!read_and_discard(client->fd, len)) + return false; + } + + if (type != STREAM_MESSAGE_STOP_DUMP_AUDIO) { + igt_warn("Unexpected response type %d\n", type); + return false; + } + if (err != STREAM_ERROR_NONE) { + igt_warn("Received error: %s (%d)\n", + stream_error_str(err), err); + return false; + } + if (len != 0) { + igt_warn("Expected an empty response, got %zu bytes\n", len); + return false; + } + + return true; +} + +/** + * chamelium_stream_audio_format: + * + * Gets the format used for audio pages. + * + * Data will always be captured in raw pages of S32_LE elements. This function + * exposes the sampling rate and the number of channels. + */ +void chamelium_stream_audio_format(struct chamelium_stream *stream, + int *rate, int *channels) +{ + /* TODO: the Chamelium streaming server doesn't expose those yet. + * Just hardcode the values for now. */ + *rate = 48000; + *channels = 8; +} + +/** + * chamelium_stream_init: + * + * Connects to the Chamelium streaming server. + */ +struct chamelium_stream *chamelium_stream_init(void) +{ + struct chamelium_stream *client; + + client = calloc(1, sizeof(*client)); + + if (!chamelium_stream_read_config(client)) + goto error_client; + if (!chamelium_stream_connect(client)) + goto error_client; + if (!chamelium_stream_check_version(client)) + goto error_fd; + + return client; + +error_fd: + close(client->fd); +error_client: + free(client); + return NULL; +} + +void chamelium_stream_deinit(struct chamelium_stream *client) +{ + if (close(client->fd) != 0) + igt_warn("close failed: %s\n", strerror(errno)); + free(client); +} diff --git a/lib/igt_chamelium_stream.h b/lib/igt_chamelium_stream.h new file mode 100644 index 00000000..de4e9931 --- /dev/null +++ b/lib/igt_chamelium_stream.h @@ -0,0 +1,52 @@ +/* + * Copyright © 2019 Intel Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Authors: Simon Ser <simon.ser@intel.com> + */ + +#ifndef IGT_CHAMELIUM_STREAM_H +#define IGT_CHAMELIUM_STREAM_H + +#include "config.h" + +enum chamelium_stream_realtime_mode { + CHAMELIUM_STREAM_REALTIME_NONE = 0, + /* stop dumping when overflow */ + CHAMELIUM_STREAM_REALTIME_STOP_WHEN_OVERFLOW = 1, + /* drop data on overflow */ + CHAMELIUM_STREAM_REALTIME_BEST_EFFORT = 2, +}; + +struct chamelium_stream; + +struct chamelium_stream *chamelium_stream_init(void); +void chamelium_stream_deinit(struct chamelium_stream *client); +bool chamelium_stream_dump_realtime_audio(struct chamelium_stream *client, + enum chamelium_stream_realtime_mode mode); +void chamelium_stream_audio_format(struct chamelium_stream *stream, + int *rate, int *channels); +bool chamelium_stream_receive_realtime_audio(struct chamelium_stream *client, + size_t *page_count, + int32_t **buf, size_t *buf_len); +bool chamelium_stream_stop_realtime_audio(struct chamelium_stream *client); + +#endif diff --git a/lib/meson.build b/lib/meson.build index 97f701c7..e0b9cf51 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -94,7 +94,7 @@ if valgrind.found() endif if gsl.found() - lib_deps += [ gsl ] + lib_deps += gsl lib_sources += [ 'igt_frame.c', 'igt_audio.c' ] endif @@ -103,9 +103,10 @@ if alsa.found() lib_sources += 'igt_alsa.c' endif -if chamelium.found() +if chamelium_found lib_deps += chamelium lib_sources += 'igt_chamelium.c' + lib_sources += 'igt_chamelium_stream.c' endif srcdir = join_paths(meson.source_root(), 'tests') |