mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-05 07:48:01 +02:00
scripts: Add audio-group-utils.lua to group audio streams
This allows grouping audio streams that have a pw-audio-namespace ancestor process name. The grouping is done by creating a loopback filter for each group or namespace. Those loopback filters are then linked in between the actual stream and device nodes. A '--target-object' flag is also supported in the ancestor process name to define a target for the loopback stream node.
This commit is contained in:
parent
86cdfaccc4
commit
0b716118c7
6 changed files with 350 additions and 2 deletions
|
|
@ -595,6 +595,10 @@ wireplumber.components = [
|
|||
name = node/software-dsp.lua, type = script/lua
|
||||
provides = node.software-dsp
|
||||
}
|
||||
{
|
||||
name = node/audio-group.lua, type = script/lua
|
||||
provides = node.audio-group
|
||||
}
|
||||
|
||||
## Linking hooks
|
||||
{
|
||||
|
|
@ -609,6 +613,11 @@ wireplumber.components = [
|
|||
name = linking/find-defined-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-defined
|
||||
}
|
||||
{
|
||||
name = linking/find-audio-group-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-audio-group
|
||||
requires = [ node.audio-group ]
|
||||
}
|
||||
{
|
||||
name = linking/find-filter-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-filter
|
||||
|
|
@ -646,6 +655,7 @@ wireplumber.components = [
|
|||
hooks.linking.target.link ]
|
||||
wants = [ hooks.linking.target.find-media-role,
|
||||
hooks.linking.target.find-defined,
|
||||
hooks.linking.target.find-audio-group,
|
||||
hooks.linking.target.find-filter,
|
||||
hooks.linking.target.find-default,
|
||||
hooks.linking.target.find-best,
|
||||
|
|
|
|||
31
src/scripts/lib/audio-group-utils.lua
Normal file
31
src/scripts/lib/audio-group-utils.lua
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- WirePlumber
|
||||
|
||||
-- Copyright © 2024 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- Script is a Lua Module of audio group Lua utility functions
|
||||
|
||||
local module = {
|
||||
node_groups = {},
|
||||
}
|
||||
|
||||
function module.set_audio_group (stream_node, audio_group)
|
||||
module.node_groups [stream_node.id] = audio_group
|
||||
end
|
||||
|
||||
function module.get_audio_group (stream_node)
|
||||
return module.node_groups [stream_node.id]
|
||||
end
|
||||
|
||||
function module.contains_audio_group (audio_group)
|
||||
for k, v in pairs(module.node_groups) do
|
||||
if v == group then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return module
|
||||
97
src/scripts/linking/find-audio-group-target.lua
Normal file
97
src/scripts/linking/find-audio-group-target.lua
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2023 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Check if the target node is a filter target.
|
||||
|
||||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
agutils = require ("audio-group-utils")
|
||||
|
||||
log = Log.open_topic ("s-linking")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "linking/find-audio-group-target",
|
||||
after = "linking/find-defined-target",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
},
|
||||
},
|
||||
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
|
||||
|
||||
local target_direction = cutils.getTargetDirection (si_props)
|
||||
local target_picked = nil
|
||||
local target_can_passthrough = false
|
||||
local node = nil
|
||||
local audio_group = nil
|
||||
|
||||
log:info (si, string.format ("handling item %d: %s (%s)", si.id,
|
||||
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||
|
||||
-- Get associated node
|
||||
node = si:get_associated_proxy ("node")
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- audio group
|
||||
audio_group = agutils.get_audio_group (node)
|
||||
if audio_group == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- find the target with same audio group, if any
|
||||
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"] },
|
||||
} do
|
||||
target_node = target:get_associated_proxy ("node")
|
||||
target_node_props = target_node.properties
|
||||
target_audio_group = target_node_props ["session.audio-group"]
|
||||
|
||||
if target_audio_group == nil then
|
||||
goto skip_linkable
|
||||
end
|
||||
|
||||
if target_audio_group ~= audio_group then
|
||||
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
|
||||
|
||||
target_picked = target
|
||||
target_can_passthrough = can_passthrough
|
||||
break
|
||||
|
||||
::skip_linkable::
|
||||
end
|
||||
|
||||
-- set target
|
||||
if target_picked then
|
||||
log:info (si,
|
||||
string.format ("... audio group target picked: %s (%s), can_passthrough:%s",
|
||||
tostring (target_picked.properties ["node.name"]),
|
||||
tostring (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 ()
|
||||
|
|
@ -33,7 +33,7 @@ end
|
|||
|
||||
SimpleEventHook {
|
||||
name = "linking/find-filter-target",
|
||||
after = "linking/find-defined-target",
|
||||
after = "linking/find-audio-group-target",
|
||||
before = "linking/prepare-link",
|
||||
interests = {
|
||||
EventInterest {
|
||||
|
|
|
|||
|
|
@ -43,9 +43,15 @@ SimpleEventHook {
|
|||
return
|
||||
end
|
||||
|
||||
-- bypass the hook if target is defined, is a filter and is targetable
|
||||
-- bypass the hook if the target is an audio group
|
||||
local target_node = target:get_associated_proxy ("node")
|
||||
local target_node_props = target_node.properties
|
||||
local target_audio_group = target_node_props ["session.audio-group"]
|
||||
if target_audio_group ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- bypass the hook if target is defined, is a filter and is targetable
|
||||
local target_link_group = target_node_props ["node.link-group"]
|
||||
if target_link_group ~= nil and si_flags.has_defined_target then
|
||||
if futils.is_filter_smart (target_direction, target_link_group) and
|
||||
|
|
|
|||
204
src/scripts/node/audio-group.lua
Normal file
204
src/scripts/node/audio-group.lua
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
-- WirePlumber
|
||||
|
||||
-- Copyright © 2024 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- audio-group.lua script takes pipewire audio stream nodes and groups them
|
||||
-- into a single unit by creating a loopback filter per group. The grouping
|
||||
-- is done by a common ancestor 'audio-group-namespace' process name.
|
||||
|
||||
agutils = require ("audio-group-utils")
|
||||
|
||||
PW_AUDIO_NAMESPACE = "pw-audio-namespace"
|
||||
|
||||
node_directions = {}
|
||||
group_loopback_modules = {}
|
||||
group_loopback_modules["input"] = {}
|
||||
group_loopback_modules["output"] = {}
|
||||
|
||||
function GetNodeDirection (id, props)
|
||||
if string.find (props["media.class"], "Stream/Input/Audio") then
|
||||
return "input"
|
||||
elseif string.find (props["media.class"], "Stream/Output/Audio") then
|
||||
return "output"
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function GetNodeAudioGroup (pid)
|
||||
local group = nil
|
||||
local target_object = nil
|
||||
|
||||
-- We group a processes by PW_AUDIO_NAMESPACE.<pid> ancestor
|
||||
local curr_pid = pid
|
||||
while curr_pid ~= 0 do
|
||||
local pid_info = ProcUtils.get_proc_info (curr_pid)
|
||||
local arg0 = pid_info:get_arg (0)
|
||||
|
||||
-- Check if ancestor process name is PW_AUDIO_NAMESPACE
|
||||
if arg0 ~= nil and string.find (arg0, PW_AUDIO_NAMESPACE, 1, true) then
|
||||
-- Check if the PW_AUDIO_NAMESPACE has a defined target
|
||||
for i = 0, pid_info:get_n_args () - 1, 1 do
|
||||
local argn = pid_info:get_arg (i)
|
||||
|
||||
-- Ignore any args after '--'
|
||||
if argn == "--" then
|
||||
break
|
||||
end
|
||||
|
||||
-- Get target node id value if any
|
||||
if (argn == "--target-object") or (argn == "-t") then
|
||||
target_object = pid_info:get_arg (i + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- We name the audio group as PW_AUDIO_NAMESPACE.<pid>
|
||||
group = PW_AUDIO_NAMESPACE .. "." .. tostring(curr_pid)
|
||||
break
|
||||
end
|
||||
|
||||
curr_pid = pid_info:get_parent_pid ()
|
||||
end
|
||||
|
||||
return group, target_object
|
||||
end
|
||||
|
||||
function CreateStreamLoopback (props, group, target_object, direction)
|
||||
local is_input = direction == "input" and true or false
|
||||
|
||||
-- Set stream properties
|
||||
local stream_props = {}
|
||||
stream_props["node.name"] = "stream.audio_group:" .. group
|
||||
stream_props["node.description"] = "Stream Audio Group for " .. group
|
||||
stream_props["media.class"] = is_input and "Stream/Input/Audio" or "Stream/Output/Audio"
|
||||
stream_props["node.passive"] = true
|
||||
stream_props["session.audio-group"] = group
|
||||
if target_object ~= nil then
|
||||
stream_props["target.object"] = tostring (target_object)
|
||||
end
|
||||
|
||||
-- Set device properties
|
||||
local device_props = {}
|
||||
device_props["node.name"] = "device.audio_group:" .. group
|
||||
device_props["node.description"] = "Device Audio Group for " .. group
|
||||
device_props["media.class"] = is_input and "Audio/Source" or "Audio/Sink"
|
||||
device_props["session.audio-group"] = group
|
||||
|
||||
-- Set loopback module args
|
||||
local args = Json.Object {
|
||||
["capture.props"] = Json.Object (is_input and stream_props or device_props),
|
||||
["playback.props"] = Json.Object (is_input and device_props or stream_props)
|
||||
}
|
||||
|
||||
-- Create module
|
||||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "lib/audio-group-utils/create-audio-group-loopback",
|
||||
interests = {
|
||||
-- on linkable added or removed, where linkable is adapter or plain node
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "#", "Stream/*Audio*", type = "pw-global" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "node.link-group", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local source = event:get_source ()
|
||||
local client_om = source:call ("get-object-manager", "client")
|
||||
local id = node.id
|
||||
local bound_id = node["bound-id"]
|
||||
local stream_props = node.properties
|
||||
local stream_name = stream_props["node.name"]
|
||||
|
||||
-- Get client
|
||||
local client = client_om:lookup {
|
||||
Constraint { "bound-id", "=", stream_props["client.id"], type = "gobject"}
|
||||
}
|
||||
if client == nil then
|
||||
Log.info (node,
|
||||
"Cannot get client, not grouping audio stream ".. stream_name)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get process ID
|
||||
local pid = tonumber (client.properties ["application.process.id"])
|
||||
if pid == nil then
|
||||
Log.info (node,
|
||||
"Cannot get process ID, not grouping audio stream ".. stream_name)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get direction and add it to the table
|
||||
local direction = GetNodeDirection (bound_id, stream_props)
|
||||
if direction == nil then
|
||||
Log.info (node,
|
||||
"Cannot get direction, not grouping audio stream ".. stream_name)
|
||||
return
|
||||
end
|
||||
node_directions [id] = direction
|
||||
|
||||
-- Get group and add it to the table
|
||||
local group, target_object = GetNodeAudioGroup (pid)
|
||||
if group == nil then
|
||||
Log.info (node,
|
||||
"Cannot get audio group, not grouping audio stream " .. stream_name)
|
||||
return
|
||||
end
|
||||
agutils.set_audio_group (node, group)
|
||||
|
||||
-- Create group loopback module if it does not exist
|
||||
local m = group_loopback_modules [direction][group]
|
||||
if m == nil then
|
||||
Log.warning ("Creating " .. direction .. " loopback for audio group " .. group ..
|
||||
(target_object and (" with target object " .. tostring (target_object)) or ""))
|
||||
m = CreateStreamLoopback (stream_props, group, target_object, direction)
|
||||
group_loopback_modules [direction][group] = m
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
|
||||
SimpleEventHook {
|
||||
name = "lib/audio-group-utils/destroy-audio-group-loopback",
|
||||
interests = {
|
||||
-- on linkable added or removed, where linkable is adapter or plain node
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "#", "Stream/*Audio*", type = "pw-global" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "node.link-group", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local id = node.id
|
||||
|
||||
-- Get node direction from table and remove it
|
||||
local direction = node_directions [id]
|
||||
if direction == nil then
|
||||
return
|
||||
end
|
||||
node_directions [id] = nil
|
||||
|
||||
-- Get node group from table and remove it
|
||||
local group = agutils.get_audio_group (node)
|
||||
if group == nil then
|
||||
return
|
||||
end
|
||||
agutils.set_audio_group (node, nil)
|
||||
|
||||
-- Destroy group loopback module if there are no more nodes with the same group
|
||||
if not agutils.contains_audio_group (group) then
|
||||
Log.info ("Destroying " .. direction .. " loopback for audio group " .. group)
|
||||
group_loopback_modules [direction][group] = nil
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
Loading…
Add table
Reference in a new issue