scripts: add bluez-midi monitor and its configuration

Add support for BLE MIDI devices and local endpoints.

Disabled by default for now, as the feature currently faces some
DBus/SELinux policy issues e.g. on Fedora.
This commit is contained in:
Pauli Virtanen 2022-10-30 14:51:41 +02:00
parent 6b32ef5e64
commit 0978c224dc
6 changed files with 227 additions and 0 deletions

View file

@ -68,6 +68,9 @@ context.modules = [
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
# Provides factories to make SPA node objects.
{ name = libpipewire-module-spa-node-factory }
]
wireplumber.components = [

View file

@ -0,0 +1,18 @@
bluez_midi_monitor = {}
bluez_midi_monitor.properties = {}
bluez_midi_monitor.rules = {}
function bluez_midi_monitor.enable()
if bluez_midi_monitor.enabled == false then
return
end
load_monitor("bluez-midi", {
properties = bluez_midi_monitor.properties,
rules = bluez_midi_monitor.rules,
})
if bluez_midi_monitor.properties["with-logind"] then
load_optional_module("logind")
end
end

View file

@ -0,0 +1,41 @@
-- BLE MIDI is currently disabled by default, because it conflicts with
-- the SELinux policy on Fedora 37 and potentially other systems using
-- SELinux. For a workaround, see
-- https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/plugins/bluez5/README-MIDI.md
bluez_midi_monitor.enabled = false
bluez_midi_monitor.properties = {
-- Enable the logind module, which arbitrates which user will be allowed
-- to have bluetooth audio enabled at any given time (particularly useful
-- if you are using GDM as a display manager, as the gdm user also launches
-- pipewire and wireplumber).
-- This requires access to the D-Bus user session; disable if you are running
-- a system-wide instance of wireplumber.
["with-logind"] = true,
-- List of MIDI server node names. Each node name given will create a new instance
-- of a BLE MIDI service. Typical BLE MIDI instruments have on service instance,
-- so adding more than one here may confuse some clients. The node property matching
-- rules below apply also to these servers.
--["servers"] = { "bluez_midi.server" },
}
bluez_midi_monitor.rules = {
-- An array of matches/actions to evaluate.
{
matches = {
{
-- Matches all nodes.
{ "node.name", "matches", "bluez_midi.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
--["monitor.channel-volumes"] = false,
},
},
}

View file

@ -1 +1,2 @@
bluez_monitor.enable()
bluez_midi_monitor.enable()

View file

@ -71,6 +71,9 @@ context.modules = [
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
# Provides factories to make SPA node objects.
{ name = libpipewire-module-spa-node-factory }
]
wireplumber.components = [

View file

@ -0,0 +1,161 @@
-- WirePlumber
--
-- Copyright © 2022 Pauli Virtanen
-- @author Pauli Virtanen
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- unique device/node name tables
node_names_table = nil
id_to_name_table = nil
-- 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
-- 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
function createNode(parent, id, type, factory, properties)
properties["factory.name"] = factory
-- set the node description
local desc = properties["node.description"]
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node name
local name =
"bluez_midi." .. properties["api.bluez5.address"]
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
-- deduplicate nodes with the same name
properties["node.name"] = name
for counter = 2, 99, 1 do
if node_names_table[properties["node.name"]] ~= true then
node_names_table[properties["node.name"]] = true
break
end
properties["node.name"] = name .. "." .. counter
end
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the node
-- it doesn't necessarily need to be a local node,
-- the other Bluetooth parts run in the local process,
-- so it's consistent to have also this here
local node = LocalNode("spa-node-factory", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
id_to_name_table[id] = properties["node.name"]
end
function createMonitor()
local monitor_props = {}
for k, v in pairs(config.properties or {}) do
monitor_props[k] = v
end
monitor_props["server"] = nil
local monitor = SpaDevice("api.bluez5.midi.enum", monitor_props)
if monitor then
monitor:connect("create-object", createNode)
monitor:connect("object-removed", function (parent, id)
node_names_table[id_to_name_table[id]] = nil
id_to_name_table[id] = nil
end)
else
Log.message("PipeWire's BlueZ MIDI SPA missing or broken. Bluetooth not supported.")
return nil
end
-- reset the name tables to make sure names are recycled
node_names_table = {}
id_to_name_table = {}
monitor:activate(Feature.SpaDevice.ENABLED)
return monitor
end
function createServers()
local props = config.properties or {}
if not props["servers"] then
return nil
end
local servers = {}
local i = 1
for k, v in pairs(props["servers"]) do
local node_props = {
["node.name"] = v,
["node.description"] = string.format(I18n.gettext("BLE MIDI %d"), i),
["api.bluez5.role"] = "server",
["factory.name"] = "api.bluez5.midi.node"
}
rulesApplyProperties(node_props)
local node = LocalNode("spa-node-factory", node_props)
if node then
node:activate(Feature.Proxy.BOUND)
table.insert(servers, node)
else
Log.message("Failed to create BLE MIDI server.")
end
i = i + 1
end
return servers
end
logind_plugin = Plugin.find("logind")
if logind_plugin then
-- if logind support is enabled, activate
-- the monitor only when the seat is active
function startStopMonitor(seat_state)
Log.info(logind_plugin, "Seat state changed: " .. seat_state)
if seat_state == "active" then
monitor = createMonitor()
servers = createServers()
elseif monitor then
monitor:deactivate(Feature.SpaDevice.ENABLED)
monitor = nil
servers = nil
end
end
logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
startStopMonitor(logind_plugin:call("get-state"))
else
monitor = createMonitor()
servers = createServers()
end