2021-01-20 09:53:57 +02:00
|
|
|
-- WirePlumber
|
|
|
|
|
--
|
|
|
|
|
-- Copyright © 2021 Collabora Ltd.
|
|
|
|
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
|
|
|
--
|
|
|
|
|
-- SPDX-License-Identifier: MIT
|
|
|
|
|
|
2024-12-08 23:35:20 +02:00
|
|
|
SPLIT_PCM_PARENT_OFFSET = 256
|
|
|
|
|
SPLIT_PCM_OFFSET = 512
|
|
|
|
|
|
2023-09-29 23:13:28 +03:00
|
|
|
cutils = require ("common-utils")
|
2023-05-19 20:12:08 +03:00
|
|
|
log = Log.open_topic ("s-monitors")
|
2022-09-06 07:31:21 +05:30
|
|
|
|
2023-09-29 23:13:28 +03:00
|
|
|
config = {}
|
2023-11-13 13:46:58 +02:00
|
|
|
config.reserve_device = Core.test_feature ("monitor.alsa.reserve-device")
|
2024-03-04 16:27:37 +02:00
|
|
|
config.properties = Conf.get_section_as_properties ("monitor.alsa.properties")
|
2024-03-04 17:03:57 +02:00
|
|
|
config.rules = Conf.get_section_as_json ("monitor.alsa.rules", Json.Array {})
|
2021-02-15 18:49:57 +02:00
|
|
|
|
2022-05-18 10:51:41 -04:00
|
|
|
-- unique device/node name tables
|
|
|
|
|
device_names_table = nil
|
|
|
|
|
node_names_table = nil
|
|
|
|
|
|
2024-12-15 12:47:57 +02:00
|
|
|
-- SPA ids to node names: name = id_name_table[device_id][node_id]
|
|
|
|
|
id_name_table = nil
|
|
|
|
|
|
|
|
|
|
|
2021-02-18 10:23:07 +02:00
|
|
|
function nonempty(str)
|
|
|
|
|
return str ~= "" and str or nil
|
|
|
|
|
end
|
|
|
|
|
|
2022-10-06 11:37:50 -04:00
|
|
|
function applyDefaultDeviceProperties (properties)
|
|
|
|
|
properties["api.alsa.use-acp"] = true
|
2024-11-28 13:56:00 +02:00
|
|
|
properties["api.acp.auto-profile"] = false
|
2022-10-06 11:37:50 -04:00
|
|
|
properties["api.acp.auto-port"] = false
|
2023-12-09 15:52:24 +02:00
|
|
|
properties["api.dbus.ReserveDevice1.Priority"] = -20
|
2024-12-08 23:35:20 +02:00
|
|
|
properties["api.alsa.split-enable"] = true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function createSplitPCMHWNode(dev_props, properties)
|
|
|
|
|
local skip_keys = {
|
|
|
|
|
"api.alsa.split.position", "card.profile.device", "device.profile.description",
|
|
|
|
|
"device.profile.name"
|
|
|
|
|
}
|
|
|
|
|
local props = {}
|
|
|
|
|
|
|
|
|
|
for k, v in pairs(properties) do
|
|
|
|
|
props[k] = v
|
|
|
|
|
end
|
|
|
|
|
for _, k in pairs(skip_keys) do
|
|
|
|
|
props[k] = nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- create the underlying hidden ALSA node
|
|
|
|
|
props["node.name"] = props["api.alsa.split.name"]
|
|
|
|
|
props["node.description"] = string.format("%s %s", dev_props["device.description"],
|
|
|
|
|
props["api.alsa.path"]:gsub("^[^,]*[,:]", ""))
|
|
|
|
|
if props["api.alsa.pcm.stream"] == "capture" then
|
|
|
|
|
props["media.class"] = "Audio/Source/Internal"
|
|
|
|
|
else
|
|
|
|
|
props["media.class"] = "Audio/Sink/Internal"
|
|
|
|
|
end
|
|
|
|
|
props["api.alsa.use-chmap"] = false
|
|
|
|
|
props["api.alsa.split.parent"] = true
|
|
|
|
|
props["audio.position"] = props["api.alsa.split.hw-position"]
|
|
|
|
|
local channels = Json.Raw (props["api.alsa.split.hw-position"]):parse ()
|
|
|
|
|
props["audio.channels"] = tostring(#channels)
|
|
|
|
|
|
|
|
|
|
props = JsonUtils.match_rules_update_properties (config.rules, props)
|
|
|
|
|
|
|
|
|
|
if cutils.parseBool (props ["node.disabled"]) then
|
|
|
|
|
log:notice ("ALSA node " .. props ["node.name"] .. " disabled")
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return Node("adapter", props)
|
2022-10-06 11:37:50 -04:00
|
|
|
end
|
|
|
|
|
|
2024-12-08 23:35:20 +02:00
|
|
|
function createSplitPCMLoopback(parent, id, obj_type, factory, properties)
|
|
|
|
|
local skip_keys = {
|
|
|
|
|
-- not suitable for loopback
|
|
|
|
|
"audio.rate",
|
|
|
|
|
"clock.quantum-limit",
|
|
|
|
|
"factory.name",
|
|
|
|
|
"node.driver",
|
|
|
|
|
"node.pause-on-idle",
|
|
|
|
|
"node.want-driver",
|
|
|
|
|
"port.group",
|
|
|
|
|
"priority.driver",
|
|
|
|
|
"resample.disable",
|
|
|
|
|
"resample.prefill",
|
|
|
|
|
}
|
|
|
|
|
local args
|
|
|
|
|
local props = {}
|
|
|
|
|
|
|
|
|
|
props["node.virtual"] = false
|
|
|
|
|
|
|
|
|
|
for k, v in pairs(properties) do
|
|
|
|
|
props[k] = v
|
|
|
|
|
end
|
|
|
|
|
for _, k in pairs(skip_keys) do
|
|
|
|
|
props[k] = nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local split_props = {
|
|
|
|
|
["node.name"] = properties["node.name"] .. ".split",
|
|
|
|
|
["node.description"] = string.format(I18n.gettext("Split %s"), properties["node.description"]),
|
|
|
|
|
["audio.position"] = properties["api.alsa.split.position"],
|
|
|
|
|
["stream.dont-remix"] = true,
|
|
|
|
|
["node.passive"] = true,
|
|
|
|
|
["node.dont-fallback"] = true,
|
|
|
|
|
["node.linger"] = true,
|
2025-01-26 21:18:18 +02:00
|
|
|
["state.restore-props"] = false,
|
2024-12-08 23:35:20 +02:00
|
|
|
["target.object"] = properties["api.alsa.split.name"],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if properties["api.alsa.pcm.stream"] == "playback" then
|
|
|
|
|
props["media.class"] = "Audio/Sink"
|
|
|
|
|
split_props["media.class"] = "Stream/Output/Audio/Internal"
|
|
|
|
|
args = Json.Object {
|
|
|
|
|
["capture.props"] = Json.Object (props),
|
|
|
|
|
["playback.props"] = Json.Object (split_props),
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
props["media.class"] = "Audio/Source"
|
|
|
|
|
split_props["media.class"] = "Stream/Input/Audio/Internal"
|
|
|
|
|
args = Json.Object {
|
|
|
|
|
["playback.props"] = Json.Object (props),
|
|
|
|
|
["capture.props"] = Json.Object (split_props),
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
devices_om = ObjectManager {
|
|
|
|
|
Interest {
|
|
|
|
|
type = "device",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
split_nodes_om = ObjectManager {
|
|
|
|
|
Interest {
|
|
|
|
|
type = "node",
|
|
|
|
|
Constraint { "api.alsa.split.position", "+", type = "pw" },
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
split_nodes_om:connect ("object-added", function(_, node)
|
|
|
|
|
-- Connect ObjectConfig events to the right node
|
|
|
|
|
if not monitor then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local interest = Interest {
|
|
|
|
|
type = "device",
|
|
|
|
|
Constraint { "object.id", "=", node.properties["device.id"] }
|
|
|
|
|
}
|
|
|
|
|
log:info("Split PCM node found: " .. tostring (node["bound-id"]))
|
|
|
|
|
|
|
|
|
|
for device in devices_om:iterate (interest) do
|
|
|
|
|
local device_id = device.properties["spa.object.id"]
|
|
|
|
|
if not device_id then
|
|
|
|
|
goto next_device
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local spa_device = monitor:get_managed_object (tonumber (device_id))
|
|
|
|
|
if not spa_device then
|
|
|
|
|
goto next_device
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local id = node.properties["card.profile.device"]
|
|
|
|
|
if id ~= nil then
|
|
|
|
|
log:info(".. assign to device: " .. tostring (device["bound-id"]) .. " node " .. tostring (id))
|
|
|
|
|
spa_device:store_managed_object (id, node)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
::next_device::
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2022-07-07 20:58:36 +03:00
|
|
|
function createNode(parent, id, obj_type, factory, properties)
|
2021-01-20 09:53:57 +02:00
|
|
|
local dev_props = parent.properties
|
2024-12-15 12:47:57 +02:00
|
|
|
local parent_id = tonumber(dev_props["spa.object.id"])
|
2021-02-15 18:49:57 +02:00
|
|
|
|
|
|
|
|
-- set the device id and spa factory name; REQUIRED, do not change
|
|
|
|
|
properties["device.id"] = parent["bound-id"]
|
|
|
|
|
properties["factory.name"] = factory
|
|
|
|
|
|
|
|
|
|
-- set the default pause-on-idle setting
|
|
|
|
|
properties["node.pause-on-idle"] = false
|
|
|
|
|
|
|
|
|
|
-- try to negotiate the max ammount of channels
|
|
|
|
|
if dev_props["api.alsa.use-acp"] ~= "true" then
|
|
|
|
|
properties["audio.channels"] = properties["audio.channels"] or "64"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local dev = properties["api.alsa.pcm.device"]
|
|
|
|
|
or properties["alsa.device"] or "0"
|
|
|
|
|
local subdev = properties["api.alsa.pcm.subdevice"]
|
|
|
|
|
or properties["alsa.subdevice"] or "0"
|
2021-01-20 09:53:57 +02:00
|
|
|
local stream = properties["api.alsa.pcm.stream"] or "unknown"
|
2021-02-15 18:49:57 +02:00
|
|
|
local profile = properties["device.profile.name"]
|
|
|
|
|
or (stream .. "." .. dev .. "." .. subdev)
|
2021-01-20 09:53:57 +02:00
|
|
|
local profile_desc = properties["device.profile.description"]
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- set priority
|
|
|
|
|
if not properties["priority.driver"] then
|
|
|
|
|
local priority = (dev == "0") and 1000 or 744
|
|
|
|
|
if stream == "capture" then
|
|
|
|
|
priority = priority + 1000
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
priority = priority - (tonumber(dev) * 16) - tonumber(subdev)
|
|
|
|
|
|
2023-02-14 17:43:25 +01:00
|
|
|
if profile:find("^pro%-") then
|
|
|
|
|
priority = priority + 500
|
|
|
|
|
elseif profile:find("^analog%-") then
|
2021-02-15 18:49:57 +02:00
|
|
|
priority = priority + 9
|
|
|
|
|
elseif profile:find("^iec958%-") then
|
|
|
|
|
priority = priority + 8
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
properties["priority.driver"] = priority
|
|
|
|
|
properties["priority.session"] = priority
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- ensure the node has a media class
|
|
|
|
|
if not properties["media.class"] then
|
|
|
|
|
if stream == "capture" then
|
|
|
|
|
properties["media.class"] = "Audio/Source"
|
|
|
|
|
else
|
|
|
|
|
properties["media.class"] = "Audio/Sink"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- ensure the node has a name
|
2021-02-15 18:49:57 +02:00
|
|
|
if not properties["node.name"] then
|
|
|
|
|
local name =
|
|
|
|
|
(stream == "capture" and "alsa_input" or "alsa_output")
|
|
|
|
|
.. "." ..
|
|
|
|
|
(dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
|
|
|
|
|
dev_props["device.name"] or
|
|
|
|
|
"unnamed-device")
|
|
|
|
|
.. "." ..
|
|
|
|
|
profile
|
|
|
|
|
|
2021-02-15 19:18:07 +02:00
|
|
|
-- sanitize name
|
|
|
|
|
name = name:gsub("([^%w_%-%.])", "_")
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
properties["node.name"] = name
|
|
|
|
|
|
2024-05-31 17:03:11 +03:00
|
|
|
log:info ("Creating node " .. name)
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- deduplicate nodes with the same name
|
|
|
|
|
for counter = 2, 99, 1 do
|
2022-05-18 10:51:41 -04:00
|
|
|
if node_names_table[properties["node.name"]] ~= true then
|
2021-02-15 18:49:57 +02:00
|
|
|
break
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
properties["node.name"] = name .. "." .. counter
|
2024-05-31 17:03:11 +03:00
|
|
|
log:info ("deduplicating node name -> " .. properties["node.name"])
|
2021-02-15 18:49:57 +02:00
|
|
|
end
|
2024-11-23 16:35:50 +02:00
|
|
|
else
|
|
|
|
|
log:info ("Creating node " .. properties["node.name"])
|
2021-02-15 18:49:57 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- and a nick
|
2022-03-01 12:11:25 +02:00
|
|
|
local nick = nonempty(properties["node.nick"])
|
|
|
|
|
or nonempty(properties["api.alsa.pcm.name"])
|
|
|
|
|
or nonempty(properties["alsa.name"])
|
|
|
|
|
or nonempty(profile_desc)
|
2021-01-20 09:53:57 +02:00
|
|
|
or dev_props["device.nick"]
|
2022-03-08 11:56:34 +01:00
|
|
|
if nick == "USB Audio" then
|
|
|
|
|
nick = dev_props["device.nick"]
|
|
|
|
|
end
|
2021-02-18 09:02:41 +02:00
|
|
|
-- also sanitize nick, replace ':' with ' '
|
|
|
|
|
properties["node.nick"] = nick:gsub("(:)", " ")
|
2021-01-20 09:53:57 +02:00
|
|
|
|
|
|
|
|
-- ensure the node has a description
|
|
|
|
|
if not properties["node.description"] then
|
2021-02-18 10:23:07 +02:00
|
|
|
local desc = nonempty(dev_props["device.description"]) or "unknown"
|
|
|
|
|
local name = nonempty(properties["api.alsa.pcm.name"]) or
|
|
|
|
|
nonempty(properties["api.alsa.pcm.id"]) or dev
|
2021-01-20 09:53:57 +02:00
|
|
|
|
|
|
|
|
if profile_desc then
|
2021-02-18 09:02:41 +02:00
|
|
|
desc = desc .. " " .. profile_desc
|
2021-02-18 10:23:07 +02:00
|
|
|
elseif subdev ~= "0" then
|
2021-02-18 09:02:41 +02:00
|
|
|
desc = desc .. " (" .. name .. " " .. subdev .. ")"
|
2021-02-18 10:23:07 +02:00
|
|
|
elseif dev ~= "0" then
|
2021-02-18 09:02:41 +02:00
|
|
|
desc = desc .. " (" .. name .. ")"
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
2021-02-18 09:02:41 +02:00
|
|
|
|
|
|
|
|
-- also sanitize description, replace ':' with ' '
|
|
|
|
|
properties["node.description"] = desc:gsub("(:)", " ")
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-03-26 17:29:25 +02:00
|
|
|
-- add api.alsa.card.* properties for rule matching purposes
|
|
|
|
|
for k, v in pairs(dev_props) do
|
|
|
|
|
if k:find("^api%.alsa%.card%..*") then
|
|
|
|
|
properties[k] = v
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-03-18 17:03:44 +02:00
|
|
|
-- add cpu.vm.name for rule matching purposes
|
monitors/alsa: remove vm.node.defaults and use match rules instead
The vm.node.defaults logic which was inherited from p-m-s is not really
good because it seems like different VM hardware requires different
values for the defaults. Also, passthrough USB hardware should not
inerhit these values, they just cause trouble.
Instead, we can use rules to match the vm.type and specific device
properties to set a more informed period & headroom.
For now, I am also decreasing the default headroom down to 2048, which
works for me and perhaps it's a good default. We can always add more
rules here and fine-tune per vm type and virtual hardware.
See !394, #316, #348, #507, #162, pipewire#3452
2023-11-14 21:32:18 +02:00
|
|
|
local vm_type = Core.get_vm_type()
|
|
|
|
|
if nonempty(vm_type) then
|
2024-03-18 17:03:44 +02:00
|
|
|
properties["cpu.vm.name"] = vm_type
|
2022-06-28 14:31:17 +03:00
|
|
|
end
|
|
|
|
|
|
2022-05-02 12:49:50 +05:30
|
|
|
-- apply properties from rules defined in JSON .conf file
|
2024-12-08 23:35:20 +02:00
|
|
|
local orig_properties = {}
|
|
|
|
|
for k, v in pairs(properties) do
|
|
|
|
|
orig_properties[k] = v
|
|
|
|
|
end
|
2024-03-04 17:03:57 +02:00
|
|
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
|
|
|
|
|
2024-03-08 12:45:26 +05:30
|
|
|
if cutils.parseBool (properties ["node.disabled"]) then
|
|
|
|
|
log:notice ("ALSA node " .. properties["node.name"] .. " disabled")
|
2022-01-12 12:13:08 +01:00
|
|
|
return
|
|
|
|
|
end
|
2021-01-20 09:53:57 +02:00
|
|
|
|
2024-12-15 12:47:57 +02:00
|
|
|
node_names_table[properties["node.name"]] = true
|
|
|
|
|
id_name_table[parent_id][id] = properties["node.name"]
|
|
|
|
|
|
2024-12-08 23:35:20 +02:00
|
|
|
-- handle split HW node
|
|
|
|
|
if properties["api.alsa.split.position"] ~= nil then
|
|
|
|
|
local split_hw_node_name = string.format("%s.%s",
|
|
|
|
|
(stream == "capture" and "alsa_input" or "alsa_output"),
|
|
|
|
|
properties["api.alsa.path"]:gsub("([:,])", "_"))
|
|
|
|
|
properties["api.alsa.split.name"] = split_hw_node_name
|
|
|
|
|
orig_properties["api.alsa.split.name"] = split_hw_node_name
|
|
|
|
|
|
|
|
|
|
if not node_names_table [split_hw_node_name] then
|
|
|
|
|
log:info ("Create ALSA SplitPCM HW node " .. split_hw_node_name)
|
|
|
|
|
|
|
|
|
|
local node = createSplitPCMHWNode(dev_props, orig_properties)
|
|
|
|
|
if node ~= nil then
|
|
|
|
|
node:activate(Feature.Proxy.BOUND)
|
|
|
|
|
parent:store_managed_object(SPLIT_PCM_PARENT_OFFSET + id, node)
|
|
|
|
|
|
|
|
|
|
node_names_table[split_hw_node_name] = true
|
|
|
|
|
id_name_table[parent_id][SPLIT_PCM_PARENT_OFFSET + id] = split_hw_node_name
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- create split PCM node
|
|
|
|
|
log:info ("Create ALSA SplitPCM split node " .. properties["node.name"])
|
|
|
|
|
|
|
|
|
|
local loopback = createSplitPCMLoopback (parent, id, obj_type, factory, properties)
|
|
|
|
|
parent:store_managed_object(SPLIT_PCM_OFFSET + id, loopback)
|
|
|
|
|
parent:set_managed_pending(id)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- create the node
|
|
|
|
|
local node = Node("adapter", properties)
|
2024-11-23 16:35:50 +02:00
|
|
|
node:activate(Feature.Proxy.BOUND, function (_, err)
|
|
|
|
|
if err then
|
|
|
|
|
log:warning ("Failed to create " .. properties ["node.name"]
|
|
|
|
|
.. ": " .. tostring(err))
|
|
|
|
|
end
|
|
|
|
|
end)
|
2021-01-20 09:53:57 +02:00
|
|
|
parent:store_managed_object(id, node)
|
|
|
|
|
end
|
|
|
|
|
|
2024-12-15 12:47:57 +02:00
|
|
|
function removeNode(parent, id)
|
|
|
|
|
local parent_id = tonumber(parent.properties["spa.object.id"])
|
2024-12-08 23:35:20 +02:00
|
|
|
local ids = {id, SPLIT_PCM_PARENT_OFFSET + id, SPLIT_PCM_OFFSET + id}
|
|
|
|
|
|
|
|
|
|
for _, j in pairs(ids) do
|
|
|
|
|
local node_name = id_name_table[parent_id][j]
|
|
|
|
|
|
|
|
|
|
parent:store_managed_object(j, nil)
|
2024-12-15 12:47:57 +02:00
|
|
|
|
2024-12-08 23:35:20 +02:00
|
|
|
if node_name ~= nil then
|
|
|
|
|
log:info ("Removing node " .. node_name)
|
|
|
|
|
node_names_table[node_name] = nil
|
|
|
|
|
id_name_table[parent_id][j] = nil
|
|
|
|
|
end
|
2024-12-15 12:47:57 +02:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
function createDevice(parent, id, factory, properties)
|
2024-12-15 12:47:57 +02:00
|
|
|
id_name_table[id] = {}
|
|
|
|
|
properties["spa.object.id"] = id
|
2021-02-15 18:49:57 +02:00
|
|
|
local device = SpaDevice(factory, properties)
|
2022-01-11 11:27:33 -05:00
|
|
|
if device then
|
|
|
|
|
device:connect("create-object", createNode)
|
2024-12-15 12:47:57 +02:00
|
|
|
device:connect("object-removed", removeNode)
|
2022-01-11 11:27:33 -05:00
|
|
|
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
|
|
|
|
parent:store_managed_object(id, device)
|
|
|
|
|
else
|
2023-05-19 20:12:08 +03:00
|
|
|
log:warning ("Failed to create '" .. factory .. "' device")
|
2022-01-11 11:27:33 -05:00
|
|
|
end
|
2021-02-15 18:49:57 +02:00
|
|
|
end
|
|
|
|
|
|
2024-12-15 12:47:57 +02:00
|
|
|
function removeDevice(parent, id)
|
|
|
|
|
if id_name_table[id] ~= nil then
|
|
|
|
|
for _, node_name in pairs(id_name_table[id]) do
|
|
|
|
|
log:info ("Release " .. node_name)
|
|
|
|
|
node_names_table[node_name] = nil
|
|
|
|
|
end
|
|
|
|
|
id_name_table[id] = nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2022-07-07 20:58:36 +03:00
|
|
|
function prepareDevice(parent, id, obj_type, factory, properties)
|
2021-02-15 18:49:57 +02:00
|
|
|
-- ensure the device has an appropriate name
|
|
|
|
|
local name = "alsa_card." ..
|
|
|
|
|
(properties["device.name"] or
|
|
|
|
|
properties["device.bus-id"] or
|
|
|
|
|
properties["device.bus-path"] or
|
2021-06-03 18:58:07 +03:00
|
|
|
tostring(id)):gsub("([^%w_%-%.])", "_")
|
2021-02-15 18:49:57 +02:00
|
|
|
|
|
|
|
|
properties["device.name"] = name
|
|
|
|
|
|
|
|
|
|
-- deduplicate devices with the same name
|
|
|
|
|
for counter = 2, 99, 1 do
|
2022-05-18 10:51:41 -04:00
|
|
|
if device_names_table[properties["device.name"]] ~= true then
|
|
|
|
|
device_names_table[properties["device.name"]] = true
|
2021-02-15 18:49:57 +02:00
|
|
|
break
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
properties["device.name"] = name .. "." .. counter
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- ensure the device has a description
|
|
|
|
|
if not properties["device.description"] then
|
|
|
|
|
local d = nil
|
|
|
|
|
local f = properties["device.form-factor"]
|
|
|
|
|
local c = properties["device.class"]
|
2022-12-13 15:19:06 +01:00
|
|
|
local n = properties["api.alsa.card.name"]
|
2021-01-20 09:53:57 +02:00
|
|
|
|
2022-12-13 15:19:06 +01:00
|
|
|
if n == "Loopback" then
|
|
|
|
|
d = I18n.gettext("Loopback")
|
|
|
|
|
elseif f == "internal" then
|
2022-04-09 14:27:42 +03:00
|
|
|
d = I18n.gettext("Built-in Audio")
|
2021-01-20 09:53:57 +02:00
|
|
|
elseif c == "modem" then
|
2022-04-09 14:27:42 +03:00
|
|
|
d = I18n.gettext("Modem")
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
d = d or properties["device.product.name"]
|
|
|
|
|
or properties["api.alsa.card.name"]
|
|
|
|
|
or properties["alsa.card_name"]
|
|
|
|
|
or "Unknown device"
|
2021-01-20 09:53:57 +02:00
|
|
|
properties["device.description"] = d
|
|
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- ensure the device has a nick
|
|
|
|
|
properties["device.nick"] =
|
|
|
|
|
properties["device.nick"] or
|
2022-03-01 11:35:55 +02:00
|
|
|
properties["api.alsa.card.name"] or
|
|
|
|
|
properties["alsa.card_name"]
|
2021-02-15 18:49:57 +02:00
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- set the icon name
|
|
|
|
|
if not properties["device.icon-name"] then
|
|
|
|
|
local icon = nil
|
2021-02-15 18:49:57 +02:00
|
|
|
local icon_map = {
|
|
|
|
|
-- form factor -> icon
|
|
|
|
|
["microphone"] = "audio-input-microphone",
|
|
|
|
|
["webcam"] = "camera-web",
|
|
|
|
|
["handset"] = "phone",
|
|
|
|
|
["portable"] = "multimedia-player",
|
|
|
|
|
["tv"] = "video-display",
|
|
|
|
|
["headset"] = "audio-headset",
|
|
|
|
|
["headphone"] = "audio-headphones",
|
|
|
|
|
["speaker"] = "audio-speakers",
|
|
|
|
|
["hands-free"] = "audio-handsfree",
|
|
|
|
|
}
|
2021-01-20 09:53:57 +02:00
|
|
|
local f = properties["device.form-factor"]
|
|
|
|
|
local c = properties["device.class"]
|
|
|
|
|
local b = properties["device.bus"]
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
|
|
|
|
|
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2022-05-02 12:49:50 +05:30
|
|
|
-- apply properties from rules defined in JSON .conf file
|
2023-11-07 12:44:13 +02:00
|
|
|
applyDefaultDeviceProperties (properties)
|
2024-03-04 17:03:57 +02:00
|
|
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
2023-11-07 12:44:13 +02:00
|
|
|
|
2024-03-08 12:45:26 +05:30
|
|
|
if cutils.parseBool (properties ["device.disabled"]) then
|
|
|
|
|
log:notice ("ALSA card/device " .. properties ["device.name"] .. " disabled")
|
2022-11-08 04:20:21 +05:30
|
|
|
device_names_table [properties ["device.name"]] = nil
|
2022-01-12 12:13:08 +01:00
|
|
|
return
|
|
|
|
|
end
|
2021-02-15 18:49:57 +02:00
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- override the device factory to use ACP
|
2024-03-08 12:45:26 +05:30
|
|
|
if cutils.parseBool (properties ["api.alsa.use-acp"]) then
|
2023-05-19 20:12:08 +03:00
|
|
|
log:info("Enabling the use of ACP on " .. properties["device.name"])
|
2021-01-20 09:53:57 +02:00
|
|
|
factory = "api.alsa.acp.device"
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-26 17:24:52 +02:00
|
|
|
-- use device reservation, if available
|
2021-02-15 18:49:57 +02:00
|
|
|
if rd_plugin and properties["api.alsa.card"] then
|
2021-01-26 17:24:52 +02:00
|
|
|
local rd_name = "Audio" .. properties["api.alsa.card"]
|
|
|
|
|
local rd = rd_plugin:call("create-reservation",
|
2021-02-15 18:49:57 +02:00
|
|
|
rd_name,
|
2023-12-09 15:52:24 +02:00
|
|
|
cutils.get_application_name (),
|
2021-02-15 18:49:57 +02:00
|
|
|
properties["device.name"],
|
2023-12-09 15:52:24 +02:00
|
|
|
properties["api.dbus.ReserveDevice1.Priority"]);
|
2021-01-26 17:24:52 +02:00
|
|
|
|
|
|
|
|
properties["api.dbus.ReserveDevice1"] = rd_name
|
|
|
|
|
|
|
|
|
|
-- unlike pipewire-media-session, this logic here keeps the device
|
|
|
|
|
-- acquired at all times and destroys it if someone else acquires
|
|
|
|
|
rd:connect("notify::state", function (rd, pspec)
|
|
|
|
|
local state = rd["state"]
|
|
|
|
|
|
|
|
|
|
if state == "acquired" then
|
|
|
|
|
-- create the device
|
2021-02-15 18:49:57 +02:00
|
|
|
createDevice(parent, id, factory, properties)
|
2021-01-26 17:24:52 +02:00
|
|
|
|
|
|
|
|
elseif state == "available" then
|
|
|
|
|
-- attempt to acquire again
|
|
|
|
|
rd:call("acquire")
|
|
|
|
|
|
|
|
|
|
elseif state == "busy" then
|
|
|
|
|
-- destroy the device
|
2024-12-15 12:47:57 +02:00
|
|
|
removeDevice(parent, id)
|
2021-01-26 17:24:52 +02:00
|
|
|
parent:store_managed_object(id, nil)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2021-11-23 13:17:29 +01:00
|
|
|
rd:connect("release-requested", function (rd)
|
2023-05-19 20:12:08 +03:00
|
|
|
log:info("release requested")
|
2021-11-23 13:17:29 +01:00
|
|
|
parent:store_managed_object(id, nil)
|
|
|
|
|
rd:call("release")
|
|
|
|
|
end)
|
|
|
|
|
|
2021-01-26 17:24:52 +02:00
|
|
|
rd:call("acquire")
|
|
|
|
|
else
|
|
|
|
|
-- create the device
|
2021-02-15 18:49:57 +02:00
|
|
|
createDevice(parent, id, factory, properties)
|
2021-01-26 17:24:52 +02:00
|
|
|
end
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
function createMonitor ()
|
2022-09-08 12:37:13 -04:00
|
|
|
local m = SpaDevice("api.alsa.enum.udev", config.properties)
|
2021-09-23 14:58:46 -04:00
|
|
|
if m == nil then
|
2024-02-10 11:20:07 +02:00
|
|
|
log:notice("PipeWire's ALSA SPA plugin is missing or broken. " ..
|
|
|
|
|
"Sound cards will not be supported")
|
2021-09-23 14:58:46 -04:00
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- handle create-object to prepare device
|
|
|
|
|
m:connect("create-object", prepareDevice)
|
|
|
|
|
|
2022-05-18 10:51:41 -04:00
|
|
|
-- handle object-removed to destroy device reservations and recycle device name
|
|
|
|
|
m:connect("object-removed", function (parent, id)
|
2024-12-15 12:47:57 +02:00
|
|
|
removeDevice(parent, id)
|
|
|
|
|
|
2022-05-18 10:51:41 -04:00
|
|
|
local device = parent:get_managed_object(id)
|
2022-11-08 04:20:21 +05:30
|
|
|
if not device then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
2022-05-18 10:51:41 -04:00
|
|
|
if rd_plugin then
|
2021-09-23 14:58:46 -04:00
|
|
|
local rd_name = device.properties["api.dbus.ReserveDevice1"]
|
|
|
|
|
if rd_name then
|
|
|
|
|
rd_plugin:call("destroy-reservation", rd_name)
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
end
|
|
|
|
|
device_names_table[device.properties["device.name"]] = nil
|
2022-06-16 15:45:29 -04:00
|
|
|
for managed_node in device:iterate_managed_objects() do
|
|
|
|
|
node_names_table[managed_node.properties["node.name"]] = nil
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
-- reset the name tables to make sure names are recycled
|
|
|
|
|
device_names_table = {}
|
|
|
|
|
node_names_table = {}
|
2024-12-15 12:47:57 +02:00
|
|
|
id_name_table = {}
|
2021-09-16 16:31:59 +05:30
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
-- activate monitor
|
2023-05-19 20:12:08 +03:00
|
|
|
log:info("Activating ALSA monitor")
|
2021-09-23 14:58:46 -04:00
|
|
|
m:activate(Feature.SpaDevice.ENABLED)
|
|
|
|
|
return m
|
|
|
|
|
end
|
2021-01-26 17:24:52 +02:00
|
|
|
|
2021-02-03 13:00:40 +02:00
|
|
|
-- if the reserve-device plugin is enabled, at the point of script execution
|
|
|
|
|
-- it is expected to be connected. if it is not, assume the d-bus connection
|
|
|
|
|
-- has failed and continue without it
|
2023-11-13 13:46:58 +02:00
|
|
|
if config.reserve_device then
|
|
|
|
|
rd_plugin = Plugin.find("reserve-device")
|
|
|
|
|
end
|
2022-07-12 15:33:23 +02:00
|
|
|
if rd_plugin and rd_plugin:call("get-dbus")["state"] ~= "connected" then
|
2023-05-19 20:12:08 +03:00
|
|
|
log:notice("reserve-device plugin is not connected to D-Bus, "
|
2021-02-15 18:49:57 +02:00
|
|
|
.. "disabling device reservation")
|
2021-02-03 13:00:40 +02:00
|
|
|
rd_plugin = nil
|
|
|
|
|
end
|
2021-01-26 17:24:52 +02:00
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
-- handle rd_plugin state changes to destroy and re-create the ALSA monitor in
|
|
|
|
|
-- case D-Bus service is restarted
|
2021-02-03 13:00:40 +02:00
|
|
|
if rd_plugin then
|
2022-05-06 09:57:37 -04:00
|
|
|
local dbus = rd_plugin:call("get-dbus")
|
|
|
|
|
dbus:connect("notify::state", function (b, pspec)
|
|
|
|
|
local state = b["state"]
|
2023-05-19 20:12:08 +03:00
|
|
|
log:info ("rd-plugin state changed to " .. state)
|
2021-09-23 14:58:46 -04:00
|
|
|
if state == "connected" then
|
2023-05-19 20:12:08 +03:00
|
|
|
log:info ("Creating ALSA monitor")
|
2021-09-23 14:58:46 -04:00
|
|
|
monitor = createMonitor()
|
|
|
|
|
elseif state == "closed" then
|
2023-05-19 20:12:08 +03:00
|
|
|
log:info ("Destroying ALSA monitor")
|
2021-09-23 14:58:46 -04:00
|
|
|
monitor = nil
|
2021-02-03 13:00:40 +02:00
|
|
|
end
|
|
|
|
|
end)
|
2021-01-26 17:24:52 +02:00
|
|
|
end
|
2021-02-03 13:00:40 +02:00
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
-- create the monitor
|
|
|
|
|
monitor = createMonitor()
|
2024-12-08 23:35:20 +02:00
|
|
|
|
|
|
|
|
devices_om:activate()
|
|
|
|
|
split_nodes_om:activate()
|