From 13e07b101e5679e4eaf38c931d1f8471034d8d24 Mon Sep 17 00:00:00 2001 From: Julian Bouzas Date: Wed, 17 Dec 2025 09:38:15 -0500 Subject: [PATCH] linking: Add ALSA loopback policy scripts This policy script is in charge of linking all nodes from all ALSA loopback collections. --- src/config/wireplumber.conf | 10 ++ .../alsa-loopback/find-alsa-target.lua | 102 ++++++++++++++++++ .../alsa-loopback/find-filter-target.lua | 97 +++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 src/scripts/linking/alsa-loopback/find-alsa-target.lua create mode 100644 src/scripts/linking/alsa-loopback/find-filter-target.lua diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index 8003e02e..231e552f 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -741,6 +741,14 @@ wireplumber.components = [ provides = hooks.linking.default.target.get-filter-from requires = [ metadata.filters ] } + { + name = linking/alsa-loopback/find-filter-target.lua, type = script/lua + provides = hooks.linking.alsa-loopback.target.find-filter + } + { + name = linking/alsa-loopback/find-alsa-target.lua, type = script/lua + provides = hooks.linking.alsa-loopback.target.find-alsa + } { name = linking/prepare-link.lua, type = script/lua provides = hooks.linking.target.prepare-link @@ -769,6 +777,8 @@ wireplumber.components = [ hooks.linking.default.target.find-default, hooks.linking.default.target.find-best, hooks.linking.default.target.get-filter-from, + hooks.linking.alsa-loopback.target.find-filter, + hooks.linking.alsa-loopback.target.find-alsa, hooks.linking.pause-playback ] } diff --git a/src/scripts/linking/alsa-loopback/find-alsa-target.lua b/src/scripts/linking/alsa-loopback/find-alsa-target.lua new file mode 100644 index 00000000..051ee561 --- /dev/null +++ b/src/scripts/linking/alsa-loopback/find-alsa-target.lua @@ -0,0 +1,102 @@ +-- WirePlumber +-- +-- Copyright © 2025 Collabora Ltd. +-- +-- SPDX-License-Identifier: MIT +-- +-- Finds the hardware target for a linkable in 'alsa_loopback.*' collection + +lutils = require ("linking-utils") +cutils = require ("common-utils") +log = Log.open_topic ("s-linking") + +SimpleEventHook { + name = "linking/alsa-loopback/find-alsa-target", + before = "linking/prepare-link", + interests = { + EventInterest { + Constraint { "event.type", "=", "select-target" }, + Constraint { "collection.name", "#", "alsa_loopback.*" }, + }, + }, + execute = function (event) + local source, om, si, si_props, si_flags, target = + lutils:unwrap_select_target_event (event) + + -- Bypass the hook if the target is already picked up + if target then + return + end + + -- Get the collection name + local collection_name = si_props["collection.name"] + assert (collection_name) + + local target_direction = cutils.getTargetDirection (si_props) + local target_picked = nil + local target_can_passthrough = false + local target_priority = 0 + + log:info (string.format ("handling item %d: %s (%s)", si.id, + si_props ["node.name"], si_props ["node.id"])) + + -- Find the hardware target (session items without node.link-group property) + -- matching the collection name + for target in om:iterate { + type = "SiLinkable", + Constraint { "item.node.type", "=", "device" }, + Constraint { "item.node.direction", "=", target_direction }, + Constraint { "media.type", "=", si_props ["media.type"] }, + Constraint { "collection.name", "=", collection_name }, + Constraint { "node.link-group", "-" }, + } do + local target_props = target.properties + local priority = tonumber (target_props ["priority.session"]) or 0 + + log:debug (string.format ("Looking at: %s (%s)", + target_props ["node.name"], target_props ["node.id"])) + + if not lutils.canLink (si_props, target) then + log:debug ("... cannot link, skip linkable") + goto skip_linkable + end + + local passthrough_compatible, can_passthrough = + lutils.checkPassthroughCompatibility (si, target) + if not passthrough_compatible then + log:debug ("... passthrough is not compatible, skip linkable") + goto skip_linkable + end + + if target_picked == nil or priority > target_priority then + log:debug ("... picked") + target_picked = target + target_can_passthrough = can_passthrough + target_priority = priority + end + + ::skip_linkable:: + end + + local can_passthrough, passthrough_compatible + if target then + passthrough_compatible, can_passthrough = + lutils.checkPassthroughCompatibility (si, target) + if lutils.canLink (si_props, target) and passthrough_compatible then + target_picked = true + end + end + + if target_picked then + log:info (si, + string.format ("... hardware target picked: %s (%s), can_passthrough:%s", + target_picked.properties ["node.name"], + target_picked.properties ["node.id"], + tostring (target_can_passthrough))) + si_flags.can_passthrough = target_can_passthrough + event:set_data ("target", target_picked) + else + event:stop_processing () + end + end +}:register () diff --git a/src/scripts/linking/alsa-loopback/find-filter-target.lua b/src/scripts/linking/alsa-loopback/find-filter-target.lua new file mode 100644 index 00000000..2296efe8 --- /dev/null +++ b/src/scripts/linking/alsa-loopback/find-filter-target.lua @@ -0,0 +1,97 @@ +-- WirePlumber +-- +-- Copyright © 2026 Collabora Ltd. +-- +-- SPDX-License-Identifier: MIT +-- +-- Finds the filter target for a linkable in 'alsa_loopback.*' collection + +lutils = require ("linking-utils") +cutils = require ("common-utils") +log = Log.open_topic ("s-linking") + +SimpleEventHook { + name = "linking/alsa-loopback/find-filter-target", + before = "linking/alsa-loopback/find-alsa-target", + interests = { + EventInterest { + Constraint { "event.type", "=", "select-target" }, + Constraint { "collection.name", "#", "alsa_loopback.*" }, + }, + }, + execute = function (event) + local source, om, si, si_props, si_flags, target = + lutils:unwrap_select_target_event (event) + + -- Bypass the hook if the target is already picked up + if target then + return + end + + -- Get the collection name + local collection_name = si_props["collection.name"] + assert (collection_name) + + local target_direction = cutils.getTargetDirection (si_props) + local target_picked = nil + local target_can_passthrough = false + + log:info (string.format ("handling item %d: %s (%s)", si.id, + si_props ["node.name"], si_props ["node.id"])) + + -- Find the first filter target (session items with node.link-group property + -- that is not an alsa loopback item) matching the collection name + for target in om:iterate { + type = "SiLinkable", + Constraint { "item.node.type", "=", "device" }, + Constraint { "item.node.direction", "=", target_direction }, + Constraint { "media.type", "=", si_props ["media.type"] }, + Constraint { "collection.name", "=", collection_name }, + Constraint { "node.link-group", "+" }, + Constraint { "alsa.loopback.id", "-" }, + } do + local target_props = target.properties + + log:debug (string.format ("Looking at: %s (%s)", + target_props ["node.name"], target_props ["node.id"])) + + if not lutils.canLink (si_props, target) then + log:debug ("... cannot link, skip linkable") + goto skip_linkable + end + + local passthrough_compatible, can_passthrough = + lutils.checkPassthroughCompatibility (si, target) + if not passthrough_compatible then + log:debug ("... passthrough is not compatible, skip linkable") + goto skip_linkable + end + + log:debug ("... picked") + target_picked = target + target_can_passthrough = can_passthrough + break + + ::skip_linkable:: + end + + local can_passthrough, passthrough_compatible + if target then + passthrough_compatible, can_passthrough = + lutils.checkPassthroughCompatibility (si, target) + if lutils.canLink (si_props, target) and passthrough_compatible then + target_picked = true + end + end + + if target_picked then + log:info (si, + string.format ("... filter target picked: %s (%s), can_passthrough:%s", + target_picked.properties ["node.name"], + target_picked.properties ["node.id"], + tostring (target_can_passthrough))) + si_flags.can_passthrough = target_can_passthrough + event:set_data ("target", target_picked) + end + end +}:register ()