summaryrefslogtreecommitdiff
path: root/lib/igt_chamelium_stream.c
diff options
context:
space:
mode:
authorSimon Ser <simon.ser@intel.com>2019-04-23 16:04:52 +0300
committerArkadiusz Hiler <arkadiusz.hiler@intel.com>2019-04-25 13:07:15 +0300
commit311baff151f90c1db6f57ee9515216b4f9da5db7 (patch)
treecff58efbdacda05b06e7d88a2ab3d68816687eb4 /lib/igt_chamelium_stream.c
parent11e10bc575516c56978640fcc697c27f277c660a (diff)
tests/kms_chamelium: add dp-audio test
This new test ensures DisplayPort audio works by using the Chamelium. It enables the DisplayPort output and sends an audio signal containing a set of frequencies we choose to all HDMI/DisplayPort audio devices. It starts recording audio on the Chamelium device and uses the stream server to retrieve captured audio pages. It then checks that the capture audio signal contains the frequencies we sent, and only those, by computing a FFT. A new library has been added to libigt to communicate with the stream server. It implements a simple custom TCP protocol. In case the test fails, a WAV file with the captured data is saved on disk. Right now the test has a few limitations: - Only the first channel is checked - IGT only generates audio with a single sampling rate (48 KHz) - Audio data is not captured in real-time These limitations will be lifted in future patches. PulseAudio must not run during the tests since ALSA is used directly. To ensure this, edit /etc/pulse/client.conf and add `autospawn=no`. Then run `pulseaudio --kill`. This commit deletes the existing audio tests. They weren't run and required an exotic configuration (HDMI audio splitter, dummy HDMI sink and a line-in port on the DUT). This patch also changes lib/igt_audio to use uint16_t instead of short. The rationale is: - The standard says a short is at least 16 bit wide, but a short can be larger (in practice it won't happen, but better use types correctly) - It makes it clearer that the audio format is S16_LE, since "16" is in the type name. This patch depends on the following Chameleon bugs: - https://crbug.com/948060 - https://crbug.com/950857 Signed-off-by: Simon Ser <simon.ser@intel.com> Reviewed-by: Martin Peres <martin.peres@linux.intel.com>
Diffstat (limited to 'lib/igt_chamelium_stream.c')
-rw-r--r--lib/igt_chamelium_stream.c589
1 files changed, 589 insertions, 0 deletions
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);
+}