summaryrefslogtreecommitdiffstats
path: root/binding/audiomixer-binding.c
diff options
context:
space:
mode:
Diffstat (limited to 'binding/audiomixer-binding.c')
-rw-r--r--binding/audiomixer-binding.c342
1 files changed, 342 insertions, 0 deletions
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,
+};