From aa5b16f8df38d544e27bc827c267b71da66f514d Mon Sep 17 00:00:00 2001
From: George Kiagiadakis <george.kiagiadakis@collabora.com>
Date: Tue, 18 Jun 2019 19:58:42 +0300
Subject: [PATCH] module-mixer: implement the "Mixer/Audio" endpoint provider

This provides high level volume controls for the AGL audiomixer
binding and the applications using it.
---
 modules/meson.build            |  11 ++
 modules/module-mixer.c         | 282 +++++++++++++++++++++++++++++++++
 modules/module-simple-policy.c |  10 +-
 src/wireplumber.conf           |   1 +
 4 files changed, 303 insertions(+), 1 deletion(-)
 create mode 100644 modules/module-mixer.c

diff --git a/modules/meson.build b/modules/meson.build
index e9d644e1..5db780e6 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -3,6 +3,17 @@ common_c_args = [
   '-DG_LOG_USE_STRUCTURED',
 ]
 
+shared_library(
+  'wireplumber-module-mixer',
+  [
+    'module-mixer.c'
+  ],
+  c_args : [common_c_args, '-DG_LOG_DOMAIN="m-mixer"'],
+  install : true,
+  install_dir : wireplumber_module_dir,
+  dependencies : [wp_dep],
+)
+
 shared_library(
   'wireplumber-module-pipewire',
   [
diff --git a/modules/module-mixer.c b/modules/module-mixer.c
new file mode 100644
index 00000000..1979c65d
--- /dev/null
+++ b/modules/module-mixer.c
@@ -0,0 +1,282 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wp/wp.h>
+
+G_DECLARE_FINAL_TYPE (WpMixerEndpoint,
+    mixer_endpoint, WP, MIXER_ENDPOINT, WpEndpoint)
+
+static const char * streams[] = {
+  "Master"
+};
+#define N_STREAMS (sizeof (streams) / sizeof (const char *))
+
+enum {
+  CONTROL_VOLUME = 0,
+  CONTROL_MUTE,
+  N_CONTROLS,
+};
+
+#define MIXER_CONTROL_ID(stream_id, control_enum) \
+    (stream_id * N_CONTROLS + control_enum)
+
+struct group
+{
+  WpMixerEndpoint *mixer;
+  const gchar *name;
+  guint32 mixer_stream_id;
+
+  GWeakRef backend;
+  guint32 backend_ctl_ids[N_CONTROLS];
+};
+
+struct _WpMixerEndpoint
+{
+  WpEndpoint parent;
+  struct group groups[N_STREAMS];
+};
+
+G_DEFINE_TYPE (WpMixerEndpoint, mixer_endpoint, WP_TYPE_ENDPOINT)
+
+static void
+backend_value_changed (WpEndpoint *backend, guint32 control_id,
+    struct group *group)
+{
+  gint i;
+  for (i = 0; i < N_CONTROLS; i++) {
+    if (control_id == group->backend_ctl_ids[i]) {
+      g_signal_emit_by_name (group->mixer, "notify-control-value",
+          MIXER_CONTROL_ID (group->mixer_stream_id, i));
+    }
+  }
+}
+
+static void
+group_find_backend (struct group *group, WpCore *core)
+{
+  g_autoptr (WpEndpoint) backend = NULL;
+  g_autoptr (WpEndpoint) old_backend = NULL;
+  guint32 stream_id;
+  GVariantDict d;
+
+  /* find the backend */
+  g_variant_dict_init (&d, NULL);
+  g_variant_dict_insert (&d, "action", "s", "mixer");
+  g_variant_dict_insert (&d, "media.class", "s", "Audio/Sink");
+  g_variant_dict_insert (&d, "media.role", "s", group->name);
+
+  backend = wp_policy_find_endpoint (core, g_variant_dict_end (&d),
+      &stream_id);
+  if (!backend)
+    return;
+
+  /* we found the same backend as before - no need to continue */
+  old_backend = g_weak_ref_get (&group->backend);
+  if (old_backend && old_backend == backend)
+    return;
+
+  /* attach to the backend */
+  g_weak_ref_set (&group->backend, backend);
+  group->backend_ctl_ids[CONTROL_VOLUME] = wp_endpoint_find_control (backend,
+      stream_id, "volume");
+  group->backend_ctl_ids[CONTROL_MUTE] = wp_endpoint_find_control (backend,
+      stream_id, "mute");
+
+  /* notify of changed values */
+  g_signal_emit_by_name (group->mixer, "notify-control-value",
+      MIXER_CONTROL_ID (group->mixer_stream_id, CONTROL_VOLUME));
+  g_signal_emit_by_name (group->mixer, "notify-control-value",
+      MIXER_CONTROL_ID (group->mixer_stream_id, CONTROL_MUTE));
+
+  /* watch for further value changes in the backend */
+  g_signal_connect (backend, "notify-control-value",
+      (GCallback) backend_value_changed, group);
+}
+
+static void
+policy_changed (WpPolicyManager *mgr, WpMixerEndpoint * self)
+{
+  int i;
+  g_autoptr (WpCore) core = wp_endpoint_get_core (WP_ENDPOINT (self));
+
+  for (i = 0; i < N_STREAMS; i++) {
+    group_find_backend (&self->groups[i], core);
+  }
+}
+
+static void
+mixer_endpoint_init (WpMixerEndpoint * self)
+{
+}
+
+static void
+mixer_endpoint_constructed (GObject * object)
+{
+  WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object);
+  g_autoptr (WpCore) core = NULL;
+  g_autoptr (WpPolicyManager) policymgr = NULL;
+  GVariantDict d;
+  gint i;
+
+  core = wp_endpoint_get_core (WP_ENDPOINT (self));
+  policymgr = wp_policy_manager_get_instance (core);
+  g_signal_connect_object (policymgr, "policy-changed",
+      (GCallback) policy_changed, self, 0);
+
+  for (i = 0; i < N_STREAMS; i++) {
+    g_variant_dict_init (&d, NULL);
+    g_variant_dict_insert (&d, "id", "u", i);
+    g_variant_dict_insert (&d, "name", "s", streams[i]);
+    wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d));
+
+    g_variant_dict_init (&d, NULL);
+    g_variant_dict_insert (&d, "id", "u", MIXER_CONTROL_ID (i, CONTROL_VOLUME));
+    g_variant_dict_insert (&d, "stream-id", "u", i);
+    g_variant_dict_insert (&d, "name", "s", "volume");
+    g_variant_dict_insert (&d, "type", "s", "d");
+    g_variant_dict_insert (&d, "range", "(dd)", 0.0, 1.0);
+    g_variant_dict_insert (&d, "default-value", "d", 1.0);
+    wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
+
+    g_variant_dict_init (&d, NULL);
+    g_variant_dict_insert (&d, "id", "u", MIXER_CONTROL_ID (i, CONTROL_MUTE));
+    g_variant_dict_insert (&d, "stream-id", "u", i);
+    g_variant_dict_insert (&d, "name", "s", "mute");
+    g_variant_dict_insert (&d, "type", "s", "b");
+    g_variant_dict_insert (&d, "default-value", "b", FALSE);
+    wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
+
+    self->groups[i].mixer = self;
+    self->groups[i].name = streams[i];
+    self->groups[i].mixer_stream_id = i;
+    g_weak_ref_init (&self->groups[i].backend, NULL);
+
+    group_find_backend (&self->groups[i], core);
+  }
+
+  G_OBJECT_CLASS (mixer_endpoint_parent_class)->constructed (object);
+}
+
+static void
+mixer_endpoint_finalize (GObject * object)
+{
+  WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object);
+  gint i;
+
+  for (i = 0; i < N_STREAMS; i++) {
+    g_autoptr (WpEndpoint) backend = g_weak_ref_get (&self->groups[i].backend);
+    if (backend)
+      g_signal_handlers_disconnect_by_data (backend, &self->groups[i]);
+    g_weak_ref_clear (&self->groups[i].backend);
+  }
+
+  G_OBJECT_CLASS (mixer_endpoint_parent_class)->finalize (object);
+}
+
+static GVariant *
+mixer_endpoint_get_control_value (WpEndpoint * ep, guint32 control_id)
+{
+  WpMixerEndpoint *self = WP_MIXER_ENDPOINT (ep);
+  guint32 stream_id;
+  struct group *group;
+  g_autoptr (WpEndpoint) backend = NULL;
+
+  stream_id = control_id / N_CONTROLS;
+  control_id = control_id % N_CONTROLS;
+
+  if (stream_id >= N_STREAMS) {
+    g_warning ("Mixer:%p Invalid stream id %u", self, stream_id);
+    return NULL;
+  }
+
+  group = &self->groups[stream_id];
+  backend = g_weak_ref_get (&group->backend);
+
+  /* if there is no backend, return the default value */
+  if (!backend) {
+    g_warning ("Mixer:%p Cannot get control value - no backend", self);
+
+    switch (control_id) {
+    case CONTROL_VOLUME:
+      return g_variant_new_double (1.0);
+    case CONTROL_MUTE:
+      return g_variant_new_boolean (FALSE);
+    default:
+      g_assert_not_reached ();
+    }
+  }
+
+  /* otherwise return the value provided by the backend */
+  return wp_endpoint_get_control_value (backend,
+      group->backend_ctl_ids[control_id]);
+}
+
+static gboolean
+mixer_endpoint_set_control_value (WpEndpoint * ep, guint32 control_id,
+    GVariant * value)
+{
+  WpMixerEndpoint *self = WP_MIXER_ENDPOINT (ep);
+  guint32 stream_id;
+  struct group *group;
+  g_autoptr (WpEndpoint) backend = NULL;
+
+  stream_id = control_id / N_CONTROLS;
+  control_id = control_id % N_CONTROLS;
+
+  if (stream_id >= N_STREAMS) {
+    g_warning ("Mixer:%p Invalid stream id %u", self, stream_id);
+    return FALSE;
+  }
+
+  group = &self->groups[stream_id];
+  backend = g_weak_ref_get (&group->backend);
+
+  if (!backend) {
+    g_warning ("Mixer:%p Cannot set control value - no backend", self);
+    return FALSE;
+  }
+
+  return wp_endpoint_set_control_value (backend,
+      group->backend_ctl_ids[control_id], value);
+}
+
+static void
+mixer_endpoint_class_init (WpMixerEndpointClass * klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+  WpEndpointClass *endpoint_class = (WpEndpointClass *) klass;
+
+  object_class->constructed = mixer_endpoint_constructed;
+  object_class->finalize = mixer_endpoint_finalize;
+
+  endpoint_class->get_control_value = mixer_endpoint_get_control_value;
+  endpoint_class->set_control_value = mixer_endpoint_set_control_value;
+}
+
+static void
+remote_connected (WpRemote *remote, WpRemoteState state, WpCore *core)
+{
+  g_autoptr (WpEndpoint) ep = g_object_new (mixer_endpoint_get_type (),
+      "core", core,
+      "name", "Mixer",
+      "media-class", "Mixer/Audio",
+      NULL);
+  wp_endpoint_register (ep);
+}
+
+void
+wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
+{
+  WpRemote *remote;
+
+  remote = wp_core_get_global (core, WP_GLOBAL_REMOTE_PIPEWIRE);
+  g_return_if_fail (remote != NULL);
+
+  g_signal_connect (remote, "state-changed::connected",
+      (GCallback) remote_connected, core);
+}
diff --git a/modules/module-simple-policy.c b/modules/module-simple-policy.c
index 79807bd4..9e053d67 100644
--- a/modules/module-simple-policy.c
+++ b/modules/module-simple-policy.c
@@ -242,12 +242,16 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props,
 {
   g_autoptr (WpCore) core = NULL;
   g_autoptr (GPtrArray) ptr_array = NULL;
+  const char *action = NULL;
   const char *media_class = NULL;
+  const char *role = NULL;
   WpEndpoint *ep;
   int i;
 
   core = wp_policy_get_core (policy);
 
+  g_variant_lookup (props, "action", "&s", &action);
+
   /* Get all the endpoints with the specific media class*/
   g_variant_lookup (props, "media.class", "&s", &media_class);
   ptr_array = wp_endpoint_find (core, media_class);
@@ -256,7 +260,11 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props,
 
   /* TODO: for now we statically return the first stream
    * we should be looking into the media.role eventually */
-  *stream_id = 0;
+  g_variant_lookup (props, "media.role", "&s", &role);
+  if (!g_strcmp0 (action, "mixer") && !g_strcmp0 (role, "Master"))
+    *stream_id = WP_STREAM_ID_NONE;
+  else
+    *stream_id = 0;
 
   /* Find and return the "selected" endpoint */
   /* FIXME: fix the endpoint API, this is terrible */
diff --git a/src/wireplumber.conf b/src/wireplumber.conf
index 9748aa75..75967301 100644
--- a/src/wireplumber.conf
+++ b/src/wireplumber.conf
@@ -2,3 +2,4 @@ load-module C libwireplumber-module-pipewire
 load-module C libwireplumber-module-pw-audio-softdsp-endpoint
 load-module C libwireplumber-module-pw-alsa-udev
 load-module C libwireplumber-module-simple-policy
+load-module C libwireplumber-module-mixer
-- 
GitLab