aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAshok Sidipotu <ashok.sidipotu@collabora.com>2023-12-07 14:14:01 +0100
committerAshok Sidipotu <ashok.sidipotu@collabora.com>2023-12-08 13:12:14 +0100
commitf6eb75678f6c08c4eb3fe232f814834319611ff7 (patch)
tree0da6c467fb0e2ffcbceb0ccdfe64fc2ef05cd11e
parent82c1c0ab04219f9453f1b3a14a9754068e360583 (diff)
audiomixer: Add gain controls
- Add Equalizer gain controls. - Add a simple app to test the controls. Bug-AGL: SPEC-4931 Change-Id: Ib33eb0e829747c401861e99acd67291462ec6a97 Signed-off-by: Ashok Sidipotu <ashok.sidipotu@collabora.com>
-rw-r--r--src/audiomixer.c233
-rw-r--r--src/audiomixer.h6
-rw-r--r--src/audiomixertest.c329
-rw-r--r--src/meson.build9
4 files changed, 570 insertions, 7 deletions
diff --git a/src/audiomixer.c b/src/audiomixer.c
index a40a38e..0351738 100644
--- a/src/audiomixer.c
+++ b/src/audiomixer.c
@@ -7,6 +7,7 @@
#include "audiomixer.h"
#include <wp/wp.h>
+#include <math.h>
#include <pipewire/pipewire.h>
struct audiomixer
@@ -22,6 +23,8 @@ struct audiomixer
gint initialized;
WpObjectManager *om;
+ WpObjectManager *eq_om;
+ WpPipewireObject *eq_node;
WpPlugin *default_nodes_api;
WpPlugin *mixer_api;
@@ -47,6 +50,11 @@ struct action
guint32 id;
gboolean mute;
} change_mute;
+ struct {
+ guint32 id;
+ gchar *control;
+ gfloat gain;
+ } change_gain;
};
};
@@ -60,6 +68,86 @@ get_mixer_controls (struct audiomixer * self, guint32 node_id, gdouble * vol, gb
g_variant_lookup (v, "mute", "b", mute);
}
+static gboolean
+get_gain (struct audiomixer *self, const char *name, gfloat *gain)
+{
+ g_autoptr (WpIterator) it = NULL;
+ g_auto (GValue) val = G_VALUE_INIT;
+ gchar param_name[20];
+ gboolean ret = FALSE;
+
+ snprintf (param_name, sizeof (param_name), "%s:%s", name, "Gain");
+
+ it = wp_pipewire_object_enum_params_sync (self->eq_node, "Props", NULL);
+
+ for (; it && wp_iterator_next (it, &val); g_value_unset (&val)) {
+ WpSpaPod *props = g_value_get_boxed (&val);
+ WpSpaPod *params = NULL;
+ g_autoptr (WpIterator) it1 = NULL;
+ g_auto (GValue) val1 = G_VALUE_INIT;
+ gboolean param_found = FALSE;
+
+ if (!wp_spa_pod_get_object (props, NULL, "params", "T", &params, NULL))
+ continue;
+
+ if (!wp_spa_pod_is_struct (params))
+ continue;
+
+ /* iterate through the params structure */
+ for (it1 = wp_spa_pod_new_iterator (params);
+ it1 && wp_iterator_next (it1, &val1);
+ g_value_unset (&val1)) {
+
+ WpSpaPod *sparams = g_value_get_boxed (&val1);
+
+ if (sparams && wp_spa_pod_is_string (sparams)) {
+ const gchar *token = NULL;
+ wp_spa_pod_get_string (sparams, &token);
+ if (g_str_equal (token, param_name)) {
+ /* read the next field to get the gain value */
+ param_found = TRUE;
+ continue;
+ }
+ }
+ else if (sparams && param_found && wp_spa_pod_is_float (sparams)) {
+ if (wp_spa_pod_get_float (sparams, gain)) {
+ g_debug ("gain for control(%s) is %f", param_name, *gain);
+ ret = TRUE;
+ break;
+ }
+ }
+
+ }
+
+ if (param_found)
+ break;
+
+ }
+ return ret;
+}
+
+static void
+add_eq_control (struct audiomixer *self, const char *name, guint32 node_id)
+{
+ struct mixer_control_impl *mixctl = NULL;
+ gfloat gain = 0.0;
+
+ /* get current gain */
+ if (!get_gain (self, name, &gain)) {
+ g_warning ("failed to get the gain value");
+ return;
+ }
+
+ /* create the control */
+ mixctl = g_new0 (struct mixer_control_impl, 1);
+ snprintf (mixctl->pub.name, sizeof (mixctl->pub.name), "%s", name);
+ mixctl->pub.gain = gain;
+ mixctl->node_id = node_id;
+ g_ptr_array_add (self->mixer_controls, mixctl);
+
+ g_debug ("added (%s) eq control and its gain is %f", mixctl->pub.name, gain);
+}
+
static void
add_control (struct audiomixer *self, const char *name, guint32 node_id)
{
@@ -69,7 +157,8 @@ add_control (struct audiomixer *self, const char *name, guint32 node_id)
/* get current values */
if (!get_mixer_controls (self, node_id, &volume, &mute)) {
- g_warning ("failed to get object controls when populating controls");
+ g_warning ("failed to get object controls when populating controls for %s",
+ name);
return;
}
@@ -81,7 +170,7 @@ add_control (struct audiomixer *self, const char *name, guint32 node_id)
mixctl->node_id = node_id;
g_ptr_array_add (self->mixer_controls, mixctl);
- g_debug ("added control %s", mixctl->pub.name);
+ g_debug ("added control %s its volume is %f", mixctl->pub.name, volume);
}
static void
@@ -154,6 +243,14 @@ rescan_controls (struct audiomixer * self)
add_control (self, name, id);
}
+ if (self->eq_node) {
+ id = wp_proxy_get_bound_id (WP_PROXY (self->eq_node));
+ if (id != 0 && id != (guint32)-1) {
+ add_eq_control (self, "bass", id);
+ add_eq_control (self, "treble", id);
+ }
+ }
+
/* notify subscribers */
if (self->events && self->events->controls_changed)
self->events->controls_changed (self->events_data);
@@ -169,8 +266,10 @@ on_default_nodes_activated (WpObject * p, GAsyncResult * res, struct audiomixer
}
if (wp_object_get_active_features (WP_OBJECT (self->mixer_api))
- & WP_PLUGIN_FEATURE_ENABLED)
- wp_core_install_object_manager (self->core, self->om);
+ & WP_PLUGIN_FEATURE_ENABLED) {
+ wp_core_install_object_manager (self->core, self->eq_om);
+ wp_core_install_object_manager (self->core, self->om);
+ }
g_signal_connect_swapped (self->default_nodes_api, "changed",
(GCallback) rescan_controls, self);
@@ -185,16 +284,73 @@ on_mixer_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self)
}
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);
+ & WP_PLUGIN_FEATURE_ENABLED) {
+ wp_core_install_object_manager (self->core, self->om);
+ wp_core_install_object_manager (self->core, self->eq_om);
+ }
g_signal_connect_swapped (self->mixer_api, "changed",
(GCallback) volume_changed, self);
}
static void
-on_core_connected (struct audiomixer * self)
+on_eq_params_changed (WpPipewireObject *obj, const gchar *param_name,
+ struct audiomixer * self)
{
+ gfloat gain = 0.0;
+ guint32 node_id = wp_proxy_get_bound_id (WP_PROXY (obj));
+
+ if (!g_str_equal (param_name, "Props"))
+ return;
+
+ 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_gain (self, ctl->pub.name, &gain)) {
+ g_warning ("failed to get cached gain value");
+ return;
+ }
+
+ if (!(fabs (ctl->pub.gain - gain) < 0.000001)) {
+ /* if gain changed */
+ ctl->pub.gain = gain;
+ change_mask |= MIXER_CONTROL_CHANGE_FLAG_GAIN;
+ if (self->events && self->events->value_changed) {
+ self->events->value_changed (self->events_data, change_mask, &ctl->pub);
+ }
+ }
+
+ break;
+ }
+}
+static void
+on_eq_added (WpObjectManager *om, WpPipewireObject *node,
+ struct audiomixer *self)
+{
+ self->eq_node = node;
+ g_signal_connect (node, "params-changed", G_CALLBACK (on_eq_params_changed),
+ self);
+ rescan_controls (self);
+}
+
+static void
+on_eq_removed (WpObjectManager *om, WpPipewireObject *node, struct audiomixer
+ *self)
+{
+ self->eq_node = NULL;
+ rescan_controls (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,
@@ -204,6 +360,23 @@ on_core_connected (struct audiomixer * self)
g_signal_connect_swapped (self->om, "objects-changed",
(GCallback) rescan_controls, self);
+ self->eq_node = NULL;
+ self->eq_om = wp_object_manager_new ();
+ /*
+ * "eq-sink" name matches with the name of sink node loaded by the equalizer
+ * module present in the config.
+ */
+ wp_object_manager_add_interest (self->eq_om, WP_TYPE_NODE,
+ WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_NODE_NAME, "=s", "eq-sink",
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "=s", "Audio/Sink",
+ NULL);
+ wp_object_manager_request_object_features (self->eq_om,
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
+ g_signal_connect (self->eq_om, "object-added", G_CALLBACK (on_eq_added),
+ self);
+ g_signal_connect (self->eq_om, "object-removed", G_CALLBACK (on_eq_removed),
+ self);
+
wp_object_activate (WP_OBJECT (self->default_nodes_api),
WP_PLUGIN_FEATURE_ENABLED, NULL,
(GAsyncReadyCallback) on_default_nodes_activated, self);
@@ -218,6 +391,7 @@ on_core_disconnected (struct audiomixer * self)
{
g_ptr_array_set_size (self->mixer_controls, 0);
g_clear_object (&self->om);
+ g_clear_object (&self->eq_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);
@@ -461,3 +635,48 @@ audiomixer_change_mute(struct audiomixer *self,
wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_mute, action,
g_free);
}
+
+static gboolean
+do_change_gain (struct action * action)
+{
+ struct audiomixer *self = action->audiomixer;
+ gboolean ret = FALSE;
+ gchar name[20];
+ g_autoptr (WpSpaPodBuilder) s = wp_spa_pod_builder_new_struct ();
+
+ snprintf (name, sizeof (name), "%s:%s", action->change_gain.control, "Gain");
+ wp_spa_pod_builder_add_string (s, name);
+ wp_spa_pod_builder_add_float (s, action->change_gain.gain);
+
+ g_autoptr (WpSpaPod) props =
+ wp_spa_pod_new_object ("Spa:Pod:Object:Param:Props", "Props",
+ "params", "P", wp_spa_pod_builder_end (s), NULL);
+
+ ret = wp_pipewire_object_set_param (self->eq_node, "Props", 0,
+ g_steal_pointer (&props));
+
+ if(!ret)
+ g_warning ("set gain failed");
+
+ return G_SOURCE_REMOVE;
+}
+
+void
+audiomixer_change_gain(struct audiomixer *self,
+ const struct mixer_control *control,
+ float gain)
+{
+ const struct mixer_control_impl *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_gain.id = impl->node_id;
+ action->change_gain.gain = gain;
+ action->change_gain.control = impl->pub.name;
+ wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_gain, action,
+ g_free);
+}
diff --git a/src/audiomixer.h b/src/audiomixer.h
index cc67a83..2e50420 100644
--- a/src/audiomixer.h
+++ b/src/audiomixer.h
@@ -20,6 +20,7 @@ struct mixer_control
{
char name[32];
double volume;
+ float gain;
bool mute;
};
@@ -30,6 +31,7 @@ struct audiomixer_events
void (*value_changed) (void *data,
#define MIXER_CONTROL_CHANGE_FLAG_VOLUME (1<<0)
#define MIXER_CONTROL_CHANGE_FLAG_MUTE (1<<1)
+#define MIXER_CONTROL_CHANGE_FLAG_GAIN (1<<2)
unsigned int change_mask,
const struct mixer_control *control);
};
@@ -60,6 +62,10 @@ void audiomixer_change_volume(struct audiomixer *self,
const struct mixer_control *control,
double volume);
+void audiomixer_change_gain(struct audiomixer *self,
+ const struct mixer_control *control,
+ float gain);
+
void audiomixer_change_mute(struct audiomixer *self,
const struct mixer_control *control,
bool mute);
diff --git a/src/audiomixertest.c b/src/audiomixertest.c
new file mode 100644
index 0000000..ddb7ac9
--- /dev/null
+++ b/src/audiomixertest.c
@@ -0,0 +1,329 @@
+/* WirePlumber
+ *
+ * Copyright © 2023 Collabora Ltd.
+ * @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wp/wp.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <math.h>
+#include "audiomixer.h"
+
+typedef struct _AudioMixerTest AudioMixerTest;
+
+struct _AudioMixerTest
+{
+ struct audiomixer *am;
+ WpCore *core;
+ const struct mixer_control **ctrls;
+ unsigned int nctrls;
+ GMainLoop *loop;
+};
+
+static void
+audio_mixer_clear (AudioMixerTest *self)
+{
+ audiomixer_free (self->am);
+ g_clear_pointer (&self->loop, g_main_loop_unref);
+}
+
+static gint
+set_mute (AudioMixerTest *self, gint id)
+{
+ gint ret = -1;
+ gboolean mute = FALSE;
+ int i;
+
+ for (i = 0; i < self->nctrls; i++) {
+ const struct mixer_control *ctrl = self->ctrls[i];
+ if (id == i) {
+
+ if (g_str_equal ("bass", ctrl->name) || g_str_equal ("treble", ctrl->name))
+ g_warning ("mute cannot be applied for %s control", ctrl->name);
+ else {
+ /* toggle mute value */
+ mute = (ctrl->mute) ? FALSE : TRUE;
+ audiomixer_change_mute (self->am, ctrl, mute);
+ ret = TRUE;
+ }
+
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static gint
+set_gain (AudioMixerTest *self, gint id, gfloat gain)
+{
+ gint ret = -1;
+ int i;
+
+ for (i = 0; i < self->nctrls; i++) {
+ const struct mixer_control *ctrl = self->ctrls[i];
+
+ if(id == i) {
+
+ if (g_str_equal ("bass", ctrl->name) || g_str_equal ("treble", ctrl->name)) {
+ if (fabs (ctrl->gain - gain) < 0.000001)
+ g_warning ("gain already at requested level %f", ctrl->gain);
+ else {
+ audiomixer_change_gain (self->am, ctrl, gain);
+ ret = TRUE;
+ }
+ }
+ else
+ g_warning ("gain cannot be applied for %s control", ctrl->name);
+
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static gint
+set_volume (AudioMixerTest *self, gint id, double vol)
+{
+ gint ret = -1;
+ int i;
+
+ for (i = 0; i < self->nctrls; i++) {
+ const struct mixer_control *ctrl = self->ctrls[i];
+
+ if (id == i) {
+ if (g_str_equal ("bass", ctrl->name) || g_str_equal ("treble", ctrl->name))
+ g_warning ("volume cannot be applied for %s control", ctrl->name);
+ else {
+
+ if (fabs (ctrl->volume - vol) < 0.000001)
+ g_warning ("volume is already at requested level %f", ctrl->volume);
+ else {
+ audiomixer_change_volume (self->am, ctrl, (double)vol);
+ ret = TRUE;
+ }
+
+ }
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static void
+print_ctrls (AudioMixerTest *self)
+{
+ const struct mixer_control **ctrls = self->ctrls;
+ unsigned int nctrls = self->nctrls;
+ int i;
+
+ fprintf (stdout, "\nControls:");
+ for (i = 0; i < nctrls; i++) {
+ const struct mixer_control *ctrl = ctrls[i];
+ if (g_str_equal ("bass", ctrl->name) || g_str_equal ("treble", ctrl->name))
+ fprintf(stdout, "\n%2d. %-25s [gain: %.2f]", i, ctrl->name,
+ ctrl->gain);
+ else
+ fprintf(stdout, "\n%2d. %-25s [vol: %.2f, mute:%d]", i, ctrl->name,
+ ctrl->volume, ctrl->mute);
+ }
+ fprintf (stdout, "\n");
+}
+
+static void
+refresh_ctrls (AudioMixerTest *self)
+{
+ self->ctrls = audiomixer_get_active_controls (self->am, &self->nctrls);
+ print_ctrls (self);
+}
+
+static void show_help (void)
+{
+ fprintf (stdout,
+ "\n"
+ " -h, --help Show this help\n"
+ " -p, --print-controls prints controls\n"
+ " -i, --id control id(serial#) of the control, take a look at the controls to get the id of control\n"
+ " Examples\n"
+ " audio-mixer-test -> prints the controls and help text\n"
+ " audio-mixer-test -p -> prints only the controls\n"
+ " -v, --set-volume set volume level for volume controls(all controls except bass and treble)\n"
+ " Examples\n"
+ " audio-mixer-test -v 0.2 -> sets volume of the 1st control with 0.2\n"
+ " audio-mixer-test -i 9 -v 0.2 -> sets volume of the 9th control with 0.2\n"
+ " -g, --set-gain gain level for gain controls like bass and treble\n"
+ " Examples\n"
+ " audio-mixer-test -i 11 -g 0.8 -> sets gain of the 11th control with 0.8\n"
+ " -m, --set-mute mute/unmute volume controls(all controls except bass and treble) takes no arguments\n"
+ " Examples\n"
+ " audio-mixer-test -m -> mutes the 1st control\n"
+ " audio-mixer-test -m -> unmutes the 1st control, if it is issued after the above command\n"
+ " audio-mixer-test -i 9 -m -> mutes 9th control (Multimedia) with 0.8\n");
+}
+
+static void
+mixer_value_change_cb (void *data,
+ unsigned int change_mask,
+ const struct mixer_control *ctrl)
+{
+ AudioMixerTest *self = (AudioMixerTest *)data;
+ refresh_ctrls (self);
+ g_main_loop_quit (self->loop);
+}
+
+static void
+mixer_controls_changed (void *data)
+{
+ AudioMixerTest *self = (AudioMixerTest *)data;
+ g_main_loop_quit (self->loop);
+}
+
+gint
+main (gint argc, gchar **argv)
+{
+ AudioMixerTest self = { 0 };
+ g_autoptr (GError) error = NULL;
+ gint c, ret = 0;
+ struct audiomixer_events audiomixer_events = { 0 };
+
+ gint id = -1;
+ double vol = 0.0;
+ gfloat gain = 0.0;
+
+ self.loop = g_main_loop_new (NULL, FALSE);
+
+ self.am = audiomixer_new ();
+
+ if (!self.am) {
+ g_warning ("unable to open audiomixer");
+ goto exit;
+ }
+
+ audiomixer_lock (self.am);
+ ret = audiomixer_ensure_controls (self.am, 3);
+ audiomixer_unlock (self.am);
+ if (ret < 0) {
+ g_warning ("ensure controls failed");
+ goto exit;
+ }
+
+ audiomixer_events.controls_changed = mixer_controls_changed;
+ audiomixer_add_event_listener (self.am, &audiomixer_events, (void *)&self);
+
+ g_debug ("waiting for controls to be available");
+
+ do {
+ self.ctrls = audiomixer_get_active_controls (self.am, &self.nctrls);
+
+ /*
+ * not a clean check but it appears like this is the best we can do at the
+ * moment.
+ */
+ if (self.nctrls <= 4)
+ /* end points are not registered, wait for them to show up */
+ g_main_loop_run (self.loop);
+ else
+ break;
+
+ } while (1);
+
+ if (argc == 1) {
+ print_ctrls (&self);
+ show_help ();
+ return 0;
+ }
+
+ audiomixer_events.value_changed = mixer_value_change_cb;
+ audiomixer_add_event_listener (self.am, &audiomixer_events, (void *)&self);
+
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "print-controls", no_argument, NULL, 'p' },
+ { "id", required_argument, NULL, 'i' },
+ { "set-volume", required_argument, NULL, 'v' },
+ { "set-mute", no_argument, NULL, 'm' },
+ { "set-gain", required_argument, NULL, 'g' },
+ { NULL, 0, NULL, 0}
+ };
+
+ while ((c = getopt_long (argc, argv, "hpi:v:mg:", long_options, NULL)) != -1) {
+ switch(c) {
+ case 'h':
+ show_help ();
+ break;
+
+ case 'p':
+ print_ctrls (&self);
+ break;
+
+ case 'i':
+ id = atoi (optarg);
+ if (!(id >= 0 && id < self.nctrls)) {
+ ret = -1;
+ g_warning ("id(%d) is invalid", id);
+ }
+ break;
+
+ case 'v':
+ vol = (double)atof (optarg);
+ if (id == -1) {
+ g_warning ("control id not given defaulting it to 0(Master Playback)");
+ id = 0;
+ }
+
+ ret = set_volume (&self, id, vol);
+ if (ret != TRUE)
+ g_warning ("set-volume failed");
+ else
+ /* wait for volume to be acked */
+ g_main_loop_run (self.loop);
+
+ break;
+
+ case 'm':
+ if (id == -1) {
+ g_warning ("control id not given defaulting it to 0(Master Playback)");
+ id = 0;
+ }
+
+ ret = set_mute (&self, id);
+ if (ret != TRUE)
+ g_warning ("set-mute failed");
+ else
+ /* wait for mute to be acked */
+ g_main_loop_run (self.loop);
+
+ break;
+
+ case 'g':
+ gain = atof (optarg);
+ if (id == -1) {
+ g_warning ("control id not given defaulting it to 11(bass)");
+ id = 11; /* bass ctrl */
+ }
+
+ ret = set_gain (&self, id, gain);
+ if (ret != TRUE)
+ g_warning ("set-gain failed");
+ else
+ /* wait for gain to be acked */
+ g_main_loop_run (self.loop);
+
+ break;
+
+ default:
+ show_help ();
+ break;
+ }
+ }
+
+exit:
+ /* clean up at program exit */
+ audio_mixer_clear (&self);
+ return ret;
+}
diff --git a/src/meson.build b/src/meson.build
index 6c50419..d8b57be 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -60,3 +60,12 @@ executable('agl-service-audiomixer',
'-D_XOPEN_SOURCE=700',
],
install_dir : get_option('sbindir'))
+
+executable('audio-mixer-test',
+ ['audiomixertest.c', 'audiomixer.c'],
+ dependencies: [dependency('wireplumber-0.4')],
+ install: true,
+ c_args : [
+ '-D_XOPEN_SOURCE=700',
+ ],
+ install_dir : get_option('bindir'))