This commit adds a new audiodev backend to allow QEMU to use Pipewire as
both an audio sink and source. This backend is available on most systems
Add Pipewire entry points for QEMU Pipewire audio backend
Add wrappers for QEMU Pipewire audio backend in qpw_pcm_ops()
qpw_write function returns the current state of the stream to pwaudio
and Writes some data to the server for playback streams using pipewire
spa_ringbuffer implementation.
qpw_read function returns the current state of the stream to pwaudio and
reads some data from the server for capture streams using pipewire
spa_ringbuffer implementation. These functions qpw_write and qpw_read
are called during playback and capture.
Added some functions that convert pw audio formats to QEMU audio format
and vice versa which would be needed in the pipewire audio sink and
source functions qpw_init_in() & qpw_init_out().
These methods that implement playback and recording will create streams
for playback and capture that will start processing and will result in
the on_process callbacks to be called.
Built a connection to the Pipewire sound system server in the
qpw_audio_init() method.
Signed-off-by: Dorinda Bassey <dbassey@redhat.com>
---
v10:
improve error handling
fix volume functions
add locks in enable_in out functions
cleanup in reverse order of intialization
add triggers for the sync method on the core object
add waiting function for pw_thread_loop_signal
improve trace
audio/audio.c | 3 +
audio/audio_template.h | 4 +
audio/meson.build | 1 +
audio/pwaudio.c | 906 ++++++++++++++++++++++++++++++++++
audio/trace-events | 8 +
meson.build | 8 +
meson_options.txt | 4 +-
qapi/audio.json | 44 ++
qemu-options.hx | 21 +
scripts/meson-buildoptions.sh | 8 +-
10 files changed, 1004 insertions(+), 3 deletions(-)
create mode 100644 audio/pwaudio.c
diff --git a/audio/audio.c b/audio/audio.c
index 70b096713c..90c7c49d11 100644
--- a/audio/audio.c
+++ b/audio/audio.c
@@ -2061,6 +2061,9 @@ void audio_create_pdos(Audiodev *dev)
#ifdef CONFIG_AUDIO_PA
CASE(PA, pa, Pa);
#endif
+#ifdef CONFIG_AUDIO_PIPEWIRE
+ CASE(PIPEWIRE, pipewire, Pipewire);
+#endif
#ifdef CONFIG_AUDIO_SDL
CASE(SDL, sdl, Sdl);
#endif
diff --git a/audio/audio_template.h b/audio/audio_template.h
index e42326c20d..dc0c74aa74 100644
--- a/audio/audio_template.h
+++ b/audio/audio_template.h
@@ -362,6 +362,10 @@ AudiodevPerDirectionOptions *glue(audio_get_pdo_,
TYPE)(Audiodev *dev)
case AUDIODEV_DRIVER_PA:
return qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.TYPE);
#endif
+#ifdef CONFIG_AUDIO_PIPEWIRE
+ case AUDIODEV_DRIVER_PIPEWIRE:
+ return
qapi_AudiodevPipewirePerDirectionOptions_base(dev->u.pipewire.TYPE);
+#endif
#ifdef CONFIG_AUDIO_SDL
case AUDIODEV_DRIVER_SDL:
return qapi_AudiodevSdlPerDirectionOptions_base(dev->u.sdl.TYPE);
diff --git a/audio/meson.build b/audio/meson.build
index 0722224ba9..65a49c1a10 100644
--- a/audio/meson.build
+++ b/audio/meson.build
@@ -19,6 +19,7 @@ foreach m : [
['sdl', sdl, files('sdlaudio.c')],
['jack', jack, files('jackaudio.c')],
['sndio', sndio, files('sndioaudio.c')],
+ ['pipewire', pipewire, files('pwaudio.c')],
['spice', spice, files('spiceaudio.c')]
]
if m[1].found()
diff --git a/audio/pwaudio.c b/audio/pwaudio.c
new file mode 100644
index 0000000000..f9da86059f
--- /dev/null
+++ b/audio/pwaudio.c
@@ -0,0 +1,906 @@
+/*
+ * QEMU Pipewire audio driver
+ *
+ * Copyright (c) 2023 Red Hat Inc.
+ *
+ * Author: Dorinda Bassey <dbassey@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/module.h"
+#include "audio.h"
+#include <errno.h>
+#include "qemu/error-report.h"
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/result.h>
+#include <spa/param/props.h>
+
+#include <pipewire/pipewire.h>
+#include "trace.h"
+
+#define AUDIO_CAP "pipewire"
+#define RINGBUFFER_SIZE (1u << 22)
+#define RINGBUFFER_MASK (RINGBUFFER_SIZE - 1)
+
+#include "audio_int.h"
+
+typedef struct pwvolume {
+ uint32_t channels;
+ float values[SPA_AUDIO_MAX_CHANNELS];
+} pwvolume;
+
+typedef struct pwaudio {
+ Audiodev *dev;
+ struct pw_thread_loop *thread_loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ int last_seq, pending_seq, error;
+} pwaudio;
+
+typedef struct PWVoice {
+ pwaudio *g;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t highwater_mark;
+ uint32_t frame_size, req;
+ struct spa_ringbuffer ring;
+ uint8_t buffer[RINGBUFFER_SIZE];
+
+ struct pw_properties *props;
+ pwvolume volume;
+ bool muted;
+} PWVoice;
+
+typedef struct PWVoiceOut {
+ HWVoiceOut hw;
+ PWVoice v;
+} PWVoiceOut;
+
+typedef struct PWVoiceIn {
+ HWVoiceIn hw;
+ PWVoice v;
+} PWVoiceIn;
+
+static void
+stream_destroy(void *data)
+{
+ PWVoice *v = (PWVoice *) data;
+ spa_hook_remove(&v->stream_listener);
+ v->stream = NULL;
+}
+
+/* output data processing function to read stuffs from the buffer */
+static void
+playback_on_process(void *data)
+{
+ PWVoice *v = (PWVoice *) data;
+ void *p;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t req, index, n_bytes;
+ int32_t avail;
+
+ assert(v->stream);
+
+ /* obtain a buffer to read from */
+ b = pw_stream_dequeue_buffer(v->stream);
+ if (b == NULL) {
+ error_report("out of buffers: %s", strerror(errno));
+ return;
+ }
+
+ buf = b->buffer;
+ p = buf->datas[0].data;
+ if (p == NULL) {
+ return;
+ }
+ /* calculate the total no of bytes to read data from buffer */
+ req = b->requested * v->frame_size;
+ if (req == 0) {
+ req = v->req;
+ }
+ n_bytes = SPA_MIN(req, buf->datas[0].maxsize);
+
+ /* get no of available bytes to read data from buffer */
+
+ avail = spa_ringbuffer_get_read_index(&v->ring, &index);
+
+ if (avail < (int32_t) n_bytes) {
+ n_bytes = avail;
+ }
+
+ spa_ringbuffer_read_data(&v->ring,
+ v->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK, p, n_bytes);
+
+ index += n_bytes;
+ spa_ringbuffer_read_update(&v->ring, index);
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = v->frame_size;
+ buf->datas[0].chunk->size = n_bytes;
+
+ /* queue the buffer for playback */
+ pw_stream_queue_buffer(v->stream, b);
+}
+
+/* output data processing function to generate stuffs in the buffer */
+static void
+capture_on_process(void *data)
+{
+ PWVoice *v = (PWVoice *) data;
+ void *p;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ int32_t filled;
+ uint32_t index, offs, n_bytes;
+
+ assert(v->stream);
+
+ /* obtain a buffer */
+ b = pw_stream_dequeue_buffer(v->stream);
+ if (b == NULL) {
+ error_report("out of buffers: %s", strerror(errno));
+ return;
+ }
+
+ /* Write data into buffer */
+ buf = b->buffer;
+ p = buf->datas[0].data;
+ if (p == NULL) {
+ return;
+ }
+ offs = SPA_MIN(buf->datas[0].chunk->offset, buf->datas[0].maxsize);
+ n_bytes = SPA_MIN(buf->datas[0].chunk->size, buf->datas[0].maxsize - offs);
+
+ filled = spa_ringbuffer_get_write_index(&v->ring, &index);
+
+
+ if (filled < 0) {
+ error_report("%p: underrun write:%u filled:%d", p, index, filled);
+ } else {
+ if ((uint32_t) filled + n_bytes > RINGBUFFER_SIZE) {
+ error_report("%p: overrun write:%u filled:%d + size:%u > max:%u",
+ p, index, filled, n_bytes, RINGBUFFER_SIZE);
+ }
+ }
+ spa_ringbuffer_write_data(&v->ring,
+ v->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ SPA_PTROFF(p, offs, void), n_bytes);
+ index += n_bytes;
+ spa_ringbuffer_write_update(&v->ring, index);
+
+ /* queue the buffer for playback */
+ pw_stream_queue_buffer(v->stream, b);
+}
+
+static void
+on_stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ PWVoice *v = (PWVoice *) data;
+
+ trace_pw_state_changed(pw_stream_get_node_id(v->stream),
+ pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_CONNECTING:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ }
+}
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = capture_on_process
+};
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = playback_on_process
+};
+
+static size_t
+qpw_read(HWVoiceIn *hw, void *data, size_t len)
+{
+ PWVoiceIn *pw = (PWVoiceIn *) hw;
+ PWVoice *v = &pw->v;
+ pwaudio *c = v->g;
+ const char *error = NULL;
+ size_t l;
+ int32_t avail;
+ uint32_t index;
+
+ pw_thread_loop_lock(c->thread_loop);
+ if (pw_stream_get_state(v->stream, &error) != PW_STREAM_STATE_STREAMING) {
+ /* wait for stream to become ready */
+ l = 0;
+ goto done_unlock;
+ }
+ /* get no of available bytes to read data from buffer */
+ avail = spa_ringbuffer_get_read_index(&v->ring, &index);
+
+ trace_pw_read(avail, index, len);
+
+ if (avail < (int32_t) len) {
+ len = avail;
+ }
+
+ spa_ringbuffer_read_data(&v->ring,
+ v->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK, data, len);
+ index += len;
+ spa_ringbuffer_read_update(&v->ring, index);
+ l = len;
+
+done_unlock:
+ pw_thread_loop_unlock(c->thread_loop);
+ return l;
+}
+
+static size_t qpw_buffer_get_free(HWVoiceOut *hw)
+{
+ PWVoiceOut *pw = (PWVoiceOut *)hw;
+ PWVoice *v = &pw->v;
+ pwaudio *c = v->g;
+ const char *error = NULL;
+ int32_t filled, avail;
+ uint32_t index;
+
+ pw_thread_loop_lock(c->thread_loop);
+ if (pw_stream_get_state(v->stream, &error) != PW_STREAM_STATE_STREAMING) {
+ /* wait for stream to become ready */
+ avail = 0;
+ goto done_unlock;
+ }
+
+ filled = spa_ringbuffer_get_write_index(&v->ring, &index);
+ avail = v->highwater_mark - filled;
+
+done_unlock:
+ pw_thread_loop_unlock(c->thread_loop);
+ return avail;
+}
+
+static size_t
+qpw_write(HWVoiceOut *hw, void *data, size_t len)
+{
+ PWVoiceOut *pw = (PWVoiceOut *) hw;
+ PWVoice *v = &pw->v;
+ pwaudio *c = v->g;
+ const char *error = NULL;
+ int32_t filled, avail;
+ uint32_t index;
+
+ pw_thread_loop_lock(c->thread_loop);
+ if (pw_stream_get_state(v->stream, &error) != PW_STREAM_STATE_STREAMING) {
+ /* wait for stream to become ready */
+ len = 0;
+ goto done_unlock;
+ }
+ filled = spa_ringbuffer_get_write_index(&v->ring, &index);
+ avail = v->highwater_mark - filled;
+
+ trace_pw_write(filled, avail, index, len);
+
+ if (len > avail) {
+ len = avail;
+ }
+
+ if (filled < 0) {
+ error_report("%p: underrun write:%u filled:%d", pw, index, filled);
+ } else {
+ if ((uint32_t) filled + len > RINGBUFFER_SIZE) {
+ error_report("%p: overrun write:%u filled:%d + size:%zu > max:%u",
+ pw, index, filled, len, RINGBUFFER_SIZE);
+ }
+ }
+
+ spa_ringbuffer_write_data(&v->ring,
+ v->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK, data, len);
+ index += len;
+ spa_ringbuffer_write_update(&v->ring, index);
+
+done_unlock:
+ pw_thread_loop_unlock(c->thread_loop);
+ return len;
+}
+
+static int
+audfmt_to_pw(AudioFormat fmt, int endianness)
+{
+ int format;
+
+ switch (fmt) {
+ case AUDIO_FORMAT_S8:
+ format = SPA_AUDIO_FORMAT_S8;
+ break;
+ case AUDIO_FORMAT_U8:
+ format = SPA_AUDIO_FORMAT_U8;
+ break;
+ case AUDIO_FORMAT_S16:
+ format = endianness ? SPA_AUDIO_FORMAT_S16_BE :
SPA_AUDIO_FORMAT_S16_LE;
+ break;
+ case AUDIO_FORMAT_U16:
+ format = endianness ? SPA_AUDIO_FORMAT_U16_BE :
SPA_AUDIO_FORMAT_U16_LE;
+ break;
+ case AUDIO_FORMAT_S32:
+ format = endianness ? SPA_AUDIO_FORMAT_S32_BE :
SPA_AUDIO_FORMAT_S32_LE;
+ break;
+ case AUDIO_FORMAT_U32:
+ format = endianness ? SPA_AUDIO_FORMAT_U32_BE :
SPA_AUDIO_FORMAT_U32_LE;
+ break;
+ case AUDIO_FORMAT_F32:
+ format = endianness ? SPA_AUDIO_FORMAT_F32_BE :
SPA_AUDIO_FORMAT_F32_LE;
+ break;
+ default:
+ dolog("Internal logic error: Bad audio format %d\n", fmt);
+ format = SPA_AUDIO_FORMAT_U8;
+ break;
+ }
+ return format;
+}
+
+static AudioFormat
+pw_to_audfmt(enum spa_audio_format fmt, int *endianness,
+ uint32_t *frame_size)
+
+ /* 75% of the timer period for faster updates */
+ buf_samples = (uint64_t)v->g->dev->timer_period * v->info.rate
+ * 3 / 4 / 1000000;
+ trace_pw_timer(buf_samples);
+ pw_properties_setf(v->props, PW_KEY_NODE_LATENCY, "%" PRIu64 "/%u",
+ buf_samples, v->info.rate);
+
+ if (name) {
+ pw_properties_set(v->props, PW_KEY_TARGET_OBJECT, name);
+ }
+ v->stream = pw_stream_new(c->core, stream_name, v->props);
+
+ if (v->stream == NULL) {
+ return -1;
+ }
+
+ if (dir == SPA_DIRECTION_INPUT) {
+ pw_stream_add_listener(v->stream,
+ &v->stream_listener, &capture_stream_events, v);
+ } else {
+ pw_stream_add_listener(v->stream,
+ &v->stream_listener, &playback_stream_events, v);
+ }
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat,
+ &v->info);
+
+ /* connect the stream to a sink or source */
+ res = pw_stream_connect(v->stream,
+ dir ==
+ SPA_DIRECTION_INPUT ? PW_DIRECTION_INPUT :
+ PW_DIRECTION_OUTPUT, PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_INACTIVE |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS, params, n_params);
+ if (res < 0) {
+ pw_stream_destroy(v->stream);
+ return -1;
+ }
+
+ return 0;
+}
+