diff mbox series

[meta-multimedia] pipewire: add pw-voiceui SVA tool patch

Message ID 20260826105059.362705-1-harsh.rai@oss.qualcomm.com
State New
Headers show
Series [meta-multimedia] pipewire: add pw-voiceui SVA tool patch | expand

Commit Message

Harsh Rai Aug. 26, 2026, 10:50 a.m. UTC
From: Kiruthika Vijayasekar <kvijayas@qti.qualcomm.com>

Add the upstream PipeWire patch that introduces the pw-voiceui command-line tool for SVA voice UI control and listen support.

Signed-off-by: Kiruthika Vijayasekar <kvijayas@qti.qualcomm.com>
---
 ...pwvoiceui-SVA-voice-UI-listen-client.patch | 693 ++++++++++++++++++
 .../pipewire/pipewire_1.6.8.bb                |   1 +
 2 files changed, 694 insertions(+)
 create mode 100644 meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pwvoiceui-SVA-voice-UI-listen-client.patch
diff mbox series

Patch

diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pwvoiceui-SVA-voice-UI-listen-client.patch b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pwvoiceui-SVA-voice-UI-listen-client.patch
new file mode 100644
index 0000000000..0b6a0585a0
--- /dev/null
+++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pwvoiceui-SVA-voice-UI-listen-client.patch
@@ -0,0 +1,693 @@ 
+From 994c22a99421ebc4a1b81d8c03c6122b192a9035 Mon Sep 17 00:00:00 2001
+From: Kiruthika Vijayasekar <kvijayas@qti.qualcomm.com>
+Date: Wed, 26 Aug 2026 12:36:40 +0530
+Subject: [PATCH] tools: add pw-voiceui SVA voice-UI control/listen client
+
+Add a pw-voiceui command-line tool for interacting with the SVA
+(Sound Trigger / Voice Activation) voice-UI node exposed on the
+PipeWire graph by a PAL-based source plugin.
+
+The tool locates the target node by media.role=VoiceUI (or an
+explicit name/id), and supports:
+  - list / info: enumerate/describe the VoiceUI node
+  - set <sva-key> <value>: set a validated SVA property (confidence
+    level, sound model path, capture-requested, mode, lab settings,
+    sample rate/channels/bit width, reset, restart)
+  - set-param / send-command: send an arbitrary POD param or node
+    command, built from a JSON payload, for lower-level control
+  - listen: subscribe to the "sva.events" metadata object and print
+    sva.state updates (detection events) as they arrive
+
+Register pw-voiceui as a built tool alongside the other pw-* utilities.
+
+Upstream-Status: Submitted [https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2969]
+
+Signed-off-by: Kiruthika Vijayasekar <kvijayas@qti.qualcomm.com>
+---
+ src/tools/meson.build  |   1 +
+ src/tools/pw-voiceui.c | 643 +++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 644 insertions(+)
+ create mode 100644 src/tools/pw-voiceui.c
+
+diff --git a/src/tools/meson.build b/src/tools/meson.build
+index 2def79257..0e597cc11 100644
+--- a/src/tools/meson.build
++++ b/src/tools/meson.build
+@@ -9,6 +9,7 @@ tools_sources = [
+   [ 'pw-metadata', [ 'pw-metadata.c' ] ],
+   [ 'pw-loopback', [ 'pw-loopback.c' ] ],
+   [ 'pw-link', [ 'pw-link.c' ] ],
++  [ 'pw-voiceui', [ 'pw-voiceui.c' ] ],
+ ]
+ 
+ foreach t : tools_sources
+diff --git a/src/tools/pw-voiceui.c b/src/tools/pw-voiceui.c
+new file mode 100644
+index 000000000..a76e6bbdd
+--- /dev/null
++++ b/src/tools/pw-voiceui.c
+@@ -0,0 +1,643 @@
++#include "config.h"
++
++#include <errno.h>
++#include <getopt.h>
++#include <stdbool.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include <string.h>
++
++#include <spa/debug/file.h>
++#include <spa/debug/pod.h>
++#include <spa/debug/types.h>
++#include <spa/param/props.h>
++#include <spa/pod/dynamic.h>
++#include <spa/utils/json-pod.h>
++#include <spa/utils/keys.h>
++#include <spa/utils/result.h>
++#include <spa/utils/string.h>
++
++#include <pipewire/pipewire.h>
++#include <pipewire/extensions/metadata.h>
++
++#define DEFAULT_TARGET_ROLE "VoiceUI"
++#define DEFAULT_TARGET_NAME "pal_source_voice_ui"
++
++struct global {
++	struct spa_list link;
++	uint32_t id;
++	uint32_t permissions;
++	uint32_t version;
++	char *type;
++	struct pw_properties *props;
++};
++
++struct data {
++	struct pw_main_loop *loop;
++	struct pw_context *context;
++	struct pw_core *core;
++	struct pw_registry *registry;
++	struct pw_node *node;
++
++	struct spa_hook core_listener;
++	struct spa_hook registry_listener;
++	struct spa_hook node_listener;
++
++	struct spa_list globals;
++
++	const char *target_name;
++	const char *target_role;
++	uint32_t target_id;
++	bool have_target_id;
++	bool list;
++	bool info;
++	bool verbose;
++
++	const char *command;
++	const char *param_id;
++	const char *json;
++	const char *sva_key;
++	const char *sva_value;
++
++	struct global *target;
++	int pending;
++	int exit_code;
++	bool listen;
++	bool listening;
++	struct pw_metadata *metadata;
++	struct spa_hook metadata_listener;
++	uint32_t sva_node_id;
++};
++
++static void usage(const char *name)
++{
++	fprintf(stderr,
++		"Usage:\n"
++		"  %s [options] list\n"
++		"  %s [options] info\n"
++		"  %s [options] listen\n"
++		"  %s [options] set <sva-key> <value>\n"
++		"  %s [options] set-param <param-id> <param-json>\n"
++		"  %s [options] send-command <command-id> <command-json>\n"
++		"\n"
++		"Options:\n"
++		"  -n, --name <node-name>     Target node.name (default: %s)\n"
++		"  -r, --role <media-role>    Target media.role (default: %s)\n"
++		"  -i, --id <node-id>         Target explicit PipeWire node id\n"
++		"  -v, --verbose              Print the POD being sent\n"
++		"  -h, --help                 Show this help\n",
++		name, name, name, name, name, name, DEFAULT_TARGET_NAME, DEFAULT_TARGET_ROLE);
++}
++
++static void global_free(struct global *global)
++{
++	if (global == NULL)
++		return;
++	spa_list_remove(&global->link);
++	pw_properties_free(global->props);
++	free(global->type);
++	free(global);
++}
++
++static const char *global_prop(struct global *global, const char *key)
++{
++	return global->props ? pw_properties_get(global->props, key) : NULL;
++}
++
++static bool global_is_voiceui_node(struct data *data, struct global *global)
++{
++	const char *name, *role, *media_class;
++
++	if (!spa_streq(global->type, PW_TYPE_INTERFACE_Node))
++		return false;
++
++	if (data->have_target_id)
++		return global->id == data->target_id;
++
++	name = global_prop(global, PW_KEY_NODE_NAME);
++	role = global_prop(global, PW_KEY_MEDIA_ROLE);
++	media_class = global_prop(global, PW_KEY_MEDIA_CLASS);
++
++	if (data->target_name && name && spa_streq(name, data->target_name))
++		return true;
++
++	return role && spa_streq(role, data->target_role) &&
++		media_class && strstr(media_class, "Audio/Source") != NULL;
++}
++
++
++static bool parse_bool_value(const char *value, bool *result)
++{
++	if (spa_streq(value, "true") || spa_streq(value, "1") || spa_streq(value, "yes")) {
++		*result = true;
++		return true;
++	}
++	if (spa_streq(value, "false") || spa_streq(value, "0") || spa_streq(value, "no")) {
++		*result = false;
++		return true;
++	}
++	return false;
++}
++
++static const char *phase2_sva_key(const char *key)
++{
++	if (spa_streq(key, "sva.model.path"))
++		return "sva.sound.model.path";
++	return key;
++}
++
++static bool validate_sva_property(const char *key, const char *value)
++{
++	char *end;
++	long level;
++	bool bool_value;
++	const char *wire_key = phase2_sva_key(key);
++
++	if (spa_streq(wire_key, "sva.confidence.level")) {
++		errno = 0;
++		level = strtol(value, &end, 10);
++		if (errno != 0 || *end != '\0' || level < 0 || level > 100) {
++			fprintf(stderr, "sva.confidence.level must be an integer in range 0..100\n");
++			return false;
++		}
++		return true;
++	}
++	if (spa_streq(wire_key, "sva.sound.model.path")) {
++		if (value[0] == '\0') {
++			fprintf(stderr, "sva.model.path must not be empty\n");
++			return false;
++		}
++		return true;
++	}
++	if (spa_streq(wire_key, "sva.capture.requested")) {
++		if (!parse_bool_value(value, &bool_value)) {
++			fprintf(stderr, "sva.capture.requested must be true/false, yes/no, or 1/0\n");
++			return false;
++		}
++		return true;
++	}
++	if (spa_streq(wire_key, "sva.mode")) {
++		if (!spa_streq(value, "LPI") && !spa_streq(value, "NLPI")) {
++			fprintf(stderr, "sva.mode must be LPI or NLPI for phase2 plugin compatibility\n");
++			return false;
++		}
++		return true;
++	}
++	if (spa_streq(wire_key, "sva.lab.enabled")) {
++		if (!parse_bool_value(value, &bool_value)) {
++			fprintf(stderr, "sva.lab.enabled must be true/false, yes/no, or 1/0\n");
++			return false;
++		}
++		return true;
++	}
++	if (spa_streq(wire_key, "sva.lab.duration") ||
++			spa_streq(wire_key, "sva.sample.rate") ||
++			spa_streq(wire_key, "sva.channels") ||
++			spa_streq(wire_key, "sva.bit.width")) {
++		errno = 0;
++		level = strtol(value, &end, 10);
++		if (errno != 0 || *end != '\0' || level < 0) {
++			fprintf(stderr, "%s must be a non-negative integer\n", key);
++			return false;
++		}
++		return true;
++	}
++	if (spa_streq(wire_key, "sva.reset")) {
++		if (!parse_bool_value(value, &bool_value)) {
++			fprintf(stderr, "sva.reset must be true/false\n");
++			return false;
++		}
++		return true;    /* ← move return true INSIDE the if block */
++	}
++	if (spa_streq(wire_key, "sva.restart")) {
++		if (!parse_bool_value(value, &bool_value)) {
++			fprintf(stderr, "sva.restart must be true/false\n");
++			return false;
++		}
++		return true;
++	}
++	/* Now this is reachable for unknown keys */
++	fprintf(stderr, "unsupported SVA key '%s'\n", key);
++	fprintf(stderr, "supported keys: sva.confidence.level, sva.model.path, "
++			"sva.sound.model.path, sva.capture.requested, sva.mode, "
++			"sva.lab.enabled, sva.lab.duration, sva.sample.rate, "
++			"sva.channels, sva.bit.width, sva.reset, sva.restart\n");
++	return false;
++}
++
++static bool build_sva_props_pod(struct spa_pod_builder *builder, const char *key,
++		const char *value, struct spa_pod **pod)
++{
++	struct spa_pod_frame frame[2];
++
++	spa_pod_builder_push_object(builder, &frame[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
++	spa_pod_builder_prop(builder, SPA_PROP_params, 0);
++	spa_pod_builder_push_struct(builder, &frame[1]);
++	key = phase2_sva_key(key);
++	spa_pod_builder_string(builder, key);
++
++	if (spa_streq(key, "sva.confidence.level") ||
++			spa_streq(key, "sva.lab.duration") ||
++			spa_streq(key, "sva.sample.rate") ||
++			spa_streq(key, "sva.channels") ||
++			spa_streq(key, "sva.bit.width"))
++		spa_pod_builder_int(builder, atoi(value));
++	else if (spa_streq(key, "sva.capture.requested") ||
++			spa_streq(key, "sva.lab.enabled")) {
++		bool bool_value;
++		parse_bool_value(value, &bool_value);
++		spa_pod_builder_bool(builder, bool_value);
++	} else if (spa_streq(key, "sva.reset") || spa_streq(key, "sva.restart")) {
++		bool bool_value;
++		parse_bool_value(value, &bool_value);
++		spa_pod_builder_bool(builder, bool_value);
++	}else {
++		spa_pod_builder_string(builder, value);
++	}
++
++	spa_pod_builder_pop(builder, &frame[1]);
++	*pod = spa_pod_builder_pop(builder, &frame[0]);
++	return *pod != NULL;
++}
++
++static void print_global(struct global *global)
++{
++	const char *name = global_prop(global, PW_KEY_NODE_NAME);
++	const char *role = global_prop(global, PW_KEY_MEDIA_ROLE);
++	const char *media_class = global_prop(global, PW_KEY_MEDIA_CLASS);
++
++	printf("id %u", global->id);
++	if (name)
++		printf(", node.name=%s", name);
++	if (media_class)
++		printf(", media.class=%s", media_class);
++	if (role)
++		printf(", media.role=%s", role);
++	printf("\n");
++}
++
++
++
++static void done(void *user_data, uint32_t id, int seq)
++{
++	struct data *data = user_data;
++
++	if (data->listening)
++		return;
++
++	if (data->pending == seq)
++		pw_main_loop_quit(data->loop);
++}
++
++static void core_error(void *user_data, uint32_t id, int seq, int res, const char *message)
++{
++	struct data *data = user_data;
++
++	fprintf(stderr, "PipeWire error id:%u seq:%d res:%d (%s): %s\n",
++		id, seq, res, spa_strerror(res), message);
++
++	if (data->listening)
++		return;
++
++	data->exit_code = EXIT_FAILURE;
++	if (seq == data->pending || data->pending == 0)
++		pw_main_loop_quit(data->loop);
++}
++
++static const struct pw_core_events core_events = {
++	PW_VERSION_CORE_EVENTS,
++	.done = done,
++	.error = core_error,
++};
++
++static int metadata_property(void *user_data, uint32_t subject,
++                             const char *key, const char *type, const char *value)
++{
++    struct data *data = user_data;
++    if (key == NULL || !spa_streq(key, "sva.state") || value == NULL)
++        return 0;
++    /* Filter by our node ID */
++    if (data->sva_node_id != 0 && subject != data->sva_node_id)
++        return 0;
++    printf("\n-- SVA State Update (Event) --\n");
++    printf("  Payload: %s\n", value);
++    printf("------------------------------\n");
++    return 0;
++}
++
++static const struct pw_metadata_events metadata_events = {
++    PW_VERSION_METADATA_EVENTS,
++    .property = metadata_property,
++};
++
++
++static void registry_global(void *user_data, uint32_t id, uint32_t permissions,
++		const char *type, uint32_t version, const struct spa_dict *props)
++{
++	struct data *data = user_data;
++	struct global *global;
++
++	global = calloc(1, sizeof(*global));
++	if (global == NULL) {
++		data->exit_code = EXIT_FAILURE;
++		pw_main_loop_quit(data->loop);
++		return;
++	}
++
++	global->id = id;
++	global->permissions = permissions;
++	global->version = version;
++	global->type = strdup(type);
++	global->props = props ? pw_properties_new_dict(props) : NULL;
++	spa_list_append(&data->globals, &global->link);
++
++	if (global_is_voiceui_node(data, global)) {
++		if (data->list)
++			print_global(global);
++		if (data->target == NULL)
++			data->target = global;
++	}
++
++	if (data->listen && spa_streq(type, PW_TYPE_INTERFACE_Metadata)) {
++		const char *name = props ? spa_dict_lookup(props, PW_KEY_METADATA_NAME) : NULL;
++		if (name && spa_streq(name, "sva.events") && data->metadata == NULL) {
++			data->metadata = pw_registry_bind(data->registry, id,
++				PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0);
++			if (data->metadata) {
++				pw_metadata_add_listener(data->metadata, &data->metadata_listener, &metadata_events, data);
++				printf("Connected to SVA Event Bus (id %u)\n", id);
++			}
++		}
++	}
++}
++
++static void registry_global_remove(void *user_data, uint32_t id)
++{
++	struct data *data = user_data;
++	struct global *global, *tmp;
++
++	spa_list_for_each_safe(global, tmp, &data->globals, link) {
++		if (global->id == id) {
++			if (data->target == global)
++				data->target = NULL;
++			global_free(global);
++			return;
++		}
++	}
++}
++
++static const struct pw_registry_events registry_events = {
++	PW_VERSION_REGISTRY_EVENTS,
++	.global = registry_global,
++	.global_remove = registry_global_remove,
++};
++
++static bool parse_pod(const char *type_name, const char *json, const struct spa_type_info *types,
++		struct spa_pod_dynamic_builder *builder, struct spa_pod **pod)
++{
++	const struct spa_type_info *type_info;
++	struct spa_error_location loc;
++	int res;
++
++	type_info = spa_debug_type_find_short(types, type_name);
++	if (type_info == NULL) {
++		fprintf(stderr, "unknown type '%s'\n", type_name);
++		return false;
++	}
++
++	res = spa_json_to_pod_checked(&builder->b, 0, type_info, json, strlen(json), &loc);
++	if (res < 0) {
++		if (loc.line != 0)
++			spa_debug_file_error_location(stderr, &loc,
++					"syntax error in json '%s': %s", json, loc.reason);
++		fprintf(stderr, "can't make POD for '%s': %s\n", type_name,
++				loc.reason ? loc.reason : spa_strerror(res));
++		return false;
++	}
++
++	*pod = spa_pod_builder_deref(&builder->b, 0);
++	if (*pod == NULL) {
++		fprintf(stderr, "can't dereference built POD\n");
++		return false;
++	}
++	return true;
++}
++
++static bool bind_target(struct data *data)
++{
++	if (data->target == NULL) {
++		fprintf(stderr, "VoiceUI node not found\n");
++		return false;
++	}
++
++	data->node = pw_registry_bind(data->registry, data->target->id,
++			PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
++	if (data->node == NULL) {
++		fprintf(stderr, "can't bind VoiceUI node id:%u: %m\n", data->target->id);
++		return false;
++	}
++	return true;
++}
++
++static bool run_action(struct data *data)
++{
++	uint8_t buffer[1024];
++	spa_auto(spa_pod_dynamic_builder) builder = { 0 };
++	struct spa_pod *pod;
++
++	if (data->list)
++		return true;
++
++	if (data->target == NULL) {
++		fprintf(stderr, "VoiceUI node not found\n");
++		return false;
++	}
++
++	if (data->info) {
++		print_global(data->target);
++		return true;
++	}
++
++	if (!bind_target(data))
++		return false;
++
++	if (data->listen) {
++		data->sva_node_id = data->target->id;
++		printf("Listening for SVA events on node %u (Ctrl+C to quit)...\n", data->sva_node_id);
++		data->listening = true;
++		pw_main_loop_run(data->loop);
++		return data->exit_code == EXIT_SUCCESS;
++	}
++
++	spa_pod_dynamic_builder_init(&builder, buffer, sizeof(buffer), 4096);
++
++	if (spa_streq(data->command, "set")) {
++		if (!build_sva_props_pod(&builder.b, data->sva_key, data->sva_value, &pod)) {
++			fprintf(stderr, "can't build SVA Props POD\n");
++			return false;
++		}
++		if (data->verbose)
++			spa_debug_pod(0, NULL, pod);
++		pw_node_set_param(data->node, SPA_PARAM_Props, 0, pod);
++	} else if (spa_streq(data->command, "set-param")) {
++		const struct spa_type_info *type_info;
++
++		if (!parse_pod(data->param_id, data->json, spa_type_param, &builder, &pod))
++			return false;
++		type_info = spa_debug_type_find_short(spa_type_param, data->param_id);
++		if (data->verbose)
++			spa_debug_pod(0, NULL, pod);
++		pw_node_set_param(data->node, type_info->type, 0, pod);
++	} else if (spa_streq(data->command, "send-command")) {
++		if (!parse_pod(data->param_id, data->json, spa_type_node_command_id, &builder, &pod))
++			return false;
++		if (data->verbose)
++			spa_debug_pod(0, NULL, pod);
++		pw_node_send_command(data->node, (struct spa_command *)pod);
++	} else {
++		fprintf(stderr, "unknown command '%s'\n", data->command);
++		return false;
++	}
++
++	data->pending = pw_core_sync(data->core, PW_ID_CORE, 0);
++	pw_main_loop_run(data->loop);
++	return data->exit_code == EXIT_SUCCESS;
++}
++
++int main(int argc, char *argv[])
++{
++	struct data data = { 0 };
++	struct global *global, *tmp;
++	int opt;
++	static const struct option long_options[] = {
++		{ "name", required_argument, NULL, 'n' },
++		{ "role", required_argument, NULL, 'r' },
++		{ "id", required_argument, NULL, 'i' },
++		{ "verbose", no_argument, NULL, 'v' },
++		{ "help", no_argument, NULL, 'h' },
++		{ NULL, 0, NULL, 0 }
++	};
++
++	data.target_name = DEFAULT_TARGET_NAME;
++	data.target_role = DEFAULT_TARGET_ROLE;
++	data.exit_code = EXIT_SUCCESS;
++	spa_list_init(&data.globals);
++
++	while ((opt = getopt_long(argc, argv, "n:r:i:vh", long_options, NULL)) != -1) {
++		char *end;
++		switch (opt) {
++		case 'n':
++			data.target_name = optarg;
++			break;
++		case 'r':
++			data.target_role = optarg;
++			break;
++		case 'i':
++			data.target_id = strtoul(optarg, &end, 10);
++			if (*end != '\0') {
++				fprintf(stderr, "invalid node id '%s'\n", optarg);
++				return EXIT_FAILURE;
++			}
++			data.have_target_id = true;
++			break;
++		case 'v':
++			data.verbose = true;
++			break;
++		case 'h':
++			usage(argv[0]);
++			return EXIT_SUCCESS;
++		default:
++			usage(argv[0]);
++			return EXIT_FAILURE;
++		}
++	}
++
++	if (optind >= argc) {
++		usage(argv[0]);
++		return EXIT_FAILURE;
++	}
++
++	data.command = argv[optind++];
++	if (spa_streq(data.command, "list")) {
++		data.list = true;
++	} else if (spa_streq(data.command, "info")) {
++		data.info = true;
++	} else if (spa_streq(data.command, "set")) {
++		if (optind + 2 != argc) {
++			usage(argv[0]);
++			return EXIT_FAILURE;
++		}
++		data.sva_key = argv[optind++];
++		data.sva_value = argv[optind++];
++		if (!validate_sva_property(data.sva_key, data.sva_value))
++			return EXIT_FAILURE;
++	} else if (spa_streq(data.command, "set-param") ||
++			spa_streq(data.command, "send-command")) {
++		if (optind + 2 != argc) {
++			usage(argv[0]);
++			return EXIT_FAILURE;
++		}
++		data.param_id = argv[optind++];
++		data.json = argv[optind++];
++	} else if (spa_streq(data.command, "listen")) {
++		data.listen = true;
++	} else {
++		usage(argv[0]);
++		return EXIT_FAILURE;
++	}
++
++	pw_init(&argc, &argv);
++
++	data.loop = pw_main_loop_new(NULL);
++	if (data.loop == NULL) {
++		fprintf(stderr, "can't create main loop: %m\n");
++		return EXIT_FAILURE;
++	}
++
++	data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
++	if (data.context == NULL) {
++		fprintf(stderr, "can't create context: %m\n");
++		data.exit_code = EXIT_FAILURE;
++		goto out;
++	}
++
++	data.core = pw_context_connect(data.context, NULL, 0);
++	if (data.core == NULL) {
++		fprintf(stderr, "can't connect: %m\n");
++		data.exit_code = EXIT_FAILURE;
++		goto out;
++	}
++	pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
++
++	data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY, 0);
++	if (data.registry == NULL) {
++		fprintf(stderr, "can't get registry: %m\n");
++		data.exit_code = EXIT_FAILURE;
++		goto out;
++	}
++	pw_registry_add_listener(data.registry, &data.registry_listener, &registry_events, &data);
++
++	data.pending = pw_core_sync(data.core, PW_ID_CORE, 0);
++	pw_main_loop_run(data.loop);
++
++	if (!run_action(&data))
++		data.exit_code = EXIT_FAILURE;
++
++out:
++	if (data.metadata)
++		pw_proxy_destroy((struct pw_proxy *)data.metadata);
++	if (data.node)
++		pw_proxy_destroy((struct pw_proxy *)data.node);
++	if (data.registry)
++		pw_proxy_destroy((struct pw_proxy *)data.registry);
++	if (data.core)
++		pw_core_disconnect(data.core);
++	if (data.context)
++		pw_context_destroy(data.context);
++	if (data.loop)
++		pw_main_loop_destroy(data.loop);
++	spa_list_for_each_safe(global, tmp, &data.globals, link)
++		global_free(global);
++	pw_deinit();
++	return data.exit_code;
++}
+-- 
\ No newline at end of file
diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.8.bb b/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.8.bb
index a3bd88cc7d..9743eeeab7 100644
--- a/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.8.bb
+++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.8.bb
@@ -17,6 +17,7 @@  BRANCH = "${@oe.utils.trim_version('${PV}', 2)}"
 SRC_URI = "git://gitlab.freedesktop.org/pipewire/pipewire.git;branch=${BRANCH};protocol=https;tag=${PV}"
 SRC_URI += "file://0001-pipewire-compress-offload.patch"
 SRC_URI += "file://0002-spa-plugins-alsa-acp-compat.h-p-is-already-const-do-.patch"
+SRC_URI += "file://0003-tools-add-pwvoiceui-SVA-voice-UI-listen-client.patch"
 
 inherit meson pkgconfig systemd gettext useradd