diff --git a/build-aux/flatpak/ca.andyholmes.Valent.json b/build-aux/flatpak/ca.andyholmes.Valent.json index b7d76947186..e0de2e7e443 100644 --- a/build-aux/flatpak/ca.andyholmes.Valent.json +++ b/build-aux/flatpak/ca.andyholmes.Valent.json @@ -9,6 +9,7 @@ "--env=PULSE_PROP_media.category=Manager", "--filesystem=xdg-download", "--filesystem=xdg-run/gvfsd", + "--filesystem=xdg-run/pipewire-0:ro", "--own-name=org.mpris.MediaPlayer2.Valent", "--share=ipc", "--share=network", @@ -201,6 +202,34 @@ } ] }, + { + "name" : "pipewire", + "buildsystem" : "meson", + "builddir" : true, + "config-opts" : [ + "-Dgstreamer=disabled", + "-Dman=disabled", + "-Dsystemd=disabled", + "-Dudev=disabled", + "-Dudevrulesdir=disabled", + "-Dsession-managers=[]", + "-Djack=enabled" + ], + "sources" : [ + { + "type" : "git", + "url" : "https://gitlab.freedesktop.org/pipewire/pipewire.git", + "commit" : "9f7d60c1e84cc0481afc3f6ccf76e127567943a8", + "tag" : "0.3.70", + "x-checker-data" : { + "type" : "anitya", + "project-id" : 57357, + "stable-only" : true, + "tag-template" : "$version" + } + } + ] + }, { "name" : "valent", "buildsystem" : "meson", diff --git a/meson.build b/meson.build index 6dd85ab2366..fae1ce0a42e 100644 --- a/meson.build +++ b/meson.build @@ -93,6 +93,8 @@ project_c_args = [ '-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_72', '-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_72', '-DGDK_VERSION_MIN_REQUIRED=GDK_VERSION_4_8', + + '-Wno-error=analyzer-va-arg-type-mismatch', ] project_link_args = [ '-Wl,-z,relro', diff --git a/meson_options.txt b/meson_options.txt index 46f548bce3c..0855c6e3c02 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -170,6 +170,12 @@ option('plugin_ping', value: true, ) +option('plugin_pipewire', + description: 'Enable Pipewire plugin', + type: 'boolean', + value: true, +) + option('plugin_presenter', description: 'Enable Presenter plugin', type: 'boolean', diff --git a/src/plugins/meson.build b/src/plugins/meson.build index 62fa5b6d10c..725c267d363 100644 --- a/src/plugins/meson.build +++ b/src/plugins/meson.build @@ -25,6 +25,7 @@ plugins = [ 'notification', 'photo', 'ping', + 'pipewire', 'presenter', 'pulseaudio', 'runcommand', diff --git a/src/plugins/pipewire/data/valent-pipewire-plugin-symbolic.svg b/src/plugins/pipewire/data/valent-pipewire-plugin-symbolic.svg new file mode 100644 index 00000000000..0a6415750ff --- /dev/null +++ b/src/plugins/pipewire/data/valent-pipewire-plugin-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/pipewire/meson.build b/src/plugins/pipewire/meson.build new file mode 100644 index 00000000000..9d875a55e66 --- /dev/null +++ b/src/plugins/pipewire/meson.build @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: Andy Holmes + +# pipewire (Pipewire) +libpipewire_dep = dependency('libpipewire-0.3', version: '>= 0.3') + +# Dependencies +plugin_pipewire_deps = [ + libvalent_dep, + + libpipewire_dep, +] + +# Sources +plugin_pipewire_sources = files([ + 'pipewire-plugin.c', + 'valent-pipewire-mixer.c', + 'valent-pipewire-stream.c', +]) + +plugin_pipewire_include_directories = [include_directories('.')] + +# Resources +plugin_pipewire_info = i18n.merge_file( + input: 'pipewire.plugin.desktop.in', + output: 'pipewire.plugin', + po_dir: po_dir, + type: 'desktop', +) + +plugin_pipewire_resources = gnome.compile_resources('pipewire-resources', + 'pipewire.gresource.xml', + c_name: 'pipewire', + dependencies: [plugin_pipewire_info], +) +plugin_pipewire_sources += plugin_pipewire_resources + +# Static Build +plugin_pipewire = static_library('plugin-pipewire', + plugin_pipewire_sources, + include_directories: plugin_pipewire_include_directories, + dependencies: plugin_pipewire_deps, + c_args: plugins_c_args + release_args, +) + +#plugins_link_args += ['-Wl,--undefined=valent_pipewire_plugin_register_types'] +plugins_static += [plugin_pipewire] + diff --git a/src/plugins/pipewire/pipewire-plugin.c b/src/plugins/pipewire/pipewire-plugin.c new file mode 100644 index 00000000000..c4aa40995b3 --- /dev/null +++ b/src/plugins/pipewire/pipewire-plugin.c @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Andy Holmes + +#include "config.h" + +#include +#include + +#include "valent-pipewire-mixer.h" + + +G_MODULE_EXPORT void +valent_pipewire_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + VALENT_TYPE_MIXER_ADAPTER, + VALENT_TYPE_PIPEWIRE_MIXER); +} diff --git a/src/plugins/pipewire/pipewire.gresource.xml b/src/plugins/pipewire/pipewire.gresource.xml new file mode 100644 index 00000000000..49471f97c15 --- /dev/null +++ b/src/plugins/pipewire/pipewire.gresource.xml @@ -0,0 +1,14 @@ + + + + + + + + pipewire.plugin + + + data/valent-pipewire-plugin-symbolic.svg + + + diff --git a/src/plugins/pipewire/pipewire.plugin.desktop.in b/src/plugins/pipewire/pipewire.plugin.desktop.in new file mode 100644 index 00000000000..16dde515a1d --- /dev/null +++ b/src/plugins/pipewire/pipewire.plugin.desktop.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: Andy Holmes + +[Plugin] +Module=pipewire +Name=PipeWire +Description=Integration with PipeWire +Icon=valent-pipewire-plugin-symbolic +Builtin=true +Embedded=valent_pipewire_plugin_register_types +Website=https://github.com/andyholmes/valent +Help=https://github.com/andyholmes/valent +Hidden=false +X-MixerAdapterPriority=50 + diff --git a/src/plugins/pipewire/valent-pipewire-mixer.c b/src/plugins/pipewire/valent-pipewire-mixer.c new file mode 100644 index 00000000000..8a36cb43c2a --- /dev/null +++ b/src/plugins/pipewire/valent-pipewire-mixer.c @@ -0,0 +1,1392 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Andy Holmes + +#define G_LOG_DOMAIN "valent-pipewire-mixer" + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "valent-pipewire-mixer.h" +#include "valent-pipewire-stream.h" + +#define MIXER_DEVICE "Audio/Device" +#define MIXER_SINK "Audio/Sink" +#define MIXER_SOURCE "Audio/Source" + + +struct _ValentPipewireMixer +{ + ValentMixerAdapter parent_instance; + + GHashTable *streams; + char *default_input; + char *default_output; + + /* PipeWire */ + struct pw_thread_loop *loop; + struct pw_context *context; + + struct pw_core *core; + struct spa_hook core_listener; + struct pw_registry *registry; + struct spa_hook registry_listener; + struct pw_metadata *metadata; + struct spa_hook metadata_listener; + struct spa_list devices; + struct spa_list nodes; + + gboolean closed; +}; + +G_DEFINE_FINAL_TYPE (ValentPipewireMixer, valent_pipewire_mixer, VALENT_TYPE_MIXER_ADAPTER) + + +/* + * Pipewire + */ +struct node_data +{ + ValentPipewireMixer *adapter; + + uint32_t id; + uint64_t serial; + uint32_t device_id; + + struct pw_node *proxy; + struct spa_hook proxy_listener; + struct spa_hook object_listener; + struct spa_list link; + + /* State*/ + char *node_name; + char *node_description; + enum spa_direction direction; + float volume; + uint8_t n_channels; + bool mute; +}; + +struct device_data +{ + ValentPipewireMixer *adapter; + + uint32_t id; + uint64_t serial; + + struct pw_device *proxy; + struct spa_hook proxy_listener; + struct spa_hook object_listener; + struct spa_list link; + + /* State*/ + char *input_description; + uint32_t input_device; + uint32_t input_port; + char *output_description; + uint32_t output_device; + uint32_t output_port; +}; + +struct registry_data +{ + ValentPipewireMixer *adapter; + + struct pw_registry *registry; + struct pw_proxy *proxy; +}; + +static const struct pw_node_events node_events; +static const struct pw_proxy_events node_proxy_events; +static const struct pw_device_events device_events; +static const struct pw_proxy_events device_proxy_events; +static const struct pw_core_events core_events; +static const struct pw_metadata_events metadata_events; +static const struct pw_registry_events registry_events; + +static void valent_pipewire_mixer_open (ValentPipewireMixer *self); +static void valent_pipewire_mixer_close (ValentPipewireMixer *self); + + +static inline struct device_data * +valent_pipewire_mixer_lookup_device (ValentPipewireMixer *self, + uint32_t device_id) +{ + struct device_data *device = { 0, }; + + spa_list_for_each (device, &self->devices, link) + { + if (device->id == device_id) + return device; + } + + return NULL; +} + +static inline struct node_data * +valent_pipewire_mixer_lookup_device_node (ValentPipewireMixer *self, + uint32_t device_id, + enum spa_direction direction) +{ + struct device_data *device = { 0, }; + struct node_data *node = { 0, }; + + if ((device = valent_pipewire_mixer_lookup_device (self, device_id)) == NULL) + return NULL; + + spa_list_for_each (node, &self->nodes, link) + { + if (node->device_id == device->id && node->direction == direction) + return node; + } + + return NULL; +} + +static inline struct node_data * +valent_pipewire_mixer_lookup_node (ValentPipewireMixer *self, + uint32_t node_id) +{ + struct node_data *node = { 0, }; + + spa_list_for_each (node, &self->nodes, link) + { + if (node->id == node_id) + return node; + } + + return NULL; +} + +static inline struct node_data * +valent_pipewire_mixer_lookup_node_name (ValentPipewireMixer *self, + const char *node_name) +{ + struct node_data *node = { 0, }; + + spa_list_for_each (node, &self->nodes, link) + { + if (g_strcmp0 (node->node_name, node_name) == 0) + return node; + } + + return NULL; +} + + +/* + * ValentMixerAdapter <-> PipeWire + */ +typedef struct +{ + GRecMutex mutex; + + ValentPipewireMixer *adapter; + uint32_t device_id; + uint32_t node_id; + + /* ValentMixerStream */ + char *name; + char *description; + ValentMixerDirection direction; + gboolean muted; + uint32_t level; +} StreamState; + +static inline void +stream_state_free (gpointer data) +{ + StreamState *state = (StreamState *)data; + + g_rec_mutex_lock (&state->mutex); + g_clear_object (&state->adapter); + g_clear_pointer (&state->name, g_free); + g_clear_pointer (&state->description, g_free); + g_rec_mutex_unlock (&state->mutex); + g_rec_mutex_clear (&state->mutex); + g_clear_pointer (&state, g_free); +} + +static inline StreamState * +stream_state_new (ValentPipewireMixer *self, + struct node_data *node) +{ + struct device_data *device = NULL; + StreamState *state = NULL; + ValentMixerDirection direction; + g_autofree char *description = NULL; + + g_assert (VALENT_IS_PIPEWIRE_MIXER (self)); + + device = valent_pipewire_mixer_lookup_device (self, node->device_id); + + if (node->direction == SPA_DIRECTION_INPUT) + direction = VALENT_MIXER_INPUT; + else + direction = VALENT_MIXER_OUTPUT; + + if (direction == VALENT_MIXER_INPUT && + (device != NULL && device->input_description != NULL)) + { + description = g_strdup_printf ("%s (%s)", + device->input_description, + node->node_description); + } + else if (direction == VALENT_MIXER_OUTPUT && + (device != NULL && device->output_description != NULL)) + { + description = g_strdup_printf ("%s (%s)", + device->output_description, + node->node_description); + } + else + { + description = g_strdup (node->node_description); + } + + state = g_new0 (StreamState, 1); + g_rec_mutex_init (&state->mutex); + g_rec_mutex_lock (&state->mutex); + state->adapter = g_object_ref (self); + state->device_id = node->device_id; + state->node_id = node->id; + + state->name = g_strdup (node->node_name); + state->description = g_steal_pointer (&description); + state->direction = direction; + state->level = (uint32_t)ceil (cbrt (node->volume) * 100.0); + state->muted = !!node->mute; + g_rec_mutex_unlock (&state->mutex); + + return state; +} + +static inline gboolean +stream_state_flush (gpointer data) +{ + StreamState *state = (StreamState *)data; + ValentPipewireMixer *self = NULL; + ValentMixerStream *stream = NULL; + + g_assert (VALENT_IS_MAIN_THREAD ()); + + g_rec_mutex_lock (&state->mutex); + self = VALENT_PIPEWIRE_MIXER (state->adapter); + + if (g_atomic_int_get (&self->closed)) + goto closed; + + if ((stream = g_hash_table_lookup (self->streams, state->name)) == NULL) + { + stream = g_object_new (VALENT_TYPE_PIPEWIRE_STREAM, + "adapter", self, + "device-id", state->device_id, + "node-id", state->node_id, + "name", state->name, + "direction", state->direction, + "level", state->level, + "muted", state->muted, + NULL); + valent_pipewire_stream_update (VALENT_PIPEWIRE_STREAM (stream), + state->description, + state->level, + state->muted); + + /* Ensure there is a default stream set when `items-changed` is emitted */ + if (self->default_input == NULL && state->direction == VALENT_MIXER_INPUT) + self->default_input = g_strdup (state->name); + if (self->default_output == NULL && state->direction == VALENT_MIXER_OUTPUT) + self->default_output = g_strdup (state->name); + + g_hash_table_replace (self->streams, g_strdup (state->name), stream); + valent_mixer_adapter_stream_added (VALENT_MIXER_ADAPTER (self), stream); + } + else + { + valent_pipewire_stream_update (VALENT_PIPEWIRE_STREAM (stream), + state->description, + state->level, + state->muted); + } + +closed: + g_rec_mutex_unlock (&state->mutex); + + return G_SOURCE_REMOVE; +} + +static inline int +stream_state_main (struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct node_data *ndata = (struct node_data *)user_data; + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (ndata->adapter); + struct device_data *ddata = NULL; + StreamState *state = NULL; + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return 0; + + if ((ddata = valent_pipewire_mixer_lookup_device (self, ndata->device_id)) == NULL) + return 0; + + state = stream_state_new (self, ndata); + g_main_context_invoke_full (NULL, + G_PRIORITY_DEFAULT, + stream_state_flush, + g_steal_pointer (&state), + stream_state_free); + + return 0; +} + +static inline int +stream_state_update (struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + StreamState *state = (StreamState *)user_data; + struct node_data *ndata = NULL; + struct device_data *ddata = NULL; + struct spa_pod_builder builder; + struct spa_pod_frame f[2]; + struct spa_pod *param; + char buffer[1024] = { 0, }; + float volumes[SPA_AUDIO_MAX_CHANNELS] = { 0.0, }; + float volume = 0.0; + uint32_t route_device = 0; + uint32_t route_index = 0; + + VALENT_ENTRY; + + g_rec_mutex_lock (&state->mutex); + if (valent_object_in_destruction (VALENT_OBJECT (state->adapter))) + VALENT_GOTO (closed); + + ndata = valent_pipewire_mixer_lookup_node (state->adapter, state->node_id); + ddata = valent_pipewire_mixer_lookup_device (state->adapter, state->device_id); + + if (ndata == NULL || ddata == NULL) + VALENT_GOTO (closed); + + if (ndata->direction == SPA_DIRECTION_OUTPUT) + { + route_device = ddata->output_device; + route_index = ddata->output_port; + } + else if (ndata->direction == SPA_DIRECTION_INPUT) + { + route_device = ddata->input_device; + route_index = ddata->input_port; + } + + volume = ((float)state->level / 100); + for (uint32_t i = 0; i < ndata->n_channels; i++) + volumes[i] = volume * volume * volume; + + + builder = SPA_POD_BUILDER_INIT (buffer, sizeof (buffer)); + spa_pod_builder_push_object (&builder, &f[0], + SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route); + spa_pod_builder_add (&builder, + SPA_PARAM_ROUTE_index, SPA_POD_Int (route_index), + SPA_PARAM_ROUTE_device, SPA_POD_Int (route_device), + 0); + + spa_pod_builder_prop (&builder, SPA_PARAM_ROUTE_props, 0); + spa_pod_builder_push_object (&builder, &f[1], + SPA_TYPE_OBJECT_Props, SPA_PARAM_Route); + spa_pod_builder_add (&builder, + SPA_PROP_mute, SPA_POD_Bool ((bool)state->muted), + SPA_PROP_channelVolumes, SPA_POD_Array (sizeof (float), + SPA_TYPE_Float, + ndata->n_channels, + volumes), + 0); + spa_pod_builder_pop (&builder, &f[1]); + + spa_pod_builder_prop (&builder, SPA_PARAM_ROUTE_save, 0); + spa_pod_builder_bool (&builder, true); + param = spa_pod_builder_pop (&builder, &f[0]); + + pw_device_set_param (ddata->proxy, SPA_PARAM_Route, 0, param); + +closed: + g_rec_mutex_unlock (&state->mutex); + g_clear_pointer (&state, stream_state_free); + + VALENT_RETURN (0); +} + + +typedef struct +{ + GRecMutex mutex; + + ValentPipewireMixer *adapter; + struct spa_source *source; + + char *default_input; + char *default_output; + GPtrArray *streams; +} MixerState; + +static inline void +mixer_state_free (gpointer data) +{ + MixerState *state = (MixerState *)data; + + g_rec_mutex_lock (&state->mutex); + g_clear_object (&state->adapter); + g_clear_pointer (&state->default_input, g_free); + g_clear_pointer (&state->default_output, g_free); + g_clear_pointer (&state->streams, g_ptr_array_unref); + g_rec_mutex_unlock (&state->mutex); + g_rec_mutex_clear (&state->mutex); + g_clear_pointer (&state, g_free); +} + +static inline gboolean +mixer_state_flush (gpointer data) +{ + MixerState *state = (MixerState *)data; + ValentPipewireMixer *self = NULL; + + g_assert (VALENT_IS_MAIN_THREAD ()); + + g_rec_mutex_lock (&state->mutex); + self = VALENT_PIPEWIRE_MIXER (state->adapter); + + if (!g_atomic_int_get (&self->closed)) + { + if (state->default_input != NULL) + { + if (valent_set_string (&self->default_input, state->default_input)) + g_object_notify (G_OBJECT (self), "default-input"); + } + + if (state->default_output != NULL) + { + if (valent_set_string (&self->default_output, state->default_output)) + g_object_notify (G_OBJECT (self), "default-output"); + } + } + g_rec_mutex_unlock (&state->mutex); + + return G_SOURCE_REMOVE; +} + +static inline gboolean +has_stream (gconstpointer a, + gconstpointer b) +{ + return g_strcmp0 (((StreamState *)a)->name, (const char *)b) == 0; +} + +static inline gboolean +mixer_streams_flush (gpointer data) +{ + MixerState *state = (MixerState *)data; + ValentPipewireMixer *self = NULL; + GHashTableIter iter; + const char *name; + ValentMixerStream *stream; + + g_assert (VALENT_IS_MAIN_THREAD ()); + + g_rec_mutex_lock (&state->mutex); + self = VALENT_PIPEWIRE_MIXER (state->adapter); + + if (!g_atomic_int_get (&self->closed)) + { + g_hash_table_iter_init (&iter, self->streams); + + while (g_hash_table_iter_next (&iter, (void **)&name, (void **)&stream)) + { + unsigned int index_ = 0; + + if (g_ptr_array_find_with_equal_func (state->streams, name, has_stream, &index_)) + { + g_ptr_array_remove_index (state->streams, index_); + continue; + } + + valent_mixer_adapter_stream_removed (VALENT_MIXER_ADAPTER (self), stream); + g_hash_table_iter_remove (&iter); + } + + for (unsigned int i = 0; i < state->streams->len; i++) + stream_state_flush (g_ptr_array_index (state->streams, i)); + } + g_rec_mutex_unlock (&state->mutex); + + return G_SOURCE_REMOVE; +} + + +/* + * Nodes + */ +static inline void +on_node_info (void *object, + const struct pw_node_info *info) +{ + struct node_data *ndata = (struct node_data *)object; + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (ndata->adapter); + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return; + + if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) + { + for (uint32_t i = 0; i < info->n_params; i++) + { + uint32_t id = info->params[i].id; + uint32_t flags = info->params[i].flags; + + if (id == SPA_PARAM_Props && (flags & SPA_PARAM_INFO_READ) != 0) + { + pw_node_enum_params (ndata->proxy, 0, id, 0, UINT32_MAX, NULL); + pw_core_sync (self->core, PW_ID_CORE, 0); + } + } + } +} + +static void +on_node_param (void *object, + int seq, + uint32_t id, + uint32_t index, + uint32_t next, + const struct spa_pod *param) +{ + struct node_data *ndata = (struct node_data *)object; + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (ndata->adapter); + gboolean notify = FALSE; + bool mute = false; + uint32_t csize, ctype; + uint32_t n_channels = 0; + float *volumes = NULL; + float volume = 0.0; + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return; + + if (id != SPA_PARAM_Props || param == NULL) + return; + + if (spa_pod_parse_object (param, SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_mute, SPA_POD_Bool (&mute), + SPA_PROP_volume, SPA_POD_Float (&volume), + SPA_PROP_channelVolumes, SPA_POD_Array (&csize, + &ctype, + &n_channels, + &volumes)) < 0) + return; + + if (ndata->mute != mute) + { + ndata->mute = mute; + notify = TRUE; + } + + if (n_channels > 0) + { + volume = 0.0; + + for (uint32_t i = 0; i < n_channels; i++) + volume = MAX (volume, volumes[i]); + } + + if (!G_APPROX_VALUE (ndata->volume, volume, 0.0000001)) + { + ndata->volume = volume; + ndata->n_channels = n_channels; + notify = TRUE; + } + + if (notify) + { + pw_loop_invoke (pw_thread_loop_get_loop (self->loop), + stream_state_main, + 0, + NULL, + 0, + false, + ndata); + } +} + +static const struct pw_node_events node_events = { + .info = on_node_info, + .param = on_node_param, +}; + + +static void +on_node_proxy_removed (void *data) +{ + struct node_data *ndata = data; + + VALENT_PROBE; + + spa_hook_remove (&ndata->object_listener); + pw_proxy_destroy ((struct pw_proxy*)ndata->proxy); +} + +static void +on_node_proxy_destroyed (void *data) +{ + struct node_data *ndata = (struct node_data *)data; + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (ndata->adapter); + MixerState *state = NULL; + + VALENT_NOTE ("id: %u, serial: %zu", ndata->id, ndata->serial); + + g_clear_pointer (&ndata->node_name, g_free); + g_clear_pointer (&ndata->node_description, g_free); + spa_list_remove (&ndata->link); + + if (valent_object_in_destruction (VALENT_OBJECT (ndata->adapter))) + return; + + state = g_new0 (MixerState, 1); + g_rec_mutex_init (&state->mutex); + g_rec_mutex_lock (&state->mutex); + state->adapter = g_object_ref (self); + state->streams = g_ptr_array_new_with_free_func (stream_state_free); + + spa_list_for_each (ndata, &self->nodes, link) + g_ptr_array_add (state->streams, stream_state_new (self, ndata)); + + g_rec_mutex_unlock (&state->mutex); + + g_main_context_invoke_full (NULL, + G_PRIORITY_DEFAULT, + mixer_streams_flush, + g_steal_pointer (&state), + mixer_state_free); +} + +static const struct pw_proxy_events node_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = on_node_proxy_removed, + .destroy = on_node_proxy_destroyed, +}; + + +static void +on_device_info (void *object, + const struct pw_device_info *info) +{ + struct device_data *ddata = (struct device_data *)object; + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (ddata->adapter); + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return; + + if ((info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) != 0) + { + for (uint32_t i = 0; i < info->n_params; i++) + { + uint32_t id = info->params[i].id; + uint32_t flags = info->params[i].flags; + + if (id == SPA_PARAM_Route && (flags & SPA_PARAM_INFO_READ) != 0) + { + pw_device_enum_params (ddata->proxy, 0, id, 0, UINT32_MAX, NULL); + pw_core_sync (self->core, PW_ID_CORE, 0); + } + } + } +} + +static void +on_device_param (void *data, + int seq, + uint32_t id, + uint32_t index, + uint32_t next, + const struct spa_pod *param) +{ + struct device_data *ddata = (struct device_data *)data; + struct node_data *ndata = NULL; + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (ddata->adapter); + const char *name; + const char *description; + uint32_t route_index = 0; + uint32_t route_device = 0; + enum spa_direction direction = 0; + enum spa_param_availability available = 0; + struct spa_pod *props = NULL; + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return; + + if (id != SPA_PARAM_Route || param == NULL) + return; + + if (spa_pod_parse_object (param, SPA_TYPE_OBJECT_ParamRoute, NULL, + SPA_PARAM_ROUTE_name, SPA_POD_String (&name), + SPA_PARAM_ROUTE_description, SPA_POD_String (&description), + SPA_PARAM_ROUTE_direction, SPA_POD_Id (&direction), + SPA_PARAM_ROUTE_index, SPA_POD_Int (&route_index), + SPA_PARAM_ROUTE_device, SPA_POD_Int (&route_device), + SPA_PARAM_ROUTE_available, SPA_POD_Id (&available), + SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod (&props)) < 0) + return; + + if (direction == SPA_DIRECTION_INPUT) + { + ddata->input_device = route_device; + ddata->input_port = route_index; + + if (!valent_set_string (&ddata->input_description, description)) + return; + } + else if (direction == SPA_DIRECTION_OUTPUT) + { + ddata->output_device = route_device; + ddata->output_port = route_index; + + if (!valent_set_string (&ddata->output_description, description)) + return; + } + + /* There may not be a node yet */ + ndata = valent_pipewire_mixer_lookup_device_node (self, ddata->id, direction); + + if (ndata != NULL) + { + pw_loop_invoke (pw_thread_loop_get_loop (self->loop), + stream_state_main, + 0, + NULL, + 0, + false, + ndata); + } +} + + +static const struct pw_device_events device_events = { + PW_VERSION_DEVICE_EVENTS, + .info = on_device_info, + .param = on_device_param, +}; + + +static void +on_device_proxy_removed (void *data) +{ + struct device_data *ddata = data; + + spa_hook_remove (&ddata->object_listener); + pw_proxy_destroy ((struct pw_proxy *)ddata->proxy); +} + +static void +on_device_proxy_destroyed (void *data) +{ + struct device_data *ddata = data; + + VALENT_NOTE ("id: %u, serial: %zu", ddata->id, ddata->serial); + + g_clear_pointer (&ddata->input_description, g_free); + g_clear_pointer (&ddata->output_description, g_free); + spa_list_remove (&ddata->link); +} + +static const struct pw_proxy_events device_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = on_device_proxy_removed, + .destroy = on_device_proxy_destroyed, +}; + + +static int +on_metadata_property (void *data, + uint32_t id, + const char *key, + const char *type, + const char *value) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (data); + MixerState *state = NULL; + g_autoptr (JsonNode) node = NULL; + JsonObject *root = NULL; + const char *name = NULL; + + VALENT_NOTE ("id: %u, key: %s, type: %s, value: %s", id, key, type, value); + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return 0; + + if G_UNLIKELY (key == NULL || type == NULL || value == NULL) + return 0; + + if (!g_str_equal (key, "default.audio.sink") && + !g_str_equal (key, "default.audio.source")) + return 0; + + if (!g_str_equal (type, "Spa:String:JSON")) + return 0; + + if ((node = json_from_string (value, NULL)) == NULL || + (root = json_node_get_object (node)) == NULL || + (name = json_object_get_string_member (root, "name")) == NULL) + { + g_warning ("%s(): Failed to parse metadata", G_STRFUNC); + return 0; + } + + state = g_new0 (MixerState, 1); + g_rec_mutex_init (&state->mutex); + g_rec_mutex_lock (&state->mutex); + state->adapter = g_object_ref (self); + + if (g_str_equal (key, "default.audio.sink")) + valent_set_string (&state->default_output, name); + else if (g_str_equal (key, "default.audio.source")) + valent_set_string (&state->default_input, name); + + g_rec_mutex_unlock (&state->mutex); + + g_main_context_invoke_full (NULL, + G_PRIORITY_DEFAULT, + mixer_state_flush, + g_steal_pointer (&state), + mixer_state_free); + pw_core_sync (self->core, PW_ID_CORE, 0); + + return 0; +} + +static const struct pw_metadata_events metadata_events = { + PW_VERSION_METADATA_EVENTS, + on_metadata_property +}; + + +/* + * Pipewire Registry + */ +static void +registry_event_global (void *data, + uint32_t id, + uint32_t permissions, + const char *type, + uint32_t version, + const struct spa_dict *props) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (data); + + if (valent_object_in_destruction (VALENT_OBJECT (self))) + return; + + if G_UNLIKELY (id == SPA_ID_INVALID) + return; + + if (g_strcmp0 (type, PW_TYPE_INTERFACE_Device) == 0) + { + struct pw_device *device = NULL; + struct device_data *ddata = NULL; + const char *media_class = NULL; + + VALENT_NOTE ("id: %u, permissions: %u, type: %s, version: %u", + id, permissions, type, version); + + /* Only audio devices are of interest, for now */ + media_class = spa_dict_lookup (props, PW_KEY_MEDIA_CLASS); + + if (g_strcmp0 (media_class, "Audio/Device") != 0) + return; + + device = pw_registry_bind (self->registry, id, type, + PW_VERSION_PORT, sizeof (*ddata)); + g_return_if_fail (device != NULL); + + ddata = pw_proxy_get_user_data ((struct pw_proxy *)device); + ddata->adapter = self; + ddata->proxy = device; + ddata->id = id; + + spa_list_append (&self->devices, &ddata->link); + pw_device_add_listener (ddata->proxy, + &ddata->object_listener, + &device_events, + ddata); + pw_proxy_add_listener ((struct pw_proxy *)ddata->proxy, + &ddata->proxy_listener, + &device_proxy_events, + ddata); + pw_core_sync (self->core, PW_ID_CORE, 0); + } + else if (g_strcmp0 (type, PW_TYPE_INTERFACE_Node) == 0) + { + struct pw_node *node = NULL; + struct node_data *ndata = NULL; + struct device_data *ddata = NULL; + uint32_t device_id; + const char *media_class = NULL; + + VALENT_NOTE ("id: %u, permissions: %u, type: %s, version: %u", + id, permissions, type, version); + + /* Only audio sinks and sources are of interest, for now */ + media_class = spa_dict_lookup (props, PW_KEY_MEDIA_CLASS); + + if (g_strcmp0 (media_class, "Audio/Sink") != 0 && + g_strcmp0 (media_class, "Audio/Source") != 0) + return; + + /* Only nodes with devices are of interest */ + if (!spa_atou32 (spa_dict_lookup (props, PW_KEY_DEVICE_ID), &device_id, 10) || + (ddata = valent_pipewire_mixer_lookup_device (self, device_id)) == NULL) + return; + + node = pw_registry_bind (self->registry, id, type, + PW_VERSION_NODE, sizeof (*ndata)); + g_return_if_fail (node != NULL); + + ndata = pw_proxy_get_user_data ((struct pw_proxy *)node); + ndata->adapter = self; + ndata->proxy = node; + ndata->id = id; + ndata->device_id = device_id; + + ndata->node_name = g_strdup (spa_dict_lookup (props, PW_KEY_NODE_NAME)); + ndata->node_description = g_strdup (spa_dict_lookup (props, PW_KEY_NODE_DESCRIPTION)); + + if (g_str_equal (media_class, "Audio/Sink")) + ndata->direction = SPA_DIRECTION_OUTPUT; + else if (g_str_equal (media_class, "Audio/Source")) + ndata->direction = SPA_DIRECTION_INPUT; + + spa_list_append (&self->nodes, &ndata->link); + pw_node_add_listener (ndata->proxy, + &ndata->object_listener, + &node_events, + ndata); + pw_proxy_add_listener ((struct pw_proxy *)ndata->proxy, + &ndata->proxy_listener, + &node_proxy_events, + ndata); + pw_core_sync (self->core, PW_ID_CORE, 0); + } + else if (g_strcmp0 (type, PW_TYPE_INTERFACE_Metadata) == 0) + { + const char *metadata_name = NULL; + + VALENT_NOTE ("id: %u, permissions: %u, type: %s, version: %u", + id, permissions, type, version); + + metadata_name = spa_dict_lookup (props, PW_KEY_METADATA_NAME); + + if (g_strcmp0 (metadata_name, "default") == 0) + { + if (self->metadata != NULL) + spa_hook_remove (&self->metadata_listener); + + self->metadata = pw_registry_bind (self->registry, id, type, + PW_VERSION_METADATA, 0); + + if (self->metadata != NULL) + { + pw_metadata_add_listener (self->metadata, + &self->metadata_listener, + &metadata_events, + self); + } + } + + pw_core_sync (self->core, PW_ID_CORE, 0); + } +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + + +static void +on_core_done (void *data, + uint32_t id, + int seq) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (data); + + VALENT_NOTE ("id: %u, seq: %d", id, seq); + + if (id == PW_ID_CORE) + pw_thread_loop_signal (self->loop, FALSE); +} + +static void +on_core_error (void *data, + uint32_t id, + int seq, + int res, + const char *message) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (data); + + VALENT_NOTE ("id: %u, seq: %i, res: %i, message: %s", id, seq, res, message); + + if (id == PW_ID_CORE) + g_warning ("%s(): %s (%i)", G_STRFUNC, message, res); + + pw_thread_loop_signal (self->loop, FALSE); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .done = on_core_done, + .error = on_core_error, +}; + + +/* + * + */ +static void +valent_pipewire_mixer_open (ValentPipewireMixer *self) +{ + struct pw_properties *context_properties = NULL; + g_autoptr (GError) error = NULL; + + g_assert (VALENT_IS_PIPEWIRE_MIXER (self)); + g_assert (VALENT_IS_MAIN_THREAD ()); + + self->loop = pw_thread_loop_new ("valent", NULL); + pw_thread_loop_lock (self->loop); + + if (self->loop == NULL || pw_thread_loop_start (self->loop) != 0) + { + pw_thread_loop_unlock (self->loop); + g_set_error_literal (&error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "failed to start the thread loop"); + valent_extension_plugin_state_changed (VALENT_EXTENSION (self), + VALENT_PLUGIN_STATE_ERROR, + error); + return; + } + + spa_list_init (&self->devices); + spa_list_init (&self->nodes); + + /* Register as a manager */ + context_properties = pw_properties_new (PW_KEY_CONFIG_NAME, "client-rt.conf", + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Manager", + PW_KEY_MEDIA_ROLE, "Music", + NULL); + self->context = pw_context_new (pw_thread_loop_get_loop (self->loop), + context_properties, + 0); + + if (self->context == NULL) + { + pw_thread_loop_unlock (self->loop); + g_set_error_literal (&error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "failed to create context"); + valent_extension_plugin_state_changed (VALENT_EXTENSION (self), + VALENT_PLUGIN_STATE_ERROR, + error); + return; + } + + /* Failure here usually means missing Flatpak permissions */ + self->core = pw_context_connect (self->context, NULL, 0); + + if (self->core == NULL) + { + pw_thread_loop_unlock (self->loop); + g_set_error_literal (&error, + G_IO_ERROR, + G_IO_ERROR_PERMISSION_DENIED, + "failed to connect context"); + valent_extension_plugin_state_changed (VALENT_EXTENSION (self), + VALENT_PLUGIN_STATE_ERROR, + error); + return; + } + + spa_zero (self->core_listener); + pw_core_add_listener (self->core, + &self->core_listener, + &core_events, + self); + + self->registry = pw_core_get_registry (self->core, PW_VERSION_REGISTRY, 0); + + if (self->registry == NULL) + { + pw_thread_loop_unlock (self->loop); + g_set_error_literal (&error, + G_IO_ERROR, + G_IO_ERROR_PERMISSION_DENIED, + "failed to connect to registry"); + valent_extension_plugin_state_changed (VALENT_EXTENSION (self), + VALENT_PLUGIN_STATE_ERROR, + error); + return; + } + + spa_zero (self->registry_listener); + pw_registry_add_listener (self->registry, + &self->registry_listener, + ®istry_events, + self); + pw_core_sync (self->core, PW_ID_CORE, 0); + pw_thread_loop_unlock (self->loop); +} + +static void +valent_pipewire_mixer_close (ValentPipewireMixer *self) +{ + VALENT_ENTRY; + + g_assert (VALENT_IS_PIPEWIRE_MIXER (self)); + g_assert (VALENT_IS_MAIN_THREAD ()); + + g_atomic_int_set (&self->closed, TRUE); + + if (self->loop != NULL) + { + pw_thread_loop_lock (self->loop); + + if (self->metadata != NULL) + { + spa_hook_remove (&self->metadata_listener); + pw_proxy_destroy ((struct pw_proxy *)self->metadata); + self->metadata = NULL; + } + + if (self->registry != NULL) + { + spa_hook_remove (&self->registry_listener); + pw_proxy_destroy ((struct pw_proxy *)self->registry); + self->registry = NULL; + } + + if (self->core != NULL) + { + spa_hook_remove (&self->core_listener); + g_clear_pointer (&self->core, pw_core_disconnect); + } + + g_clear_pointer (&self->context, pw_context_destroy); + + pw_thread_loop_unlock (self->loop); + pw_thread_loop_stop (self->loop); + + g_clear_pointer (&self->loop, pw_thread_loop_destroy); + } + + VALENT_EXIT; +} + + +/* + * ValentMixerAdapter + */ +static ValentMixerStream * +valent_pipewire_mixer_get_default_input (ValentMixerAdapter *adapter) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (adapter); + + return g_hash_table_lookup (self->streams, self->default_input); +} + +static void +valent_pipewire_mixer_set_default_input (ValentMixerAdapter *adapter, + ValentMixerStream *stream) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (adapter); + struct node_data *ndata = NULL; + const char *name = NULL; + + g_assert (VALENT_IS_PIPEWIRE_MIXER (self)); + g_assert (VALENT_IS_MIXER_STREAM (stream)); + + name = valent_mixer_stream_get_name (stream); + + if (g_strcmp0 (self->default_input, name) == 0) + return; + + pw_thread_loop_lock (self->loop); + if ((ndata = valent_pipewire_mixer_lookup_node_name (self, name)) != NULL) + { + g_autofree char *json = NULL; + + json = g_strdup_printf ("{\"name\": \"%s\"}", name); + pw_metadata_set_property (self->metadata, PW_ID_CORE, + "default.audio.source", "Spa:Id", json); + + /* Emit now, since we won't get notification from pipewire */ + if (valent_set_string (&self->default_input, name)) + g_object_notify (G_OBJECT (self), "default-input"); + } + pw_thread_loop_unlock (self->loop); +} + +static ValentMixerStream * +valent_pipewire_mixer_get_default_output (ValentMixerAdapter *adapter) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (adapter); + + return g_hash_table_lookup (self->streams, self->default_output); +} + +static void +valent_pipewire_mixer_set_default_output (ValentMixerAdapter *adapter, + ValentMixerStream *stream) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (adapter); + struct node_data *ndata = NULL; + const char *name = NULL; + + g_assert (VALENT_IS_PIPEWIRE_MIXER (self)); + g_assert (VALENT_IS_MIXER_STREAM (stream)); + + name = valent_mixer_stream_get_name (stream); + + if (g_strcmp0 (self->default_output, name) == 0) + return; + + pw_thread_loop_lock (self->loop); + if ((ndata = valent_pipewire_mixer_lookup_node_name (self, name)) != NULL) + { + g_autofree char *json = NULL; + + json = g_strdup_printf ("{\"name\": \"%s\"}", name); + pw_metadata_set_property (self->metadata, PW_ID_CORE, + "default.audio.sink", "Spa:Id", json); + + /* Emit now, since we won't get notification from pipewire */ + if (valent_set_string (&self->default_output, name)) + g_object_notify (G_OBJECT (self), "default-output"); + } + pw_thread_loop_unlock (self->loop); +} + +/* + * GObject + */ +static void +valent_pipewire_mixer_constructed (GObject *object) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (object); + + valent_pipewire_mixer_open (self); + + G_OBJECT_CLASS (valent_pipewire_mixer_parent_class)->constructed (object); +} + +static void +valent_pipewire_mixer_dispose (GObject *object) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (object); + + valent_pipewire_mixer_close (self); + g_hash_table_remove_all (self->streams); + + G_OBJECT_CLASS (valent_pipewire_mixer_parent_class)->dispose (object); +} + +static void +valent_pipewire_mixer_finalize (GObject *object) +{ + ValentPipewireMixer *self = VALENT_PIPEWIRE_MIXER (object); + + pw_deinit (); + g_clear_pointer (&self->streams, g_hash_table_unref); + + G_OBJECT_CLASS (valent_pipewire_mixer_parent_class)->finalize (object); +} + +static void +valent_pipewire_mixer_class_init (ValentPipewireMixerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ValentMixerAdapterClass *adapter_class = VALENT_MIXER_ADAPTER_CLASS (klass); + + object_class->constructed = valent_pipewire_mixer_constructed; + object_class->dispose = valent_pipewire_mixer_dispose; + object_class->finalize = valent_pipewire_mixer_finalize; + + adapter_class->get_default_input = valent_pipewire_mixer_get_default_input; + adapter_class->set_default_input = valent_pipewire_mixer_set_default_input; + adapter_class->get_default_output = valent_pipewire_mixer_get_default_output; + adapter_class->set_default_output = valent_pipewire_mixer_set_default_output; +} + +static void +valent_pipewire_mixer_init (ValentPipewireMixer *self) +{ + self->closed = FALSE; + self->streams = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); + pw_init (NULL, NULL); +} + +void +valent_pipewire_mixer_set_stream_state (ValentPipewireMixer *adapter, + uint32_t device_id, + uint32_t node_id, + unsigned int level, + gboolean muted) +{ + StreamState *state = NULL; + + g_assert (VALENT_IS_PIPEWIRE_MIXER (adapter)); + g_assert (device_id > 0); + g_assert (node_id > 0); + + VALENT_NOTE ("device: %u, node: %u, level: %u, muted: %u", + device_id, node_id, level, muted); + + state = g_new0 (StreamState, 1); + g_rec_mutex_init (&state->mutex); + g_rec_mutex_lock (&state->mutex); + state->adapter = g_object_ref (adapter); + state->device_id = device_id; + state->node_id = node_id; + state->level = level; + state->muted = muted; + g_rec_mutex_unlock (&state->mutex); + + pw_thread_loop_lock (adapter->loop); + pw_loop_invoke (pw_thread_loop_get_loop (adapter->loop), + stream_state_update, + 0, + NULL, + 0, + false, + state); + pw_thread_loop_unlock (adapter->loop); +} + diff --git a/src/plugins/pipewire/valent-pipewire-mixer.h b/src/plugins/pipewire/valent-pipewire-mixer.h new file mode 100644 index 00000000000..ca618ff2c6b --- /dev/null +++ b/src/plugins/pipewire/valent-pipewire-mixer.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Andy Holmes + +#pragma once + +#include + +G_BEGIN_DECLS + +#define VALENT_TYPE_PIPEWIRE_MIXER (valent_pipewire_mixer_get_type ()) + +G_DECLARE_FINAL_TYPE (ValentPipewireMixer, valent_pipewire_mixer, VALENT, PIPEWIRE_MIXER, ValentMixerAdapter) + +void valent_pipewire_mixer_set_stream_state (ValentPipewireMixer *adapter, + uint32_t device_id, + uint32_t node_id, + unsigned int level, + gboolean muted); + +G_END_DECLS + diff --git a/src/plugins/pipewire/valent-pipewire-stream.c b/src/plugins/pipewire/valent-pipewire-stream.c new file mode 100644 index 00000000000..57ba0a6320c --- /dev/null +++ b/src/plugins/pipewire/valent-pipewire-stream.c @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Andy Holmes + +#define G_LOG_DOMAIN "valent-pipewire-stream" + +#include "config.h" + +#include + +#include +#include + +#include "valent-pipewire-mixer.h" +#include "valent-pipewire-stream.h" + + +struct _ValentPipewireStream +{ + ValentMixerStream parent_instance; + + ValentPipewireMixer *adapter; + uint32_t device_id; + uint32_t node_id; + + char *description; + unsigned int level; + gboolean muted; +}; + +G_DEFINE_FINAL_TYPE (ValentPipewireStream, valent_pipewire_stream, VALENT_TYPE_MIXER_STREAM) + +enum { + PROP_0, + PROP_ADAPTER, + PROP_DEVICE_ID, + PROP_NODE_ID, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + + +/* + * ValentMixerStream + */ +static const char * +valent_pipewire_stream_get_description (ValentMixerStream *stream) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (stream); + + g_assert (VALENT_IS_PIPEWIRE_STREAM (self)); + + return self->description; +} + +static unsigned int +valent_pipewire_stream_get_level (ValentMixerStream *stream) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (stream); + + g_assert (VALENT_IS_PIPEWIRE_STREAM (self)); + + return self->level; +} + +static void +valent_pipewire_stream_set_level (ValentMixerStream *stream, + unsigned int level) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (stream); + + g_assert (VALENT_IS_PIPEWIRE_STREAM (self)); + + if (self->level == level || self->adapter == NULL) + return; + + valent_pipewire_mixer_set_stream_state (self->adapter, + self->device_id, + self->node_id, + level, + self->muted); +} + +static gboolean +valent_pipewire_stream_get_muted (ValentMixerStream *stream) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (stream); + + g_assert (VALENT_IS_PIPEWIRE_STREAM (self)); + + return self->muted; +} + +static void +valent_pipewire_stream_set_muted (ValentMixerStream *stream, + gboolean state) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (stream); + + g_assert (VALENT_IS_PIPEWIRE_STREAM (self)); + + if (self->muted == state || self->adapter == NULL) + return; + + valent_pipewire_mixer_set_stream_state (self->adapter, + self->device_id, + self->node_id, + self->level, + state); +} + +/* + * GObject + */ +static void +valent_pipewire_stream_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (object); + + switch (prop_id) + { + case PROP_ADAPTER: + g_value_set_object (value, self->adapter); + break; + + case PROP_DEVICE_ID: + g_value_set_uint (value, self->device_id); + break; + + case PROP_NODE_ID: + g_value_set_uint (value, self->node_id); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +valent_pipewire_stream_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (object); + + switch (prop_id) + { + case PROP_ADAPTER: + self->adapter = g_value_get_object (value); + g_object_add_weak_pointer (G_OBJECT (self->adapter), + (gpointer *)&self->adapter); + break; + + case PROP_DEVICE_ID: + self->device_id = g_value_get_uint (value); + break; + + case PROP_NODE_ID: + self->node_id = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +valent_pipewire_stream_finalize (GObject *object) +{ + ValentPipewireStream *self = VALENT_PIPEWIRE_STREAM (object); + + g_clear_weak_pointer (&self->adapter); + g_clear_pointer (&self->description, g_free); + + G_OBJECT_CLASS (valent_pipewire_stream_parent_class)->finalize (object); +} + +static void +valent_pipewire_stream_class_init (ValentPipewireStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ValentMixerStreamClass *stream_class = VALENT_MIXER_STREAM_CLASS (klass); + + object_class->finalize = valent_pipewire_stream_finalize; + object_class->get_property = valent_pipewire_stream_get_property; + object_class->set_property = valent_pipewire_stream_set_property; + + stream_class->get_description = valent_pipewire_stream_get_description; + stream_class->get_level = valent_pipewire_stream_get_level; + stream_class->set_level = valent_pipewire_stream_set_level; + stream_class->get_muted = valent_pipewire_stream_get_muted; + stream_class->set_muted = valent_pipewire_stream_set_muted; + + /** + * ValentPaStream:adapter: + * + * The #GvcMixerStream this stream wraps. + */ + properties [PROP_ADAPTER] = + g_param_spec_object ("adapter", NULL, NULL, + VALENT_TYPE_PIPEWIRE_MIXER, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + /** + * ValentPaStream:device-id: + * + * The PipeWire device ID. + */ + properties [PROP_DEVICE_ID] = + g_param_spec_uint ("device-id", NULL, NULL, + 0, G_MAXUINT32, + 0, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + /** + * ValentPaStream:node-id: + * + * The PipeWire node ID. + */ + properties [PROP_NODE_ID] = + g_param_spec_uint ("node-id", NULL, NULL, + 0, G_MAXUINT32, + 0, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +static void +valent_pipewire_stream_init (ValentPipewireStream *self) +{ +} + +/*< private > + * valent_pipewire_stream_update: + * @stream: `ValentPipewireStream` + * @description: the new description + * @level: the new volume level + * @state: the new mute state + * + * Update the stream state. + */ +void +valent_pipewire_stream_update (ValentPipewireStream *stream, + const char *description, + uint32_t level, + gboolean state) +{ + g_return_if_fail (VALENT_IS_MIXER_STREAM (stream)); + + if (valent_set_string (&stream->description, description)) + g_object_notify (G_OBJECT (stream), "description"); + + if (stream->level != level) + { + stream->level = level; + g_object_notify (G_OBJECT (stream), "level"); + } + + if (stream->muted != state) + { + stream->muted = state; + g_object_notify (G_OBJECT (stream), "muted"); + } +} + diff --git a/src/plugins/pipewire/valent-pipewire-stream.h b/src/plugins/pipewire/valent-pipewire-stream.h new file mode 100644 index 00000000000..7fead6a666c --- /dev/null +++ b/src/plugins/pipewire/valent-pipewire-stream.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Andy Holmes + +#pragma once + +#include + +G_BEGIN_DECLS + +#define VALENT_TYPE_PIPEWIRE_STREAM (valent_pipewire_stream_get_type ()) + +G_DECLARE_FINAL_TYPE (ValentPipewireStream, valent_pipewire_stream, VALENT, PIPEWIRE_STREAM, ValentMixerStream) + +void valent_pipewire_stream_update (ValentPipewireStream *stream, + const char *description, + uint32_t level, + gboolean state); + +G_END_DECLS + diff --git a/tests/extra/cppcheck.cfg b/tests/extra/cppcheck.cfg index 32cd2931490..0c4e4d6ae08 100644 --- a/tests/extra/cppcheck.cfg +++ b/tests/extra/cppcheck.cfg @@ -9,4 +9,6 @@ + + diff --git a/tests/extra/cppcheck.supp b/tests/extra/cppcheck.supp index 254c62fcd4f..300d1741252 100644 --- a/tests/extra/cppcheck.supp +++ b/tests/extra/cppcheck.supp @@ -13,6 +13,9 @@ leakNoVarFunctionCall:src/libvalent/notifications/valent-notification.c:866 leakNoVarFunctionCall:src/libvalent/notifications/valent-notification.c:885 leakNoVarFunctionCall:src/libvalent/notifications/valent-notification.c:888 +# src/plugins/pipewire/ +memleak:src/plugins/pipewire/valent-pipewire-mixer.c:684 + # src/plugins/sms/ memleak:src/plugins/sms/valent-sms-store.c:814 memleak:src/plugins/sms/valent-sms-store.c:864