From f6eb75678f6c08c4eb3fe232f814834319611ff7 Mon Sep 17 00:00:00 2001 From: Ashok Sidipotu Date: Thu, 7 Dec 2023 14:14:01 +0100 Subject: 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 --- src/audiomixer.c | 233 ++++++++++++++++++++++++++++++++++-- src/audiomixer.h | 6 + src/audiomixertest.c | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/meson.build | 9 ++ 4 files changed, 570 insertions(+), 7 deletions(-) create mode 100644 src/audiomixertest.c 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 +#include #include 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", ¶ms, 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 + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#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')) -- cgit 1.2.3-korg