aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/audiomixer-service.cpp129
-rw-r--r--src/audiomixer-service.hpp44
-rw-r--r--src/audiomixer.c463
-rw-r--r--src/audiomixer.h71
-rw-r--r--src/main.cpp34
-rw-r--r--src/meson.build18
-rw-r--r--src/vis-config.cpp157
-rw-r--r--src/vis-config.hpp43
-rw-r--r--src/vis-session.cpp374
-rw-r--r--src/vis-session.hpp78
10 files changed, 1411 insertions, 0 deletions
diff --git a/src/audiomixer-service.cpp b/src/audiomixer-service.cpp
new file mode 100644
index 0000000..5787153
--- /dev/null
+++ b/src/audiomixer-service.cpp
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "audiomixer-service.hpp"
+#include <iostream>
+#include <algorithm>
+
+
+AudiomixerService::AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ VisSession(config, ioc, ctx)
+{
+ m_audiomixer = audiomixer_new();
+ if (m_audiomixer) {
+ // Set up callbacks for WirePlumber events
+ m_audiomixer_events.controls_changed = audiomixer_control_change_cb;
+ m_audiomixer_events.value_changed = audiomixer_value_change_cb;
+ audiomixer_add_event_listener(m_audiomixer, &m_audiomixer_events, this);
+
+ // Drive connecting to PipeWire core and refreshing controls list
+ audiomixer_lock(m_audiomixer);
+ audiomixer_ensure_controls(m_audiomixer, 3);
+ audiomixer_unlock(m_audiomixer);
+ } else {
+ std::cerr << "Could not create WirePlumber connection" << std::endl;
+ }
+}
+
+AudiomixerService::~AudiomixerService()
+{
+ audiomixer_free(m_audiomixer);
+}
+
+void AudiomixerService::handle_authorized_response(void)
+{
+ subscribe("Vehicle.Cabin.Infotainment.Media.Volume");
+ subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeUp");
+ subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeDown");
+ subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeMute");
+
+ // Set initial volume in VSS
+ // For now a value of 50 matches the default in the homescreen app.
+ // Ideally there would be some form of persistence scheme to restore
+ // the last value on restart.
+ set("Vehicle.Cabin.Infotainment.Media.Volume", "50");
+}
+
+void AudiomixerService::handle_get_response(std::string &path, std::string &value, std::string &timestamp)
+{
+ // Placeholder since no gets are performed ATM
+}
+
+void AudiomixerService::handle_notification(std::string &path, std::string &value, std::string &timestamp)
+{
+ if (!m_audiomixer) {
+ return;
+ }
+
+ audiomixer_lock(m_audiomixer);
+
+ const struct mixer_control *ctl = audiomixer_find_control(m_audiomixer, "Master Playback");
+ if (!ctl) {
+ audiomixer_unlock(m_audiomixer);
+ return;
+ }
+
+ if (path == "Vehicle.Cabin.Infotainment.Media.Volume") {
+ try {
+ int volume = std::stoi(value);
+ if (volume >= 0 && volume <= 100) {
+ double v = (double) volume / 100.0;
+ if (m_config.verbose() > 1)
+ std::cout << "Setting volume to " << v << std::endl;
+ audiomixer_change_volume(m_audiomixer, ctl, v);
+ }
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeUp" && value == "true") {
+ double volume = ctl->volume;
+ volume += 0.05; // up 5%
+ if (volume > 1.0)
+ volume = 1.0; // clamp to 100%
+ if (m_config.verbose() > 1)
+ std::cout << "Increasing volume to " << volume << std::endl;
+ audiomixer_change_volume(m_audiomixer, ctl, volume);
+
+ } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeDown" && value == "true") {
+ double volume = ctl->volume;
+ volume -= 0.05; // down 5%
+ if (volume < 0.0)
+ volume = 0.0; // clamp to 0%
+ if (m_config.verbose() > 1)
+ std::cout << "Decreasing volume to " << volume << std::endl;
+ audiomixer_change_volume(m_audiomixer, ctl, volume);
+
+ } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeMute" && value == "true") {
+ if (m_config.verbose() > 1) {
+ if (ctl->mute)
+ std::cout << "Unmuting" << std::endl;
+ else
+ std::cout << "Muting" << std::endl;
+ }
+ audiomixer_change_mute(m_audiomixer, ctl, !ctl->mute);
+ }
+ // else ignore
+
+ audiomixer_unlock(m_audiomixer);
+}
+
+void AudiomixerService::handle_control_change(void)
+{
+ // Ignore for now
+}
+
+void AudiomixerService::handle_value_change(unsigned int change_mask, const struct mixer_control *control)
+{
+ if (!control)
+ return;
+
+ if (change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) {
+ if (std::string(control->name) == "Master Playback") {
+ // Push change into VIS
+ std::string value = std::to_string((int) (control->volume * 100.0));
+ set("Vehicle.Cabin.Infotainment.Media.Volume", value);
+ }
+ } else if (change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) {
+ // For now, do nothing, new state is in control->mute
+ }
+}
diff --git a/src/audiomixer-service.hpp b/src/audiomixer-service.hpp
new file mode 100644
index 0000000..cb00584
--- /dev/null
+++ b/src/audiomixer-service.hpp
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _AUDIOMIXER_SERVICE_HPP
+#define _AUDIOMIXER_SERVICE_HPP
+
+#include "vis-session.hpp"
+#include "audiomixer.h"
+
+class AudiomixerService : public VisSession
+{
+ struct audiomixer *m_audiomixer;
+
+public:
+ AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+ ~AudiomixerService();
+
+ static void audiomixer_control_change_cb(void *data) {
+ if (data)
+ ((AudiomixerService*) data)->handle_control_change();
+ };
+
+ static void audiomixer_value_change_cb(void *data,
+ unsigned int change_mask,
+ const struct mixer_control *control) {
+ if (data)
+ ((AudiomixerService*) data)->handle_value_change(change_mask, control);
+ }
+
+protected:
+ struct audiomixer_events m_audiomixer_events;
+
+ virtual void handle_authorized_response(void) override;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) override;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) override;
+
+ virtual void handle_control_change(void);
+
+ virtual void handle_value_change(unsigned int change_mask, const struct mixer_control *control);
+};
+
+#endif // _AUDIOMIXER_SERVICE_HPP
diff --git a/src/audiomixer.c b/src/audiomixer.c
new file mode 100644
index 0000000..a40a38e
--- /dev/null
+++ b/src/audiomixer.c
@@ -0,0 +1,463 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "audiomixer.h"
+#include <wp/wp.h>
+#include <pipewire/pipewire.h>
+
+struct audiomixer
+{
+ WpCore *core;
+ GMainLoop *loop;
+ GMainContext *context;
+ GThread *thread;
+ GMutex lock;
+ GCond cond;
+
+ GPtrArray *mixer_controls;
+
+ gint initialized;
+ WpObjectManager *om;
+ WpPlugin *default_nodes_api;
+ WpPlugin *mixer_api;
+
+ const struct audiomixer_events *events;
+ void *events_data;
+};
+
+struct mixer_control_impl
+{
+ struct mixer_control pub;
+ guint32 node_id;
+};
+
+struct action
+{
+ struct audiomixer *audiomixer;
+ union {
+ struct {
+ guint32 id;
+ gfloat volume;
+ } change_volume;
+ struct {
+ guint32 id;
+ gboolean mute;
+ } change_mute;
+ };
+};
+
+static gboolean
+get_mixer_controls (struct audiomixer * self, guint32 node_id, gdouble * vol, gboolean * mute)
+{
+ g_autoptr (GVariant) v = NULL;
+ g_signal_emit_by_name (self->mixer_api, "get-volume", node_id, &v);
+ return v &&
+ g_variant_lookup (v, "volume", "d", vol) &&
+ g_variant_lookup (v, "mute", "b", mute);
+}
+
+static void
+add_control (struct audiomixer *self, const char *name, guint32 node_id)
+{
+ struct mixer_control_impl *mixctl = NULL;
+ gdouble volume = 1.0;
+ gboolean mute = FALSE;
+
+ /* get current values */
+ if (!get_mixer_controls (self, node_id, &volume, &mute)) {
+ g_warning ("failed to get object controls when populating controls");
+ return;
+ }
+
+ /* create the control */
+ mixctl = g_new0 (struct mixer_control_impl, 1);
+ snprintf (mixctl->pub.name, sizeof (mixctl->pub.name), "%s", name);
+ mixctl->pub.volume = volume;
+ mixctl->pub.mute = mute;
+ mixctl->node_id = node_id;
+ g_ptr_array_add (self->mixer_controls, mixctl);
+
+ g_debug ("added control %s", mixctl->pub.name);
+}
+
+static void
+volume_changed (struct audiomixer * self, guint32 node_id)
+{
+ g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock);
+ gdouble vol = 1.0;
+ gboolean mute = FALSE;
+
+ for (guint i = 0; i < self->mixer_controls->len; i++) {
+ struct mixer_control_impl *ctl;
+ guint change_mask = 0;
+
+ ctl = g_ptr_array_index (self->mixer_controls, i);
+ if (ctl->node_id != node_id)
+ continue;
+
+ if (!get_mixer_controls (self, node_id, &vol, &mute)) {
+ g_warning ("failed to get object controls when volume changed");
+ return;
+ }
+
+ if ((ctl->pub.volume - 0.01f) > vol || (ctl->pub.volume + 0.01f) < vol) {
+ ctl->pub.volume = vol;
+ change_mask |= MIXER_CONTROL_CHANGE_FLAG_VOLUME;
+ }
+ if (ctl->pub.mute != mute) {
+ ctl->pub.mute = mute;
+ change_mask |= MIXER_CONTROL_CHANGE_FLAG_MUTE;
+ }
+
+ if (self->events && self->events->value_changed)
+ self->events->value_changed (self->events_data, change_mask, &ctl->pub);
+ break;
+ }
+}
+
+static void
+rescan_controls (struct audiomixer * self)
+{
+ g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock);
+ g_autoptr (WpIterator) it = NULL;
+ g_auto (GValue) val = G_VALUE_INIT;
+ guint32 id = -1;
+
+ g_debug ("rescan");
+
+ /* clear previous */
+ g_ptr_array_set_size (self->mixer_controls, 0);
+
+ /* add master controls */
+ g_signal_emit_by_name (self->default_nodes_api, "get-default-node",
+ "Audio/Sink", &id);
+ if (id != (guint32)-1)
+ add_control (self, "Master Playback", id);
+
+ g_signal_emit_by_name (self->default_nodes_api, "get-default-node",
+ "Audio/Source", &id);
+ if (id != (guint32)-1)
+ add_control (self, "Master Capture", id);
+
+ /* add endpoints */
+ it = wp_object_manager_new_iterator (self->om);
+ for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
+ WpPipewireObject *ep = g_value_get_object (&val);
+ const gchar *name = wp_pipewire_object_get_property (ep, "endpoint.description");
+ const gchar *node = wp_pipewire_object_get_property (ep, "node.id");
+ id = node ? atoi(node) : 0;
+ if (name && id != 0 && id != (guint32)-1)
+ add_control (self, name, id);
+ }
+
+ /* notify subscribers */
+ if (self->events && self->events->controls_changed)
+ self->events->controls_changed (self->events_data);
+ g_cond_broadcast (&self->cond);
+}
+
+static void
+on_default_nodes_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self)
+{
+ g_autoptr (GError) error = NULL;
+ if (!wp_object_activate_finish (p, res, &error)) {
+ g_warning ("%s", error->message);
+ }
+
+ if (wp_object_get_active_features (WP_OBJECT (self->mixer_api))
+ & WP_PLUGIN_FEATURE_ENABLED)
+ wp_core_install_object_manager (self->core, self->om);
+
+ g_signal_connect_swapped (self->default_nodes_api, "changed",
+ (GCallback) rescan_controls, self);
+}
+
+static void
+on_mixer_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self)
+{
+ g_autoptr (GError) error = NULL;
+ if (!wp_object_activate_finish (p, res, &error)) {
+ g_warning ("%s", error->message);
+ }
+
+ if (wp_object_get_active_features (WP_OBJECT (self->default_nodes_api))
+ & WP_PLUGIN_FEATURE_ENABLED)
+ wp_core_install_object_manager (self->core, self->om);
+
+ g_signal_connect_swapped (self->mixer_api, "changed",
+ (GCallback) volume_changed, self);
+}
+
+static void
+on_core_connected (struct audiomixer * self)
+{
+ self->om = wp_object_manager_new ();
+ wp_object_manager_add_interest (self->om, WP_TYPE_ENDPOINT,
+ WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
+ PW_KEY_MEDIA_CLASS, "#s", "Audio/*", NULL);
+ wp_object_manager_request_object_features (self->om,
+ WP_TYPE_ENDPOINT, WP_OBJECT_FEATURES_ALL);
+ g_signal_connect_swapped (self->om, "objects-changed",
+ (GCallback) rescan_controls, self);
+
+ wp_object_activate (WP_OBJECT (self->default_nodes_api),
+ WP_PLUGIN_FEATURE_ENABLED, NULL,
+ (GAsyncReadyCallback) on_default_nodes_activated, self);
+
+ wp_object_activate (WP_OBJECT (self->mixer_api),
+ WP_PLUGIN_FEATURE_ENABLED, NULL,
+ (GAsyncReadyCallback) on_mixer_activated, self);
+}
+
+static void
+on_core_disconnected (struct audiomixer * self)
+{
+ g_ptr_array_set_size (self->mixer_controls, 0);
+ g_clear_object (&self->om);
+ g_signal_handlers_disconnect_by_data (self->default_nodes_api, self);
+ g_signal_handlers_disconnect_by_data (self->mixer_api, self);
+ wp_object_deactivate (WP_OBJECT (self->default_nodes_api), WP_PLUGIN_FEATURE_ENABLED);
+ wp_object_deactivate (WP_OBJECT (self->mixer_api), WP_PLUGIN_FEATURE_ENABLED);
+}
+
+static void
+audiomixer_init_in_thread (struct audiomixer * self)
+{
+ g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock);
+ g_autoptr (GError) error = NULL;
+
+ self->context = g_main_context_new ();
+ g_main_context_push_thread_default (self->context);
+
+ self->loop = g_main_loop_new (self->context, FALSE);
+ self->core = wp_core_new (self->context, NULL);
+
+ /* load required API modules */
+ if (!wp_core_load_component (self->core,
+ "libwireplumber-module-default-nodes-api", "module", NULL, &error)) {
+ g_warning ("%s", error->message);
+ self->initialized = -1;
+ goto out;
+ }
+ if (!wp_core_load_component (self->core,
+ "libwireplumber-module-mixer-api", "module", NULL, &error)) {
+ g_warning ("%s", error->message);
+ self->initialized = -1;
+ goto out;
+ }
+
+ self->default_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
+ self->mixer_api = wp_plugin_find (self->core, "mixer-api");
+ g_object_set (G_OBJECT (self->mixer_api), "scale", 1 /* cubic */, NULL);
+
+ g_signal_connect_swapped (self->core, "connected",
+ G_CALLBACK (on_core_connected), self);
+ g_signal_connect_swapped (self->core, "disconnected",
+ G_CALLBACK (on_core_disconnected), self);
+
+ self->initialized = 1;
+
+out:
+ g_cond_broadcast (&self->cond);
+}
+
+static void *
+audiomixer_thread (struct audiomixer * self)
+{
+ audiomixer_init_in_thread (self);
+
+ /* main loop for the thread; quits only when audiomixer_free() is called */
+ g_main_loop_run (self->loop);
+
+ wp_core_disconnect (self->core);
+ g_clear_object (&self->default_nodes_api);
+ g_clear_object (&self->mixer_api);
+ g_object_unref (self->core);
+
+ g_main_context_pop_thread_default (self->context);
+ g_main_loop_unref (self->loop);
+ g_main_context_unref (self->context);
+
+ return NULL;
+}
+
+struct audiomixer *
+audiomixer_new (void)
+{
+ struct audiomixer *self = calloc(1, sizeof(struct audiomixer));
+
+ wp_init (WP_INIT_ALL);
+
+ g_mutex_init (&self->lock);
+ g_cond_init (&self->cond);
+ self->mixer_controls = g_ptr_array_new_with_free_func (g_free);
+
+ g_mutex_lock (&self->lock);
+ self->initialized = 0;
+ self->thread = g_thread_new ("audiomixer", (GThreadFunc) audiomixer_thread,
+ self);
+ while (self->initialized == 0)
+ g_cond_wait (&self->cond, &self->lock);
+ g_mutex_unlock (&self->lock);
+
+ return self;
+}
+
+void
+audiomixer_free(struct audiomixer *self)
+{
+ g_main_loop_quit (self->loop);
+ g_thread_join (self->thread);
+
+ g_ptr_array_unref (self->mixer_controls);
+ g_cond_clear (&self->cond);
+ g_mutex_clear (&self->lock);
+
+ free (self);
+}
+
+void
+audiomixer_lock(struct audiomixer *self)
+{
+ g_mutex_lock (&self->lock);
+}
+
+void
+audiomixer_unlock(struct audiomixer *self)
+{
+ g_mutex_unlock (&self->lock);
+}
+
+static gboolean
+do_connect (WpCore * core)
+{
+ if (!wp_core_connect (core))
+ g_warning ("Failed to connect to PipeWire");
+ return G_SOURCE_REMOVE;
+}
+
+int
+audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec)
+{
+ gint64 end_time = g_get_monotonic_time () + timeout_sec * G_TIME_SPAN_SECOND;
+
+ g_return_val_if_fail (self->initialized == 1, -EIO);
+
+ if (!wp_core_is_connected (self->core))
+ g_main_context_invoke (self->context, (GSourceFunc) do_connect, self->core);
+
+ while (self->mixer_controls->len == 0) {
+ if (!g_cond_wait_until (&self->cond, &self->lock, end_time))
+ return -ETIMEDOUT;
+ }
+ return 0;
+}
+
+const struct mixer_control **
+audiomixer_get_active_controls(struct audiomixer *self,
+ unsigned int *n_controls)
+{
+ *n_controls = self->mixer_controls->len;
+ return (const struct mixer_control **) self->mixer_controls->pdata;
+}
+
+const struct mixer_control *
+audiomixer_find_control(struct audiomixer *self, const char *name)
+{
+ struct mixer_control *ctl;
+
+ for (guint i = 0; i < self->mixer_controls->len; i++) {
+ ctl = g_ptr_array_index (self->mixer_controls, i);
+ if (!strcmp(ctl->name, name)) {
+ return ctl;
+ }
+ }
+ return NULL;
+}
+
+void
+audiomixer_add_event_listener(struct audiomixer *self,
+ const struct audiomixer_events *events,
+ void *data)
+{
+ self->events = events;
+ self->events_data = data;
+}
+
+static gboolean
+do_change_volume (struct action * action)
+{
+ struct audiomixer *self = action->audiomixer;
+ GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
+ gboolean ret = FALSE;
+
+ g_variant_builder_add (&b, "{sv}", "volume",
+ g_variant_new_double (action->change_volume.volume));
+ g_signal_emit_by_name (self->mixer_api, "set-volume",
+ action->change_volume.id, g_variant_builder_end (&b), &ret);
+ if (!ret)
+ g_warning ("mixer api set-volume failed");
+
+ return G_SOURCE_REMOVE;
+}
+
+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 action * action;
+
+ g_return_if_fail (self->initialized == 1);
+
+ /* schedule the action to run on the audiomixer thread */
+ action = g_new0 (struct action, 1);
+ action->audiomixer = self;
+ action->change_volume.id = impl->node_id;
+ action->change_volume.volume = (gfloat) volume;
+ wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_volume, action,
+ g_free);
+}
+
+static gboolean
+do_change_mute (struct action * action)
+{
+ struct audiomixer *self = action->audiomixer;
+ GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
+ gboolean ret = FALSE;
+
+ g_variant_builder_add (&b, "{sv}", "mute",
+ g_variant_new_boolean (action->change_mute.mute));
+ g_signal_emit_by_name (self->mixer_api, "set-volume",
+ action->change_mute.id, g_variant_builder_end (&b), &ret);
+ if (!ret)
+ g_warning ("mixer api set-volume failed");
+
+ return G_SOURCE_REMOVE;
+}
+
+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 action * action;
+
+ g_return_if_fail (self->initialized == 1);
+
+ /* schedule the action to run on the audiomixer thread */
+ action = g_new0 (struct action, 1);
+ action->audiomixer = self;
+ action->change_mute.id = impl->node_id;
+ action->change_mute.mute = mute;
+ wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_mute, action,
+ g_free);
+}
diff --git a/src/audiomixer.h b/src/audiomixer.h
new file mode 100644
index 0000000..cc67a83
--- /dev/null
+++ b/src/audiomixer.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef _AUDIOMIXER_H
+#define _AUDIOMIXER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#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_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);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // _AUDIOMIXER_H
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..0960f1b
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <iostream>
+#include <iomanip>
+#include <boost/asio/signal_set.hpp>
+#include <boost/bind.hpp>
+#include "audiomixer-service.hpp"
+
+using work_guard_type = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;
+
+int main(int argc, char** argv)
+{
+ // The io_context is required for all I/O
+ net::io_context ioc;
+
+ // Register to stop I/O context on SIGINT and SIGTERM
+ net::signal_set signals(ioc, SIGINT, SIGTERM);
+ signals.async_wait(boost::bind(&net::io_context::stop, &ioc));
+
+ // The SSL context is required, and holds certificates
+ ssl::context ctx{ssl::context::tlsv12_client};
+
+ // Launch the asynchronous operation
+ VisConfig config("agl-service-audiomixer");
+ std::make_shared<AudiomixerService>(config, ioc, ctx)->run();
+
+ // Ensure I/O context continues running even if there's no work
+ work_guard_type work_guard(ioc.get_executor());
+
+ // Run the I/O context
+ ioc.run();
+
+ return 0;
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..282d130
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,18 @@
+boost_dep = dependency('boost',
+ version : '>=1.72',
+ modules : [ 'thread', 'filesystem', 'program_options', 'log', 'system' ])
+openssl_dep = dependency('openssl')
+thread_dep = dependency('threads')
+wp_dep = dependency('wireplumber-0.4')
+
+src = [ 'vis-config.cpp',
+ 'vis-session.cpp',
+ 'audiomixer-service.cpp',
+ 'audiomixer.c',
+ 'main.cpp'
+]
+executable('agl-service-audiomixer',
+ src,
+ dependencies: [boost_dep, openssl_dep, thread_dep, systemd_dep, wp_dep],
+ install: true,
+ install_dir : get_option('sbindir'))
diff --git a/src/vis-config.cpp b/src/vis-config.cpp
new file mode 100644
index 0000000..b8d9266
--- /dev/null
+++ b/src/vis-config.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-config.hpp"
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/filesystem.hpp>
+
+namespace property_tree = boost::property_tree;
+namespace filesystem = boost::filesystem;
+
+#define DEFAULT_CLIENT_KEY_FILE "/etc/kuksa-val/Client.key"
+#define DEFAULT_CLIENT_CERT_FILE "/etc/kuksa-val/Client.pem"
+#define DEFAULT_CA_CERT_FILE "/etc/kuksa-val/CA.pem"
+
+
+VisConfig::VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer) :
+ m_hostname(hostname),
+ m_port(port),
+ m_clientKey(clientKey),
+ m_clientCert(clientCert),
+ m_caCert(caCert),
+ m_authToken(authToken),
+ m_verifyPeer(verifyPeer),
+ m_verbose(0),
+ m_valid(true)
+{
+ // Potentially could do some certificate validation here...
+}
+
+VisConfig::VisConfig(const std::string &appname) :
+ m_valid(false)
+{
+ std::string config("/etc/xdg/AGL/");
+ config += appname;
+ config += ".conf";
+ char *home = getenv("XDG_CONFIG_HOME");
+ if (home) {
+ config = home;
+ config += "/AGL/";
+ config += appname;
+ config += ".conf";
+ }
+
+ std::cout << "Using configuration " << config << std::endl;
+ property_tree::ptree pt;
+ try {
+ property_tree::ini_parser::read_ini(config, pt);
+ }
+ catch (std::exception &ex) {
+ std::cerr << "Could not read " << config << std::endl;
+ return;
+ }
+ const property_tree::ptree &settings =
+ pt.get_child("vis-client", property_tree::ptree());
+
+ m_hostname = settings.get("server", "localhost");
+ std::stringstream ss;
+ ss << m_hostname;
+ ss >> std::quoted(m_hostname);
+ if (m_hostname.empty()) {
+ std::cerr << "Invalid server hostname" << std::endl;
+ return;
+ }
+
+ m_port = settings.get("port", 8090);
+ if (m_port == 0) {
+ std::cerr << "Invalid server port" << std::endl;
+ return;
+ }
+
+ // Default to disabling peer verification for now to be able
+ // to use the default upstream KUKSA.val certificates for
+ // testing. Wrangling server and CA certificate generation
+ // and management to be able to verify will require further
+ // investigation.
+ m_verifyPeer = settings.get("verify-server", false);
+
+ std::string keyFileName = settings.get("key", DEFAULT_CLIENT_KEY_FILE);
+ std::stringstream().swap(ss);
+ ss << keyFileName;
+ ss >> std::quoted(keyFileName);
+ ss.str("");
+ if (keyFileName.empty()) {
+ std::cerr << "Invalid client key filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(keyFileName, m_clientKey);
+ if (m_clientKey.empty()) {
+ std::cerr << "Invalid client key file" << std::endl;
+ return;
+ }
+
+ std::string certFileName = settings.get("certificate", DEFAULT_CLIENT_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << certFileName;
+ ss >> std::quoted(certFileName);
+ if (certFileName.empty()) {
+ std::cerr << "Invalid client certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(certFileName, m_clientCert);
+ if (m_clientCert.empty()) {
+ std::cerr << "Invalid client certificate file" << std::endl;
+ return;
+ }
+
+ std::string caCertFileName = settings.get("ca-certificate", DEFAULT_CA_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << caCertFileName;
+ ss >> std::quoted(caCertFileName);
+ if (caCertFileName.empty()) {
+ std::cerr << "Invalid CA certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(caCertFileName, m_caCert);
+ if (m_caCert.empty()) {
+ std::cerr << "Invalid CA certificate file" << std::endl;
+ return;
+ }
+
+ std::string authTokenFileName = settings.get("authorization", "");
+ std::stringstream().swap(ss);
+ ss << authTokenFileName;
+ ss >> std::quoted(authTokenFileName);
+ if (authTokenFileName.empty()) {
+ std::cerr << "Invalid authorization token filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(authTokenFileName, m_authToken);
+ if (m_authToken.empty()) {
+ std::cerr << "Invalid authorization token file" << std::endl;
+ return;
+ }
+
+ m_verbose = 0;
+ std::string verbose = settings.get("verbose", "");
+ std::stringstream().swap(ss);
+ ss << verbose;
+ ss >> std::quoted(verbose);
+ if (!verbose.empty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_valid = true;
+}
diff --git a/src/vis-config.hpp b/src/vis-config.hpp
new file mode 100644
index 0000000..b0f72f9
--- /dev/null
+++ b/src/vis-config.hpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_CONFIG_HPP
+#define _VIS_CONFIG_HPP
+
+#include <string>
+
+class VisConfig
+{
+public:
+ explicit VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer = true);
+ explicit VisConfig(const std::string &appname);
+ ~VisConfig() {};
+
+ std::string hostname() { return m_hostname; };
+ unsigned port() { return m_port; };
+ std::string clientKey() { return m_clientKey; };
+ std::string clientCert() { return m_clientCert; };
+ std::string caCert() { return m_caCert; };
+ std::string authToken() { return m_authToken; };
+ bool verifyPeer() { return m_verifyPeer; };
+ bool valid() { return m_valid; };
+ unsigned verbose() { return m_verbose; };
+
+private:
+ std::string m_hostname;
+ unsigned m_port;
+ std::string m_clientKey;
+ std::string m_clientCert;
+ std::string m_caCert;
+ std::string m_authToken;
+ bool m_verifyPeer;
+ unsigned m_verbose;
+ bool m_valid;
+};
+
+#endif // _VIS_CONFIG_HPP
diff --git a/src/vis-session.cpp b/src/vis-session.cpp
new file mode 100644
index 0000000..880e3ae
--- /dev/null
+++ b/src/vis-session.cpp
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-session.hpp"
+#include <iostream>
+#include <sstream>
+#include <thread>
+
+
+// Logging helper
+static void log_error(beast::error_code error, char const* what)
+{
+ std::cerr << what << " error: " << error.message() << std::endl;
+}
+
+
+// Resolver and socket require an io_context
+VisSession::VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ m_config(config),
+ m_resolver(net::make_strand(ioc)),
+ m_ws(net::make_strand(ioc), ctx)
+{
+}
+
+// Start the asynchronous operation
+void VisSession::run()
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ // Start by resolving hostname
+ m_resolver.async_resolve(m_config.hostname(),
+ std::to_string(m_config.port()),
+ beast::bind_front_handler(&VisSession::on_resolve,
+ shared_from_this()));
+}
+
+void VisSession::on_resolve(beast::error_code error,
+ tcp::resolver::results_type results)
+{
+ if(error) {
+ log_error(error, "resolve");
+ return;
+ }
+
+ // Set a timeout on the connect operation
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Connect to resolved address
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+ m_results = results;
+ connect();
+}
+
+void VisSession::connect()
+{
+ beast::get_lowest_layer(m_ws).async_connect(m_results,
+ beast::bind_front_handler(&VisSession::on_connect,
+ shared_from_this()));
+}
+
+void VisSession::on_connect(beast::error_code error,
+ tcp::resolver::results_type::endpoint_type endpoint)
+{
+ if(error) {
+ // The server can take a while to be ready to accept connections,
+ // so keep retrying until we hit the timeout.
+ if (error == net::error::timed_out) {
+ log_error(error, "connect");
+ return;
+ }
+
+ // Delay 500 ms before retrying
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+
+ connect();
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Connected" << std::endl;
+
+ // Set handshake timeout
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if(!SSL_set_tlsext_host_name(m_ws.next_layer().native_handle(),
+ m_config.hostname().c_str()))
+ {
+ error = beast::error_code(static_cast<int>(::ERR_get_error()),
+ net::error::get_ssl_category());
+ log_error(error, "connect");
+ return;
+ }
+
+ // Update the hostname. This will provide the value of the
+ // Host HTTP header during the WebSocket handshake.
+ // See https://tools.ietf.org/html/rfc7230#section-5.4
+ m_hostname = m_config.hostname() + ':' + std::to_string(endpoint.port());
+
+ if (m_config.verbose())
+ std::cout << "Negotiating SSL handshake" << std::endl;
+
+ // Perform the SSL handshake
+ m_ws.next_layer().async_handshake(ssl::stream_base::client,
+ beast::bind_front_handler(&VisSession::on_ssl_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_ssl_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "SSL handshake");
+ return;
+ }
+
+ // Turn off the timeout on the tcp_stream, because
+ // the websocket stream has its own timeout system.
+ beast::get_lowest_layer(m_ws).expires_never();
+
+ // NOTE: Explicitly not setting websocket stream timeout here,
+ // as the client is long-running.
+
+ if (m_config.verbose())
+ std::cout << "Negotiating WSS handshake" << std::endl;
+
+ // Perform handshake
+ m_ws.async_handshake(m_hostname,
+ "/",
+ beast::bind_front_handler(&VisSession::on_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "WSS handshake");
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Authorizing" << std::endl;
+
+ // Authorize
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"]= "authorize";
+ req["tokens"] = m_config.authToken();
+
+ m_ws.async_write(net::buffer(req.dump(4)),
+ beast::bind_front_handler(&VisSession::on_authorize,
+ shared_from_this()));
+}
+
+void VisSession::on_authorize(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "authorize");
+ return;
+ }
+
+ // Read response
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+// NOTE: Placeholder for now
+void VisSession::on_write(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "write");
+ return;
+ }
+
+ // Do nothing...
+}
+
+void VisSession::on_read(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "read");
+ return;
+ }
+
+ // Handle message
+ std::string s = beast::buffers_to_string(m_buffer.data());
+ json response = json::parse(s, nullptr, false);
+ if (!response.is_discarded()) {
+ handle_message(response);
+ } else {
+ std::cerr << "json::parse failed? got " << s << std::endl;
+ }
+ m_buffer.consume(m_buffer.size());
+
+ // Read next message
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+void VisSession::get(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "get";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::set(const std::string &path, const std::string &value)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "set";
+ req["path"] = path;
+ req["value"] = value;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::subscribe(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "subscribe";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+bool VisSession::parseData(const json &message, std::string &path, std::string &value, std::string &timestamp)
+{
+ if (message.contains("error")) {
+ std::string error = message["error"];
+ return false;
+ }
+
+ if (!(message.contains("data") && message["data"].is_object())) {
+ std::cerr << "Malformed message (data missing)" << std::endl;
+ return false;
+ }
+ auto data = message["data"];
+ if (!(data.contains("path") && data["path"].is_string())) {
+ std::cerr << "Malformed message (path missing)" << std::endl;
+ return false;
+ }
+ path = data["path"];
+ // Convert '/' to '.' in paths to ensure consistency for clients
+ std::replace(path.begin(), path.end(), '/', '.');
+
+ if (!(data.contains("dp") && data["dp"].is_object())) {
+ std::cerr << "Malformed message (datapoint missing)" << std::endl;
+ return false;
+ }
+ auto dp = data["dp"];
+ if (!dp.contains("value")) {
+ std::cerr << "Malformed message (value missing)" << std::endl;
+ return false;
+ } else if (dp["value"].is_string()) {
+ value = dp["value"];
+ } else if (dp["value"].is_number_float()) {
+ double num = dp["value"];
+ value = std::to_string(num);
+ } else if (dp["value"].is_boolean()) {
+ value = dp["value"] ? "true" : "false";
+ } else {
+ std::cerr << "Malformed message (unsupported value type)" << std::endl;
+ return false;
+ }
+
+ if (!(dp.contains("ts") && dp["ts"].is_string())) {
+ std::cerr << "Malformed message (timestamp missing)" << std::endl;
+ return false;
+ }
+ timestamp = dp["ts"];
+
+ return true;
+}
+
+void VisSession::handle_message(const json &message)
+{
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: enter, message = " << to_string(message) << std::endl;
+
+ if (!message.contains("action")) {
+ std::cerr << "Received unknown message (no action), discarding" << std::endl;
+ return;
+ }
+
+ std::string action = message["action"];
+ if (action == "authorize") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS authorization failed: " << error << std::endl;
+ } else {
+ if (m_config.verbose() > 1)
+ std::cout << "authorized" << std::endl;
+
+ handle_authorized_response();
+ }
+ } else if (action == "subscribe") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS subscription failed: " << error << std::endl;
+ }
+ } else if (action == "get") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS get failed: " << error << std::endl;
+ } else {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got response " << path << " = " << value << std::endl;
+
+ handle_get_response(path, value, ts);
+ }
+ }
+ } else if (action == "set") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS set failed: " << error;
+ }
+ } else if (action == "subscription") {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got notification " << path << " = " << value << std::endl;
+
+ handle_notification(path, value, ts);
+ }
+ } else {
+ std::cerr << "unhandled VIS response of type: " << action;
+ }
+
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: exit" << std::endl;
+}
+
diff --git a/src/vis-session.hpp b/src/vis-session.hpp
new file mode 100644
index 0000000..8c7b0d9
--- /dev/null
+++ b/src/vis-session.hpp
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_SESSION_HPP
+#define _VIS_SESSION_HPP
+
+#include "vis-config.hpp"
+#include <atomic>
+#include <string>
+#include <boost/beast/core.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/asio/strand.hpp>
+#include <nlohmann/json.hpp>
+
+namespace beast = boost::beast;
+namespace websocket = beast::websocket;
+namespace net = boost::asio;
+namespace ssl = boost::asio::ssl;
+using tcp = boost::asio::ip::tcp;
+using json = nlohmann::json;
+
+
+class VisSession : public std::enable_shared_from_this<VisSession>
+{
+ //net::io_context m_ioc;
+ tcp::resolver m_resolver;
+ tcp::resolver::results_type m_results;
+ std::string m_hostname;
+ websocket::stream<beast::ssl_stream<beast::tcp_stream>> m_ws;
+ beast::flat_buffer m_buffer;
+
+public:
+ // Resolver and socket require an io_context
+ explicit VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+ // Start the asynchronous operation
+ void run();
+
+protected:
+ VisConfig m_config;
+ std::atomic_uint m_requestid;
+
+ void on_resolve(beast::error_code error, tcp::resolver::results_type results);
+
+ void connect();
+
+ void on_connect(beast::error_code error, tcp::resolver::results_type::endpoint_type endpoint);
+
+ void on_ssl_handshake(beast::error_code error);
+
+ void on_handshake(beast::error_code error);
+
+ void on_authorize(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_write(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_read(beast::error_code error, std::size_t bytes_transferred);
+
+ void get(const std::string &path);
+
+ void set(const std::string &path, const std::string &value);
+
+ void subscribe(const std::string &path);
+
+ void handle_message(const json &message);
+
+ bool parseData(const json &message, std::string &path, std::string &value, std::string &timestamp);
+
+ virtual void handle_authorized_response(void) = 0;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+};
+
+#endif // _VIS_SESSION_HPP