Skip to content
Snippets Groups Projects
Commit 73a07d00 authored by George Kiagiadakis's avatar George Kiagiadakis
Browse files

monitor-alsa: sync logic, properties and configuration with media-session

parent b3ff7347
No related branches found
No related tags found
No related merge requests found
......@@ -86,14 +86,6 @@ function enable_audio()
-- Enables device reservation via org.freedesktop.ReserveDevice1 on D-Bus
load_module("reserve-device")
-- ALSA device management via udev
load_monitor("alsa", {
use_acp = true,
use_device_reservation = true,
enable_midi = true,
enable_jack_client = false,
})
end
function enable_endpoints()
......
-- ALSA monitor config file --
local properties = {
-- Create a JACK device. This is not enabled by default because
-- it requires that the PipeWire JACK replacement libraries are
-- not used by the session manager, in order to be able to
-- connect to the real JACK server.
--["alsa.jack-device"] = false,
-- Reserve devices.
--["alsa.reserve"] = true,
--["alsa.reserve.priority"] = -20,
--["alsa.reserve.application-name"] = "WirePlumber",
}
local rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "alsa_card.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- Use ALSA-Card-Profile devices. They use UCM or the profile
-- configuration to configure the device and mixer settings.
["api.alsa.use-acp"] = true,
-- Use UCM instead of profile when available. Can be
-- disabled to skip trying to use the UCM profile.
--["api.alsa.use-ucm"] = true,
-- Don't use the hardware mixer for volume control. It
-- will only use software volume. The mixer is still used
-- to mute unused paths based on the selected port.
--["api.alsa.soft-mixer"] = false,
-- Ignore decibel settings of the driver. Can be used to
-- work around buggy drivers that report wrong values.
--["api.alsa.ignore-dB"] = false,
-- The profile set to use for the device. Usually this is
-- "default.conf" but can be changed with a udev rule or here.
--["device.profile-set"] = "profileset-name",
-- The default active profile. Is by default set to "Off".
--["device.profile"] = "default profile name",
-- Automatically select the best profile. This is the
-- highest priority available profile. This is disabled
-- here and instead implemented in the session manager
-- where it can save and load previous preferences.
["api.acp.auto-profile"] = false,
-- Automatically switch to the highest priority available port.
-- This is disabled here and implemented in the session manager instead.
["api.acp.auto-port"] = false,
-- Other properties can be set here.
--["device.nick"] = "My Device",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "alsa_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "alsa_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
--["resample.quality"] = 4,
--["channelmix.normalize"] = false,
--["channelmix.mix-lfe"] = false,
--["audio.channels"] = 2,
--["audio.format"] = "S16LE",
--["audio.rate"] = 44100,
--["audio.position"] = "FL,FR",
--["api.alsa.period-size"] = 1024,
--["api.alsa.headroom"] = 0,
--["api.alsa.disable-mmap"] = false,
--["api.alsa.disable-batch"] = false,
}
}
}
function enable_alsa()
load_monitor("alsa", {
properties = properties,
rules = rules,
})
end
enable_audio()
enable_alsa()
enable_bluetooth()
......@@ -6,33 +6,94 @@
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local Config = ...
if Config.enable_midi then
midi_bridge = Node("spa-node-factory", {
["factory.name"] = "api.alsa.seq.bridge",
["node.name"] = "MIDI Bridge"
})
local config = ...
-- ensure config.properties is not nil
config.properties = config.properties or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
if Config.enable_jack_client then
jack_device = Device("spa-device-factory", {
["factory.name"] = "api.jack.device"
})
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
if Config.use_device_reservation then
rd_plugin = Plugin("reserve-device")
function findDuplicate(parent, id, property, value)
for i = 0, id - 1, 1 do
local obj = parent:get_managed_object(i)
if obj and obj.properties[property] == value then
return true
end
end
return false
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
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"
-- 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"
local stream = properties["api.alsa.pcm.stream"] or "unknown"
local profile = properties["device.profile.name"] or "unknown"
local profile = properties["device.profile.name"]
or (stream .. "." .. dev .. "." .. subdev)
local profile_desc = properties["device.profile.description"]
-- 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)
if profile:find("^analog%-") then
priority = priority + 9
elseif profile:find("^iec958%-") then
priority = priority + 8
end
properties["priority.driver"] = priority
properties["priority.session"] = priority
end
-- ensure the node has a media class
if not properties["media.class"] then
if stream == "capture" then
......@@ -43,14 +104,34 @@ function createNode(parent, id, type, factory, properties)
end
-- ensure the node has a name
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
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
properties["node.name"] = name .. "." .. counter
else
break
end
end
end
-- and a nick
properties["node.nick"] = properties["node.nick"]
or dev_props["device.nick"]
or dev_props["api.alsa.card_name"]
or dev_props["api.alsa.card.name"]
or dev_props["alsa.card_name"]
properties["node.name"] = properties["node.name"]
or (dev_props["device.name"] or "unknown") .. "." .. stream .. "." .. dev .. "." .. subdev
-- ensure the node has a description
if not properties["node.description"] then
local desc = dev_props["device.description"] or "unknown"
......@@ -67,9 +148,8 @@ function createNode(parent, id, type, factory, properties)
end
end
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the node
local node = Node("adapter", properties)
......@@ -77,11 +157,30 @@ function createNode(parent, id, type, factory, properties)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, type, factory, properties)
-- ensure the device has a name
if not properties["device.name"] then
local s = properties["device.bus-id"] or properties["device.bus-path"] or "unknown"
properties["device.name"] = "alsa_card." .. s
function createDevice(parent, id, factory, properties)
local device = SpaDevice(factory, properties)
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
end
function prepareDevice(parent, id, type, factory, properties)
-- 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
tostring(id))
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
properties["device.name"] = name .. "." .. counter
else
break
end
end
-- ensure the device has a description
......@@ -96,55 +195,58 @@ function createDevice(parent, id, type, factory, properties)
d = "Modem"
end
d = d or properties["device.product.name"] or "Unknown device"
d = d or properties["device.product.name"]
or properties["api.alsa.card.name"]
or properties["alsa.card_name"]
or "Unknown device"
properties["device.description"] = d
end
-- ensure the device has a nick
properties["device.nick"] =
properties["device.nick"] or
properties["api.alsa.card.name"]
-- set the icon name
if not properties["device.icon-name"] then
local icon = nil
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",
}
local f = properties["device.form-factor"]
local c = properties["device.class"]
local b = properties["device.bus"]
if f == "microphone" then
icon = "audio-input-microphone"
elseif f == "webcam" then
icon = "camera-web"
elseif f == "handset" then
icon = "phone"
elseif f == "portable" then
icon = "multimedia-player"
elseif f == "tv" then
icon = "video-display"
elseif f == "headset" then
icon = "audio-headset"
elseif f == "headphone" then
icon = "audio-headphones"
elseif f == "speaker" then
icon = "audio-speakers"
elseif f == "hands-free" then
icon = "audio-handsfree"
elseif c == "modem" then
icon = "modem"
end
icon = icon or "audio-card"
if b then b = ("-" .. b) else b = "" end
properties["device.icon-name"] = icon .. "-analog" .. b
icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
end
-- apply properties from config.rules
rulesApplyProperties(properties)
-- override the device factory to use ACP
if Config.use_acp then
if properties["api.alsa.use-acp"] then
Log.info("Enabling the use of ACP on " .. properties["device.name"])
factory = "api.alsa.acp.device"
end
-- use device reservation, if available
if rd_plugin then
if rd_plugin and properties["api.alsa.card"] then
local rd_name = "Audio" .. properties["api.alsa.card"]
local rd = rd_plugin:call("create-reservation",
rd_name, "WirePlumber", properties["device.name"], -20);
rd_name,
config.properties["alsa.reserve.application-name"] or "WirePlumber",
properties["device.name"],
config.properties["alsa.reserve.priority"] or -20);
properties["api.dbus.ReserveDevice1"] = rd_name
......@@ -155,10 +257,7 @@ function createDevice(parent, id, type, factory, properties)
if state == "acquired" then
-- create the device
local device = SpaDevice(factory, properties)
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
createDevice(parent, id, factory, properties)
elseif state == "available" then
-- attempt to acquire again
......@@ -184,38 +283,37 @@ function createDevice(parent, id, type, factory, properties)
rd:call("acquire")
else
-- create the device
local device = SpaDevice(factory, properties)
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
createDevice(parent, id, factory, properties)
end
end
monitor = SpaDevice("api.alsa.enum.udev")
monitor:connect("create-object", createDevice)
monitor = SpaDevice("api.alsa.enum.udev", config.properties)
monitor:connect("create-object", prepareDevice)
function activateMonitor()
if rd_plugin then
monitor:connect("object-removed", function (parent, id)
local device = parent:get_managed_object(id)
local rd_name = device.properties["api.dbus.ReserveDevice1"]
if rd_name then
rd_plugin:call("destroy-reservation", rd_name)
end
end)
end
-- create the JACK device (for PipeWire to act as client to a JACK server)
if config.properties["alsa.jack-device"] then
jack_device = Device("spa-device-factory", {
["factory.name"] = "api.jack.device",
["node.name"] = "JACK-Device",
})
end
Log.info("Activating ALSA monitor")
monitor:activate(Feature.SpaDevice.ENABLED)
-- reservation is only disabled by explicitly setting it to false
if config.properties["alsa.reserve"] == true or
config.properties["alsa.reserve"] == nil then
rd_plugin = Plugin("reserve-device")
end
-- 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
if rd_plugin and rd_plugin["state"] ~= "connected" then
Log.message("reserve-device plugin is not connected to D-Bus, "
.. "disabling device reservation")
rd_plugin = nil
end
-- destroy device reservations when the corresponding devices are removed
if rd_plugin then
monitor:connect("object-removed", function (parent, id)
local device = parent:get_managed_object(id)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment