mirror of
https://github.com/ublue-os/bazzite.git
synced 2025-01-29 09:32:55 +00:00
2098 lines
70 KiB
Diff
2098 lines
70 KiB
Diff
From 676e39c83f985f11fc5d87fb62040c6afd574d7c Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Tue, 21 Mar 2023 09:18:30 -0400
|
|
Subject: [PATCH 1/8] m-default-nodes: remove echo-cancel configuration
|
|
|
|
This will be possible to do with the new module-filters-api.
|
|
---
|
|
modules/module-default-nodes.c | 75 --------------------
|
|
src/config/main.lua.d/40-device-defaults.lua | 9 ---
|
|
2 files changed, 84 deletions(-)
|
|
|
|
diff --git a/modules/module-default-nodes.c b/modules/module-default-nodes.c
|
|
index aaf2938..3bb93f0 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/src/config/main.lua.d/40-device-defaults.lua b/src/config/main.lua.d/40-device-defaults.lua
|
|
index 1920291..91c4e18 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
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From d2b4bf11b37714bd1df561aa3d5b80c7e6a93920 Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Fri, 17 Mar 2023 11:06:11 -0400
|
|
Subject: [PATCH 2/8] modules: add new module-filters-api to enable smart
|
|
filter policy
|
|
|
|
This module provides an API to link filter nodes using the logic configured in
|
|
'policy.lua.d/30-filters-config.lua'. This configuration file allows grouping
|
|
filters together (sorted by priority) to use a common target node. A node is
|
|
considered a filter node if it has the node.link-group property.
|
|
|
|
This is disabled by default. You can enable smart filters policy in the
|
|
'10-default-policy.lya' configuration file.
|
|
---
|
|
modules/meson.build | 11 +
|
|
modules/module-filters-api.c | 905 ++++++++++++++++++
|
|
modules/module-lua-scripting/api/json.c | 2 +-
|
|
src/config/policy.lua.d/10-default-policy.lua | 9 +
|
|
src/config/policy.lua.d/30-filters-config.lua | 73 ++
|
|
src/scripts/filters-metadata.lua | 39 +
|
|
src/scripts/policy-node.lua | 120 ++-
|
|
7 files changed, 1156 insertions(+), 3 deletions(-)
|
|
create mode 100644 modules/module-filters-api.c
|
|
create mode 100644 src/config/policy.lua.d/30-filters-config.lua
|
|
create mode 100644 src/scripts/filters-metadata.lua
|
|
|
|
diff --git a/modules/meson.build b/modules/meson.build
|
|
index 4930bfa..4a33701 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-filters-api.c b/modules/module-filters-api.c
|
|
new file mode 100644
|
|
index 0000000..32c67c8
|
|
--- /dev/null
|
|
+++ b/modules/module-filters-api.c
|
|
@@ -0,0 +1,905 @@
|
|
+/* 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;
|
|
+
|
|
+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 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 gint
|
|
+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_return_val_if_fail (direction, -1);
|
|
+ g_return_val_if_fail (link_group, -1);
|
|
+
|
|
+ /* 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 -1;
|
|
+ found = filters->data;
|
|
+ if (!found->enabled)
|
|
+ return -1;
|
|
+
|
|
+ /* 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_proxy_get_bound_id (WP_PROXY (prev->node));
|
|
+ }
|
|
+
|
|
+ /* Find the target */
|
|
+ if (found->target) {
|
|
+ WpNode *node = g_hash_table_lookup (self->targets, found->target);
|
|
+ if (node)
|
|
+ return wp_proxy_get_bound_id (WP_PROXY (node));
|
|
+ }
|
|
+
|
|
+ return -1;
|
|
+}
|
|
+
|
|
+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;
|
|
+ gint f_target_id = wp_filters_api_get_filter_target (self, direction,
|
|
+ f->link_group);
|
|
+ if (f_target_id == target_id && f->enabled) {
|
|
+ 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 gint
|
|
+wp_filters_api_get_default_filter (WpFiltersApi * self, const gchar *direction)
|
|
+{
|
|
+ WpDirection dir = WP_DIRECTION_INPUT;
|
|
+ GList *filters;
|
|
+
|
|
+ g_return_val_if_fail (direction, -1);
|
|
+
|
|
+ /* 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];
|
|
+
|
|
+ /* The default filter is the highest priority filter without target, this is
|
|
+ * the first filer that is enabled because the list is sorted by priority */
|
|
+ while (filters) {
|
|
+ Filter *f = (Filter *) filters->data;
|
|
+ if (f->enabled && !f->target)
|
|
+ return wp_proxy_get_bound_id (WP_PROXY (f->node));
|
|
+
|
|
+ /* Advance */
|
|
+ filters = g_list_next (filters);
|
|
+ }
|
|
+
|
|
+ return -1;
|
|
+}
|
|
+
|
|
+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 *props;
|
|
+ g_autoptr (WpNode) target = NULL;
|
|
+ WpNode *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;
|
|
+ }
|
|
+ props = g_value_get_boxed (&item);
|
|
+
|
|
+ /* Get current target */
|
|
+ curr_target = g_hash_table_lookup (self->targets, key);
|
|
+
|
|
+ /* Find the node and insert it into the table if found */
|
|
+ target = find_target_node (self, props);
|
|
+ if (target) {
|
|
+ /* Check if the target changed */
|
|
+ if (curr_target) {
|
|
+ guint32 target_bound_id = wp_proxy_get_bound_id (WP_PROXY (target));
|
|
+ guint32 curr_bound_id = wp_proxy_get_bound_id (WP_PROXY (curr_target));
|
|
+ if (target_bound_id != curr_bound_id)
|
|
+ changed = TRUE;
|
|
+ }
|
|
+
|
|
+ g_hash_table_insert (self->targets, g_strdup (key),
|
|
+ g_steal_pointer (&target));
|
|
+ } else {
|
|
+ if (curr_target)
|
|
+ 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,
|
|
+ g_object_unref);
|
|
+
|
|
+ /* 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,
|
|
+ G_TYPE_INT, 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[ACTION_GET_DEFAULT_FILTER] = g_signal_new_class_handler (
|
|
+ "get-default-filter", G_TYPE_FROM_CLASS (klass),
|
|
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
+ (GCallback) wp_filters_api_get_default_filter,
|
|
+ NULL, NULL, NULL,
|
|
+ G_TYPE_INT, 1, G_TYPE_STRING);
|
|
+
|
|
+ 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 2883a02..3d486be 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/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
|
|
index 83d0a3b..d3621a7 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"] = false,
|
|
+
|
|
-- 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 0000000..76aecad
|
|
--- /dev/null
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -0,0 +1,73 @@
|
|
+-- 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 target node properties (any node properties can be defined)
|
|
+ ["targets"] = {
|
|
+ ["speakers"] = {
|
|
+ ["media.class"] = "Audio/Sink",
|
|
+ ["alsa.card_name"] = "my-speakers-card-name",
|
|
+ },
|
|
+ ["microphone"] = {
|
|
+ ["media.class"] = "Audio/Source",
|
|
+ ["alsa.card_name"] = "my-microphone-card-name",
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/scripts/filters-metadata.lua b/src/scripts/filters-metadata.lua
|
|
new file mode 100644
|
|
index 0000000..04e3e6c
|
|
--- /dev/null
|
|
+++ b/src/scripts/filters-metadata.lua
|
|
@@ -0,0 +1,39 @@
|
|
+-- 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, props in pairs(config["targets"]) do
|
|
+ targets[name] = Json.Object (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-node.lua b/src/scripts/policy-node.lua
|
|
index 99ad847..f249f34 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,37 @@ function checkFollowDefault (si, si_target, has_node_defined_target)
|
|
end
|
|
end
|
|
|
|
+function findFilterTarget (si)
|
|
+ local node = si:get_associated_proxy ("node")
|
|
+ local direction = getTargetDirection (si.properties)
|
|
+ local link_group = node.properties["node.link-group"]
|
|
+ local target_id = -1
|
|
+
|
|
+ -- always return nil if filters API is not loaded
|
|
+ if self.filters_api == nil then
|
|
+ return nil
|
|
+ end
|
|
+
|
|
+ if link_group == nil then
|
|
+ -- if this is a client stream that is not a filter, link it to the highest
|
|
+ -- priority filter that does not have a group, if any.
|
|
+ target_id = self.filters_api:call("get-default-filter", direction)
|
|
+ else
|
|
+ -- if this is a filter, get its target
|
|
+ target_id = self.filters_api:call("get-filter-target",
|
|
+ direction, link_group)
|
|
+ end
|
|
+
|
|
+ if (target_id == -1) then
|
|
+ return nil
|
|
+ end
|
|
+
|
|
+ Log.info (".. filter target ID is " .. tostring(target_id))
|
|
+ return linkables_om:lookup {
|
|
+ Constraint { "node.id", "=", tostring(target_id) }
|
|
+ }
|
|
+end
|
|
+
|
|
function handleLinkable (si)
|
|
if checkPending () then
|
|
return
|
|
@@ -683,11 +780,19 @@ 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 = findFilterTarget(si)
|
|
+ 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 +981,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)
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 2e409744d3c027feb37da07db461aaccffeb4c05 Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Thu, 23 Mar 2023 09:26:45 -0400
|
|
Subject: [PATCH 3/8] config: do not restore stream target by default
|
|
|
|
---
|
|
src/config/main.lua.d/40-stream-defaults.lua | 2 +-
|
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
|
|
diff --git a/src/config/main.lua.d/40-stream-defaults.lua b/src/config/main.lua.d/40-stream-defaults.lua
|
|
index b869099..d25aab0 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.
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From eab5f0342bd4054184f26763db495b9f18c02b03 Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Thu, 23 Mar 2023 09:41:23 -0400
|
|
Subject: [PATCH 4/8] config: enable smart filter policy
|
|
|
|
---
|
|
src/config/policy.lua.d/10-default-policy.lua | 2 +-
|
|
src/config/policy.lua.d/30-filters-config.lua | 5 +++--
|
|
2 files changed, 4 insertions(+), 3 deletions(-)
|
|
|
|
diff --git a/src/config/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
|
|
index d3621a7..412d47a 100644
|
|
--- a/src/config/policy.lua.d/10-default-policy.lua
|
|
+++ b/src/config/policy.lua.d/10-default-policy.lua
|
|
@@ -13,7 +13,7 @@ default_policy.policy = {
|
|
["filter.forward-format"] = false,
|
|
|
|
-- Whether to enable smart filter policy or not (experimental feature)
|
|
- ["filter.smart"] = false,
|
|
+ ["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.
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
index 76aecad..8d919fb 100644
|
|
--- a/src/config/policy.lua.d/30-filters-config.lua
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -63,11 +63,12 @@ default_policy.filters_metadata = {
|
|
["targets"] = {
|
|
["speakers"] = {
|
|
["media.class"] = "Audio/Sink",
|
|
- ["alsa.card_name"] = "my-speakers-card-name",
|
|
+ ["alsa.card_name"] = "acp5x",
|
|
+ ["device.profile.description"] = "Speaker",
|
|
},
|
|
["microphone"] = {
|
|
["media.class"] = "Audio/Source",
|
|
- ["alsa.card_name"] = "my-microphone-card-name",
|
|
+ ["alsa.card_name"] = "acp5x",
|
|
}
|
|
}
|
|
}
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 4ef67b81a2eb4cd85cbbf32dd4bf599382b58761 Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Fri, 3 Nov 2023 09:24:59 -0400
|
|
Subject: [PATCH 5/8] m-filters-api: remove get-default-filter API
|
|
|
|
This was redundant as you can get the default filter by calling
|
|
'get-filter-from-target' using the default target. The policy logic
|
|
should not change.
|
|
---
|
|
modules/module-filters-api.c | 34 ----------------------------------
|
|
src/scripts/policy-node.lua | 17 +++++++----------
|
|
2 files changed, 7 insertions(+), 44 deletions(-)
|
|
|
|
diff --git a/modules/module-filters-api.c b/modules/module-filters-api.c
|
|
index 32c67c8..d7dc781 100644
|
|
--- a/modules/module-filters-api.c
|
|
+++ b/modules/module-filters-api.c
|
|
@@ -230,33 +230,6 @@ wp_filters_api_get_filter_from_target (WpFiltersApi * self,
|
|
return res;
|
|
}
|
|
|
|
-static gint
|
|
-wp_filters_api_get_default_filter (WpFiltersApi * self, const gchar *direction)
|
|
-{
|
|
- WpDirection dir = WP_DIRECTION_INPUT;
|
|
- GList *filters;
|
|
-
|
|
- g_return_val_if_fail (direction, -1);
|
|
-
|
|
- /* 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];
|
|
-
|
|
- /* The default filter is the highest priority filter without target, this is
|
|
- * the first filer that is enabled because the list is sorted by priority */
|
|
- while (filters) {
|
|
- Filter *f = (Filter *) filters->data;
|
|
- if (f->enabled && !f->target)
|
|
- return wp_proxy_get_bound_id (WP_PROXY (f->node));
|
|
-
|
|
- /* Advance */
|
|
- filters = g_list_next (filters);
|
|
- }
|
|
-
|
|
- return -1;
|
|
-}
|
|
-
|
|
static void
|
|
sync_changed (WpCore * core, GAsyncResult * res, WpFiltersApi * self)
|
|
{
|
|
@@ -881,13 +854,6 @@ wp_filters_api_class_init (WpFiltersApiClass * klass)
|
|
NULL, NULL, NULL,
|
|
G_TYPE_INT, 2, G_TYPE_STRING, G_TYPE_INT);
|
|
|
|
- signals[ACTION_GET_DEFAULT_FILTER] = g_signal_new_class_handler (
|
|
- "get-default-filter", G_TYPE_FROM_CLASS (klass),
|
|
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
- (GCallback) wp_filters_api_get_default_filter,
|
|
- NULL, NULL, NULL,
|
|
- G_TYPE_INT, 1, G_TYPE_STRING);
|
|
-
|
|
signals[SIGNAL_CHANGED] = g_signal_new (
|
|
"changed", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
|
diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua
|
|
index f249f34..c5a15ec 100644
|
|
--- a/src/scripts/policy-node.lua
|
|
+++ b/src/scripts/policy-node.lua
|
|
@@ -719,9 +719,6 @@ function checkFollowDefault (si, si_target, has_node_defined_target)
|
|
end
|
|
|
|
function findFilterTarget (si)
|
|
- local node = si:get_associated_proxy ("node")
|
|
- local direction = getTargetDirection (si.properties)
|
|
- local link_group = node.properties["node.link-group"]
|
|
local target_id = -1
|
|
|
|
-- always return nil if filters API is not loaded
|
|
@@ -729,16 +726,16 @@ function findFilterTarget (si)
|
|
return nil
|
|
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
|
|
- -- if this is a client stream that is not a filter, link it to the highest
|
|
- -- priority filter that does not have a group, if any.
|
|
- target_id = self.filters_api:call("get-default-filter", direction)
|
|
- else
|
|
- -- if this is a filter, get its target
|
|
- target_id = self.filters_api:call("get-filter-target",
|
|
- direction, link_group)
|
|
+ return nil
|
|
end
|
|
|
|
+ -- get the filter target
|
|
+ local direction = getTargetDirection (si.properties)
|
|
+ target_id = self.filters_api:call("get-filter-target", direction, link_group)
|
|
if (target_id == -1) then
|
|
return nil
|
|
end
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 3908c5bdb3e65c1021a8e8a8f5c74d0866b2b0cf Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Fri, 3 Nov 2023 11:44:57 -0400
|
|
Subject: [PATCH 6/8] m-filters-api: add support for exclusive targets
|
|
|
|
Filters whose target are exclusive won't be linked to the default device if the
|
|
target does not exist.
|
|
---
|
|
modules/module-filters-api.c | 127 ++++++++++++------
|
|
src/config/policy.lua.d/30-filters-config.lua | 18 ++-
|
|
src/scripts/filters-metadata.lua | 7 +-
|
|
src/scripts/policy-node.lua | 26 ++--
|
|
4 files changed, 120 insertions(+), 58 deletions(-)
|
|
|
|
diff --git a/modules/module-filters-api.c b/modules/module-filters-api.c
|
|
index d7dc781..8852a6c 100644
|
|
--- a/modules/module-filters-api.c
|
|
+++ b/modules/module-filters-api.c
|
|
@@ -49,6 +49,12 @@ struct _Filter {
|
|
};
|
|
typedef struct _Filter Filter;
|
|
|
|
+struct _Target {
|
|
+ gboolean exclusive;
|
|
+ WpNode *node;
|
|
+};
|
|
+typedef struct _Target Target;
|
|
+
|
|
static guint
|
|
get_filter_priority (const gchar *link_group)
|
|
{
|
|
@@ -87,6 +93,22 @@ filter_free (Filter *f)
|
|
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)
|
|
{
|
|
@@ -133,16 +155,18 @@ wp_filters_api_is_filter_enabled (WpFiltersApi * self, const gchar *direction,
|
|
return found->enabled;
|
|
}
|
|
|
|
-static gint
|
|
+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, -1);
|
|
- g_return_val_if_fail (link_group, -1);
|
|
+ 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"))
|
|
@@ -153,10 +177,10 @@ wp_filters_api_get_filter_target (WpFiltersApi * self, const gchar *direction,
|
|
filters = g_list_find_custom (filters, link_group,
|
|
(GCompareFunc) filter_equal_func);
|
|
if (!filters)
|
|
- return -1;
|
|
+ return g_steal_pointer (&res);
|
|
found = filters->data;
|
|
if (!found->enabled)
|
|
- return -1;
|
|
+ return g_steal_pointer (&res);
|
|
|
|
/* Return the previous filter with matching target that is enabled */
|
|
while ((filters = g_list_previous (filters))) {
|
|
@@ -164,18 +188,27 @@ wp_filters_api_get_filter_target (WpFiltersApi * self, const gchar *direction,
|
|
if ((prev->target == found->target ||
|
|
(prev->target && found->target &&
|
|
g_str_equal (prev->target, found->target))) &&
|
|
- prev->enabled)
|
|
- return wp_proxy_get_bound_id (WP_PROXY (prev->node));
|
|
+ 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) {
|
|
- WpNode *node = g_hash_table_lookup (self->targets, found->target);
|
|
- if (node)
|
|
- return wp_proxy_get_bound_id (WP_PROXY (node));
|
|
+ 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 -1;
|
|
+ return g_steal_pointer (&res);
|
|
}
|
|
|
|
static gint
|
|
@@ -198,12 +231,17 @@ wp_filters_api_get_filter_from_target (WpFiltersApi * self,
|
|
/* Find the first target matching target_id */
|
|
while (filters) {
|
|
Filter *f = (Filter *) filters->data;
|
|
- gint f_target_id = wp_filters_api_get_filter_target (self, direction,
|
|
- f->link_group);
|
|
- if (f_target_id == target_id && f->enabled) {
|
|
- target = f->target;
|
|
- found = TRUE;
|
|
- break;
|
|
+ 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 */
|
|
@@ -323,9 +361,11 @@ reevaluate_targets (WpFiltersApi *self)
|
|
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
WpSpaJson *j = g_value_get_boxed (&item);
|
|
g_autofree gchar *key = NULL;
|
|
- WpSpaJson *props;
|
|
- g_autoptr (WpNode) target = NULL;
|
|
- WpNode *curr_target;
|
|
+ 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);
|
|
@@ -334,27 +374,36 @@ reevaluate_targets (WpFiltersApi *self)
|
|
"Could not get valid key-value pairs from target object");
|
|
break;
|
|
}
|
|
- props = g_value_get_boxed (&item);
|
|
+ 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 current target */
|
|
- curr_target = g_hash_table_lookup (self->targets, key);
|
|
+ /* Get exclusive */
|
|
+ wp_spa_json_object_get (value, "exclusive", "b", &exclusive, NULL);
|
|
|
|
- /* Find the node and insert it into the table if found */
|
|
- target = find_target_node (self, props);
|
|
- if (target) {
|
|
- /* Check if the target changed */
|
|
- if (curr_target) {
|
|
- guint32 target_bound_id = wp_proxy_get_bound_id (WP_PROXY (target));
|
|
- guint32 curr_bound_id = wp_proxy_get_bound_id (WP_PROXY (curr_target));
|
|
- if (target_bound_id != curr_bound_id)
|
|
- changed = TRUE;
|
|
- }
|
|
+ /* Get target node */
|
|
+ wp_spa_json_object_get (value, "props", "J", &props, NULL);
|
|
+ if (props)
|
|
+ target_node = find_target_node (self, props);
|
|
|
|
- g_hash_table_insert (self->targets, g_strdup (key),
|
|
- g_steal_pointer (&target));
|
|
- } else {
|
|
- if (curr_target)
|
|
+ /* 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;
|
|
}
|
|
}
|
|
|
|
@@ -790,7 +839,7 @@ wp_filters_api_enable (WpPlugin * plugin, WpTransition * transition)
|
|
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,
|
|
- g_object_unref);
|
|
+ (GDestroyNotify) target_free);
|
|
|
|
/* Create the metadata object manager */
|
|
self->metadata_om = wp_object_manager_new ();
|
|
@@ -845,7 +894,7 @@ wp_filters_api_class_init (WpFiltersApiClass * klass)
|
|
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
(GCallback) wp_filters_api_get_filter_target,
|
|
NULL, NULL, NULL,
|
|
- G_TYPE_INT, 2, G_TYPE_STRING, G_TYPE_STRING);
|
|
+ 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),
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
index 8d919fb..bf4b8d7 100644
|
|
--- a/src/config/policy.lua.d/30-filters-config.lua
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -59,16 +59,22 @@ default_policy.filters_metadata = {
|
|
}
|
|
},
|
|
|
|
- -- The target node properties (any node properties can be defined)
|
|
+ -- The filter targets
|
|
["targets"] = {
|
|
["speakers"] = {
|
|
- ["media.class"] = "Audio/Sink",
|
|
- ["alsa.card_name"] = "acp5x",
|
|
- ["device.profile.description"] = "Speaker",
|
|
+ ["exclusive"] = false,
|
|
+ ["props"] = {
|
|
+ ["media.class"] = "Audio/Sink",
|
|
+ ["alsa.card_name"] = "acp5x",
|
|
+ ["device.profile.description"] = "Speaker",
|
|
+ }
|
|
},
|
|
["microphone"] = {
|
|
- ["media.class"] = "Audio/Source",
|
|
- ["alsa.card_name"] = "acp5x",
|
|
+ ["exclusive"] = false,
|
|
+ ["props"] = {
|
|
+ ["media.class"] = "Audio/Source",
|
|
+ ["alsa.card_name"] = "acp5x",
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/scripts/filters-metadata.lua b/src/scripts/filters-metadata.lua
|
|
index 04e3e6c..49b4d0e 100644
|
|
--- a/src/scripts/filters-metadata.lua
|
|
+++ b/src/scripts/filters-metadata.lua
|
|
@@ -30,8 +30,11 @@ f_metadata:activate(Features.ALL, function (m, e)
|
|
|
|
-- Set targets metadata
|
|
local targets = {}
|
|
- for name, props in pairs(config["targets"]) do
|
|
- targets[name] = Json.Object (props)
|
|
+ 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",
|
|
diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua
|
|
index c5a15ec..035c300 100644
|
|
--- a/src/scripts/policy-node.lua
|
|
+++ b/src/scripts/policy-node.lua
|
|
@@ -719,31 +719,31 @@ function checkFollowDefault (si, si_target, has_node_defined_target)
|
|
end
|
|
|
|
function findFilterTarget (si)
|
|
- local target_id = -1
|
|
-
|
|
-- always return nil if filters API is not loaded
|
|
if self.filters_api == nil then
|
|
- return nil
|
|
+ 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
|
|
+ return nil, false
|
|
end
|
|
|
|
-- get the filter target
|
|
local direction = getTargetDirection (si.properties)
|
|
- target_id = self.filters_api:call("get-filter-target", direction, link_group)
|
|
- if (target_id == -1) then
|
|
- return nil
|
|
+ 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_id))
|
|
+ Log.info (".. filter target ID is " .. tostring(target.bound_id) ..
|
|
+ " (" .. tostring (target.exclusive) .. ")")
|
|
return linkables_om:lookup {
|
|
- Constraint { "node.id", "=", tostring(target_id) }
|
|
- }
|
|
+ Constraint { "node.id", "=", tostring(target.bound_id) }
|
|
+ }, target.exclusive
|
|
end
|
|
|
|
function handleLinkable (si)
|
|
@@ -783,7 +783,11 @@ function handleLinkable (si)
|
|
|
|
-- find filter target (always returns nil for non filters)
|
|
if si_target == nil then
|
|
- si_target = findFilterTarget(si)
|
|
+ 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
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 56054fb96e82014ed99138faa1ee02ec9ce91140 Mon Sep 17 00:00:00 2001
|
|
From: Ethan Geller <ethang@valvesoftware.com>
|
|
Date: Mon, 13 Nov 2023 22:10:05 -0800
|
|
Subject: [PATCH 7/8] fix speaker tunings for galileo
|
|
|
|
---
|
|
src/config/policy.lua.d/30-filters-config.lua | 2 +-
|
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
index bf4b8d7..37badd8 100644
|
|
--- a/src/config/policy.lua.d/30-filters-config.lua
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -65,7 +65,7 @@ default_policy.filters_metadata = {
|
|
["exclusive"] = false,
|
|
["props"] = {
|
|
["media.class"] = "Audio/Sink",
|
|
- ["alsa.card_name"] = "acp5x",
|
|
+ ["alsa.card_name"] = "sof-nau8821-max",
|
|
["device.profile.description"] = "Speaker",
|
|
}
|
|
},
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 9c3daaebe8aefedee01fc7fbe51c148cb12f6bd0 Mon Sep 17 00:00:00 2001
|
|
From: Ethan Geller <ethang@valvesoftware.com>
|
|
Date: Wed, 15 Nov 2023 14:35:00 -0800
|
|
Subject: [PATCH 8/8] fix filter chain targeting for mic.
|
|
|
|
---
|
|
src/config/policy.lua.d/30-filters-config.lua | 2 +-
|
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
index 37badd8..8e8725f 100644
|
|
--- a/src/config/policy.lua.d/30-filters-config.lua
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -73,7 +73,7 @@ default_policy.filters_metadata = {
|
|
["exclusive"] = false,
|
|
["props"] = {
|
|
["media.class"] = "Audio/Source",
|
|
- ["alsa.card_name"] = "acp5x",
|
|
+ ["alsa.card_name"] = "sof-nau8821-max",
|
|
}
|
|
}
|
|
}
|
|
--
|
|
2.42.0
|
|
|