summaryrefslogtreecommitdiffstats
path: root/binding
diff options
context:
space:
mode:
authorGeorge Kiagiadakis <george.kiagiadakis@collabora.com>2019-06-07 17:44:35 +0300
committerGeorge Kiagiadakis <george.kiagiadakis@collabora.com>2019-06-24 13:32:08 +0300
commit224712ad5f07dccb6acb19ddf8333d822c7928ce (patch)
tree397ba7656f0589b04fea3d5b7bb0e4e3b1dca4e2 /binding
parent7dff10809be5feeda6f81b942c8671cdda2e3a27 (diff)
Initial binding version
Signed-off-by: George Kiagiadakis <george.kiagiadakis@collabora.com> Change-Id: I89e493d88c7fa1309f1b2991d346fc496caa6898
Diffstat (limited to 'binding')
-rw-r--r--binding/CMakeLists.txt20
-rw-r--r--binding/audiomixer-binding.c342
-rw-r--r--binding/audiomixer.c597
-rw-r--r--binding/audiomixer.h60
4 files changed, 1019 insertions, 0 deletions
diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt
new file mode 100644
index 0000000..587d137
--- /dev/null
+++ b/binding/CMakeLists.txt
@@ -0,0 +1,20 @@
+PROJECT_TARGET_ADD(audiomixer-binding)
+
+ add_definitions(-DAFB_BINDING_VERSION=3)
+ add_definitions(-DBUILDING_APPFW_BINDING)
+
+ set(audiomixer_SOURCES
+ audiomixer-binding.c
+ audiomixer.c
+ )
+
+ add_library(${TARGET_NAME} MODULE ${audiomixer_SOURCES})
+
+ SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES
+ PREFIX "libafm-"
+ LABELS "BINDING"
+ LINK_FLAGS ${BINDINGS_LINK_FLAG}
+ OUTPUT_NAME ${TARGET_NAME}
+ )
+
+ TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries})
diff --git a/binding/audiomixer-binding.c b/binding/audiomixer-binding.c
new file mode 100644
index 0000000..005f5c7
--- /dev/null
+++ b/binding/audiomixer-binding.c
@@ -0,0 +1,342 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <string.h>
+#include <json-c/json.h>
+#include <afb/afb-binding.h>
+#include <systemd/sd-event.h>
+#include "audiomixer.h"
+
+static struct audiomixer *audiomixer;
+static afb_event_t controls_changed;
+static afb_event_t volume_changed;
+static afb_event_t mute_changed;
+static sd_event_source *controls_changed_source;
+
+static int
+audiomixer_controls_changed_deferred(sd_event_source *s, void *data)
+{
+ afb_event_push(controls_changed, NULL);
+
+ sd_event_source_unref(controls_changed_source);
+ controls_changed_source = NULL;
+ return 0;
+}
+
+struct value_changed_data
+{
+ unsigned int change_mask;
+ struct mixer_control control;
+ sd_event_source *source;
+};
+
+static int
+audiomixer_value_changed_deferred(sd_event_source *s, void *data)
+{
+ struct value_changed_data *d = data;
+ json_object *json;
+
+ json = json_object_new_object();
+ json_object_object_add(json, "control",
+ json_object_new_string(d->control.name));
+
+ if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) {
+ json_object_object_add(json, "value",
+ json_object_new_double(d->control.volume));
+ afb_event_push(volume_changed, json);
+ } else if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) {
+ json_object_object_add(json, "value",
+ json_object_new_int(d->control.mute));
+ afb_event_push(mute_changed, json);
+ }
+
+ sd_event_source_unref(d->source);
+ free(d);
+ return 0;
+}
+
+/* called in audiomixer's thread */
+static void
+audiomixer_controls_changed(void *data)
+{
+ sd_event *e = afb_daemon_get_event_loop();
+ sd_event_add_defer(e, &controls_changed_source,
+ audiomixer_controls_changed_deferred, NULL);
+}
+
+
+/* called in audiomixer's thread */
+static void
+audiomixer_value_changed(void *data,
+ unsigned int change_mask,
+ const struct mixer_control *control)
+{
+ sd_event *e = afb_daemon_get_event_loop();
+ struct value_changed_data *d = calloc(1, sizeof(*d));
+
+ d->change_mask = change_mask;
+ d->control = *control;
+
+ if (sd_event_add_defer(e, &d->source,
+ audiomixer_value_changed_deferred, d) < 0)
+ free(d);
+}
+
+static const struct audiomixer_events audiomixer_events = {
+ .controls_changed = audiomixer_controls_changed,
+ .value_changed = audiomixer_value_changed,
+};
+
+static int
+cleanup(sd_event_source *s, void *data)
+{
+ audiomixer_free(audiomixer);
+ audiomixer = NULL;
+ return 0;
+}
+
+static int
+init(afb_api_t api)
+{
+ sd_event *e = afb_daemon_get_event_loop();
+
+ controls_changed = afb_api_make_event(api, "controls_changed");
+ volume_changed = afb_api_make_event(api, "volume_changed");
+ mute_changed = afb_api_make_event(api, "mute_changed");
+
+ audiomixer = audiomixer_new();
+ sd_event_add_exit(e, NULL, cleanup, NULL);
+
+ audiomixer_add_event_listener(audiomixer, &audiomixer_events, NULL);
+
+ return 0;
+}
+
+static void
+list_controls_cb(afb_req_t request)
+{
+ json_object *ret_json, *nest_json;
+ const struct mixer_control **ctls;
+ unsigned int n_controls, i;
+
+ audiomixer_lock(audiomixer);
+
+ if (audiomixer_ensure_connected(audiomixer, 3) < 0) {
+ afb_req_fail(request, "failed",
+ "Could not connect to the PipeWire daemon");
+ goto unlock;
+ }
+
+ if (audiomixer_ensure_controls(audiomixer, 3) < 0) {
+ AFB_REQ_NOTICE(request, "No mixer controls were exposed "
+ "in PipeWire after 3 seconds");
+ }
+
+ ctls = audiomixer_get_active_controls(audiomixer, &n_controls);
+
+ ret_json = json_object_new_array();
+ for (i = 0; i < n_controls; i++) {
+ nest_json = json_object_new_object();
+ json_object_object_add(nest_json, "control",
+ json_object_new_string(ctls[i]->name));
+ json_object_object_add(nest_json, "volume",
+ json_object_new_double(ctls[i]->volume));
+ json_object_object_add(nest_json, "mute",
+ json_object_new_int(ctls[i]->mute));
+ json_object_array_add(ret_json, nest_json);
+ }
+ afb_req_success(request, ret_json, NULL);
+
+unlock:
+ audiomixer_unlock(audiomixer);
+}
+
+static void
+volume_cb(afb_req_t request)
+{
+ json_object *ret_json;
+ const char *control = afb_req_value(request, "control");
+ const char *value = afb_req_value(request, "value");
+ const struct mixer_control *ctl;
+ double volume;
+
+ audiomixer_lock(audiomixer);
+
+ if (!control) {
+ afb_req_fail(request, "failed",
+ "Invalid arguments: missing 'control'");
+ goto unlock;
+ }
+
+ if (audiomixer_ensure_connected(audiomixer, 3) < 0) {
+ afb_req_fail(request, "failed",
+ "Could not connect to the PipeWire daemon");
+ goto unlock;
+ }
+
+ if (audiomixer_ensure_controls(audiomixer, 3) < 0) {
+ AFB_REQ_NOTICE(request, "No mixer controls were exposed "
+ "in PipeWire after 3 seconds");
+ }
+
+ ctl = audiomixer_find_control(audiomixer, control);
+ if (!ctl) {
+ afb_req_fail(request, "failed", "Could not find control");
+ goto unlock;
+ }
+
+ if(value) {
+ char *endptr;
+ volume = strtod(value, &endptr);
+ if (endptr == value || volume < -0.00001 || volume > 1.00001) {
+ afb_req_fail(request, "failed",
+ "Invalid volume value (must be between 0.0 and 1.0)");
+ goto unlock;
+ }
+
+ audiomixer_change_volume(audiomixer, ctl, volume);
+ } else {
+ volume = ctl->volume;
+ }
+
+ ret_json = json_object_new_object();
+ json_object_object_add(ret_json, "volume", json_object_new_double(volume));
+ afb_req_success(request, ret_json, NULL);
+
+unlock:
+ audiomixer_unlock(audiomixer);
+}
+
+static void
+mute_cb(afb_req_t request)
+{
+ json_object *ret_json;
+ const char *control = afb_req_value(request, "control");
+ const char *value = afb_req_value(request, "value");
+ const struct mixer_control *ctl;
+ int mute;
+
+ audiomixer_lock(audiomixer);
+
+ if (!control) {
+ afb_req_fail(request, "failed",
+ "Invalid arguments: missing 'control'");
+ goto unlock;
+ }
+
+ if (audiomixer_ensure_connected(audiomixer, 3) < 0) {
+ afb_req_fail(request, "failed",
+ "Could not connect to the PipeWire daemon");
+ goto unlock;
+ }
+
+ if (audiomixer_ensure_controls(audiomixer, 3) < 0) {
+ AFB_REQ_NOTICE(request, "No mixer controls were exposed "
+ "in PipeWire after 3 seconds");
+ }
+
+ ctl = audiomixer_find_control(audiomixer, control);
+ if (!ctl) {
+ afb_req_fail(request, "failed", "Could not find control");
+ goto unlock;
+ }
+
+ if(value) {
+ char *endptr;
+ mute = (int) strtol(value, &endptr, 10);
+ if (endptr == value || mute < 0 || mute > 1) {
+ afb_req_fail(request, "failed",
+ "Invalid mute value (must be integer 0 or 1)");
+ goto unlock;
+ }
+
+ audiomixer_change_mute(audiomixer, ctl, mute);
+ } else {
+ mute = ctl->mute;
+ }
+
+ ret_json = json_object_new_object();
+ json_object_object_add(ret_json, "mute", json_object_new_int(mute));
+ afb_req_success(request, ret_json, NULL);
+
+unlock:
+ audiomixer_unlock(audiomixer);
+}
+
+static void
+subscribe_cb(afb_req_t request)
+{
+ const char *eventstr = afb_req_value(request, "event");
+ afb_event_t event;
+
+ if (!eventstr) {
+ afb_req_fail(request, "failed",
+ "Invalid arguments: missing 'event'");
+ return;
+ }
+
+ if (!strcmp(eventstr, "controls_changed"))
+ event = controls_changed;
+ else if (!strcmp(eventstr, "volume_changed"))
+ event = volume_changed;
+ else if (!strcmp(eventstr, "mute_changed"))
+ event = mute_changed;
+ else {
+ afb_req_fail(request, "failed", "Invalid event name");
+ return;
+ }
+
+ if (afb_req_subscribe(request, event) != 0)
+ afb_req_fail(request, "failed", "Failed to subscribe to event");
+ else
+ afb_req_success(request, NULL, "Subscribed");
+}
+
+static void
+unsubscribe_cb(afb_req_t request)
+{
+ const char *eventstr = afb_req_value(request, "event");
+ afb_event_t event;
+
+ if (!eventstr) {
+ afb_req_fail(request, "failed",
+ "Invalid arguments: missing 'event'");
+ return;
+ }
+
+ if (!strcmp(eventstr, "controls_changed"))
+ event = controls_changed;
+ else if (!strcmp(eventstr, "volume_changed"))
+ event = volume_changed;
+ else if (!strcmp(eventstr, "mute_changed"))
+ event = mute_changed;
+ else {
+ afb_req_fail(request, "failed", "Invalid event name");
+ return;
+ }
+
+ if (afb_req_unsubscribe(request, event) != 0)
+ afb_req_fail(request, "failed", "Failed to unsubscribe from event");
+ else
+ afb_req_success(request, NULL, "Unsubscribed");
+}
+
+static const afb_verb_t verbs[]= {
+ { .verb = "list_controls", .callback = list_controls_cb, .info = "List the available controls" },
+ { .verb = "volume", .callback = volume_cb, .info = "Get/Set volume" },
+ { .verb = "mute", .callback = mute_cb, .info = "Get/Set mute" },
+ { .verb = "subscribe", .callback = subscribe_cb, .info = "Subscribe to mixer events" },
+ { .verb = "unsubscribe", .callback = unsubscribe_cb, .info = "Unsubscribe from mixer events" },
+ { }
+};
+
+const afb_binding_t afbBindingV3 = {
+ .api = "audiomixer",
+ .specification = "AudioMixer API",
+ .verbs = verbs,
+ .init = init,
+};
diff --git a/binding/audiomixer.c b/binding/audiomixer.c
new file mode 100644
index 0000000..c4ccc12
--- /dev/null
+++ b/binding/audiomixer.c
@@ -0,0 +1,597 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "audiomixer.h"
+#include <pipewire/pipewire.h>
+#include <pipewire/array.h>
+#include <pipewire/extensions/endpoint.h>
+
+#if !defined(BUILDING_APPFW_BINDING)
+#define debug(...) fprintf(stdout, __VA_ARGS__)
+#else
+#include <afb/afb-binding.h>
+#define debug(...) AFB_DEBUG(__VA_ARGS__)
+#endif
+
+struct audiomixer
+{
+ struct pw_thread_loop *main_loop;
+
+ struct pw_core *core;
+ struct pw_remote *remote;
+ struct spa_hook remote_listener;
+
+ struct pw_core_proxy *core_proxy;
+ struct spa_hook remote_core_listener;
+ struct pw_registry_proxy *registry_proxy;
+ struct spa_hook registry_listener;
+
+ struct pw_array endpoints;
+ struct pw_array all_mixer_controls;
+
+ const struct audiomixer_events *events;
+ void *events_data;
+};
+
+enum endpoint_state {
+ EP_STATE_INITIAL,
+ EP_STATE_COLLECT_ENUM_STREAM,
+ EP_STATE_COLLECT_ENUM_CONTROL,
+ EP_STATE_COLLECT_CONTROL,
+ EP_STATE_ACTIVE,
+};
+
+struct endpoint
+{
+ struct audiomixer *audiomixer;
+ struct pw_endpoint_proxy *proxy;
+
+ struct pw_properties *properties;
+ enum endpoint_state state;
+
+ struct spa_hook proxy_listener;
+ struct spa_hook endpoint_listener;
+
+ struct pw_array mixer_controls;
+};
+
+struct mixer_control_impl
+{
+ struct mixer_control pub;
+ struct endpoint *endpoint;
+ uint32_t stream_id;
+ uint32_t volume_control_id;
+ uint32_t mute_control_id;
+};
+
+static void
+emit_controls_changed(struct audiomixer *self)
+{
+ pw_thread_loop_signal(self->main_loop, false);
+
+ if (!self->events || !self->events->controls_changed)
+ return;
+
+ self->events->controls_changed(self->events_data);
+}
+
+static void
+emit_value_changed(struct audiomixer *self,
+ unsigned int change_mask,
+ struct mixer_control *ctl)
+{
+ if (!self->events || !self->events->value_changed)
+ return;
+
+ self->events->value_changed(self->events_data, change_mask, ctl);
+}
+
+static void
+advance_endpoint_state(struct endpoint *endpoint)
+{
+ debug("%p advance endpoint state, was:%d", endpoint, endpoint->state);
+
+ switch (endpoint->state) {
+ case EP_STATE_INITIAL:
+ endpoint->state = EP_STATE_COLLECT_ENUM_STREAM;
+ pw_endpoint_proxy_enum_params(endpoint->proxy, 0,
+ PW_ENDPOINT_PARAM_EnumStream, 0, -1, NULL);
+ pw_proxy_sync((struct pw_proxy *) endpoint->proxy, 0);
+ break;
+ case EP_STATE_COLLECT_ENUM_STREAM:
+ endpoint->state = EP_STATE_COLLECT_ENUM_CONTROL;
+ pw_endpoint_proxy_enum_params(endpoint->proxy, 0,
+ PW_ENDPOINT_PARAM_EnumControl, 0, -1, NULL);
+ pw_proxy_sync((struct pw_proxy *) endpoint->proxy, 0);
+ break;
+ case EP_STATE_COLLECT_ENUM_CONTROL: {
+ uint32_t ids[1] = { PW_ENDPOINT_PARAM_Control };
+
+ endpoint->state = EP_STATE_COLLECT_CONTROL;
+ pw_endpoint_proxy_subscribe_params(endpoint->proxy, ids, 1);
+ pw_proxy_sync((struct pw_proxy *) endpoint->proxy, 0);
+ break;
+ }
+ case EP_STATE_COLLECT_CONTROL: {
+ struct mixer_control_impl *ctl;
+ struct audiomixer *self = endpoint->audiomixer;
+
+ endpoint->state = EP_STATE_ACTIVE;
+ pw_array_for_each(ctl, &endpoint->mixer_controls) {
+ pw_array_add_ptr(&self->all_mixer_controls, ctl);
+ }
+ emit_controls_changed(self);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+endpoint_param (void *object, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct endpoint *endpoint = object;
+ struct mixer_control_impl *ctl;
+ const struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+
+ if (!spa_pod_is_object(param)) {
+ debug("endpoint_param: bad param - not an object");
+ return;
+ }
+
+ switch (id) {
+ case PW_ENDPOINT_PARAM_EnumStream:
+ /* verify conditions */
+ if (endpoint->state != EP_STATE_COLLECT_ENUM_STREAM) {
+ debug("endpoint_param EnumStream: wrong state");
+ return;
+ }
+ if (SPA_POD_OBJECT_TYPE(obj) != PW_ENDPOINT_OBJECT_ParamStream) {
+ debug("endpoint_param EnumStream: invalid param");
+ return;
+ }
+
+ /* create new mixer control */
+ ctl = pw_array_add(&endpoint->mixer_controls, sizeof(*ctl));
+ ctl->endpoint = endpoint;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case PW_ENDPOINT_PARAM_STREAM_id:
+ spa_pod_get_int(&prop->value, &ctl->stream_id);
+ break;
+ case PW_ENDPOINT_PARAM_STREAM_name:
+ spa_pod_copy_string(&prop->value,
+ SPA_N_ELEMENTS(ctl->pub.name),
+ ctl->pub.name);
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case PW_ENDPOINT_PARAM_EnumControl: {
+ uint32_t tmp_id = -1;
+ const char *name = NULL;
+
+ /* verify conditions */
+ if (endpoint->state != EP_STATE_COLLECT_ENUM_CONTROL) {
+ debug("endpoint_param EnumControl: wrong state");
+ return;
+ }
+ if (SPA_POD_OBJECT_TYPE(obj) != PW_ENDPOINT_OBJECT_ParamControl) {
+ debug("endpoint_param EnumControl: invalid param");
+ return;
+ }
+
+ /* find the mixer control */
+ prop = spa_pod_object_find_prop(obj, NULL,
+ PW_ENDPOINT_PARAM_CONTROL_stream_id);
+ if (prop)
+ spa_pod_get_int(&prop->value, &tmp_id);
+ else {
+ debug("endpoint_param EnumControl: invalid control without stream");
+ return;
+ }
+
+ pw_array_for_each(ctl, &endpoint->mixer_controls) {
+ if (ctl->stream_id == tmp_id)
+ break;
+ }
+
+ /* check if we reached the end of the array
+ * without finding the stream */
+ if (!pw_array_check(&endpoint->mixer_controls, ctl)) {
+ debug("endpoint_param EnumControl: could not find "
+ "stream id %u", tmp_id);
+ return;
+ }
+
+ /* store the control id based on the control's name */
+ prop = spa_pod_object_find_prop(obj, NULL,
+ PW_ENDPOINT_PARAM_CONTROL_name);
+ if (prop)
+ spa_pod_get_string(&prop->value, &name);
+ else {
+ debug("endpoint_param EnumControl: invalid control without name");
+ return;
+ }
+
+ prop = spa_pod_object_find_prop(obj, NULL,
+ PW_ENDPOINT_PARAM_CONTROL_id);
+ if (!prop) {
+ debug("endpoint_param EnumControl: invalid control without id");
+ return;
+ }
+
+ if (strcmp (name, "volume")) {
+ spa_pod_get_int(&prop->value, &ctl->volume_control_id);
+ prop = spa_pod_object_find_prop(obj, NULL,
+ PW_ENDPOINT_PARAM_CONTROL_type);
+ } else if (strcmp (name, "mute")) {
+ spa_pod_get_int(&prop->value, &ctl->mute_control_id);
+ }
+
+ break;
+ }
+ case PW_ENDPOINT_PARAM_Control: {
+ uint32_t tmp_id = -1;
+
+ /* verify conditions */
+ if (endpoint->state != EP_STATE_COLLECT_CONTROL ||
+ endpoint->state != EP_STATE_ACTIVE) {
+ debug("endpoint_param Control: wrong state");
+ return;
+ }
+ if (SPA_POD_OBJECT_TYPE(obj) != PW_ENDPOINT_OBJECT_ParamControl) {
+ debug("endpoint_param Control: invalid param");
+ return;
+ }
+
+ /* match the control id and set the value */
+ prop = spa_pod_object_find_prop(obj, NULL,
+ PW_ENDPOINT_PARAM_CONTROL_id);
+ if (prop)
+ spa_pod_get_int(&prop->value, &tmp_id);
+ else {
+ debug("endpoint_param Control: invalid control without id");
+ return;
+ }
+
+ prop = spa_pod_object_find_prop(obj, NULL,
+ PW_ENDPOINT_PARAM_CONTROL_value);
+ if (!prop) {
+ debug("endpoint_param Control: invalid control without value");
+ return;
+ }
+
+ pw_array_for_each(ctl, &endpoint->mixer_controls) {
+ if (ctl->volume_control_id == tmp_id) {
+ spa_pod_get_double(&prop->value, &ctl->pub.volume);
+
+ if (endpoint->state == EP_STATE_ACTIVE) {
+ emit_value_changed(endpoint->audiomixer,
+ MIXER_CONTROL_CHANGE_FLAG_VOLUME,
+ &ctl->pub);
+ }
+ break;
+ } else if (ctl->mute_control_id == tmp_id) {
+ spa_pod_get_bool(&prop->value, &ctl->pub.mute);
+
+ if (endpoint->state == EP_STATE_ACTIVE) {
+ emit_value_changed(endpoint->audiomixer,
+ MIXER_CONTROL_CHANGE_FLAG_MUTE,
+ &ctl->pub);
+ }
+ break;
+ }
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+struct pw_endpoint_proxy_events endpoint_events = {
+ PW_VERSION_ENDPOINT_PROXY_EVENTS,
+ .param = endpoint_param,
+};
+
+static void
+endpoint_proxy_destroyed(void *object)
+{
+ struct endpoint *endpoint = object;
+ struct audiomixer *self = endpoint->audiomixer;
+ struct mixer_control_impl *ctl;
+ struct mixer_control **ctlptr;
+ struct endpoint **epptr;
+
+ debug("%p endpoint destroyed", endpoint);
+
+ if (endpoint->properties)
+ pw_properties_free(endpoint->properties);
+
+ pw_array_for_each(ctl, &endpoint->mixer_controls) {
+ pw_array_for_each(ctlptr, &self->all_mixer_controls) {
+ if (*ctlptr == &ctl->pub) {
+ pw_array_remove(&self->all_mixer_controls, ctlptr);
+ break;
+ }
+ }
+ }
+ emit_controls_changed(self);
+ pw_array_clear(&endpoint->mixer_controls);
+
+ pw_array_for_each(epptr, &self->endpoints) {
+ if (*epptr == endpoint) {
+ pw_array_remove(&self->endpoints, epptr);
+ break;
+ }
+ }
+}
+
+static void
+endpoint_proxy_done(void *object, int seq)
+{
+ struct endpoint *endpoint = object;
+ advance_endpoint_state(endpoint);
+}
+
+struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = endpoint_proxy_destroyed,
+ .done = endpoint_proxy_done,
+};
+
+static void
+registry_event_global(void *data, uint32_t id, uint32_t parent_id,
+ uint32_t permissions, uint32_t type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct audiomixer *self = data;
+ const char *media_class;
+ struct pw_proxy *proxy;
+ struct endpoint *endpoint;
+
+ if (type != PW_TYPE_INTERFACE_Endpoint)
+ return;
+
+ /* we are only interested in mixer endpoints */
+ media_class = props ? spa_dict_lookup(props, "media.class") : NULL;
+ if (!media_class || strcmp(media_class, "Mixer/Audio") != 0)
+ return;
+
+ proxy = pw_registry_proxy_bind(self->registry_proxy,
+ id, type, PW_VERSION_ENDPOINT, sizeof(struct endpoint));
+
+ endpoint = pw_proxy_get_user_data(proxy);
+ endpoint->audiomixer = self;
+ endpoint->proxy = (struct pw_endpoint_proxy *) proxy;
+ endpoint->properties = props ? pw_properties_new_dict(props) : NULL;
+ endpoint->state = EP_STATE_INITIAL;
+ pw_array_init(&endpoint->mixer_controls, 4 * sizeof(struct mixer_control));
+
+ pw_proxy_add_listener(proxy, &endpoint->proxy_listener,
+ &proxy_events, endpoint);
+ pw_endpoint_proxy_add_listener(endpoint->proxy,
+ &endpoint->endpoint_listener,
+ &endpoint_events, endpoint);
+
+ debug("%p added endpoint: %u", endpoint, id);
+
+ pw_array_add_ptr(&self->endpoints, endpoint);
+ advance_endpoint_state(endpoint);
+}
+
+
+static const struct pw_registry_proxy_events registry_events = {
+ PW_VERSION_REGISTRY_PROXY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void
+on_remote_state_changed(void *data, enum pw_remote_state old,
+ enum pw_remote_state state, const char *error)
+{
+ struct audiomixer *self = data;
+
+ if (state == PW_REMOTE_STATE_CONNECTED) {
+ self->core_proxy = pw_remote_get_core_proxy(self->remote);
+ self->registry_proxy = pw_core_proxy_get_registry(
+ self->core_proxy,
+ PW_TYPE_INTERFACE_Registry,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_proxy_add_listener(self->registry_proxy,
+ &self->registry_listener,
+ &registry_events, self);
+ }
+
+ pw_thread_loop_signal(self->main_loop, false);
+}
+
+static const struct pw_remote_events remote_events = {
+ PW_VERSION_REMOTE_EVENTS,
+ .state_changed = on_remote_state_changed,
+};
+
+struct audiomixer *
+audiomixer_new(void)
+{
+ struct audiomixer *self;
+ struct pw_loop *loop;
+
+ pw_init(NULL, NULL);
+
+ self = calloc(1, sizeof(struct audiomixer));
+ loop = pw_loop_new(NULL);
+ self->main_loop = pw_thread_loop_new(loop, "audiomixer-loop");
+ self->core = pw_core_new(loop, NULL, 0);
+ self->remote = pw_remote_new(self->core, NULL, 0);
+ pw_array_init(&self->endpoints, 1 * sizeof(void*));
+ pw_array_init(&self->all_mixer_controls, 8 * sizeof(void*));
+
+ pw_module_load(self->core, "libpipewire-module-endpoint", NULL, NULL,
+ NULL, NULL);
+ pw_thread_loop_start(self->main_loop);
+
+ return self;
+}
+
+void
+audiomixer_free(struct audiomixer *self)
+{
+ struct pw_loop *loop;
+
+ pw_thread_loop_lock(self->main_loop);
+ self->events = NULL;
+ self->events_data = NULL;
+ pw_remote_disconnect(self->remote);
+ pw_thread_loop_unlock(self->main_loop);
+ pw_thread_loop_stop(self->main_loop);
+
+ pw_array_clear(&self->endpoints);
+ pw_array_clear(&self->all_mixer_controls);
+ pw_remote_destroy(self->remote);
+ pw_core_destroy(self->core);
+
+ loop = pw_thread_loop_get_loop(self->main_loop);
+ pw_thread_loop_destroy(self->main_loop);
+ pw_loop_destroy(loop);
+
+ free(self);
+}
+
+void
+audiomixer_lock(struct audiomixer *self)
+{
+ pw_thread_loop_lock(self->main_loop);
+}
+
+void
+audiomixer_unlock(struct audiomixer *self)
+{
+ pw_thread_loop_unlock(self->main_loop);
+}
+
+int
+audiomixer_ensure_connected(struct audiomixer *self, int timeout_sec)
+{
+ enum pw_remote_state state;
+ int res;
+
+ state = pw_remote_get_state(self->remote, NULL);
+ if (state == PW_REMOTE_STATE_CONNECTED)
+ return 0;
+
+ if ((res = pw_remote_connect(self->remote)) < 0)
+ return res;
+
+ while (true) {
+ state = pw_remote_get_state(self->remote, NULL);
+ if (state == PW_REMOTE_STATE_CONNECTED)
+ return 0;
+ else if (state == PW_REMOTE_STATE_ERROR)
+ return -EIO;
+
+ if (pw_thread_loop_timed_wait(self->main_loop, timeout_sec) != 0)
+ return -ETIMEDOUT;
+ }
+}
+
+int
+audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec)
+{
+ while (pw_array_get_len(&self->all_mixer_controls, void*) == 0) {
+ if (pw_thread_loop_timed_wait(self->main_loop, timeout_sec) != 0)
+ return -ETIMEDOUT;
+ }
+ return 0;
+}
+
+const struct mixer_control **
+audiomixer_get_active_controls(struct audiomixer *self,
+ unsigned int *n_controls)
+{
+ const struct mixer_control **ret;
+
+ *n_controls = pw_array_get_len(&self->all_mixer_controls, void*);
+ ret = (const struct mixer_control **)
+ pw_array_first(&self->all_mixer_controls);
+
+ return ret;
+}
+
+const struct mixer_control *
+audiomixer_find_control(struct audiomixer *self, const char *name)
+{
+ struct mixer_control **ctlptr;
+
+ pw_array_for_each(ctlptr, &self->all_mixer_controls) {
+ if (!strcmp((*ctlptr)->name, name)) {
+ return *ctlptr;
+ }
+ }
+ return NULL;
+}
+
+void
+audiomixer_add_event_listener(struct audiomixer *self,
+ const struct audiomixer_events *events,
+ void *data)
+{
+ self->events = events;
+ self->events_data = data;
+}
+
+void
+audiomixer_change_volume(struct audiomixer *self,
+ const struct mixer_control *control,
+ double volume)
+{
+ const struct mixer_control_impl *impl =
+ (const struct mixer_control_impl *) control;
+ struct endpoint *endpoint = impl->endpoint;
+ char buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 1024);
+ struct spa_pod *param;
+
+ param = spa_pod_builder_add_object(&b,
+ PW_ENDPOINT_OBJECT_ParamControl, PW_ENDPOINT_PARAM_Control,
+ PW_ENDPOINT_PARAM_CONTROL_id, SPA_POD_Int(impl->volume_control_id),
+ PW_ENDPOINT_PARAM_CONTROL_value, SPA_POD_Double(volume),
+ NULL);
+ pw_endpoint_proxy_set_param(endpoint->proxy,
+ PW_ENDPOINT_PARAM_Control, 0, param);
+}
+
+void
+audiomixer_change_mute(struct audiomixer *self,
+ const struct mixer_control *control,
+ bool mute)
+{
+ const struct mixer_control_impl *impl =
+ (const struct mixer_control_impl *) control;
+ struct endpoint *endpoint = impl->endpoint;
+ char buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 1024);
+ struct spa_pod *param;
+
+ param = spa_pod_builder_add_object(&b,
+ PW_ENDPOINT_OBJECT_ParamControl, PW_ENDPOINT_PARAM_Control,
+ PW_ENDPOINT_PARAM_CONTROL_id, SPA_POD_Int(impl->mute_control_id),
+ PW_ENDPOINT_PARAM_CONTROL_value, SPA_POD_Bool(mute),
+ NULL);
+ pw_endpoint_proxy_set_param(endpoint->proxy,
+ PW_ENDPOINT_PARAM_Control, 0, param);
+}
diff --git a/binding/audiomixer.h b/binding/audiomixer.h
new file mode 100644
index 0000000..f4e83c7
--- /dev/null
+++ b/binding/audiomixer.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <stdbool.h>
+
+struct audiomixer;
+
+struct mixer_control
+{
+ char name[32];
+ double volume;
+ bool mute;
+};
+
+struct audiomixer_events
+{
+ void (*controls_changed) (void *data);
+
+ void (*value_changed) (void *data,
+#define MIXER_CONTROL_CHANGE_FLAG_VOLUME (1<<0)
+#define MIXER_CONTROL_CHANGE_FLAG_MUTE (1<<1)
+ unsigned int change_mask,
+ const struct mixer_control *control);
+};
+
+struct audiomixer * audiomixer_new(void);
+void audiomixer_free(struct audiomixer *self);
+
+/* locking is required to call any of the methods below
+ * and to access any structure maintained by audiomixer */
+void audiomixer_lock(struct audiomixer *self);
+void audiomixer_unlock(struct audiomixer *self);
+
+int audiomixer_ensure_connected(struct audiomixer *self, int timeout_sec);
+int audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec);
+
+const struct mixer_control ** audiomixer_get_active_controls(
+ struct audiomixer *self,
+ unsigned int *n_controls);
+
+const struct mixer_control * audiomixer_find_control(
+ struct audiomixer *self,
+ const char *name);
+
+void audiomixer_add_event_listener(struct audiomixer *self,
+ const struct audiomixer_events *events,
+ void *data);
+
+void audiomixer_change_volume(struct audiomixer *self,
+ const struct mixer_control *control,
+ double volume);
+
+void audiomixer_change_mute(struct audiomixer *self,
+ const struct mixer_control *control,
+ bool mute);
+