|
| 1 | +-- WirePlumber |
| 2 | + |
| 3 | +-- Copyright © 2024 Collabora Ltd. |
| 4 | +-- @author Julian Bouzas <[email protected]> |
| 5 | + |
| 6 | +-- SPDX-License-Identifier: MIT |
| 7 | + |
| 8 | +-- audio-group.lua script takes pipewire audio stream nodes and groups them |
| 9 | +-- into a single unit by creating a loopback filter per group. The grouping |
| 10 | +-- is done by a common ancestor 'audio-group-namespace' process name. |
| 11 | + |
| 12 | +agutils = require ("audio-group-utils") |
| 13 | + |
| 14 | +PW_AUDIO_NAMESPACE = "pw-audio-namespace" |
| 15 | + |
| 16 | +node_directions = {} |
| 17 | +group_loopback_modules = {} |
| 18 | +group_loopback_modules["input"] = {} |
| 19 | +group_loopback_modules["output"] = {} |
| 20 | + |
| 21 | +function GetNodeDirection (id, props) |
| 22 | + if string.find (props["media.class"], "Stream/Input/Audio") then |
| 23 | + return "input" |
| 24 | + elseif string.find (props["media.class"], "Stream/Output/Audio") then |
| 25 | + return "output" |
| 26 | + end |
| 27 | + |
| 28 | + return nil |
| 29 | +end |
| 30 | + |
| 31 | +function GetNodeAudioGroup (pid) |
| 32 | + local group = nil |
| 33 | + local target_object = nil |
| 34 | + |
| 35 | + -- We group a processes by PW_AUDIO_NAMESPACE.<pid> ancestor |
| 36 | + local curr_pid = pid |
| 37 | + while curr_pid ~= 0 do |
| 38 | + local pid_info = ProcUtils.get_proc_info (curr_pid) |
| 39 | + local arg0 = pid_info:get_arg (0) |
| 40 | + |
| 41 | + -- Check if ancestor process name is PW_AUDIO_NAMESPACE |
| 42 | + if arg0 ~= nil and string.find (arg0, PW_AUDIO_NAMESPACE, 1, true) then |
| 43 | + -- Check if the PW_AUDIO_NAMESPACE has a defined target |
| 44 | + for i = 0, pid_info:get_n_args () - 1, 1 do |
| 45 | + local argn = pid_info:get_arg (i) |
| 46 | + |
| 47 | + -- Ignore any args after '--' |
| 48 | + if argn == "--" then |
| 49 | + break |
| 50 | + end |
| 51 | + |
| 52 | + -- Get target node id value if any |
| 53 | + if (argn == "--target-object") or (argn == "-t") then |
| 54 | + target_object = pid_info:get_arg (i + 1) |
| 55 | + break |
| 56 | + end |
| 57 | + end |
| 58 | + |
| 59 | + -- We name the audio group as PW_AUDIO_NAMESPACE.<pid> |
| 60 | + group = PW_AUDIO_NAMESPACE .. "." .. tostring(curr_pid) |
| 61 | + break |
| 62 | + end |
| 63 | + |
| 64 | + curr_pid = pid_info:get_parent_pid () |
| 65 | + end |
| 66 | + |
| 67 | + return group, target_object |
| 68 | +end |
| 69 | + |
| 70 | +function CreateStreamLoopback (props, group, target_object, direction) |
| 71 | + local is_input = direction == "input" and true or false |
| 72 | + |
| 73 | + -- Set stream properties |
| 74 | + local stream_props = {} |
| 75 | + stream_props["node.name"] = "stream.audio_group:" .. group |
| 76 | + stream_props["node.description"] = "Stream Audio Group for " .. group |
| 77 | + stream_props["media.class"] = is_input and "Stream/Input/Audio" or "Stream/Output/Audio" |
| 78 | + stream_props["node.passive"] = true |
| 79 | + stream_props["session.audio-group"] = group |
| 80 | + if target_object ~= nil then |
| 81 | + stream_props["target.object"] = tostring (target_object) |
| 82 | + end |
| 83 | + |
| 84 | + -- Set device properties |
| 85 | + local device_props = {} |
| 86 | + device_props["node.name"] = "device.audio_group:" .. group |
| 87 | + device_props["node.description"] = "Device Audio Group for " .. group |
| 88 | + device_props["media.class"] = is_input and "Audio/Source" or "Audio/Sink" |
| 89 | + device_props["session.audio-group"] = group |
| 90 | + |
| 91 | + -- Set loopback module args |
| 92 | + local args = Json.Object { |
| 93 | + ["capture.props"] = Json.Object (is_input and stream_props or device_props), |
| 94 | + ["playback.props"] = Json.Object (is_input and device_props or stream_props) |
| 95 | + } |
| 96 | + |
| 97 | + -- Create module |
| 98 | + return LocalModule("libpipewire-module-loopback", args:get_data(), {}) |
| 99 | +end |
| 100 | + |
| 101 | +SimpleEventHook { |
| 102 | + name = "lib/audio-group-utils/create-audio-group-loopback", |
| 103 | + interests = { |
| 104 | + -- on linkable added or removed, where linkable is adapter or plain node |
| 105 | + EventInterest { |
| 106 | + Constraint { "event.type", "=", "node-added" }, |
| 107 | + Constraint { "media.class", "#", "Stream/*Audio*", type = "pw-global" }, |
| 108 | + Constraint { "stream.monitor", "!", "true", type = "pw" }, |
| 109 | + Constraint { "node.link-group", "-" }, |
| 110 | + }, |
| 111 | + }, |
| 112 | + execute = function (event) |
| 113 | + local node = event:get_subject () |
| 114 | + local source = event:get_source () |
| 115 | + local client_om = source:call ("get-object-manager", "client") |
| 116 | + local id = node.id |
| 117 | + local bound_id = node["bound-id"] |
| 118 | + local stream_props = node.properties |
| 119 | + local stream_name = stream_props["node.name"] |
| 120 | + |
| 121 | + -- Get client |
| 122 | + local client = client_om:lookup { |
| 123 | + Constraint { "bound-id", "=", stream_props["client.id"], type = "gobject"} |
| 124 | + } |
| 125 | + if client == nil then |
| 126 | + Log.info (node, |
| 127 | + "Cannot get client, not grouping audio stream ".. stream_name) |
| 128 | + return |
| 129 | + end |
| 130 | + |
| 131 | + -- Get process ID |
| 132 | + local pid = tonumber (client.properties ["application.process.id"]) |
| 133 | + if pid == nil then |
| 134 | + Log.info (node, |
| 135 | + "Cannot get process ID, not grouping audio stream ".. stream_name) |
| 136 | + return |
| 137 | + end |
| 138 | + |
| 139 | + -- Get direction and add it to the table |
| 140 | + local direction = GetNodeDirection (bound_id, stream_props) |
| 141 | + if direction == nil then |
| 142 | + Log.info (node, |
| 143 | + "Cannot get direction, not grouping audio stream ".. stream_name) |
| 144 | + return |
| 145 | + end |
| 146 | + node_directions [id] = direction |
| 147 | + |
| 148 | + -- Get group and add it to the table |
| 149 | + local group, target_object = GetNodeAudioGroup (pid) |
| 150 | + if group == nil then |
| 151 | + Log.info (node, |
| 152 | + "Cannot get audio group, not grouping audio stream " .. stream_name) |
| 153 | + return |
| 154 | + end |
| 155 | + agutils.set_audio_group (node, group) |
| 156 | + |
| 157 | + -- Create group loopback module if it does not exist |
| 158 | + local m = group_loopback_modules [direction][group] |
| 159 | + if m == nil then |
| 160 | + Log.warning ("Creating " .. direction .. " loopback for audio group " .. group .. |
| 161 | + (target_object and (" with target object " .. tostring (target_object)) or "")) |
| 162 | + m = CreateStreamLoopback (stream_props, group, target_object, direction) |
| 163 | + group_loopback_modules [direction][group] = m |
| 164 | + end |
| 165 | + end |
| 166 | +}:register () |
| 167 | + |
| 168 | + |
| 169 | +SimpleEventHook { |
| 170 | + name = "lib/audio-group-utils/destroy-audio-group-loopback", |
| 171 | + interests = { |
| 172 | + -- on linkable added or removed, where linkable is adapter or plain node |
| 173 | + EventInterest { |
| 174 | + Constraint { "event.type", "=", "node-removed" }, |
| 175 | + Constraint { "media.class", "#", "Stream/*Audio*", type = "pw-global" }, |
| 176 | + Constraint { "stream.monitor", "!", "true", type = "pw" }, |
| 177 | + Constraint { "node.link-group", "-" }, |
| 178 | + }, |
| 179 | + }, |
| 180 | + execute = function (event) |
| 181 | + local node = event:get_subject () |
| 182 | + local id = node.id |
| 183 | + |
| 184 | + -- Get node direction from table and remove it |
| 185 | + local direction = node_directions [id] |
| 186 | + if direction == nil then |
| 187 | + return |
| 188 | + end |
| 189 | + node_directions [id] = nil |
| 190 | + |
| 191 | + -- Get node group from table and remove it |
| 192 | + local group = agutils.get_audio_group (node) |
| 193 | + if group == nil then |
| 194 | + return |
| 195 | + end |
| 196 | + agutils.set_audio_group (node, nil) |
| 197 | + |
| 198 | + -- Destroy group loopback module if there are no more nodes with the same group |
| 199 | + if not agutils.contains_audio_group (group) then |
| 200 | + Log.info ("Destroying " .. direction .. " loopback for audio group " .. group) |
| 201 | + group_loopback_modules [direction][group] = nil |
| 202 | + end |
| 203 | + end |
| 204 | +}:register () |
0 commit comments