bazzite/spec_files/wireplumber/steamdeck.patch
2023-12-06 21:15:47 -08:00

1604 lines
53 KiB
Diff

diff --git a/modules/meson.build b/modules/meson.build
index 4930bfae..4a337017 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -159,6 +159,17 @@ shared_library(
dependencies : [wp_dep, pipewire_dep],
)
+shared_library(
+ 'wireplumber-module-filters-api',
+ [
+ 'module-filters-api.c',
+ ],
+ c_args : [common_c_args, '-DG_LOG_DOMAIN="m-filters-api"'],
+ install : true,
+ install_dir : wireplumber_module_dir,
+ dependencies : [wp_dep, pipewire_dep],
+)
+
if libsystemd_dep.found() or libelogind_dep.found()
shared_library(
'wireplumber-module-logind',
diff --git a/modules/module-default-nodes.c b/modules/module-default-nodes.c
index aaf29389..3bb93f00 100644
--- a/modules/module-default-nodes.c
+++ b/modules/module-default-nodes.c
@@ -16,18 +16,12 @@
#define NAME "default-nodes"
#define DEFAULT_SAVE_INTERVAL_MS 1000
#define DEFAULT_USE_PERSISTENT_STORAGE TRUE
-#define DEFAULT_AUTO_ECHO_CANCEL TRUE
-#define DEFAULT_ECHO_CANCEL_SINK_NAME "echo-cancel-sink"
-#define DEFAULT_ECHO_CANCEL_SOURCE_NAME "echo-cancel-source"
#define N_PREV_CONFIGS 16
enum {
PROP_0,
PROP_SAVE_INTERVAL_MS,
PROP_USE_PERSISTENT_STORAGE,
- PROP_AUTO_ECHO_CANCEL,
- PROP_ECHO_CANCEL_SINK_NAME,
- PROP_ECHO_CANCEL_SOURCE_NAME,
};
typedef struct _WpDefaultNode WpDefaultNode;
@@ -51,8 +45,6 @@ struct _WpDefaultNodes
/* properties */
guint save_interval_ms;
gboolean use_persistent_storage;
- gboolean auto_echo_cancel;
- gchar *echo_cancel_names[2];
};
G_DECLARE_FINAL_TYPE (WpDefaultNodes, wp_default_nodes,
@@ -243,21 +235,6 @@ node_has_available_routes (WpDefaultNodes * self, WpNode *node)
return FALSE;
}
-static gboolean
-is_echo_cancel_node (WpDefaultNodes * self, WpNode *node, WpDirection direction)
-{
- const gchar *name = wp_pipewire_object_get_property (
- WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME);
- const gchar *virtual_str = wp_pipewire_object_get_property (
- WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_VIRTUAL);
- gboolean virtual = virtual_str && pw_properties_parse_bool (virtual_str);
-
- if (!name || !virtual)
- return FALSE;
-
- return g_strcmp0 (name, self->echo_cancel_names[direction]) == 0;
-}
-
static WpNode *
find_best_media_class_node (WpDefaultNodes * self, const gchar *media_class,
const WpDefaultNode *def, WpDirection direction, gint *priority)
@@ -291,9 +268,6 @@ find_best_media_class_node (WpDefaultNodes * self, const gchar *media_class,
if (!node_has_available_routes (self, node))
continue;
- if (self->auto_echo_cancel && is_echo_cancel_node (self, node, direction))
- prio += 10000;
-
if (name && def->config_value && g_strcmp0 (name, def->config_value) == 0) {
prio += 20000 * (N_PREV_CONFIGS + 1);
} else if (name) {
@@ -597,41 +571,18 @@ wp_default_nodes_set_property (GObject * object, guint property_id,
case PROP_USE_PERSISTENT_STORAGE:
self->use_persistent_storage = g_value_get_boolean (value);
break;
- case PROP_AUTO_ECHO_CANCEL:
- self->auto_echo_cancel = g_value_get_boolean (value);
- break;
- case PROP_ECHO_CANCEL_SINK_NAME:
- g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_INPUT], g_free);
- self->echo_cancel_names[WP_DIRECTION_INPUT] = g_value_dup_string (value);
- break;
- case PROP_ECHO_CANCEL_SOURCE_NAME:
- g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_OUTPUT], g_free);
- self->echo_cancel_names[WP_DIRECTION_OUTPUT] = g_value_dup_string (value);
- break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
-static void
-wp_default_nodes_finalize (GObject * object)
-{
- WpDefaultNodes * self = WP_DEFAULT_NODES (object);
-
- g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_INPUT], g_free);
- g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_OUTPUT], g_free);
-
- G_OBJECT_CLASS (wp_default_nodes_parent_class)->finalize (object);
-}
-
static void
wp_default_nodes_class_init (WpDefaultNodesClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpPluginClass *plugin_class = (WpPluginClass *) klass;
- object_class->finalize = wp_default_nodes_finalize;
object_class->set_property = wp_default_nodes_set_property;
plugin_class->enable = wp_default_nodes_enable;
@@ -646,21 +597,6 @@ wp_default_nodes_class_init (WpDefaultNodesClass * klass)
g_param_spec_boolean ("use-persistent-storage", "use-persistent-storage",
"use-persistent-storage", DEFAULT_USE_PERSISTENT_STORAGE,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
- g_object_class_install_property (object_class, PROP_AUTO_ECHO_CANCEL,
- g_param_spec_boolean ("auto-echo-cancel", "auto-echo-cancel",
- "auto-echo-cancel", DEFAULT_AUTO_ECHO_CANCEL,
- G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
- g_object_class_install_property (object_class, PROP_ECHO_CANCEL_SINK_NAME,
- g_param_spec_string ("echo-cancel-sink-name", "echo-cancel-sink-name",
- "echo-cancel-sink-name", DEFAULT_ECHO_CANCEL_SINK_NAME,
- G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
- g_object_class_install_property (object_class, PROP_ECHO_CANCEL_SOURCE_NAME,
- g_param_spec_string ("echo-cancel-source-name", "echo-cancel-source-name",
- "echo-cancel-source-name", DEFAULT_ECHO_CANCEL_SOURCE_NAME,
- G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
WP_PLUGIN_EXPORT gboolean
@@ -668,19 +604,11 @@ wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
{
guint save_interval_ms = DEFAULT_SAVE_INTERVAL_MS;
gboolean use_persistent_storage = DEFAULT_USE_PERSISTENT_STORAGE;
- gboolean auto_echo_cancel = DEFAULT_AUTO_ECHO_CANCEL;
- const gchar *echo_cancel_sink_name = DEFAULT_ECHO_CANCEL_SINK_NAME;
- const gchar *echo_cancel_source_name = DEFAULT_ECHO_CANCEL_SOURCE_NAME;
if (args) {
g_variant_lookup (args, "save-interval-ms", "u", &save_interval_ms);
g_variant_lookup (args, "use-persistent-storage", "b",
&use_persistent_storage);
- g_variant_lookup (args, "auto-echo-cancel", "&s", &auto_echo_cancel);
- g_variant_lookup (args, "echo-cancel-sink-name", "&s",
- &echo_cancel_sink_name);
- g_variant_lookup (args, "echo-cancel-source-name", "&s",
- &echo_cancel_source_name);
}
wp_plugin_register (g_object_new (wp_default_nodes_get_type (),
@@ -688,9 +616,6 @@ wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
"core", core,
"save-interval-ms", save_interval_ms,
"use-persistent-storage", use_persistent_storage,
- "auto-echo-cancel", auto_echo_cancel,
- "echo-cancel-sink-name", echo_cancel_sink_name,
- "echo-cancel-source-name", echo_cancel_source_name,
NULL));
return TRUE;
}
diff --git a/modules/module-filters-api.c b/modules/module-filters-api.c
new file mode 100644
index 00000000..8852a6c5
--- /dev/null
+++ b/modules/module-filters-api.c
@@ -0,0 +1,920 @@
+/* WirePlumber
+ *
+ * Copyright © 2023 Collabora Ltd.
+ * @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <pipewire/keys.h>
+
+#include <wp/wp.h>
+
+struct _WpFiltersApi
+{
+ WpPlugin parent;
+
+ WpObjectManager *metadata_om;
+ WpObjectManager *stream_nodes_om;
+ WpObjectManager *nodes_om;
+ WpObjectManager *filter_nodes_om;
+ guint n_playback_stream_nodes;
+ guint n_capture_stream_nodes;
+ GList *filters[2];
+ GHashTable *targets;
+};
+
+enum {
+ ACTION_IS_FILTER_ENABLED,
+ ACTION_GET_FILTER_TARGET,
+ ACTION_GET_FILTER_FROM_TARGET,
+ ACTION_GET_DEFAULT_FILTER,
+ SIGNAL_CHANGED,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS] = {0};
+
+G_DECLARE_FINAL_TYPE (WpFiltersApi, wp_filters_api, WP, FILTERS_API, WpPlugin)
+G_DEFINE_TYPE (WpFiltersApi, wp_filters_api, WP_TYPE_PLUGIN)
+
+struct _Filter {
+ gchar *link_group;
+ WpDirection direction;
+ WpNode *node;
+ WpNode *stream;
+ gchar *target;
+ gboolean enabled;
+ gint priority;
+};
+typedef struct _Filter Filter;
+
+struct _Target {
+ gboolean exclusive;
+ WpNode *node;
+};
+typedef struct _Target Target;
+
+static guint
+get_filter_priority (const gchar *link_group)
+{
+ if (strstr (link_group, "loopback"))
+ return 300;
+ if (strstr (link_group, "filter-chain"))
+ return 200;
+ /* By default echo-cancel is the lowest priority to properly cancel audio */
+ if (strstr (link_group, "echo-cancel"))
+ return 0;
+ return 100;
+}
+
+static Filter *
+filter_new (const gchar *link_group, WpDirection dir, gboolean is_stream,
+ WpNode *node)
+{
+ Filter *f = g_malloc0 (sizeof (Filter));
+ f->link_group = g_strdup (link_group);
+ f->direction = dir;
+ f->node = is_stream ? NULL : g_object_ref (node);
+ f->stream = is_stream ? g_object_ref (node) : NULL;
+ f->target = NULL;
+ f->enabled = TRUE;
+ f->priority = get_filter_priority (link_group);
+ return f;
+}
+
+static void
+filter_free (Filter *f)
+{
+ g_clear_pointer (&f->link_group, g_free);
+ g_clear_pointer (&f->target, g_free);
+ g_clear_object (&f->node);
+ g_clear_object (&f->stream);
+ g_free (f);
+}
+
+static Target *
+target_new (gboolean exclusive, WpNode *node)
+{
+ Target *t = g_malloc0 (sizeof (Target));
+ t->exclusive = exclusive;
+ t->node = node ? g_object_ref (node) : NULL;
+ return t;
+}
+
+static void
+target_free (Target *t)
+{
+ g_clear_object (&t->node);
+ g_free (t);
+}
+
+static gint
+filter_equal_func (const Filter *f, const gchar *link_group)
+{
+ return g_str_equal (f->link_group, link_group) ? 0 : 1;
+}
+
+static gint
+filter_compare_func (const Filter *a, const Filter *b)
+{
+ gint diff = a->priority - b->priority;
+ if (diff != 0)
+ return diff;
+ return g_strcmp0 (a->link_group, b->link_group);
+}
+
+static void
+wp_filters_api_init (WpFiltersApi * self)
+{
+}
+
+static gboolean
+wp_filters_api_is_filter_enabled (WpFiltersApi * self, const gchar *direction,
+ const gchar *link_group)
+{
+ WpDirection dir = WP_DIRECTION_INPUT;
+ GList *filters;
+ Filter *found = NULL;
+
+ g_return_val_if_fail (direction, FALSE);
+ g_return_val_if_fail (link_group, FALSE);
+
+ /* Get the filters for the given direction */
+ if (g_str_equal (direction, "output") || g_str_equal (direction, "Output"))
+ dir = WP_DIRECTION_OUTPUT;
+ filters = self->filters[dir];
+
+ /* Find the filter in the filters list */
+ filters = g_list_find_custom (filters, link_group,
+ (GCompareFunc) filter_equal_func);
+ if (!filters)
+ return FALSE;
+
+ found = filters->data;
+ return found->enabled;
+}
+
+static WpSpaJson *
+wp_filters_api_get_filter_target (WpFiltersApi * self, const gchar *direction,
+ const gchar *link_group)
+{
+ WpDirection dir = WP_DIRECTION_INPUT;
+ GList *filters;
+ Filter *found;
+ g_autoptr (WpSpaJson) res = wp_spa_json_new_object (
+ "exclusive", "b", FALSE, "bound_id", "i", -1, NULL);
+
+ g_return_val_if_fail (direction, g_steal_pointer (&res));
+ g_return_val_if_fail (link_group, g_steal_pointer (&res));
+
+ /* Get the filters for the given direction */
+ if (g_str_equal (direction, "output") || g_str_equal (direction, "Output"))
+ dir = WP_DIRECTION_OUTPUT;
+ filters = self->filters[dir];
+
+ /* Find the filter in the filters list */
+ filters = g_list_find_custom (filters, link_group,
+ (GCompareFunc) filter_equal_func);
+ if (!filters)
+ return g_steal_pointer (&res);
+ found = filters->data;
+ if (!found->enabled)
+ return g_steal_pointer (&res);
+
+ /* Return the previous filter with matching target that is enabled */
+ while ((filters = g_list_previous (filters))) {
+ Filter *prev = (Filter *) filters->data;
+ if ((prev->target == found->target ||
+ (prev->target && found->target &&
+ g_str_equal (prev->target, found->target))) &&
+ prev->enabled) {
+ return wp_spa_json_new_object (
+ "exclusive", "b", FALSE,
+ "bound_id", "i", wp_proxy_get_bound_id (WP_PROXY (prev->node)),
+ NULL);
+ }
+ }
+
+ /* Find the target */
+ if (found->target) {
+ Target *t = g_hash_table_lookup (self->targets, found->target);
+ if (t) {
+ return wp_spa_json_new_object (
+ "exclusive", "b", t->exclusive,
+ "bound_id", "i",
+ t->node ? (gint)wp_proxy_get_bound_id (WP_PROXY (t->node)) : -1,
+ NULL);
+ }
+ }
+
+ return g_steal_pointer (&res);
+}
+
+static gint
+wp_filters_api_get_filter_from_target (WpFiltersApi * self,
+ const gchar *direction, gint target_id)
+{
+ WpDirection dir = WP_DIRECTION_INPUT;
+ GList *filters;
+ gboolean found = FALSE;
+ const gchar *target = NULL;
+ gint res = target_id;
+
+ g_return_val_if_fail (direction, res);
+
+ /* Get the filters for the given direction */
+ if (g_str_equal (direction, "output") || g_str_equal (direction, "Output"))
+ dir = WP_DIRECTION_OUTPUT;
+ filters = self->filters[dir];
+
+ /* Find the first target matching target_id */
+ while (filters) {
+ Filter *f = (Filter *) filters->data;
+ if (f->enabled) {
+ gint f_target_id;
+ g_autoptr (WpSpaJson) f_target = wp_filters_api_get_filter_target (self,
+ direction, f->link_group);
+ if (f_target && wp_spa_json_is_object (f_target) &&
+ wp_spa_json_object_get (f_target, "bound_id", "i", &f_target_id, NULL)
+ && f_target_id == target_id) {
+ target = f->target;
+ found = TRUE;
+ break;
+ }
+ }
+
+ /* Advance */
+ filters = g_list_next (filters);
+ }
+
+ /* Just return if target was not found */
+ if (!found)
+ return res;
+
+ /* Get the last filter node ID of the target found */
+ filters = self->filters[dir];
+ while (filters) {
+ Filter *f = (Filter *) filters->data;
+ if ((f->target == target ||
+ (f->target && target && g_str_equal (f->target, target))) &&
+ f->enabled)
+ res = wp_proxy_get_bound_id (WP_PROXY (f->node));
+
+ /* Advance */
+ filters = g_list_next (filters);
+ }
+
+ return res;
+}
+
+static void
+sync_changed (WpCore * core, GAsyncResult * res, WpFiltersApi * self)
+{
+ g_autoptr (GError) error = NULL;
+
+ if (!wp_core_sync_finish (core, res, &error)) {
+ wp_warning_object (self, "core sync error: %s", error->message);
+ return;
+ }
+
+ g_signal_emit (self, signals[SIGNAL_CHANGED], 0);
+}
+
+static WpNode *
+find_target_node (WpFiltersApi *self, WpSpaJson *props_json)
+{
+ g_auto (GValue) item = G_VALUE_INIT;
+ g_autoptr (WpIterator) it = NULL;
+ g_autoptr (WpObjectInterest) interest = NULL;
+
+ /* Make sure the properties are a JSON object */
+ if (!props_json || !wp_spa_json_is_object (props_json)) {
+ wp_warning_object (self, "Target properties must be a JSON object");
+ return NULL;
+ }
+
+ /* Create the object intereset with the target properties */
+ interest = wp_object_interest_new (WP_TYPE_NODE, NULL);
+ it = wp_spa_json_new_iterator (props_json);
+ for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
+ WpSpaJson *j = g_value_get_boxed (&item);
+ g_autofree gchar *key = NULL;
+ WpSpaJson *value_json;
+ g_autofree gchar *value = NULL;
+
+ key = wp_spa_json_parse_string (j);
+ g_value_unset (&item);
+ if (!wp_iterator_next (it, &item)) {
+ wp_warning_object (self,
+ "Could not get valid key-value pairs from target properties");
+ break;
+ }
+ value_json = g_value_get_boxed (&item);
+ value = wp_spa_json_parse_string (value_json);
+ if (!value) {
+ wp_warning_object (self,
+ "Could not get '%s' value from target properties", key);
+ break;
+ }
+
+ wp_object_interest_add_constraint (interest, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+ key, WP_CONSTRAINT_VERB_MATCHES, g_variant_new_string (value));
+ }
+
+ return wp_object_manager_lookup_full (self->nodes_om,
+ wp_object_interest_ref (interest));
+}
+
+static gboolean
+reevaluate_targets (WpFiltersApi *self)
+{
+ g_autoptr (WpMetadata) m = NULL;
+ const gchar *json_str;
+ g_autoptr (WpSpaJson) json = NULL;
+ g_auto (GValue) item = G_VALUE_INIT;
+ g_autoptr (WpIterator) it = NULL;
+ gboolean changed = FALSE;
+
+ g_hash_table_remove_all (self->targets);
+
+ /* Make sure the metadata exists */
+ m = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA, NULL);
+ if (!m)
+ return FALSE;
+
+ /* Don't update anything if the metadata value is not set */
+ json_str = wp_metadata_find (m, 0, "filters.configured.targets", NULL);
+ if (!json_str)
+ return FALSE;
+
+ /* Make sure the metadata value is an object */
+ json = wp_spa_json_new_from_string (json_str);
+ if (!json || !wp_spa_json_is_object (json)) {
+ wp_warning_object (self,
+ "ignoring metadata value as it is not a JSON object: %s", json_str);
+ return FALSE;
+ }
+
+ /* Find the target node for each target, and add it to the hash table */
+ it = wp_spa_json_new_iterator (json);
+ for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
+ WpSpaJson *j = g_value_get_boxed (&item);
+ g_autofree gchar *key = NULL;
+ WpSpaJson *value;
+ gboolean exclusive = FALSE;
+ g_autoptr (WpSpaJson) props = NULL;
+ g_autoptr (WpNode) target_node = NULL;
+ Target *curr_target;
+
+ key = wp_spa_json_parse_string (j);
+ g_value_unset (&item);
+ if (!wp_iterator_next (it, &item)) {
+ wp_warning_object (self,
+ "Could not get valid key-value pairs from target object");
+ break;
+ }
+ value = g_value_get_boxed (&item);
+ if (!value || !wp_spa_json_is_object (value)) {
+ wp_warning_object (self, "Target value must be a JSON object");
+ break;
+ }
+
+ /* Get exclusive */
+ wp_spa_json_object_get (value, "exclusive", "b", &exclusive, NULL);
+
+ /* Get target node */
+ wp_spa_json_object_get (value, "props", "J", &props, NULL);
+ if (props)
+ target_node = find_target_node (self, props);
+
+ /* Update values if target exists in the table, otherwise add new target */
+ curr_target = g_hash_table_lookup (self->targets, key);
+ if (curr_target) {
+ if (curr_target->exclusive != exclusive) {
+ curr_target->exclusive = exclusive;
+ changed = TRUE;
+ }
+ if (curr_target->node != target_node) {
+ g_clear_object (&curr_target->node);
+ curr_target->node = g_steal_pointer (&target_node);
+ changed = TRUE;
+ }
+ } else {
+ g_hash_table_insert (self->targets, g_strdup (key),
+ target_new (exclusive, target_node));
+ changed = TRUE;
+ }
+ }
+
+ return changed;
+}
+
+static gboolean
+update_values_from_metadata (WpFiltersApi * self, Filter *f)
+{
+ g_autoptr (WpMetadata) m = NULL;
+ const gchar *f_stream_name;
+ const gchar *f_node_name;
+ const gchar *json_str;
+ g_autoptr (WpSpaJson) json = NULL;
+ g_auto (GValue) item = G_VALUE_INIT;
+ g_autoptr (WpIterator) it = NULL;
+ gboolean changed = FALSE;
+
+ /* Make sure the metadata exists */
+ m = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA, NULL);
+ if (!m)
+ return FALSE;
+
+ /* Make sure both the stream and node are available */
+ if (!f->stream || !f->node)
+ return FALSE;
+ f_stream_name = wp_pipewire_object_get_property (
+ WP_PIPEWIRE_OBJECT (f->stream), PW_KEY_NODE_NAME);
+ f_node_name = wp_pipewire_object_get_property (
+ WP_PIPEWIRE_OBJECT (f->node), PW_KEY_NODE_NAME);
+
+ /* Don't update anything if the metadata value is not set */
+ json_str = wp_metadata_find (m, 0, "filters.configured.filters", NULL);
+ if (!json_str)
+ return FALSE;
+
+ /* Make sure the metadata value is an array */
+ json = wp_spa_json_new_from_string (json_str);
+ if (!json || !wp_spa_json_is_array (json)) {
+ wp_warning_object (self,
+ "ignoring metadata value as it is not a JSON array: %s", json_str);
+ return FALSE;
+ }
+
+ /* Find the filter values in the metadata */
+ it = wp_spa_json_new_iterator (json);
+ for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
+ WpSpaJson *j = g_value_get_boxed (&item);
+ g_autofree gchar *stream_name = NULL;
+ g_autofree gchar *node_name = NULL;
+ g_autofree gchar *direction = NULL;
+ g_autofree gchar *target = NULL;
+ g_autofree gchar *mode = NULL;
+ WpDirection dir = WP_DIRECTION_INPUT;
+ gint priority;
+
+ if (!j || !wp_spa_json_is_object (j))
+ continue;
+
+ /* Parse mandatory fields */
+ if (!wp_spa_json_object_get (j, "stream-name", "s", &stream_name,
+ "node-name", "s", &node_name, "direction", "s", &direction, NULL)) {
+ g_autofree gchar *str = wp_spa_json_to_string (j);
+ wp_warning_object (self,
+ "failed to parse stream-name, node-name and direction in filter: %s",
+ str);
+ continue;
+ }
+
+ /* Make sure direction is valid */
+ if (g_str_equal (direction, "input")) {
+ dir = WP_DIRECTION_INPUT;
+ } else if (g_str_equal (direction, "output")) {
+ dir = WP_DIRECTION_OUTPUT;
+ } else {
+ g_autofree gchar *str = wp_spa_json_to_string (j);
+ wp_warning_object (self,
+ "direction %s is not valid for filter: %s", direction, str);
+ }
+
+ /* Find first filter matching stream-name, node-name and direction */
+ if (g_str_equal (f_stream_name, stream_name) &&
+ g_str_equal (f_node_name, node_name) &&
+ f->direction == dir) {
+
+ /* Update target */
+ if (wp_spa_json_object_get (j, "target", "s", &target, NULL)) {
+ if (!f->target || !g_str_equal (f->target, target)) {
+ g_clear_pointer (&f->target, g_free);
+ f->target = g_strdup (target);
+ changed = TRUE;
+ }
+ } else {
+ if (f->target) {
+ g_clear_pointer (&f->target, g_free);
+ changed = TRUE;
+ }
+ }
+
+ /* Update mode */
+ if (wp_spa_json_object_get (j, "mode", "s", &mode, NULL)) {
+ if (g_str_equal (mode, "always")) {
+ if (!f->enabled) {
+ f->enabled = TRUE;
+ changed = TRUE;
+ }
+ } else if (g_str_equal (mode, "never")) {
+ if (f->enabled) {
+ f->enabled = FALSE;
+ changed = TRUE;
+ }
+ } else if (g_str_equal (mode, "playback-only")) {
+ if (f->enabled != (self->n_playback_stream_nodes > 0)) {
+ f->enabled = self->n_playback_stream_nodes > 0;
+ changed = TRUE;
+ }
+ } else if (g_str_equal (mode, "capture-only")) {
+ if (f->enabled != (self->n_capture_stream_nodes > 0)) {
+ f->enabled = self->n_capture_stream_nodes > 0;
+ changed = TRUE;
+ }
+ } else {
+ wp_warning_object (self,
+ "The '%s' value is not a valid for the 'mode' filter field",
+ mode);
+ }
+ }
+
+ /* Update priority */
+ if (wp_spa_json_object_get (j, "priority", "i", &priority, NULL)) {
+ if (f->priority != priority) {
+ f->priority = priority;
+ changed = TRUE;
+ }
+ }
+ break;
+ }
+ }
+
+ return changed;
+}
+
+static gboolean
+reevaluate_filters (WpFiltersApi *self, WpDirection direction)
+{
+ GList *filters;
+ gboolean changed = FALSE;
+
+ /* Update filter values */
+ filters = self->filters[direction];
+ while (filters) {
+ Filter *f = (Filter *) filters->data;
+ if (update_values_from_metadata (self, f))
+ changed = TRUE;
+ filters = g_list_next (filters);
+ }
+
+ /* Sort filters if changed */
+ if (changed)
+ self->filters[direction] = g_list_sort (self->filters[direction],
+ (GCompareFunc) filter_compare_func);
+
+ return changed;
+}
+
+static void
+schedule_changed (WpFiltersApi * self)
+{
+ g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+ g_return_if_fail (core);
+
+ wp_core_sync_closure (core, NULL, g_cclosure_new_object (
+ G_CALLBACK (sync_changed), G_OBJECT (self)));
+}
+
+static void
+on_stream_node_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ const gchar* media_class = wp_pipewire_object_get_property (proxy,
+ PW_KEY_MEDIA_CLASS);
+
+ if (g_str_equal (media_class, "Stream/Output/Audio"))
+ self->n_playback_stream_nodes++;
+ else if (g_str_equal (media_class, "Stream/Input/Audio"))
+ self->n_capture_stream_nodes++;
+}
+
+static void
+on_stream_node_removed (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ const gchar* media_class = wp_pipewire_object_get_property (proxy,
+ PW_KEY_MEDIA_CLASS);
+
+ if (g_str_equal (media_class, "Stream/Output/Audio") &&
+ self->n_playback_stream_nodes > 0)
+ self->n_playback_stream_nodes--;
+ else if (g_str_equal (media_class, "Stream/Input/Audio") &&
+ self->n_capture_stream_nodes > 0)
+ self->n_capture_stream_nodes--;
+}
+
+static void
+on_stream_nodes_changed (WpObjectManager *om, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ gboolean changed = FALSE;
+
+ /* Reevaluate everything */
+ for (guint i = 0; i < 2; i++)
+ if (reevaluate_filters (self, i))
+ changed = TRUE;
+
+ if (changed)
+ schedule_changed (self);
+}
+
+static void
+on_node_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ reevaluate_targets (self);
+}
+
+static void
+on_node_removed (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ reevaluate_targets (self);
+}
+
+static void
+on_filter_node_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ const gchar *key;
+ WpDirection dir;
+ gboolean is_stream;
+ GList *found;
+
+ /* Get direction */
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_MEDIA_CLASS);
+ if (!key)
+ return;
+
+ if (g_str_equal (key, "Audio/Sink") ||
+ g_str_equal (key, "Stream/Output/Audio")) {
+ dir = WP_DIRECTION_INPUT;
+ } else if (g_str_equal (key, "Audio/Source") ||
+ g_str_equal (key, "Stream/Input/Audio")) {
+ dir = WP_DIRECTION_OUTPUT;
+ } else {
+ wp_debug_object (self, "ignoring node with media class: %s", key);
+ return;
+ }
+
+ /* Check whether the proxy is a stream or not */
+ is_stream = FALSE;
+ if (g_str_equal (key, "Stream/Output/Audio") ||
+ g_str_equal (key, "Stream/Input/Audio"))
+ is_stream = TRUE;
+
+ /* We use the link group as filter name */
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_NODE_LINK_GROUP);
+ if (!key) {
+ wp_debug_object (self, "ignoring node without link group");
+ return;
+ }
+
+ /* Check if the filter already exists, and add it if it does not exist */
+ found = g_list_find_custom (self->filters[dir], key,
+ (GCompareFunc) filter_equal_func);
+ if (!found) {
+ Filter *f = filter_new (key, dir, is_stream, WP_NODE (proxy));
+ update_values_from_metadata (self, f);
+ self->filters[dir] = g_list_insert_sorted (self->filters[dir],
+ f, (GCompareFunc) filter_compare_func);
+ } else {
+ Filter *f = found->data;
+ if (is_stream) {
+ g_clear_object (&f->stream);
+ f->stream = g_object_ref (WP_NODE (proxy));
+ } else {
+ g_clear_object (&f->node);
+ f->node = g_object_ref (WP_NODE (proxy));
+ }
+ update_values_from_metadata (self, f);
+ self->filters[dir] = g_list_sort (self->filters[dir],
+ (GCompareFunc) filter_compare_func);
+ }
+}
+
+static void
+on_filter_node_removed (WpObjectManager *om, WpPipewireObject *proxy,
+ gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+
+ const gchar *key;
+ WpDirection dir;
+ GList *found;
+
+ /* Get direction */
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_MEDIA_CLASS);
+ if (!key)
+ return;
+
+ if (g_str_equal (key, "Audio/Sink") ||
+ g_str_equal (key, "Stream/Output/Audio")) {
+ dir = WP_DIRECTION_INPUT;
+ } else if (g_str_equal (key, "Audio/Source") ||
+ g_str_equal (key, "Stream/Input/Audio")) {
+ dir = WP_DIRECTION_OUTPUT;
+ } else {
+ wp_debug_object (self, "ignoring node with media class: %s", key);
+ return;
+ }
+
+ /* We use the link group as filter name */
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_NODE_LINK_GROUP);
+ if (!key) {
+ wp_debug_object (self, "ignoring node without link group");
+ return;
+ }
+
+ /* Find and remove the filter */
+ found = g_list_find_custom (self->filters[dir], key,
+ (GCompareFunc) filter_equal_func);
+ if (found) {
+ self->filters[dir] = g_list_remove (self->filters[dir], found->data);
+ }
+}
+
+static void
+on_metadata_changed (WpMetadata *m, guint32 subject,
+ const gchar *key, const gchar *type, const gchar *value, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ gboolean changed = FALSE;
+
+ /* Reevaluate everything */
+ if (reevaluate_targets (self))
+ changed = TRUE;
+ for (guint i = 0; i < 2; i++)
+ if (reevaluate_filters (self, i))
+ changed = TRUE;
+
+ if (changed)
+ schedule_changed (self);
+}
+
+static void
+on_metadata_added (WpObjectManager *om, WpMetadata *metadata, gpointer d)
+{
+ WpFiltersApi * self = WP_FILTERS_API (d);
+ gboolean changed = FALSE;
+
+ /* Handle the changed signal */
+ g_signal_connect_object (metadata, "changed",
+ G_CALLBACK (on_metadata_changed), self, 0);
+
+ /* Reevaluate everything */
+ if (reevaluate_targets (self))
+ changed = TRUE;
+ for (guint i = 0; i < 2; i++)
+ if (reevaluate_filters (self, i))
+ changed = TRUE;
+
+ if (changed)
+ schedule_changed (self);
+}
+
+static void
+on_metadata_installed (WpObjectManager * om, WpFiltersApi * self)
+{
+ g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+
+ /* Create the stream nodes object manager */
+ self->stream_nodes_om = wp_object_manager_new ();
+ wp_object_manager_add_interest (self->stream_nodes_om, WP_TYPE_NODE,
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "Stream/*/Audio",
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
+ NULL);
+ wp_object_manager_request_object_features (self->stream_nodes_om,
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
+ g_signal_connect_object (self->stream_nodes_om, "object-added",
+ G_CALLBACK (on_stream_node_added), self, 0);
+ g_signal_connect_object (self->stream_nodes_om, "object-removed",
+ G_CALLBACK (on_stream_node_removed), self, 0);
+ g_signal_connect_object (self->stream_nodes_om, "objects-changed",
+ G_CALLBACK (on_stream_nodes_changed), self, 0);
+ wp_core_install_object_manager (core, self->stream_nodes_om);
+
+ /* Create the nodes object manager */
+ self->nodes_om = wp_object_manager_new ();
+ wp_object_manager_add_interest (self->nodes_om, WP_TYPE_NODE,
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "Audio/*",
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
+ NULL);
+ wp_object_manager_request_object_features (self->nodes_om,
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
+ g_signal_connect_object (self->nodes_om, "object-added",
+ G_CALLBACK (on_node_added), self, 0);
+ g_signal_connect_object (self->nodes_om, "object-removed",
+ G_CALLBACK (on_node_removed), self, 0);
+ g_signal_connect_object (self->nodes_om, "objects-changed",
+ G_CALLBACK (schedule_changed), self, G_CONNECT_SWAPPED);
+ wp_core_install_object_manager (core, self->nodes_om);
+
+ /* Create the filter nodes object manager */
+ self->filter_nodes_om = wp_object_manager_new ();
+ wp_object_manager_add_interest (self->filter_nodes_om, WP_TYPE_NODE,
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "+",
+ NULL);
+ wp_object_manager_request_object_features (self->filter_nodes_om,
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
+ g_signal_connect_object (self->filter_nodes_om, "object-added",
+ G_CALLBACK (on_filter_node_added), self, 0);
+ g_signal_connect_object (self->filter_nodes_om, "object-removed",
+ G_CALLBACK (on_filter_node_removed), self, 0);
+ g_signal_connect_object (self->filter_nodes_om, "objects-changed",
+ G_CALLBACK (schedule_changed), self, G_CONNECT_SWAPPED);
+ wp_core_install_object_manager (core, self->filter_nodes_om);
+
+ wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
+}
+
+static void
+wp_filters_api_enable (WpPlugin * plugin, WpTransition * transition)
+{
+ WpFiltersApi * self = WP_FILTERS_API (plugin);
+ g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+
+ self->targets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+ (GDestroyNotify) target_free);
+
+ /* Create the metadata object manager */
+ self->metadata_om = wp_object_manager_new ();
+ wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
+ WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "filters",
+ NULL);
+ wp_object_manager_request_object_features (self->metadata_om,
+ WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
+ g_signal_connect_object (self->metadata_om, "object-added",
+ G_CALLBACK (on_metadata_added), self, 0);
+ g_signal_connect_object (self->metadata_om, "installed",
+ G_CALLBACK (on_metadata_installed), self, 0);
+ wp_core_install_object_manager (core, self->metadata_om);
+}
+
+static void
+wp_filters_api_disable (WpPlugin * plugin)
+{
+ WpFiltersApi * self = WP_FILTERS_API (plugin);
+
+ for (guint i = 0; i < 2; i++) {
+ if (self->filters[i]) {
+ g_list_free_full (self->filters[i], (GDestroyNotify) filter_free);
+ self->filters[i] = NULL;
+ }
+ }
+ g_clear_pointer (&self->targets, g_hash_table_unref);
+
+ g_clear_object (&self->metadata_om);
+ g_clear_object (&self->stream_nodes_om);
+ g_clear_object (&self->nodes_om);
+ g_clear_object (&self->filter_nodes_om);
+}
+
+static void
+wp_filters_api_class_init (WpFiltersApiClass * klass)
+{
+ WpPluginClass *plugin_class = (WpPluginClass *) klass;
+
+ plugin_class->enable = wp_filters_api_enable;
+ plugin_class->disable = wp_filters_api_disable;
+
+ signals[ACTION_IS_FILTER_ENABLED] = g_signal_new_class_handler (
+ "is-filter-enabled", G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ (GCallback) wp_filters_api_is_filter_enabled,
+ NULL, NULL, NULL,
+ G_TYPE_BOOLEAN, 2, G_TYPE_STRING, G_TYPE_STRING);
+
+ signals[ACTION_GET_FILTER_TARGET] = g_signal_new_class_handler (
+ "get-filter-target", G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ (GCallback) wp_filters_api_get_filter_target,
+ NULL, NULL, NULL,
+ WP_TYPE_SPA_JSON, 2, G_TYPE_STRING, G_TYPE_STRING);
+
+ signals[ACTION_GET_FILTER_FROM_TARGET] = g_signal_new_class_handler (
+ "get-filter-from-target", G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ (GCallback) wp_filters_api_get_filter_from_target,
+ NULL, NULL, NULL,
+ G_TYPE_INT, 2, G_TYPE_STRING, G_TYPE_INT);
+
+ signals[SIGNAL_CHANGED] = g_signal_new (
+ "changed", G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+WP_PLUGIN_EXPORT gboolean
+wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
+{
+ wp_plugin_register (g_object_new (wp_filters_api_get_type (),
+ "name", "filters-api",
+ "core", core,
+ NULL));
+ return TRUE;
+}
diff --git a/modules/module-lua-scripting/api/json.c b/modules/module-lua-scripting/api/json.c
index 2883a021..3d486be8 100644
--- a/modules/module-lua-scripting/api/json.c
+++ b/modules/module-lua-scripting/api/json.c
@@ -264,7 +264,7 @@ spa_json_array_new (lua_State *L)
break;
}
default:
- luaL_error (L, "Json does not support lua type ",
+ luaL_error (L, "Json does not support lua type %s",
lua_typename(L, lua_type(L, -1)));
break;
}
diff --git a/src/config/main.lua.d/40-device-defaults.lua b/src/config/main.lua.d/40-device-defaults.lua
index 19202914..b87eec51 100644
--- a/src/config/main.lua.d/40-device-defaults.lua
+++ b/src/config/main.lua.d/40-device-defaults.lua
@@ -10,15 +10,6 @@ device_defaults.properties = {
-- the default volumes to apply to ACP device nodes, in the linear scale
--["default-volume"] = 0.064,
--["default-input-volume"] = 1.0,
-
- -- Whether to auto-switch to echo cancel sink and source nodes or not
- ["auto-echo-cancel"] = true,
-
- -- Sets the default echo-cancel-sink node name to automatically switch to
- ["echo-cancel-sink-name"] = "echo-cancel-sink",
-
- -- Sets the default echo-cancel-source node name to automatically switch to
- ["echo-cancel-source-name"] = "echo-cancel-source",
}
-- Sets persistent device profiles that should never change when wireplumber is
@@ -48,13 +39,13 @@ device_defaults.profile_priorities = {
},
-- lower the index higher the priority
priorities = {
- -- "a2dp-sink-sbc",
- -- "a2dp-sink-aptx_ll",
- -- "a2dp-sink-aptx",
- -- "a2dp-sink-aptx_hd",
- -- "a2dp-sink-ldac",
- -- "a2dp-sink-aac",
- -- "a2dp-sink-sbc_xq",
+ "a2dp-sink-aptx_ll",
+ "a2dp-sink-aptx",
+ "a2dp-sink-aptx_hd",
+ "a2dp-sink-ldac",
+ "a2dp-sink-aac",
+ "a2dp-sink-sbc",
+ "a2dp-sink-sbc_xq",
}
},
}
diff --git a/src/config/main.lua.d/40-stream-defaults.lua b/src/config/main.lua.d/40-stream-defaults.lua
index b869099b..d25aab0d 100644
--- a/src/config/main.lua.d/40-stream-defaults.lua
+++ b/src/config/main.lua.d/40-stream-defaults.lua
@@ -6,7 +6,7 @@ stream_defaults.properties = {
["restore-props"] = true,
-- whether to restore the last stream target or not
- ["restore-target"] = true,
+ ["restore-target"] = false,
-- the default channel volume for new streams whose props were never saved
-- previously. This is only used if "restore-props" is set to true.
diff --git a/src/config/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
index 83d0a3b2..412d47a8 100644
--- a/src/config/policy.lua.d/10-default-policy.lua
+++ b/src/config/policy.lua.d/10-default-policy.lua
@@ -12,6 +12,9 @@ default_policy.policy = {
-- surround audio if echo-cancel is enabled.
["filter.forward-format"] = false,
+ -- Whether to enable smart filter policy or not (experimental feature)
+ ["filter.smart"] = true,
+
-- Set to 'true' to disable channel splitting & merging on nodes and enable
-- passthrough of audio in the same format as the format of the device.
-- Note that this breaks JACK support; it is generally not recommended
@@ -71,6 +74,12 @@ function default_policy.enable()
-- API to access default nodes from scripts
load_module("default-nodes-api")
+ -- Load smart filter policy if requested
+ if default_policy.policy["filter.smart"] then
+ load_script("filters-metadata.lua", default_policy.filters_metadata)
+ load_module("filters-api")
+ end
+
-- API to access mixer controls, needed for volume ducking
load_module("mixer-api")
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
new file mode 100644
index 00000000..8e8725fc
--- /dev/null
+++ b/src/config/policy.lua.d/30-filters-config.lua
@@ -0,0 +1,80 @@
+-- The smart filter policy configuration.
+-- You need to enable "filter.smart" in 10-default-policy.lua
+--
+
+-- The default filter metadata configuration when wireplumber starts. They also
+-- can be changed at runtime.
+default_policy.filters_metadata = {
+ ["filters"] = {
+ -- Input filters (meant to be linked with Audio/Sink device nodes)
+ {
+ ["stream-name"] = "output.virtual-sink", -- loopback playback
+ ["node-name"] = "input.virtual-sink", -- loopback sink
+ ["direction"] = "input", -- can only be 'input' or 'output'
+ ["target"] = nil, -- if nil, the default node will be used as target
+ ["mode"] = "always", -- can be 'always', 'never', 'capture-only' or 'playback-only'
+ ["priority"] = 30,
+ },
+ {
+ ["stream-name"] = "filter-chain-playback", -- filter-chain playback
+ ["node-name"] = "filter-chain-sink", -- filter-chain sink
+ ["direction"] = "input", -- can only be 'input' or 'output'
+ ["target"] = "speakers", -- if nil, the default node will be used as target
+ ["mode"] = "always", -- can be 'always', 'never', 'capture-only' or 'playback-only'
+ ["priority"] = 20,
+ },
+ {
+ ["stream-name"] = "echo-cancel-playback", -- echo-cancel playback
+ ["node-name"] = "echo-cancel-sink", -- echo-cancel sink
+ ["direction"] = "input", -- can only be 'input' or 'output'
+ ["target"] = "speakers", -- if nil, the default node will be used as target
+ ["mode"] = "capture-only", -- can be 'always', 'never', 'playback-only' or 'capture-only'
+ ["priority"] = 10,
+ },
+
+ -- Output filters (meant to be linked with Audio/Source device nodes)
+ {
+ ["stream-name"] = "input.virtual-source", -- loopback capture
+ ["node-name"] = "output.virtual-source", -- loopback source
+ ["direction"] = "output", -- can only be 'input' or 'output'
+ ["target"] = nil, -- if nil, the default node will be used as target
+ ["mode"] = "always", -- can be 'always', 'never', 'playback-only' or 'capture-only'
+ ["priority"] = 30,
+ },
+ {
+ ["stream-name"] = "filter-chain-capture", -- filter-chain capture
+ ["node-name"] = "filter-chain-source", -- filter-chain source
+ ["direction"] = "output", -- can only be 'input' or 'output'
+ ["target"] = "microphone", -- if nil, the default node will be used as target
+ ["mode"] = "capture-only", -- can be 'always', 'never', 'playback-only' or 'capture-only'
+ ["priority"] = 20,
+ },
+ {
+ ["stream-name"] = "echo-cancel-capture", -- echo-cancel capture
+ ["node-name"] = "echo-cancel-source", -- echo-cancel source
+ ["direction"] = "output", -- can only be 'input' or 'output'
+ ["target"] = "microphone", -- if nil, the default node will be used as target
+ ["mode"] = "capture-only", -- can be 'always', 'never', 'playback-only' or 'capture-only'
+ ["priority"] = 10,
+ }
+ },
+
+ -- The filter targets
+ ["targets"] = {
+ ["speakers"] = {
+ ["exclusive"] = false,
+ ["props"] = {
+ ["media.class"] = "Audio/Sink",
+ ["alsa.card_name"] = "sof-nau8821-max",
+ ["device.profile.description"] = "Speaker",
+ }
+ },
+ ["microphone"] = {
+ ["exclusive"] = false,
+ ["props"] = {
+ ["media.class"] = "Audio/Source",
+ ["alsa.card_name"] = "sof-nau8821-max",
+ }
+ }
+ }
+}
diff --git a/src/scripts/filters-metadata.lua b/src/scripts/filters-metadata.lua
new file mode 100644
index 00000000..49b4d0e8
--- /dev/null
+++ b/src/scripts/filters-metadata.lua
@@ -0,0 +1,42 @@
+-- WirePlumber
+--
+-- Copyright © 2023 Collabora Ltd.
+-- @author Julian Bouzas <julian.bouzas@collabora.com>
+--
+-- SPDX-License-Identifier: MIT
+
+-- Receive script arguments
+local config = ... or {}
+config["filters"] = config["filters"] or {}
+config["targets"] = config["targets"] or {}
+
+f_metadata = ImplMetadata("filters")
+f_metadata:activate(Features.ALL, function (m, e)
+ if e then
+ Log.warning("failed to activate filters metadata: " .. tostring(e))
+ return
+ end
+
+ Log.info("activated filters metadata")
+
+ -- Set filters metadata
+ local filters = {}
+ for _, f in ipairs(config["filters"]) do
+ table.insert (filters, Json.Object (f))
+ end
+ local filters_json = Json.Array (filters)
+ m:set (0, "filters.configured.filters", "Spa:String:JSON",
+ filters_json:to_string())
+
+ -- Set targets metadata
+ local targets = {}
+ for name, value in pairs(config["targets"]) do
+ targets[name] = Json.Object {
+ exclusive = value.exclusive and true or false,
+ props = Json.Object (value.props)
+ }
+ end
+ local targets_json = Json.Object (targets)
+ m:set (0, "filters.configured.targets", "Spa:String:JSON",
+ targets_json:to_string())
+end)
diff --git a/src/scripts/policy-device-profile.lua b/src/scripts/policy-device-profile.lua
index af10f9b7..8d3a6d7e 100644
--- a/src/scripts/policy-device-profile.lua
+++ b/src/scripts/policy-device-profile.lua
@@ -34,12 +34,12 @@ createIntrestObjects(self.config.persistent)
createIntrestObjects(self.config.priorities)
-- Checks whether a device profile is persistent or not
-function isProfilePersistent(device_props, profile_name)
- for _, p in ipairs(self.config.persistent or {}) do
+function isProfilePersistent (device_props, profile_name)
+ for _, p in ipairs (self.config.persistent or {}) do
if p.profile_names then
- for _, interest in ipairs(p.interests) do
- if interest:matches(device_props) then
- for _, pn in ipairs(p.profile_names) do
+ for _, interest in ipairs (p.interests) do
+ if interest:matches (device_props) then
+ for _, pn in ipairs (p.profile_names) do
if pn == profile_name then
return true
end
@@ -72,7 +72,7 @@ function setDeviceProfile (device, dev_id, dev_name, profile)
index = profile.index,
}
Log.info ("Setting profile " .. profile.name .. " on " .. dev_name)
- device:set_param("Profile", param)
+ device:set_param ("Profile", param)
end
function findDefaultProfile (device)
@@ -85,8 +85,8 @@ function findDefaultProfile (device)
return nil
end
- for p in device:iterate_params("EnumProfile") do
- local profile = parseParam(p, "EnumProfile")
+ for p in device:iterate_params ("EnumProfile") do
+ local profile = parseParam (p, "EnumProfile")
if profile.name == def_name then
return profile
end
@@ -189,7 +189,7 @@ function handleProfiles (device, new_device)
isProfilePersistent (device.properties, self.active_profiles[dev_id].name) and
def_profile ~= nil and
self.active_profiles[dev_id].name == def_profile.name
- then
+ then
local active_profile = self.active_profiles[dev_id].name
Log.info ("Device profile " .. active_profile .. " is persistent for " .. dev_name)
return
@@ -233,14 +233,14 @@ self.om = ObjectManager {
}
}
-self.om:connect("object-added", function (_, device)
+self.om:connect ("object-added", function(_, device)
device:connect ("params-changed", onDeviceParamsChanged)
handleProfiles (device, true)
end)
-self.om:connect("object-removed", function (_, device)
+self.om:connect ("object-removed", function(_, device)
local dev_id = device["bound-id"]
self.active_profiles[dev_id] = nil
end)
-self.om:activate()
+self.om:activate ()
diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua
index 99ad8473..035c3006 100644
--- a/src/scripts/policy-node.lua
+++ b/src/scripts/policy-node.lua
@@ -18,6 +18,7 @@ self.scanning = false
self.pending_rescan = false
self.events_skipped = false
self.pending_error_timer = nil
+self.filters_api = Plugin.find("filters-api")
function rescan()
for si in linkables_om:iterate() do
@@ -437,9 +438,21 @@ function findDefaultLinkable (si)
local si_props = si.properties
local target_direction = getTargetDirection(si_props)
local def_node_id = getDefaultNode(si_props, target_direction)
- return linkables_om:lookup {
+ local si_target = linkables_om:lookup {
Constraint { "node.id", "=", tostring(def_node_id) }
}
+
+ -- get origin filter from default target if filters API is enabled
+ if self.filters_api ~= nil and si_target ~= nil then
+ local target_node_id = si_target.properties["node.id"]
+ target_node_id = self.filters_api:call("get-filter-from-target",
+ target_direction, target_node_id)
+ si_target = linkables_om:lookup {
+ Constraint { "node.id", "=", tostring(target_node_id) }
+ }
+ end
+
+ return si_target
end
function checkPassthroughCompatibility (si, si_target)
@@ -468,12 +481,19 @@ function findBestLinkable (si)
} do
local si_target_props = si_target.properties
local si_target_node_id = si_target_props["node.id"]
+ local si_target_node = si:get_associated_proxy ("node")
+ local si_target_link_group = si_target_node.properties["node.link-group"]
local priority = tonumber(si_target_props["priority.session"]) or 0
Log.debug(string.format("Looking at: %s (%s)",
tostring(si_target_props["node.name"]),
tostring(si_target_node_id)))
+ -- Never use a filter as a best linkable if filters API is loaded
+ if self.filters_api ~= nil and si_target_link_group ~= nil then
+ goto skip_linkable
+ end
+
if not canLink (si_props, si_target) then
Log.debug("... cannot link, skip linkable")
goto skip_linkable
@@ -517,6 +537,25 @@ function findBestLinkable (si)
tostring(target_picked.properties["node.name"]),
tostring(target_picked.properties["node.id"]),
tostring(target_can_passthrough)))
+
+ -- get origin filter from target if filters API is enabled
+ if self.filters_api ~= nil and target_picked ~= nil then
+ local target_node_id = target_picked.properties["node.id"]
+ target_node_id = self.filters_api:call("get-filter-from-target",
+ target_direction, target_node_id)
+ target_picked = linkables_om:lookup {
+ Constraint { "node.id", "=", tostring(target_node_id) }
+ }
+ if target_picked == nil then
+ return nil, nil
+ end
+ target_can_passthrough = checkPassthroughCompatibility (si, target_picked)
+ Log.info(string.format("... best filter picked: %s (%s), can_passthrough:%s",
+ tostring(target_picked.properties["node.name"]),
+ tostring(target_picked.properties["node.id"]),
+ tostring(target_can_passthrough)))
+ end
+
return target_picked, target_can_passthrough
else
return nil, nil
@@ -564,6 +603,28 @@ function lookupLink (si_id, si_target_id)
return link
end
+function checkFilter (si, handle_nonstreams)
+ -- handle all filter nodes if filters API is not loaded
+ if self.filters_api == nil then
+ return true
+ end
+
+ -- always handle filters if handle_nonstreams is true, even if it is disabled
+ if handle_nonstreams then
+ return true
+ end
+
+ -- always return true if this is not a filter
+ local node = si:get_associated_proxy ("node")
+ local link_group = node.properties["node.link-group"]
+ if link_group == nil then
+ return true
+ end
+
+ local direction = getTargetDirection(si.properties)
+ return self.filters_api:call("is-filter-enabled", direction, link_group)
+end
+
function checkLinkable(si, handle_nonstreams)
-- only handle stream session items
local si_props = si.properties
@@ -572,6 +633,11 @@ function checkLinkable(si, handle_nonstreams)
return false
end
+ -- check filters
+ if not checkFilter (si, handle_nonstreams) then
+ return false
+ end
+
-- Determine if we can handle item by this policy
if endpoints_om:get_n_objects () > 0 and
si_props["item.factory.name"] == "si-audio-adapter" then
@@ -652,6 +718,34 @@ function checkFollowDefault (si, si_target, has_node_defined_target)
end
end
+function findFilterTarget (si)
+ -- always return nil if filters API is not loaded
+ if self.filters_api == nil then
+ return nil, false
+ end
+
+ -- always return nil if this is not a filter
+ local node = si:get_associated_proxy ("node")
+ local link_group = node.properties["node.link-group"]
+ if link_group == nil then
+ return nil, false
+ end
+
+ -- get the filter target
+ local direction = getTargetDirection (si.properties)
+ local target_json = self.filters_api:call("get-filter-target", direction, link_group)
+ if target_json == nil then
+ return nil, false
+ end
+ target = target_json:parse()
+
+ Log.info (".. filter target ID is " .. tostring(target.bound_id) ..
+ " (" .. tostring (target.exclusive) .. ")")
+ return linkables_om:lookup {
+ Constraint { "node.id", "=", tostring(target.bound_id) }
+ }, target.exclusive
+end
+
function handleLinkable (si)
if checkPending () then
return
@@ -683,11 +777,23 @@ function handleLinkable (si)
local si_target, has_defined_target, has_node_defined_target
= findDefinedTarget(si_props)
local can_passthrough = si_target and canPassthrough(si, si_target)
-
if si_target and si_must_passthrough and not can_passthrough then
si_target = nil
end
+ -- find filter target (always returns nil for non filters)
+ if si_target == nil then
+ si_target, exclusive = findFilterTarget(si)
+ -- don't fallback if filter target is not found and exclusive is true
+ if si_target == nil and exclusive then
+ return
+ end
+ local can_passthrough = si_target and canPassthrough(si, si_target)
+ if si_target and si_must_passthrough and not can_passthrough then
+ si_target = nil
+ end
+ end
+
-- if the client has seen a target that we haven't yet prepared, schedule
-- a rescan one more time and hope for the best
local si_id = si.id
@@ -876,6 +982,17 @@ if config.follow and default_nodes ~= nil then
end)
end
+-- listen for filter node changes
+if self.filters_api ~= nil then
+ self.filters_api:connect("changed", function ()
+ -- unlink all the filters and schedule rescan
+ for si in linkables_om:iterate { Constraint { "node.link-group", "+" } } do
+ unhandleLinkable (si)
+ end
+ scheduleRescan ()
+ end)
+end
+
-- listen for target.node metadata changes if config.move is enabled
if config.move then
metadata_om:connect("object-added", function (om, metadata)