diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pw-voiceui-SVA-voice-UI-control-listen-cl.patch b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pw-voiceui-SVA-voice-UI-control-listen-cl.patch new file mode 100644 index 0000000000..846af137d8 --- /dev/null +++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0003-tools-add-pw-voiceui-SVA-voice-UI-control-listen-cl.patch @@ -0,0 +1,694 @@ +From 2ad473fced6d52b14abd4fe3b9f83ed9c0c668ef Mon Sep 17 00:00:00 2001 +From: Kiruthika Vijayasekar +Date: Tue, 25 Aug 2026 21:00:18 +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 the pw-pal-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 : 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 + +src/tools/meson.build: register pw-voiceui as a built tool alongside +the other pw-* utilities. + +Upstream-Status: Pending +--- + 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 8147906fb..ffb3a2276 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..6321b7913 +--- /dev/null ++++ b/src/tools/pw-voiceui.c +@@ -0,0 +1,643 @@ ++#include "config.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++ ++#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 \n" ++ " %s [options] set-param \n" ++ " %s [options] send-command \n" ++ "\n" ++ "Options:\n" ++ " -n, --name Target node.name (default: %s)\n" ++ " -r, --role Target media.role (default: %s)\n" ++ " -i, --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, ®istry_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; ++} +-- +2.34.1 + 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..d462b315af 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-pw-voiceui-SVA-voice-UI-control-listen-cl.patch" inherit meson pkgconfig systemd gettext useradd @@ -331,6 +332,7 @@ FILES:${PN}-tools = " \ ${bindir}/pw-reserve \ ${bindir}/pw-sysex \ ${bindir}/pw-top \ + ${bindir}/pw-voiceui \ " # This is a shim daemon that is intended to be used as a