mirror of
https://github.com/ublue-os/bazzite.git
synced 2025-01-30 03:32:36 +00:00
2814 lines
93 KiB
Diff
2814 lines
93 KiB
Diff
From 9911c8532eb9072d93eb39b02aa94699f0a735a1 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 01/11] 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 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/src/config/main.lua.d/40-device-defaults.lua b/src/config/main.lua.d/40-device-defaults.lua
|
|
index 19202914..91c4e189 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 a9b6f3bcb4c525c1e257873875feb6223fa6b660 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 02/11] 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 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-filters-api.c b/modules/module-filters-api.c
|
|
new file mode 100644
|
|
index 00000000..32c67c85
|
|
--- /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 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/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
|
|
index 83d0a3b2..d3621a73 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 00000000..76aecad0
|
|
--- /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 00000000..04e3e6c6
|
|
--- /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 99ad8473..f249f343 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 bea7195a12ba1014f62e40f80c0f2e127a1aaa2e 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 03/11] 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 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.
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From f4d88774670d561b3a2d37c0d6534b204cdcb14f 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 04/11] 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 d3621a73..412d47a8 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 76aecad0..8d919fb7 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 30d26697a83ce11c4a2343c0ad4ddb113e64f72b Mon Sep 17 00:00:00 2001
|
|
From: Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
|
Date: Wed, 12 Jul 2023 10:17:37 +0530
|
|
Subject: [PATCH 05/11] policy-device-profile.lua: introduce user profile
|
|
priority list
|
|
|
|
---
|
|
src/config/main.lua.d/40-device-defaults.lua | 14 +++++++-------
|
|
1 file changed, 7 insertions(+), 7 deletions(-)
|
|
|
|
diff --git a/src/config/main.lua.d/40-device-defaults.lua b/src/config/main.lua.d/40-device-defaults.lua
|
|
index 91c4e189..b87eec51 100644
|
|
--- a/src/config/main.lua.d/40-device-defaults.lua
|
|
+++ b/src/config/main.lua.d/40-device-defaults.lua
|
|
@@ -39,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",
|
|
}
|
|
},
|
|
}
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 4c6762c41a0d8f6298e2f35cea42a86ea3ef8ff9 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 06/11] 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 32c67c85..d7dc781a 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 f249f343..c5a15ec6 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 1acb97e5606dfd842a71c9a2de520ee665f17fd0 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 07/11] 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 d7dc781a..8852a6c5 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 8d919fb7..bf4b8d75 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 04e3e6c6..49b4d0e8 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 c5a15ec6..035c3006 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 0010f03fafa2841f7618dad4b609466b6dad11c7 Mon Sep 17 00:00:00 2001
|
|
From: Julian Bouzas <julian.bouzas@collabora.com>
|
|
Date: Mon, 6 Nov 2023 14:33:34 -0500
|
|
Subject: [PATCH 08/11] policy-bluetooth: remove application names array and
|
|
use BT loopback filter
|
|
|
|
Uses a BT loopback filter to know when an application wants to capture audio
|
|
from the current BT device. If the BT loopback filter is used, wireplumber will
|
|
automatically switch the device to HSP/HFP profile, otherwise the BT device
|
|
profile is always set to A2DP.
|
|
---
|
|
src/config/policy.lua.d/10-default-policy.lua | 10 --
|
|
src/config/policy.lua.d/30-filters-config.lua | 17 ++-
|
|
src/config/wireplumber.conf | 20 +++
|
|
src/scripts/policy-bluetooth.lua | 139 ++++++++++--------
|
|
4 files changed, 110 insertions(+), 76 deletions(-)
|
|
|
|
diff --git a/src/config/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
|
|
index 412d47a8..7d4ea77c 100644
|
|
--- a/src/config/policy.lua.d/10-default-policy.lua
|
|
+++ b/src/config/policy.lua.d/10-default-policy.lua
|
|
@@ -33,16 +33,6 @@ bluetooth_policy.policy = {
|
|
|
|
-- Whether to use headset profile in the presence of an input stream.
|
|
["media-role.use-headset-profile"] = true,
|
|
-
|
|
- -- Application names correspond to application.name in stream properties.
|
|
- -- Applications which do not set media.role but which should be considered
|
|
- -- for role based profile switching can be specified here.
|
|
- ["media-role.applications"] = {
|
|
- "Firefox", "Chromium input", "Google Chrome input", "Brave input",
|
|
- "Microsoft Edge input", "Vivaldi input", "ZOOM VoiceEngine",
|
|
- "Telegram Desktop", "telegram-desktop", "linphone", "Mumble",
|
|
- "WEBRTC VoiceEngine", "Skype", "Firefox Developer Edition",
|
|
- },
|
|
}
|
|
|
|
dsp_policy = {}
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
index bf4b8d75..88429cdc 100644
|
|
--- a/src/config/policy.lua.d/30-filters-config.lua
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -33,11 +33,19 @@ default_policy.filters_metadata = {
|
|
},
|
|
|
|
-- Output filters (meant to be linked with Audio/Source device nodes)
|
|
+ {
|
|
+ ["stream-name"] = "virtual-bluetooth-source-in", -- loopback bluetooth capture
|
|
+ ["node-name"] = "virtual-bluetooth-source-out", -- loopback bluetooth source
|
|
+ ["direction"] = "output", -- can only be 'input' or 'output'
|
|
+ ["target"] = "bluetooth-source", -- 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"] = "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
|
|
+ ["target"] = "microphone", -- if nil, the default node will be used as target
|
|
["mode"] = "always", -- can be 'always', 'never', 'playback-only' or 'capture-only'
|
|
["priority"] = 30,
|
|
},
|
|
@@ -75,6 +83,13 @@ default_policy.filters_metadata = {
|
|
["media.class"] = "Audio/Source",
|
|
["alsa.card_name"] = "acp5x",
|
|
}
|
|
+ },
|
|
+ ["bluetooth-source"] = {
|
|
+ ["exclusive"] = true,
|
|
+ ["props"] = {
|
|
+ ["media.class"] = "Audio/Source",
|
|
+ ["device.api"] = "bluez5"
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf
|
|
index 85d7be12..4c9dd568 100644
|
|
--- a/src/config/wireplumber.conf
|
|
+++ b/src/config/wireplumber.conf
|
|
@@ -77,6 +77,26 @@ context.modules = [
|
|
|
|
# Provides factories to make SPA node objects.
|
|
{ name = libpipewire-module-spa-node-factory }
|
|
+
|
|
+ # Virtual Bluetooth Source
|
|
+ {
|
|
+ name = libpipewire-module-loopback
|
|
+ args = {
|
|
+ capture.props = {
|
|
+ node.name = virtual-bluetooth-source-in
|
|
+ node.description = "Virtual Bluetooth Source In"
|
|
+ audio.position = [ MONO ]
|
|
+ stream.dont-remix = true
|
|
+ node.passive = true
|
|
+ }
|
|
+ playback.props = {
|
|
+ node.name = virtual-bluetooth-source-out
|
|
+ node.description = "Virtual Bluetooth Source Out"
|
|
+ audio.position = [ MONO ]
|
|
+ media.class = Audio/Source
|
|
+ }
|
|
+ }
|
|
+ }
|
|
]
|
|
|
|
wireplumber.components = [
|
|
diff --git a/src/scripts/policy-bluetooth.lua b/src/scripts/policy-bluetooth.lua
|
|
index f8f69a14..7aecb8b0 100644
|
|
--- a/src/scripts/policy-bluetooth.lua
|
|
+++ b/src/scripts/policy-bluetooth.lua
|
|
@@ -26,7 +26,6 @@
|
|
|
|
local config = ...
|
|
local use_persistent_storage = config["use-persistent-storage"] or false
|
|
-local applications = {}
|
|
local use_headset_profile = config["media-role.use-headset-profile"] or false
|
|
local profile_restore_timeout_msec = 2000
|
|
|
|
@@ -41,17 +40,6 @@ local last_profiles = {}
|
|
local active_streams = {}
|
|
local previous_streams = {}
|
|
|
|
-for _, value in ipairs(config["media-role.applications"] or {}) do
|
|
- applications[value] = true
|
|
-end
|
|
-
|
|
-metadata_om = ObjectManager {
|
|
- Interest {
|
|
- type = "metadata",
|
|
- Constraint { "metadata.name", "=", "default" },
|
|
- }
|
|
-}
|
|
-
|
|
devices_om = ObjectManager {
|
|
Interest {
|
|
type = "device",
|
|
@@ -68,6 +56,16 @@ streams_om = ObjectManager {
|
|
}
|
|
}
|
|
|
|
+nodes_om = ObjectManager {
|
|
+ Interest {
|
|
+ type = "node",
|
|
+ Constraint { "node.name", "=", "virtual-bluetooth-source-out", type = "pw-global" },
|
|
+ Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
|
+ }
|
|
+}
|
|
+
|
|
+links_om = ObjectManager { Interest { type = "link" } }
|
|
+
|
|
local function parseParam(param_to_parse, id)
|
|
local param = param_to_parse:parse()
|
|
if param.pod_type == "Object" and param.object_id == id then
|
|
@@ -117,19 +115,6 @@ local function isSwitched(device)
|
|
return getSavedLastProfile(device) ~= nil
|
|
end
|
|
|
|
-local function isBluez5AudioSink(sink_name)
|
|
- if sink_name and string.find(sink_name, "bluez_output.") ~= nil then
|
|
- return true
|
|
- end
|
|
- return false
|
|
-end
|
|
-
|
|
-local function isBluez5DefaultAudioSink()
|
|
- local metadata = metadata_om:lookup()
|
|
- local default_audio_sink = metadata:find(0, "default.audio.sink")
|
|
- return isBluez5AudioSink(default_audio_sink)
|
|
-end
|
|
-
|
|
local function findProfile(device, index, name)
|
|
for p in device:iterate_params("EnumProfile") do
|
|
local profile = parseParam(p, "EnumProfile")
|
|
@@ -228,7 +213,6 @@ local function switchProfile()
|
|
end
|
|
|
|
local cur_profile_name = getCurrentProfile(device)
|
|
- saveLastProfile(device, cur_profile_name)
|
|
|
|
_, index, name = findProfile(device, nil, cur_profile_name)
|
|
if hasProfileInputRoute(device, index) then
|
|
@@ -251,6 +235,8 @@ local function switchProfile()
|
|
index = index
|
|
}
|
|
|
|
+ saveLastProfile(device, cur_profile_name)
|
|
+
|
|
Log.info("Setting profile of '"
|
|
.. device.properties["device.description"]
|
|
.. "' from: " .. cur_profile_name
|
|
@@ -270,8 +256,6 @@ local function restoreProfile()
|
|
local profile_name = getSavedLastProfile(device)
|
|
local cur_profile_name = getCurrentProfile(device)
|
|
|
|
- saveLastProfile(device, nil)
|
|
-
|
|
if cur_profile_name then
|
|
Log.info("Setting saved headset profile to: " .. cur_profile_name)
|
|
saveHeadsetProfile(device, cur_profile_name)
|
|
@@ -286,6 +270,8 @@ local function restoreProfile()
|
|
index = index
|
|
}
|
|
|
|
+ saveLastProfile(device, nil)
|
|
+
|
|
Log.info("Restoring profile of '"
|
|
.. device.properties["device.description"]
|
|
.. "' from: " .. cur_profile_name
|
|
@@ -312,18 +298,14 @@ local function triggerRestoreProfile()
|
|
end)
|
|
end
|
|
|
|
--- We consider a Stream of interest to have role Communication if it has
|
|
--- media.role set to Communication in props or it is in our list of
|
|
--- applications as these applications do not set media.role correctly or at
|
|
--- all.
|
|
-local function checkStreamStatus(stream)
|
|
- local app_name = stream.properties["application.name"]
|
|
- local stream_role = stream.properties["media.role"]
|
|
+function parseBool(var)
|
|
+ return var and (var:lower() == "true" or var == "1")
|
|
+end
|
|
|
|
- if not (stream_role == "Communication" or applications[app_name]) then
|
|
- return false
|
|
- end
|
|
- if not isBluez5DefaultAudioSink() then
|
|
+local function checkStreamStatus (stream)
|
|
+ -- Ignore monitor streams
|
|
+ local is_monitor = parseBool (stream.properties["stream.monitor"])
|
|
+ if is_monitor then
|
|
return false
|
|
end
|
|
|
|
@@ -334,7 +316,25 @@ local function checkStreamStatus(stream)
|
|
return false
|
|
end
|
|
|
|
- return true
|
|
+ -- Make sure the virtual BT filter node exists
|
|
+ local node = nodes_om:lookup ()
|
|
+ if node == nil then
|
|
+ return false
|
|
+ end
|
|
+
|
|
+ -- Check if the stream is linked to the bluetooth loopback filter
|
|
+ local stream_id = tonumber(stream["bound-id"])
|
|
+ local bt_out_id = tonumber(node["bound-id"])
|
|
+ for l in links_om:iterate() do
|
|
+ local p = l.properties
|
|
+ local out_id = tonumber(p["link.output.node"])
|
|
+ local in_id = tonumber(p["link.input.node"])
|
|
+ if in_id == stream_id and out_id == bt_out_id then
|
|
+ return true
|
|
+ end
|
|
+ end
|
|
+
|
|
+ return false
|
|
end
|
|
|
|
local function handleStream(stream)
|
|
@@ -361,38 +361,47 @@ local function handleAllStreams()
|
|
end
|
|
end
|
|
|
|
-streams_om:connect("object-added", function (_, stream)
|
|
- stream:connect("state-changed", function (stream, old_state, cur_state)
|
|
- handleStream(stream)
|
|
- end)
|
|
- stream:connect("params-changed", handleStream)
|
|
- handleStream(stream)
|
|
-end)
|
|
-
|
|
-streams_om:connect("object-removed", function (_, stream)
|
|
- active_streams[stream["bound-id"]] = nil
|
|
- previous_streams[stream["bound-id"]] = nil
|
|
- triggerRestoreProfile()
|
|
-end)
|
|
-
|
|
devices_om:connect("object-added", function (_, device)
|
|
-- Devices are unswitched initially
|
|
- if isSwitched(device) then
|
|
- saveLastProfile(device, nil)
|
|
- end
|
|
+ saveLastProfile(device, nil)
|
|
handleAllStreams()
|
|
end)
|
|
|
|
-metadata_om:connect("object-added", function (_, metadata)
|
|
- metadata:connect("changed", function (m, subject, key, t, value)
|
|
- if (use_headset_profile and subject == 0 and key == "default.audio.sink"
|
|
- and isBluez5AudioSink(value)) then
|
|
- -- If bluez sink is set as default, rescan for active input streams
|
|
- handleAllStreams()
|
|
+links_om:connect("object-added", function (_, link)
|
|
+ if handleAllStreams then
|
|
+ local p = link.properties
|
|
+ for stream in streams_om:iterate {
|
|
+ Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
+ Constraint { "stream.monitor", "!", "true" }
|
|
+ } do
|
|
+ local in_id = tonumber(p["link.input.node"])
|
|
+ local stream_id = tonumber(stream["bound-id"])
|
|
+ if in_id == stream_id then
|
|
+ handleStream(stream)
|
|
+ end
|
|
end
|
|
- end)
|
|
+ end
|
|
+end)
|
|
+
|
|
+links_om:connect("object-removed", function (_, link)
|
|
+ if handleAllStreams then
|
|
+ local p = link.properties
|
|
+ for stream in streams_om:iterate {
|
|
+ Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
+ Constraint { "stream.monitor", "!", "true" }
|
|
+ } do
|
|
+ local in_id = tonumber(p["link.input.node"])
|
|
+ local stream_id = tonumber(stream["bound-id"])
|
|
+ if in_id == stream_id then
|
|
+ active_streams[stream["bound-id"]] = nil
|
|
+ previous_streams[stream["bound-id"]] = nil
|
|
+ triggerRestoreProfile()
|
|
+ end
|
|
+ end
|
|
+ end
|
|
end)
|
|
|
|
-metadata_om:activate()
|
|
devices_om:activate()
|
|
streams_om:activate()
|
|
+nodes_om:activate()
|
|
+links_om:activate()
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From ae7cde6dd7c7660da5bbd07f27fe18a624c8c67d Mon Sep 17 00:00:00 2001
|
|
From: Ethan Geller <ethang@valvesoftware.com>
|
|
Date: Mon, 13 Nov 2023 22:10:05 -0800
|
|
Subject: [PATCH 09/11] 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 88429cdc..a4b617d8 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/Sink",
|
|
- ["alsa.card_name"] = "acp5x",
|
|
+ ["alsa.card_name"] = "sof-nau8821-max",
|
|
["device.profile.description"] = "Speaker",
|
|
}
|
|
},
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 0ed6dc783bcb4033fb82c0f9b3e2c26977c3e19e Mon Sep 17 00:00:00 2001
|
|
From: Ethan Geller <ethang@valvesoftware.com>
|
|
Date: Wed, 15 Nov 2023 14:32:56 -0800
|
|
Subject: [PATCH 10/11] Revert "policy-bluetooth: remove application names
|
|
array and use BT loopback filter"
|
|
|
|
This reverts commit 5a760629b6e81268383f32406119fbb4ac3a42b0.
|
|
---
|
|
src/config/policy.lua.d/10-default-policy.lua | 10 ++
|
|
src/config/policy.lua.d/30-filters-config.lua | 17 +--
|
|
src/config/wireplumber.conf | 20 ---
|
|
src/scripts/policy-bluetooth.lua | 139 ++++++++----------
|
|
4 files changed, 76 insertions(+), 110 deletions(-)
|
|
|
|
diff --git a/src/config/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
|
|
index 7d4ea77c..412d47a8 100644
|
|
--- a/src/config/policy.lua.d/10-default-policy.lua
|
|
+++ b/src/config/policy.lua.d/10-default-policy.lua
|
|
@@ -33,6 +33,16 @@ bluetooth_policy.policy = {
|
|
|
|
-- Whether to use headset profile in the presence of an input stream.
|
|
["media-role.use-headset-profile"] = true,
|
|
+
|
|
+ -- Application names correspond to application.name in stream properties.
|
|
+ -- Applications which do not set media.role but which should be considered
|
|
+ -- for role based profile switching can be specified here.
|
|
+ ["media-role.applications"] = {
|
|
+ "Firefox", "Chromium input", "Google Chrome input", "Brave input",
|
|
+ "Microsoft Edge input", "Vivaldi input", "ZOOM VoiceEngine",
|
|
+ "Telegram Desktop", "telegram-desktop", "linphone", "Mumble",
|
|
+ "WEBRTC VoiceEngine", "Skype", "Firefox Developer Edition",
|
|
+ },
|
|
}
|
|
|
|
dsp_policy = {}
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
index a4b617d8..37badd80 100644
|
|
--- a/src/config/policy.lua.d/30-filters-config.lua
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -33,19 +33,11 @@ default_policy.filters_metadata = {
|
|
},
|
|
|
|
-- Output filters (meant to be linked with Audio/Source device nodes)
|
|
- {
|
|
- ["stream-name"] = "virtual-bluetooth-source-in", -- loopback bluetooth capture
|
|
- ["node-name"] = "virtual-bluetooth-source-out", -- loopback bluetooth source
|
|
- ["direction"] = "output", -- can only be 'input' or 'output'
|
|
- ["target"] = "bluetooth-source", -- 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"] = "input.virtual-source", -- loopback capture
|
|
["node-name"] = "output.virtual-source", -- loopback source
|
|
["direction"] = "output", -- can only be 'input' or 'output'
|
|
- ["target"] = "microphone", -- if nil, the default node will be used as target
|
|
+ ["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,
|
|
},
|
|
@@ -83,13 +75,6 @@ default_policy.filters_metadata = {
|
|
["media.class"] = "Audio/Source",
|
|
["alsa.card_name"] = "acp5x",
|
|
}
|
|
- },
|
|
- ["bluetooth-source"] = {
|
|
- ["exclusive"] = true,
|
|
- ["props"] = {
|
|
- ["media.class"] = "Audio/Source",
|
|
- ["device.api"] = "bluez5"
|
|
- }
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf
|
|
index 4c9dd568..85d7be12 100644
|
|
--- a/src/config/wireplumber.conf
|
|
+++ b/src/config/wireplumber.conf
|
|
@@ -77,26 +77,6 @@ context.modules = [
|
|
|
|
# Provides factories to make SPA node objects.
|
|
{ name = libpipewire-module-spa-node-factory }
|
|
-
|
|
- # Virtual Bluetooth Source
|
|
- {
|
|
- name = libpipewire-module-loopback
|
|
- args = {
|
|
- capture.props = {
|
|
- node.name = virtual-bluetooth-source-in
|
|
- node.description = "Virtual Bluetooth Source In"
|
|
- audio.position = [ MONO ]
|
|
- stream.dont-remix = true
|
|
- node.passive = true
|
|
- }
|
|
- playback.props = {
|
|
- node.name = virtual-bluetooth-source-out
|
|
- node.description = "Virtual Bluetooth Source Out"
|
|
- audio.position = [ MONO ]
|
|
- media.class = Audio/Source
|
|
- }
|
|
- }
|
|
- }
|
|
]
|
|
|
|
wireplumber.components = [
|
|
diff --git a/src/scripts/policy-bluetooth.lua b/src/scripts/policy-bluetooth.lua
|
|
index 7aecb8b0..f8f69a14 100644
|
|
--- a/src/scripts/policy-bluetooth.lua
|
|
+++ b/src/scripts/policy-bluetooth.lua
|
|
@@ -26,6 +26,7 @@
|
|
|
|
local config = ...
|
|
local use_persistent_storage = config["use-persistent-storage"] or false
|
|
+local applications = {}
|
|
local use_headset_profile = config["media-role.use-headset-profile"] or false
|
|
local profile_restore_timeout_msec = 2000
|
|
|
|
@@ -40,6 +41,17 @@ local last_profiles = {}
|
|
local active_streams = {}
|
|
local previous_streams = {}
|
|
|
|
+for _, value in ipairs(config["media-role.applications"] or {}) do
|
|
+ applications[value] = true
|
|
+end
|
|
+
|
|
+metadata_om = ObjectManager {
|
|
+ Interest {
|
|
+ type = "metadata",
|
|
+ Constraint { "metadata.name", "=", "default" },
|
|
+ }
|
|
+}
|
|
+
|
|
devices_om = ObjectManager {
|
|
Interest {
|
|
type = "device",
|
|
@@ -56,16 +68,6 @@ streams_om = ObjectManager {
|
|
}
|
|
}
|
|
|
|
-nodes_om = ObjectManager {
|
|
- Interest {
|
|
- type = "node",
|
|
- Constraint { "node.name", "=", "virtual-bluetooth-source-out", type = "pw-global" },
|
|
- Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
|
- }
|
|
-}
|
|
-
|
|
-links_om = ObjectManager { Interest { type = "link" } }
|
|
-
|
|
local function parseParam(param_to_parse, id)
|
|
local param = param_to_parse:parse()
|
|
if param.pod_type == "Object" and param.object_id == id then
|
|
@@ -115,6 +117,19 @@ local function isSwitched(device)
|
|
return getSavedLastProfile(device) ~= nil
|
|
end
|
|
|
|
+local function isBluez5AudioSink(sink_name)
|
|
+ if sink_name and string.find(sink_name, "bluez_output.") ~= nil then
|
|
+ return true
|
|
+ end
|
|
+ return false
|
|
+end
|
|
+
|
|
+local function isBluez5DefaultAudioSink()
|
|
+ local metadata = metadata_om:lookup()
|
|
+ local default_audio_sink = metadata:find(0, "default.audio.sink")
|
|
+ return isBluez5AudioSink(default_audio_sink)
|
|
+end
|
|
+
|
|
local function findProfile(device, index, name)
|
|
for p in device:iterate_params("EnumProfile") do
|
|
local profile = parseParam(p, "EnumProfile")
|
|
@@ -213,6 +228,7 @@ local function switchProfile()
|
|
end
|
|
|
|
local cur_profile_name = getCurrentProfile(device)
|
|
+ saveLastProfile(device, cur_profile_name)
|
|
|
|
_, index, name = findProfile(device, nil, cur_profile_name)
|
|
if hasProfileInputRoute(device, index) then
|
|
@@ -235,8 +251,6 @@ local function switchProfile()
|
|
index = index
|
|
}
|
|
|
|
- saveLastProfile(device, cur_profile_name)
|
|
-
|
|
Log.info("Setting profile of '"
|
|
.. device.properties["device.description"]
|
|
.. "' from: " .. cur_profile_name
|
|
@@ -256,6 +270,8 @@ local function restoreProfile()
|
|
local profile_name = getSavedLastProfile(device)
|
|
local cur_profile_name = getCurrentProfile(device)
|
|
|
|
+ saveLastProfile(device, nil)
|
|
+
|
|
if cur_profile_name then
|
|
Log.info("Setting saved headset profile to: " .. cur_profile_name)
|
|
saveHeadsetProfile(device, cur_profile_name)
|
|
@@ -270,8 +286,6 @@ local function restoreProfile()
|
|
index = index
|
|
}
|
|
|
|
- saveLastProfile(device, nil)
|
|
-
|
|
Log.info("Restoring profile of '"
|
|
.. device.properties["device.description"]
|
|
.. "' from: " .. cur_profile_name
|
|
@@ -298,14 +312,18 @@ local function triggerRestoreProfile()
|
|
end)
|
|
end
|
|
|
|
-function parseBool(var)
|
|
- return var and (var:lower() == "true" or var == "1")
|
|
-end
|
|
+-- We consider a Stream of interest to have role Communication if it has
|
|
+-- media.role set to Communication in props or it is in our list of
|
|
+-- applications as these applications do not set media.role correctly or at
|
|
+-- all.
|
|
+local function checkStreamStatus(stream)
|
|
+ local app_name = stream.properties["application.name"]
|
|
+ local stream_role = stream.properties["media.role"]
|
|
|
|
-local function checkStreamStatus (stream)
|
|
- -- Ignore monitor streams
|
|
- local is_monitor = parseBool (stream.properties["stream.monitor"])
|
|
- if is_monitor then
|
|
+ if not (stream_role == "Communication" or applications[app_name]) then
|
|
+ return false
|
|
+ end
|
|
+ if not isBluez5DefaultAudioSink() then
|
|
return false
|
|
end
|
|
|
|
@@ -316,25 +334,7 @@ local function checkStreamStatus (stream)
|
|
return false
|
|
end
|
|
|
|
- -- Make sure the virtual BT filter node exists
|
|
- local node = nodes_om:lookup ()
|
|
- if node == nil then
|
|
- return false
|
|
- end
|
|
-
|
|
- -- Check if the stream is linked to the bluetooth loopback filter
|
|
- local stream_id = tonumber(stream["bound-id"])
|
|
- local bt_out_id = tonumber(node["bound-id"])
|
|
- for l in links_om:iterate() do
|
|
- local p = l.properties
|
|
- local out_id = tonumber(p["link.output.node"])
|
|
- local in_id = tonumber(p["link.input.node"])
|
|
- if in_id == stream_id and out_id == bt_out_id then
|
|
- return true
|
|
- end
|
|
- end
|
|
-
|
|
- return false
|
|
+ return true
|
|
end
|
|
|
|
local function handleStream(stream)
|
|
@@ -361,47 +361,38 @@ local function handleAllStreams()
|
|
end
|
|
end
|
|
|
|
-devices_om:connect("object-added", function (_, device)
|
|
- -- Devices are unswitched initially
|
|
- saveLastProfile(device, nil)
|
|
- handleAllStreams()
|
|
+streams_om:connect("object-added", function (_, stream)
|
|
+ stream:connect("state-changed", function (stream, old_state, cur_state)
|
|
+ handleStream(stream)
|
|
+ end)
|
|
+ stream:connect("params-changed", handleStream)
|
|
+ handleStream(stream)
|
|
end)
|
|
|
|
-links_om:connect("object-added", function (_, link)
|
|
- if handleAllStreams then
|
|
- local p = link.properties
|
|
- for stream in streams_om:iterate {
|
|
- Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
- Constraint { "stream.monitor", "!", "true" }
|
|
- } do
|
|
- local in_id = tonumber(p["link.input.node"])
|
|
- local stream_id = tonumber(stream["bound-id"])
|
|
- if in_id == stream_id then
|
|
- handleStream(stream)
|
|
- end
|
|
- end
|
|
+streams_om:connect("object-removed", function (_, stream)
|
|
+ active_streams[stream["bound-id"]] = nil
|
|
+ previous_streams[stream["bound-id"]] = nil
|
|
+ triggerRestoreProfile()
|
|
+end)
|
|
+
|
|
+devices_om:connect("object-added", function (_, device)
|
|
+ -- Devices are unswitched initially
|
|
+ if isSwitched(device) then
|
|
+ saveLastProfile(device, nil)
|
|
end
|
|
+ handleAllStreams()
|
|
end)
|
|
|
|
-links_om:connect("object-removed", function (_, link)
|
|
- if handleAllStreams then
|
|
- local p = link.properties
|
|
- for stream in streams_om:iterate {
|
|
- Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
- Constraint { "stream.monitor", "!", "true" }
|
|
- } do
|
|
- local in_id = tonumber(p["link.input.node"])
|
|
- local stream_id = tonumber(stream["bound-id"])
|
|
- if in_id == stream_id then
|
|
- active_streams[stream["bound-id"]] = nil
|
|
- previous_streams[stream["bound-id"]] = nil
|
|
- triggerRestoreProfile()
|
|
- end
|
|
+metadata_om:connect("object-added", function (_, metadata)
|
|
+ metadata:connect("changed", function (m, subject, key, t, value)
|
|
+ if (use_headset_profile and subject == 0 and key == "default.audio.sink"
|
|
+ and isBluez5AudioSink(value)) then
|
|
+ -- If bluez sink is set as default, rescan for active input streams
|
|
+ handleAllStreams()
|
|
end
|
|
- end
|
|
+ end)
|
|
end)
|
|
|
|
+metadata_om:activate()
|
|
devices_om:activate()
|
|
streams_om:activate()
|
|
-nodes_om:activate()
|
|
-links_om:activate()
|
|
--
|
|
2.42.0
|
|
|
|
|
|
From 043390080937c05df74a48eaff5a9713ff0ea12f Mon Sep 17 00:00:00 2001
|
|
From: Ethan Geller <ethang@valvesoftware.com>
|
|
Date: Wed, 15 Nov 2023 14:35:00 -0800
|
|
Subject: [PATCH 11/11] 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 37badd80..8e8725fc 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
|
|
|