diff options
author | Marc-André Lureau <marcandre.lureau@redhat.com> | 2021-03-09 17:15:28 +0400 |
---|---|---|
committer | Marc-André Lureau <marcandre.lureau@redhat.com> | 2021-12-21 10:50:22 +0400 |
commit | 739362d4205cd90686118fe5af3e236c2f8c6be9 (patch) | |
tree | 10d27d3efc227f29ffee0dd02d345e0faa707623 /audio/dbusaudio.c | |
parent | b4dd5b6a60eb525437c8d315d0d59dc25d4e4cb1 (diff) |
audio: add "dbus" audio backend
Add a new -audio backend that accepts D-Bus clients/listeners to handle
playback & recording, to be exported via the -display dbus.
Example usage:
-audiodev dbus,in.mixing-engine=off,out.mixing-engine=off,id=dbus
-display dbus,audiodev=dbus
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Acked-by: Gerd Hoffmann <kraxel@redhat.com>
Diffstat (limited to 'audio/dbusaudio.c')
-rw-r--r-- | audio/dbusaudio.c | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/audio/dbusaudio.c b/audio/dbusaudio.c new file mode 100644 index 0000000000..f178b47dee --- /dev/null +++ b/audio/dbusaudio.c @@ -0,0 +1,654 @@ +/* + * QEMU DBus audio + * + * Copyright (c) 2021 Red Hat, 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 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 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. + */ + +#include "qemu/osdep.h" +#include "qemu/error-report.h" +#include "qemu/host-utils.h" +#include "qemu/module.h" +#include "qemu/timer.h" +#include "qemu/dbus.h" + +#include <gio/gunixfdlist.h> +#include "ui/dbus-display1.h" + +#define AUDIO_CAP "dbus" +#include "audio.h" +#include "audio_int.h" +#include "trace.h" + +#define DBUS_DISPLAY1_AUDIO_PATH DBUS_DISPLAY1_ROOT "/Audio" + +#define DBUS_AUDIO_NSAMPLES 1024 /* could be configured? */ + +typedef struct DBusAudio { + GDBusObjectManagerServer *server; + GDBusObjectSkeleton *audio; + QemuDBusDisplay1Audio *iface; + GHashTable *out_listeners; + GHashTable *in_listeners; +} DBusAudio; + +typedef struct DBusVoiceOut { + HWVoiceOut hw; + bool enabled; + RateCtl rate; + + void *buf; + size_t buf_pos; + size_t buf_size; + + bool has_volume; + Volume volume; +} DBusVoiceOut; + +typedef struct DBusVoiceIn { + HWVoiceIn hw; + bool enabled; + RateCtl rate; + + bool has_volume; + Volume volume; +} DBusVoiceIn; + +static void *dbus_get_buffer_out(HWVoiceOut *hw, size_t *size) +{ + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + + if (!vo->buf) { + vo->buf_size = hw->samples * hw->info.bytes_per_frame; + vo->buf = g_malloc(vo->buf_size); + vo->buf_pos = 0; + } + + *size = MIN(vo->buf_size - vo->buf_pos, *size); + *size = audio_rate_get_bytes(&hw->info, &vo->rate, *size); + + return vo->buf + vo->buf_pos; + +} + +static size_t dbus_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GVariant) v_data = NULL; + + assert(buf == vo->buf + vo->buf_pos && vo->buf_pos + size <= vo->buf_size); + vo->buf_pos += size; + + trace_dbus_audio_put_buffer_out(size); + + if (vo->buf_pos < vo->buf_size) { + return size; + } + + bytes = g_bytes_new_take(g_steal_pointer(&vo->buf), vo->buf_size); + v_data = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE); + g_variant_ref_sink(v_data); + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_out_listener_call_write( + listener, + (uintptr_t)hw, + v_data, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + + return size; +} + +#ifdef HOST_WORDS_BIGENDIAN +#define AUDIO_HOST_BE TRUE +#else +#define AUDIO_HOST_BE FALSE +#endif + +static void +dbus_init_out_listener(QemuDBusDisplay1AudioOutListener *listener, + HWVoiceOut *hw) +{ + qemu_dbus_display1_audio_out_listener_call_init( + listener, + (uintptr_t)hw, + hw->info.bits, + hw->info.is_signed, + hw->info.is_float, + hw->info.freq, + hw->info.nchannels, + hw->info.bytes_per_frame, + hw->info.bytes_per_second, + hw->info.swap_endianness ? !AUDIO_HOST_BE : AUDIO_HOST_BE, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static int +dbus_init_out(HWVoiceOut *hw, struct audsettings *as, void *drv_opaque) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + audio_pcm_init_info(&hw->info, as); + hw->samples = DBUS_AUDIO_NSAMPLES; + audio_rate_start(&vo->rate); + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_init_out_listener(listener, hw); + } + return 0; +} + +static void +dbus_fini_out(HWVoiceOut *hw) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_out_listener_call_fini( + listener, + (uintptr_t)hw, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + + g_clear_pointer(&vo->buf, g_free); +} + +static void +dbus_enable_out(HWVoiceOut *hw, bool enable) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + vo->enabled = enable; + if (enable) { + audio_rate_start(&vo->rate); + } + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_out_listener_call_set_enabled( + listener, (uintptr_t)hw, enable, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } +} + +static void +dbus_volume_out_listener(HWVoiceOut *hw, + QemuDBusDisplay1AudioOutListener *listener) +{ + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + Volume *vol = &vo->volume; + g_autoptr(GBytes) bytes = NULL; + GVariant *v_vol = NULL; + + if (!vo->has_volume) { + return; + } + + assert(vol->channels < sizeof(vol->vol)); + bytes = g_bytes_new(vol->vol, vol->channels); + v_vol = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE); + qemu_dbus_display1_audio_out_listener_call_set_volume( + listener, (uintptr_t)hw, vol->mute, v_vol, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void +dbus_volume_out(HWVoiceOut *hw, Volume *vol) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioOutListener *listener = NULL; + + vo->has_volume = true; + vo->volume = *vol; + + g_hash_table_iter_init(&iter, da->out_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_volume_out_listener(hw, listener); + } +} + +static void +dbus_init_in_listener(QemuDBusDisplay1AudioInListener *listener, HWVoiceIn *hw) +{ + qemu_dbus_display1_audio_in_listener_call_init( + listener, + (uintptr_t)hw, + hw->info.bits, + hw->info.is_signed, + hw->info.is_float, + hw->info.freq, + hw->info.nchannels, + hw->info.bytes_per_frame, + hw->info.bytes_per_second, + hw->info.swap_endianness ? !AUDIO_HOST_BE : AUDIO_HOST_BE, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static int +dbus_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + audio_pcm_init_info(&hw->info, as); + hw->samples = DBUS_AUDIO_NSAMPLES; + audio_rate_start(&vo->rate); + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_init_in_listener(listener, hw); + } + return 0; +} + +static void +dbus_fini_in(HWVoiceIn *hw) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_in_listener_call_fini( + listener, + (uintptr_t)hw, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } +} + +static void +dbus_volume_in_listener(HWVoiceIn *hw, + QemuDBusDisplay1AudioInListener *listener) +{ + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + Volume *vol = &vo->volume; + g_autoptr(GBytes) bytes = NULL; + GVariant *v_vol = NULL; + + if (!vo->has_volume) { + return; + } + + assert(vol->channels < sizeof(vol->vol)); + bytes = g_bytes_new(vol->vol, vol->channels); + v_vol = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE); + qemu_dbus_display1_audio_in_listener_call_set_volume( + listener, (uintptr_t)hw, vol->mute, v_vol, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void +dbus_volume_in(HWVoiceIn *hw, Volume *vol) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + vo->has_volume = true; + vo->volume = *vol; + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + dbus_volume_in_listener(hw, listener); + } +} + +static size_t +dbus_read(HWVoiceIn *hw, void *buf, size_t size) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + /* DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); */ + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + trace_dbus_audio_read(size); + + /* size = audio_rate_get_bytes(&hw->info, &vo->rate, size); */ + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + g_autoptr(GVariant) v_data = NULL; + const char *data; + gsize n = 0; + + if (qemu_dbus_display1_audio_in_listener_call_read_sync( + listener, + (uintptr_t)hw, + size, + G_DBUS_CALL_FLAGS_NONE, -1, + &v_data, NULL, NULL)) { + data = g_variant_get_fixed_array(v_data, &n, 1); + g_warn_if_fail(n <= size); + size = MIN(n, size); + memcpy(buf, data, size); + break; + } + } + + return size; +} + +static void +dbus_enable_in(HWVoiceIn *hw, bool enable) +{ + DBusAudio *da = (DBusAudio *)hw->s->drv_opaque; + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + GHashTableIter iter; + QemuDBusDisplay1AudioInListener *listener = NULL; + + vo->enabled = enable; + if (enable) { + audio_rate_start(&vo->rate); + } + + g_hash_table_iter_init(&iter, da->in_listeners); + while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) { + qemu_dbus_display1_audio_in_listener_call_set_enabled( + listener, (uintptr_t)hw, enable, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } +} + +static void * +dbus_audio_init(Audiodev *dev) +{ + DBusAudio *da = g_new0(DBusAudio, 1); + + da->out_listeners = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_object_unref); + da->in_listeners = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_object_unref); + return da; +} + +static void +dbus_audio_fini(void *opaque) +{ + DBusAudio *da = opaque; + + if (da->server) { + g_dbus_object_manager_server_unexport(da->server, + DBUS_DISPLAY1_AUDIO_PATH); + } + g_clear_object(&da->audio); + g_clear_object(&da->iface); + g_clear_pointer(&da->in_listeners, g_hash_table_unref); + g_clear_pointer(&da->out_listeners, g_hash_table_unref); + g_clear_object(&da->server); + g_free(da); +} + +static void +listener_out_vanished_cb(GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + DBusAudio *da) +{ + char *name = g_object_get_data(G_OBJECT(connection), "name"); + + g_hash_table_remove(da->out_listeners, name); +} + +static void +listener_in_vanished_cb(GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + DBusAudio *da) +{ + char *name = g_object_get_data(G_OBJECT(connection), "name"); + + g_hash_table_remove(da->in_listeners, name); +} + +static gboolean +dbus_audio_register_listener(AudioState *s, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener, + bool out) +{ + DBusAudio *da = s->drv_opaque; + const char *sender = g_dbus_method_invocation_get_sender(invocation); + g_autoptr(GDBusConnection) listener_conn = NULL; + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) socket_conn = NULL; + g_autofree char *guid = g_dbus_generate_guid(); + GHashTable *listeners = out ? da->out_listeners : da->in_listeners; + GObject *listener; + int fd; + + trace_dbus_audio_register(sender, out ? "out" : "in"); + + if (g_hash_table_contains(listeners, sender)) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "`%s` is already registered!", + sender); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_listener), &err); + if (err) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't get peer fd: %s", + err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + socket = g_socket_new_from_fd(fd, &err); + if (err) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't make a socket: %s", + err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + socket_conn = g_socket_connection_factory_create_connection(socket); + if (out) { + qemu_dbus_display1_audio_complete_register_out_listener( + da->iface, invocation, NULL); + } else { + qemu_dbus_display1_audio_complete_register_in_listener( + da->iface, invocation, NULL); + } + + listener_conn = + g_dbus_connection_new_sync( + G_IO_STREAM(socket_conn), + guid, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER, + NULL, NULL, &err); + if (err) { + error_report("Failed to setup peer connection: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + listener = out ? + G_OBJECT(qemu_dbus_display1_audio_out_listener_proxy_new_sync( + listener_conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + "/org/qemu/Display1/AudioOutListener", + NULL, + &err)) : + G_OBJECT(qemu_dbus_display1_audio_in_listener_proxy_new_sync( + listener_conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + "/org/qemu/Display1/AudioInListener", + NULL, + &err)); + if (!listener) { + error_report("Failed to setup proxy: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (out) { + HWVoiceOut *hw; + + QLIST_FOREACH(hw, &s->hw_head_out, entries) { + DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw); + QemuDBusDisplay1AudioOutListener *l = + QEMU_DBUS_DISPLAY1_AUDIO_OUT_LISTENER(listener); + + dbus_init_out_listener(l, hw); + qemu_dbus_display1_audio_out_listener_call_set_enabled( + l, (uintptr_t)hw, vo->enabled, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + } else { + HWVoiceIn *hw; + + QLIST_FOREACH(hw, &s->hw_head_in, entries) { + DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); + QemuDBusDisplay1AudioInListener *l = + QEMU_DBUS_DISPLAY1_AUDIO_IN_LISTENER(listener); + + dbus_init_in_listener( + QEMU_DBUS_DISPLAY1_AUDIO_IN_LISTENER(listener), hw); + qemu_dbus_display1_audio_in_listener_call_set_enabled( + l, (uintptr_t)hw, vo->enabled, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + } + + g_object_set_data_full(G_OBJECT(listener_conn), "name", + g_strdup(sender), g_free); + g_hash_table_insert(listeners, g_strdup(sender), listener); + g_object_connect(listener_conn, + "signal::closed", + out ? listener_out_vanished_cb : listener_in_vanished_cb, + da, + NULL); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_audio_register_out_listener(AudioState *s, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener) +{ + return dbus_audio_register_listener(s, invocation, + fd_list, arg_listener, true); + +} + +static gboolean +dbus_audio_register_in_listener(AudioState *s, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener) +{ + return dbus_audio_register_listener(s, invocation, + fd_list, arg_listener, false); +} + +static void +dbus_audio_set_server(AudioState *s, GDBusObjectManagerServer *server) +{ + DBusAudio *da = s->drv_opaque; + + g_assert(da); + g_assert(!da->server); + + da->server = g_object_ref(server); + + da->audio = g_dbus_object_skeleton_new(DBUS_DISPLAY1_AUDIO_PATH); + da->iface = qemu_dbus_display1_audio_skeleton_new(); + g_object_connect(da->iface, + "swapped-signal::handle-register-in-listener", + dbus_audio_register_in_listener, s, + "swapped-signal::handle-register-out-listener", + dbus_audio_register_out_listener, s, + NULL); + + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(da->audio), + G_DBUS_INTERFACE_SKELETON(da->iface)); + g_dbus_object_manager_server_export(da->server, da->audio); +} + +static struct audio_pcm_ops dbus_pcm_ops = { + .init_out = dbus_init_out, + .fini_out = dbus_fini_out, + .write = audio_generic_write, + .get_buffer_out = dbus_get_buffer_out, + .put_buffer_out = dbus_put_buffer_out, + .enable_out = dbus_enable_out, + .volume_out = dbus_volume_out, + + .init_in = dbus_init_in, + .fini_in = dbus_fini_in, + .read = dbus_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = dbus_enable_in, + .volume_in = dbus_volume_in, +}; + +static struct audio_driver dbus_audio_driver = { + .name = "dbus", + .descr = "Timer based audio exposed with DBus interface", + .init = dbus_audio_init, + .fini = dbus_audio_fini, + .set_dbus_server = dbus_audio_set_server, + .pcm_ops = &dbus_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof(DBusVoiceOut), + .voice_size_in = sizeof(DBusVoiceIn) +}; + +static void register_audio_dbus(void) +{ + audio_driver_register(&dbus_audio_driver); +} +type_init(register_audio_dbus); + +module_dep("ui-dbus") |