Merge branch 'master' into next

This commit is contained in:
George Kiagiadakis
2023-10-24 11:09:52 +03:00
12 changed files with 295 additions and 27 deletions

View File

@@ -1,6 +1,62 @@
WirePlumber 0.4.14
WirePlumber 0.4.15
~~~~~~~~~~~~~~~~~~
Additions:
- A new "DSP policy" module has been added; its purpose is to automatically
load a filter-chain when a certain hardware device is present, so that
audio always goes through this software DSP before reaching the device.
This is mainly to support Apple M1/M2 devices, which require a software
DSP to be always present
- WpImplModule now supports loading module arguments directly from a SPA-JSON
config file; this is mainly to support DSP configuration for Apple M1/M2
and will likely be reworked for 0.5
- Added support for automatically combining Bluetooth LE Audio device sets
(e.g. pairs of earbuds) (!500)
- Added command line options in ``wpctl`` to display device/node names and
nicknames instead of descriptions
- Added zsh completions file for ``wpctl``
- The device profile selection policy now respects the ``device.profile``
property if it is set on the device; this is useful to hand-pick a profile
based on static configuration rules (alsa_monitor.rules)
Changes/Fixes:
- Linking policy now sends an error to the client before destroying the node,
if it determines that the node cannot be linked to any target; this fixes
error reporting on the client side
- Fixed a crash in suspend-node that could happen when destroying virtual
sinks that were loaded from another process such as pw-loopback (#467)
- Virtual machine default period size has been bumped to 1024 (#507)
- Updated bluez5 default configuration, using ``bluez5.roles`` instead of
``bluez5.headset-roles`` now (!498)
- Disabled Bluetooth autoconnect by default (!514)
- Removed ``RestrictNamespaces`` option from the systemd services in order to
allow libcamera to load sandboxed IPA modules (#466)
- Fixed a JSON encoding bug with empty strings (#471)
- Lua code can now parse strings without quotes from SPA-JSON
- Added some missing `\since` annotations and made them show up in the
generated gobject-introspection file, to help bindings generators
Past releases
~~~~~~~~~~~~~
WirePlumber 0.4.14
..................
Additions:
- Added support for managing Bluetooth-MIDI, complimenting the parts that
@@ -24,9 +80,6 @@ Additions:
- Added support for disabling libcamera nodes & devices with ``node.disabled``
and ``device.disabled``, like it works for ALSA and V4L2 (#418)
Past releases
~~~~~~~~~~~~~
WirePlumber 0.4.13
..................
@@ -76,9 +129,6 @@ Packaging:
- Added pkg-config and header information in the gir file
Past releases
~~~~~~~~~~~~~
WirePlumber 0.4.12
..................

View File

@@ -439,10 +439,24 @@ on the ALSA device.
This can be done in 3 different ways:
1. Use pavucontrol and toggle the codecs in the output advanced section
1. Use pavucontrol and toggle the codecs in the output advanced section.
2. Modify the ``["iec958.codecs"] = "[ PCM DTS AC3 MPEG MPEG2-AAC EAC3 TrueHD DTS-HD ]"``
node property to something.
2. Modify the ``["iec958.codecs"]`` node property to contain suported codecs.
Example ``~/.config/wireplumber/main.lua.d/51-alsa-spdif.lua``:
.. code-block:: lua
table.insert (alsa_monitor.rules, {
matches = {
{
{ "node.name", "matches", "alsa_output.*" },
},
},
apply_properties = {
["iec958.codecs"] = "[ PCM DTS AC3 EAC3 TrueHD DTS-HD ]",
}
})
3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify
the codecs at runtime.

View File

@@ -11,7 +11,7 @@ dropped, the module is unloaded.
Constructors
~~~~~~~~~~~~
.. function:: LocalModule(name, arguments, properties)
.. function:: LocalModule(name, arguments, properties, [load_args_from_file])
Loads the named module with the provided arguments and properties (either of
which can be ``nil``).
@@ -21,6 +21,7 @@ Constructors
module arguments
:param table properties: can be ``nil`` or a table that can be
:ref:`converted <lua_gobject_lua_to_c>` to :c:struct:`WpProperties`
:param load_args_from_file: (since 0.4.15) optional. if true, arguments param is treated as a file path to load args from
:returns: a new LocalModule
:rtype: LocalModule (:c:struct:`WpImplModule`)
:since: 0.4.2

View File

@@ -10,6 +10,10 @@
#include "log.h"
#include <pipewire/impl.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-module")
@@ -261,3 +265,63 @@ wp_impl_module_load (WpCore * core, const gchar * name,
return module;
}
/*!
* \brief Loads a PipeWire module with arguments from file into the WirePlumber process
*
* \ingroup wpimplmodule
* \since 0.4.15
* \param core (transfer none): The WirePlumber core
* \param name (transfer none): the name of the module to load
* \param filename (transfer none): filename to be used as arguments
* \param properties (nullable) (transfer none): additional properties to be
* provided to the module
* \returns (nullable) (transfer full): the WpImplModule for the module that
* was loaded on success, %NULL on failure.
*/
WpImplModule *
wp_impl_module_load_file (WpCore * core, const gchar * name,
const gchar * filename, WpProperties * properties)
{
char *config = "";
int fd = open(filename, O_RDONLY);
if (fd < 0) {
g_warning("Failed to open config file %s: %m", filename);
return NULL;
}
struct stat stats;
int err = fstat(fd, &stats);
if (err < 0) {
g_warning("Failed to stat config file %s: %m", filename);
close(fd);
return NULL;
}
config = mmap(NULL, stats.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (config == MAP_FAILED){
g_warning("Failed to mmap config file %s: %m", filename);
close(fd);
return NULL;
}
close(fd);
WpImplModule *module = WP_IMPL_MODULE (
g_object_new (WP_TYPE_IMPL_MODULE,
"core", core,
"name", name,
"arguments", config,
"properties", properties,
NULL)
);
munmap(config, stats.st_size);
if (!module->pw_impl_module) {
/* Module loading failed, free and return */
g_object_unref (module);
return NULL;
}
return module;
}

View File

@@ -29,6 +29,9 @@ G_DECLARE_FINAL_TYPE (WpImplModule, wp_impl_module, WP, IMPL_MODULE, GObject);
WP_API
WpImplModule * wp_impl_module_load (WpCore * core, const gchar * name,
const gchar * arguments, WpProperties * properties);
WP_API
WpImplModule * wp_impl_module_load_file (WpCore * core, const gchar * name,
const gchar * filename, WpProperties * properties);
G_END_DECLS

View File

@@ -638,8 +638,8 @@ wp_object_manager_is_interested_in_global (WpObjectManager * self,
/* and consider the manager interested if the type and the globals match...
if pw_properties / g_properties fail, that's ok because they are not
known yet (the proxy is likely NULL and properties not yet retrieved) */
if (match & (WP_INTEREST_MATCH_GTYPE |
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES)) {
if (SPA_FLAG_IS_SET (match, (WP_INTEREST_MATCH_GTYPE |
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES))) {
gpointer ft = g_hash_table_lookup (self->features,
GSIZE_TO_POINTER (global->type));
*wanted_features = (WpObjectFeatures) GPOINTER_TO_UINT (ft);
@@ -1049,18 +1049,15 @@ wp_registry_detach (WpRegistry *self)
}
}
static void
expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
static gboolean
expose_tmp_globals (WpCore *core)
{
g_autoptr (GError) error = NULL;
WpRegistry *self = wp_core_get_registry (core);
g_autoptr (GPtrArray) tmp_globals = NULL;
if (!wp_core_sync_finish (core, res, &error))
wp_warning_object (core, "core sync error: %s", error->message);
/* in case the registry was cleared in the meantime... */
if (G_UNLIKELY (!self->tmp_globals))
return;
return G_SOURCE_REMOVE;
/* steal the tmp_globals list and replace it with an empty one */
tmp_globals = self->tmp_globals;
@@ -1084,8 +1081,8 @@ expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
}
g_return_if_fail (self->globals->len <= g->id ||
g_ptr_array_index (self->globals, g->id) == NULL);
g_return_val_if_fail (self->globals->len <= g->id ||
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
/* set the registry, so that wp_global_rm_flag() can work full-scale */
g->registry = self;
@@ -1111,6 +1108,8 @@ expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
}
wp_object_manager_maybe_objects_changed (om);
}
return G_SOURCE_REMOVE;
}
/*
@@ -1161,7 +1160,8 @@ wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
/* schedule exposing when adding the first global */
if (self->tmp_globals->len == 1) {
wp_core_sync (core, NULL, (GAsyncReadyCallback) expose_tmp_globals, self);
wp_core_idle_add_closure (core, NULL,
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
}
} else {
/* store the most permissive permissions */

View File

@@ -6,6 +6,7 @@
* SPDX-License-Identifier: MIT
*/
#include "lua.h"
#include <glib/gstdio.h>
#include <wp/wp.h>
#include <pipewire/pipewire.h>
@@ -1536,8 +1537,20 @@ impl_module_new (lua_State *L)
properties = wplua_table_to_properties (L, 3);
}
WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
name, args, properties);
bool load_file = false; // Load args as file path
if (lua_type (L, 4) != LUA_TNONE && lua_type (L, 4) != LUA_TNIL) {
luaL_checktype (L, 4, LUA_TBOOLEAN);
load_file = lua_toboolean(L, 4);
}
WpImplModule *m = NULL;
if (load_file) {
m = wp_impl_module_load_file (get_wp_export_core (L),
name, args, properties);
} else {
m = wp_impl_module_load (get_wp_export_core (L),
name, args, properties);
}
if (m) {
wplua_pushobject (L, m);

View File

@@ -35,13 +35,17 @@ monitor.alsa.midi.node-properties = {
## Removes longname/number from MIDI port names
api.alsa.disable-longname = true
## Set priorities so that it can be used as a fallback driver (see pipewire#3562)
priority.session = 100
priority.driver = 1
}
monitor.alsa.vm.node.defaults = {
## These properties override node defaults when running in a virtual machine.
## The rules below still override those.
api.alsa.period-size = 256
api.alsa.period-size = 1024
api.alsa.headroom = 8192
}

View File

@@ -26,6 +26,8 @@ SimpleEventHook {
local off_profile = nil
local best_profile = nil
local unk_profile = nil
-- Takes absolute priority if available or unknown
local profile_prop = device.properties["device.profile"]
-- skip hook if profile is already selected
if selected_profile then
@@ -34,7 +36,9 @@ SimpleEventHook {
for p in device:iterate_params ("EnumProfile") do
profile = cutils.parseParam (p, "EnumProfile")
if profile and profile.name ~= "pro-audio" then
if profile and profile.name == profile_prop and profile.available ~= "no" then
selected_profile = profile
elseif profile and profile.name ~= "pro-audio" then
if profile.name == "off" then
off_profile = profile
elseif profile.available == "yes" then

View File

@@ -0,0 +1,61 @@
-- WirePlumber
--
-- Copyright © 2022-2023 The WirePlumber project contributors
-- @author Dmitry Sharshakov <d3dx12.xx@gmail.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
config.rules = config.rules or {}
for _, r in ipairs(config.rules) 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
end
-- TODO: only check for hotplug of nodes with known DSP rules
nodes_om = ObjectManager {
Interest { type = "node" },
}
filter_chains = {}
nodes_om:connect("object-added", function (om, node)
for _, r in ipairs(config.rules or {}) do
for _, interest in ipairs(r.interests) do
if interest:matches(node["global-properties"]) then
local id = node["global-properties"]["object.id"]
if r.filter_chain then
if filter_chains[id] then
Log.warning("Sink " .. id .. " has been plugged now, but has a filter chain loaded. Skipping")
else
filter_chains[id] = LocalModule("libpipewire-module-filter-chain", r.filter_chain, {}, true)
end
end
end
end
end
end)
nodes_om:connect("object-removed", function (om, node)
local id = node["global-properties"]["object.id"]
if filter_chains[id] then
Log.debug("Unloading filter chain associated with sink " .. id)
filter_chains[id] = nil
else
Log.debug("Disconnected sink " .. id .. " does not have any filters to be removed")
end
end)
nodes_om:activate()

View File

@@ -8,6 +8,11 @@ executable('wpctl',
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
)
install_data('shell-completion/wpctl.zsh',
install_dir: get_option('datadir') / 'zsh/site-functions',
rename: '_wpctl'
)
executable('wpexec',
'wpexec.c',
c_args : [

View File

@@ -0,0 +1,49 @@
#compdef wpctl
(( $+functions[_wpctl_pw_nodes] )) ||
_wpctl_pw_nodes() {
local -a pw_objects
if (( $+commands[pw-dump] )) && (( $+commands[jq] )); then
local -a pw_objects=(${(@f)"$(2>/dev/null {
command pw-dump |
command jq -r '.[] | select(
.type == "PipeWire:Interface:Node"
) |
{id, type, name: (
.info.name //
(.info.props | (
."application.name" //
."node.name")
) //
.type)
} |
"\(.id):\(.name | gsub(":"; "\\:"))"'
})"})
fi
_wpctl_describe_nodes() {_describe "node id" pw_objects "$@"}
_alternative \
'pw-defaults:defaults:(@DEFAULT_SINK@ @DEFAULT_SOURCE@)' \
'pw-node-id:node id:_wpctl_describe_nodes'
}
local -a node_id=(/$'[^\0]#\0'/ ':pw-node-id:node id:_wpctl_pw_nodes')
local -a volume=(/$'[0-9]##(%|)([+-]|)\0'/ ':volume:volume:( )')
local -a toggle=(/$'[^\0]#\0'/ ':(0 1 toggle)')
local -a set_volume=( "$node_id[@]" "$volume[@]" )
local -a set_mute=( "$node_id[@]" "$toggle[@]" )
_regex_words options 'wpctl options' \
{-h,--help}':show help message and exit'
local -a options=( "$reply[@]" )
_regex_words wpctl-commands 'wpctl commands' \
'status:show wireplumber status' \
'get-volume:get object volume:$node_id' \
'set-default:set a default sink:$node_id' \
'set-volume:set object volume:$set_volume' \
'set-mute:set object mute:$set_mute' \
'set-profile:set object profile:$node_id' \
'clear-default:unset default sink:$node_id'
local -a wpctlcmd=( /$'[^\0]#\0'/ "$options[@]" "#" "$reply[@]")
_regex_arguments _wpctl "$wpctlcmd[@]"
_wpctl "$@"