diff --git a/NEWS.rst b/NEWS.rst index 494a7bc0..409156f6 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -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 .................. diff --git a/docs/rst/configuration/alsa.rst b/docs/rst/configuration/alsa.rst index 1983d867..95dea8bc 100644 --- a/docs/rst/configuration/alsa.rst +++ b/docs/rst/configuration/alsa.rst @@ -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 Props '{ iec958Codecs : [ PCM ] }'`` to modify the codecs at runtime. diff --git a/docs/rst/lua_api/lua_local_module_api.rst b/docs/rst/lua_api/lua_local_module_api.rst index 8390bb1a..c7f122f5 100644 --- a/docs/rst/lua_api/lua_local_module_api.rst +++ b/docs/rst/lua_api/lua_local_module_api.rst @@ -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 ` 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 diff --git a/lib/wp/module.c b/lib/wp/module.c index 11a000c9..ba56141a 100644 --- a/lib/wp/module.c +++ b/lib/wp/module.c @@ -10,6 +10,10 @@ #include "log.h" #include +#include +#include +#include +#include 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; +} diff --git a/lib/wp/module.h b/lib/wp/module.h index 6abf1a4e..88ca1265 100644 --- a/lib/wp/module.h +++ b/lib/wp/module.h @@ -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 diff --git a/lib/wp/object-manager.c b/lib/wp/object-manager.c index 60552b4a..981e9926 100644 --- a/lib/wp/object-manager.c +++ b/lib/wp/object-manager.c @@ -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 */ diff --git a/modules/module-lua-scripting/api/api.c b/modules/module-lua-scripting/api/api.c index 64bc7e00..86adfdc5 100644 --- a/modules/module-lua-scripting/api/api.c +++ b/modules/module-lua-scripting/api/api.c @@ -6,6 +6,7 @@ * SPDX-License-Identifier: MIT */ +#include "lua.h" #include #include #include @@ -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); diff --git a/src/config/wireplumber.conf.d/alsa.conf b/src/config/wireplumber.conf.d/alsa.conf index 2728b417..08f3da43 100644 --- a/src/config/wireplumber.conf.d/alsa.conf +++ b/src/config/wireplumber.conf.d/alsa.conf @@ -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 } diff --git a/src/scripts/device/find-best-profile.lua b/src/scripts/device/find-best-profile.lua index 4bb8ec1b..4555d9e0 100644 --- a/src/scripts/device/find-best-profile.lua +++ b/src/scripts/device/find-best-profile.lua @@ -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 diff --git a/src/scripts/policy-dsp.lua b/src/scripts/policy-dsp.lua new file mode 100644 index 00000000..55f86c68 --- /dev/null +++ b/src/scripts/policy-dsp.lua @@ -0,0 +1,61 @@ +-- WirePlumber +-- +-- Copyright © 2022-2023 The WirePlumber project contributors +-- @author Dmitry Sharshakov +-- +-- 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() diff --git a/src/tools/meson.build b/src/tools/meson.build index 9259b9eb..9e7d66bc 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -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 : [ diff --git a/src/tools/shell-completion/wpctl.zsh b/src/tools/shell-completion/wpctl.zsh new file mode 100644 index 00000000..3f9e53c8 --- /dev/null +++ b/src/tools/shell-completion/wpctl.zsh @@ -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 "$@"