[Spice-devel] [xf86-video-qxl PATCH (resend 2)] Implement sending audio to the client from a directory of FIFO queues

Andrew Eikum aeikum at codeweavers.com
Fri Mar 15 07:19:49 PDT 2013


This introduces a new Xorg.conf option, SpicePlaybackFIFODir, which will
be monitored for files. The XSpice driver will mix and forward the audio
data sent to those pipes to the Spice client.

This is designed to work with PulseAudio's module-pipe-sink, but should
work with any audio output to a pipe. For example, use with this PA
configuration option:

  load-module module-pipe-sink file=$FIFO_DIR/playback.fifo format=s16 rate=44100 channels=2

making sure the format, rate, and channels match the Spice protocol
settings.
---

Resending again, with fixed code format.

 src/Makefile.am      |   2 +
 src/qxl.h            |   6 +
 src/qxl_driver.c     |  18 ++-
 src/spiceqxl_audio.c | 343 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/spiceqxl_audio.h |  31 +++++
 5 files changed, 399 insertions(+), 1 deletion(-)
 create mode 100644 src/spiceqxl_audio.c
 create mode 100644 src/spiceqxl_audio.h

diff --git a/src/Makefile.am b/src/Makefile.am
index f9557da..8632297 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -82,6 +82,8 @@ spiceqxl_drv_la_SOURCES =				\
 	spiceqxl_main_loop.h		\
 	spiceqxl_display.c			\
 	spiceqxl_display.h			\
+	spiceqxl_audio.c			\
+	spiceqxl_audio.h			\
 	spiceqxl_inputs.c			\
 	spiceqxl_inputs.h			\
 	qxl_driver.c				\
diff --git a/src/qxl.h b/src/qxl.h
index 017eab5..c26ea8f 100644
--- a/src/qxl.h
+++ b/src/qxl.h
@@ -130,6 +130,7 @@ enum {
     OPTION_SPICE_DH_FILE,
     OPTION_SPICE_DEFERRED_FPS,
     OPTION_SPICE_EXIT_ON_DISCONNECT,
+    OPTION_SPICE_PLAYBACK_FIFO_DIR,
 #endif
     OPTION_COUNT,
 };
@@ -284,9 +285,12 @@ struct _qxl_screen_t
     QXLWorker *         worker;
     int                 worker_running;
     QXLInstance         display_sin;
+    SpicePlaybackInstance playback_sin;
     /* XSpice specific, dragged from the Device */
     QXLReleaseInfo     *last_release;
 
+    pthread_t audio_thread;
+
     uint32_t           cmdflags;
     uint32_t           oom_running;
     uint32_t           num_free_res; /* is having a release ring effective
@@ -304,6 +308,8 @@ struct _qxl_screen_t
     } guest_primary;
 
     uint32_t           deferred_fps;
+
+    char playback_fifo_dir[PATH_MAX];
 #endif /* XSPICE */
 
     struct xorg_list ums_bos;
diff --git a/src/qxl_driver.c b/src/qxl_driver.c
index 1d58f01..335e095 100644
--- a/src/qxl_driver.c
+++ b/src/qxl_driver.c
@@ -55,6 +55,7 @@
 #include "spiceqxl_io_port.h"
 #include "spiceqxl_spice_server.h"
 #include "dfps.h"
+#include "spiceqxl_audio.h"
 #endif /* XSPICE */
 
 extern void compat_init_scrn (ScrnInfoPtr);
@@ -121,6 +122,8 @@ const OptionInfoRec DefaultOptions[] =
       "SpiceDeferredFPS",         OPTV_INTEGER,   {0}, FALSE},
     { OPTION_SPICE_EXIT_ON_DISCONNECT,
       "SpiceExitOnDisconnect",    OPTV_BOOLEAN,   {0}, FALSE},
+    { OPTION_SPICE_PLAYBACK_FIFO_DIR,
+      "SpicePlaybackFIFODir",     OPTV_STRING,    {0}, FALSE},
 #endif
     
     { -1, NULL, OPTV_NONE, {0}, FALSE }
@@ -639,6 +642,7 @@ spiceqxl_screen_init (ScrnInfoPtr pScrn, qxl_screen_t *qxl)
 	qxl->core = basic_event_loop_init ();
 	spice_server_init (qxl->spice_server, qxl->core);
 	qxl_add_spice_display_interface (qxl);
+	qxl_add_spice_playback_interface (qxl);
 	qxl->worker->start (qxl->worker);
 	qxl->worker_running = TRUE;
         if (qxl->deferred_fps)
@@ -1007,7 +1011,10 @@ qxl_pre_init (ScrnInfoPtr pScrn, int flags)
     //int *linePitches = NULL;
     //DisplayModePtr mode;
     unsigned int max_x, max_y;
-    
+#ifdef XSPICE
+    const char *playback_fifo_dir;
+#endif
+
     /* In X server 1.7.5, Xorg -configure will cause this
      * function to get called without a confScreen.
      */
@@ -1053,6 +1060,15 @@ qxl_pre_init (ScrnInfoPtr pScrn, int flags)
     if (!qxl_pre_init_common(pScrn))
 	goto out;
 
+#ifdef XSPICE
+    playback_fifo_dir = get_str_option(qxl->options, OPTION_SPICE_PLAYBACK_FIFO_DIR,
+               "XSPICE_PLAYBACK_FIFO_DIR");
+    if (playback_fifo_dir)
+        strncpy(qxl->playback_fifo_dir, playback_fifo_dir, sizeof(qxl->playback_fifo_dir));
+    else
+        qxl->playback_fifo_dir[0] = '\0';
+#endif
+
     if (!qxl_map_memory (qxl, scrnIndex))
 	goto out;
     
diff --git a/src/spiceqxl_audio.c b/src/spiceqxl_audio.c
new file mode 100644
index 0000000..3cd80ff
--- /dev/null
+++ b/src/spiceqxl_audio.c
@@ -0,0 +1,343 @@
+/*
+ * Copyright 2012 Andrew Eikum for CodeWeavers Inc.
+ *
+ * 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
+ * on the rights to use, copy, modify, merge, publish, distribute, sub
+ * license, 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 NON-INFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS 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.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "spiceqxl_audio.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#define BUFFER_PERIODS 10
+#define PERIOD_MS 10
+#define MAX_FIFOS 16
+
+struct audio_data {
+    int fifo_fds[MAX_FIFOS];
+    ino_t inodes[MAX_FIFOS];
+    uint32_t valid_bytes, write_offs;
+    char *buffer, *spice_buffer;
+    int period_frames;
+    uint32_t spice_write_offs, spice_buffer_bytes;
+    uint32_t frame_bytes, period_bytes, fed, buffer_bytes;
+    struct timeval last_read_time;
+};
+
+static ssize_t
+read_from_fifos (struct audio_data *data)
+{
+    size_t to_read_bytes = min(data->period_bytes, data->buffer_bytes - data->write_offs);
+    int i;
+    ssize_t max_readed = 0;
+    int16_t *out_buf = (int16_t*)(data->buffer + data->write_offs), *buf;
+
+    buf = malloc(to_read_bytes);
+    if (!buf)
+    {
+        ErrorF("playback: malloc failed: %s\n", strerror(errno));
+        return 0;
+    }
+
+    memset(out_buf, 0, to_read_bytes);
+
+    for (i = 0; i < MAX_FIFOS; ++i)
+    {
+        unsigned int s;
+        ssize_t readed;
+
+        if (data->fifo_fds[i] < 0)
+            continue;
+
+        readed = read(data->fifo_fds[i], buf, to_read_bytes);
+        if (readed < 0)
+        {
+            if (errno != EAGAIN && errno != EINTR)
+                ErrorF("playback: read from FIFO %d failed: %s\n", data->fifo_fds[i], strerror(errno));
+            continue;
+        }
+
+        if (readed == 0)
+        {
+            ErrorF("playback: FIFO %d gave EOF\n", data->fifo_fds[i]);
+            close(data->fifo_fds[i]);
+            data->fifo_fds[i] = -1;
+            data->inodes[i] = 0;
+            continue;
+        }
+
+        if (readed > max_readed)
+            max_readed = readed;
+
+        for (s = 0; s < readed / sizeof(int16_t); ++s)
+        {
+            /* FIXME: Ehhh, this'd be better as floats. With this algorithm,
+             * samples mixed after being clipped will have undue weight. But
+             * if we're clipping, then we're distorted anyway, so whatever. */
+            if (out_buf[s] + buf[s] > INT16_MAX)
+                out_buf[s] = INT16_MAX;
+            else if (out_buf[s] + buf[s] < -INT16_MAX)
+                out_buf[s] = -INT16_MAX;
+            else
+                out_buf[s] += buf[s];
+        }
+    }
+
+    free(buf);
+
+    if (!max_readed)
+        return 0;
+
+    data->valid_bytes = min(data->valid_bytes + max_readed,
+            data->buffer_bytes);
+
+    data->write_offs += max_readed;
+    data->write_offs %= data->buffer_bytes;
+
+    ++data->fed;
+
+    return max_readed;
+}
+
+static int
+scan_fifos (struct audio_data *data, const char *dirname)
+{
+    DIR *dir;
+    struct dirent *ent;
+    int i;
+
+    dir = opendir(dirname);
+    if (!dir)
+    {
+        ErrorF("playback: failed to open FIFO directory '%s': %s\n", dirname, strerror(errno));
+        return 1;
+    }
+
+    while ((ent = readdir(dir)))
+    {
+        char path[PATH_MAX];
+
+        if (ent->d_name[0] == '.')
+            /* skip dot-files */
+            continue;
+
+        for (i = 0; i < MAX_FIFOS; ++i)
+            if (ent->d_ino == data->inodes[i])
+                break;
+        if (i < MAX_FIFOS)
+            /* file already open */
+            continue;
+
+        for (i = 0; i < MAX_FIFOS; ++i)
+            if (data->fifo_fds[i] < 0)
+                break;
+        if (i == MAX_FIFOS)
+        {
+            static int once = 0;
+            if (!once)
+            {
+                ErrorF("playback: Too many FIFOs already open\n");
+                ++once;
+            }
+            closedir(dir);
+            return 0;
+        }
+
+        strncpy(path, dirname, sizeof(path));
+        strncat(path, "/", sizeof(path));
+        strncat(path, ent->d_name, sizeof(path));
+
+        data->fifo_fds[i] = open(path, O_RDONLY | O_RSYNC | O_NONBLOCK);
+        if (data->fifo_fds[i] < 0)
+        {
+            ErrorF("playback: open FIFO '%s' failed: %s\n", path, strerror(errno));
+            continue;
+        }
+        ErrorF("playback: opened FIFO '%s' as %d\n", path, data->fifo_fds[i]);
+
+        data->inodes[i] = ent->d_ino;
+    }
+
+    closedir(dir);
+
+    return 0;
+}
+
+static void *
+audio_thread_main (void *p)
+{
+    qxl_screen_t *qxl = p;
+    int i;
+    struct audio_data data;
+
+    for (i = 0; i < MAX_FIFOS; ++i)
+        data.fifo_fds[i] = -1;
+
+    data.valid_bytes = data.fed = 0;
+    data.period_frames = SPICE_INTERFACE_PLAYBACK_FREQ * PERIOD_MS / 1000;
+
+    data.frame_bytes = sizeof(int16_t) * SPICE_INTERFACE_PLAYBACK_CHAN;
+
+    data.period_bytes = data.period_frames * data.frame_bytes;
+    data.buffer_bytes = data.period_bytes * BUFFER_PERIODS;
+    data.buffer = malloc(data.buffer_bytes);
+    memset(data.buffer, 0, data.buffer_bytes);
+
+    spice_server_playback_start(&qxl->playback_sin);
+
+    gettimeofday(&data.last_read_time, NULL);
+
+    while (1)
+    {
+        struct timeval end, diff, period_tv;
+
+        if (scan_fifos(&data, qxl->playback_fifo_dir))
+            goto cleanup;
+
+        while (data.fed < BUFFER_PERIODS)
+        {
+            if (!read_from_fifos(&data))
+                break;
+
+            while (data.valid_bytes)
+            {
+                int to_copy_bytes;
+                uint32_t read_offs;
+
+                if (!data.spice_buffer)
+                {
+                    uint32_t chunk_frames;
+                    spice_server_playback_get_buffer(&qxl->playback_sin, (uint32_t**)&data.spice_buffer, &chunk_frames);
+                    data.spice_buffer_bytes = chunk_frames * data.frame_bytes;
+                }
+                if (!data.spice_buffer)
+                    break;
+
+                if (data.valid_bytes > data.write_offs)
+                {
+                    read_offs = data.buffer_bytes + data.write_offs - data.valid_bytes;
+                    to_copy_bytes = min(data.buffer_bytes - read_offs,
+                            data.spice_buffer_bytes - data.spice_write_offs);
+                }
+                else
+                {
+                    read_offs = data.write_offs - data.valid_bytes;
+                    to_copy_bytes = min(data.valid_bytes,
+                            data.spice_buffer_bytes - data.spice_write_offs);
+                }
+
+                memcpy(data.spice_buffer + data.spice_write_offs,
+                        data.buffer + read_offs, to_copy_bytes);
+
+                data.valid_bytes -= to_copy_bytes;
+
+                data.spice_write_offs += to_copy_bytes;
+
+                if (data.spice_write_offs >= data.spice_buffer_bytes)
+                {
+                    spice_server_playback_put_samples(&qxl->playback_sin, (uint32_t*)data.spice_buffer);
+                    data.spice_buffer = NULL;
+                    data.spice_buffer_bytes = data.spice_write_offs = 0;
+                }
+            }
+        }
+
+        period_tv.tv_sec = 0;
+        period_tv.tv_usec = PERIOD_MS * 1000;
+
+        usleep(period_tv.tv_usec);
+
+        gettimeofday(&end, NULL);
+
+        timersub(&end, &data.last_read_time, &diff);
+
+        while (data.fed &&
+                (diff.tv_sec > 0 || diff.tv_usec >= period_tv.tv_usec))
+        {
+            timersub(&diff, &period_tv, &diff);
+
+            --data.fed;
+
+            timeradd(&data.last_read_time, &period_tv, &data.last_read_time);
+        }
+
+        if (!data.fed)
+            data.last_read_time = end;
+    }
+
+cleanup:
+    if (data.spice_buffer)
+    {
+        memset(data.spice_buffer, 0, data.spice_buffer_bytes - data.spice_write_offs);
+        spice_server_playback_put_samples(&qxl->playback_sin, (uint32_t*)data.spice_buffer);
+        data.spice_buffer = NULL;
+        data.spice_buffer_bytes = data.spice_write_offs = 0;
+    }
+
+    free(data.buffer);
+
+    spice_server_playback_stop(&qxl->playback_sin);
+
+    return NULL;
+}
+
+static const SpicePlaybackInterface playback_sif = {
+    {
+        SPICE_INTERFACE_PLAYBACK,
+        "playback",
+        SPICE_INTERFACE_PLAYBACK_MAJOR,
+        SPICE_INTERFACE_PLAYBACK_MINOR
+    }
+};
+
+int
+qxl_add_spice_playback_interface (qxl_screen_t *qxl)
+{
+    int ret;
+
+    if (qxl->playback_fifo_dir[0] == 0)
+    {
+        ErrorF("playback: no audio FIFO directory, audio is disabled\n");
+        return 0;
+    }
+
+    qxl->playback_sin.base.sif = &playback_sif.base;
+    ret = spice_server_add_interface(qxl->spice_server, &qxl->playback_sin.base);
+    if (ret < 0)
+        return errno;
+
+    /* disable CELT */
+    ret = spice_server_set_playback_compression(qxl->spice_server, 0);
+    if (ret < 0)
+        return errno;
+
+    ret = pthread_create(&qxl->audio_thread, NULL, &audio_thread_main, qxl);
+    if (ret < 0)
+        return errno;
+
+    return 0;
+}
diff --git a/src/spiceqxl_audio.h b/src/spiceqxl_audio.h
new file mode 100644
index 0000000..695ba25
--- /dev/null
+++ b/src/spiceqxl_audio.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012 Andrew Eikum for CodeWeavers Inc.
+ *
+ * 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
+ * on the rights to use, copy, modify, merge, publish, distribute, sub
+ * license, 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 NON-INFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS 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.
+ */
+
+#ifndef QXL_SPICE_AUDIO_H
+#define QXL_SPICE_AUDIO_H
+
+#include "qxl.h"
+#include <spice.h>
+
+int qxl_add_spice_playback_interface(qxl_screen_t *qxl);
+
+#endif
-- 
1.8.2



More information about the Spice-devel mailing list