diff --git a/src/tools/meson.build b/src/tools/meson.build index 8147906fb..c082f169f 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -95,6 +95,50 @@ if build_pw_cat summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool') endif +build_avb_virtual = get_option('avb').require( + host_machine.system() == 'linux', + error_message: 'AVB support is only available on Linux' +).allowed() + +if build_avb_virtual + avb_tool_inc = include_directories('../modules') + avb_tool_sources = [ + 'pw-avb-virtual.c', + '../modules/module-avb/avb.c', + '../modules/module-avb/adp.c', + '../modules/module-avb/acmp.c', + '../modules/module-avb/aecp.c', + '../modules/module-avb/aecp-aem.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-available.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c', + '../modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c', + '../modules/module-avb/es-builder.c', + '../modules/module-avb/avdecc.c', + '../modules/module-avb/descriptors.c', + '../modules/module-avb/maap.c', + '../modules/module-avb/mmrp.c', + '../modules/module-avb/mrp.c', + '../modules/module-avb/msrp.c', + '../modules/module-avb/mvrp.c', + '../modules/module-avb/srp.c', + '../modules/module-avb/stream.c', + ] + executable('pw-avb-virtual', + avb_tool_sources, + install: true, + include_directories: [configinc, avb_tool_inc], + dependencies: [mathlib, dl_lib, rt_lib, pipewire_dep], + ) +endif + if dbus_dep.found() executable('pw-reserve', 'reserve.h', diff --git a/src/tools/pw-avb-virtual.c b/src/tools/pw-avb-virtual.c new file mode 100644 index 000000000..6b123949e --- /dev/null +++ b/src/tools/pw-avb-virtual.c @@ -0,0 +1,283 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +/** + * pw-avb-virtual: Create virtual AVB audio devices in the PipeWire graph. + * + * This tool creates virtual AVB talker/listener endpoints that appear + * as Audio/Source and Audio/Sink nodes in the PipeWire graph (visible + * in tools like Helvum). No AVB hardware or network access is needed — + * the loopback transport is used for all protocol and stream operations. + * + * The sink node consumes audio silently (data goes nowhere). + * The source node produces silence (no network data to receive). + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "module-avb/internal.h" +#include "module-avb/stream.h" +#include "module-avb/avb-transport-loopback.h" +#include "module-avb/descriptors.h" +#include "module-avb/mrp.h" +#include "module-avb/adp.h" +#include "module-avb/acmp.h" +#include "module-avb/aecp.h" +#include "module-avb/maap.h" +#include "module-avb/mmrp.h" +#include "module-avb/msrp.h" +#include "module-avb/mvrp.h" + +struct data { + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct spa_hook core_listener; + + struct impl impl; + struct server *server; + + const char *opt_remote; + const char *opt_name; + bool opt_milan; +}; + +static void do_quit(void *data, int signal_number) +{ + struct data *d = data; + pw_main_loop_quit(d->loop); +} + +static void on_core_error(void *data, uint32_t id, int seq, + int res, const char *message) +{ + struct data *d = data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_main_loop_quit(d->loop); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +static struct server *create_virtual_server(struct data *data) +{ + struct impl *impl = &data->impl; + struct server *server; + struct stream *stream; + uint16_t idx; + char name_buf[256]; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + server->ifname = strdup("virtual0"); + server->avb_mode = data->opt_milan ? AVB_MODE_MILAN_V12 : AVB_MODE_LEGACY; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + spa_list_init(&server->streams); + + if (server->transport->setup(server) < 0) + goto error; + + server->mrp = avb_mrp_new(server); + if (server->mrp == NULL) + goto error; + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_maap_reserve(server->maap, 1); + + init_descriptors(server); + + /* Update stream properties and activate */ + idx = 0; + spa_list_for_each(stream, &server->streams, link) { + if (stream->direction == SPA_DIRECTION_INPUT) { + snprintf(name_buf, sizeof(name_buf), "%s.source.%u", + data->opt_name, idx); + pw_stream_update_properties(stream->stream, + &SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) { + { PW_KEY_NODE_NAME, name_buf }, + { PW_KEY_NODE_DESCRIPTION, "AVB Virtual Source" }, + { PW_KEY_NODE_VIRTUAL, "true" }, + }))); + } else { + snprintf(name_buf, sizeof(name_buf), "%s.sink.%u", + data->opt_name, idx); + pw_stream_update_properties(stream->stream, + &SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) { + { PW_KEY_NODE_NAME, name_buf }, + { PW_KEY_NODE_DESCRIPTION, "AVB Virtual Sink" }, + { PW_KEY_NODE_VIRTUAL, "true" }, + }))); + } + + if (stream_activate_virtual(stream, idx) < 0) + pw_log_warn("failed to activate stream %u", idx); + + idx++; + } + + return server; + +error: + spa_list_remove(&server->link); + free(server->ifname); + free(server); + return NULL; +} + +static void show_help(const char *name) +{ + printf("%s [options]\n" + " -h, --help Show this help\n" + " --version Show version\n" + " -r, --remote NAME Remote daemon name\n" + " -n, --name PREFIX Node name prefix (default: avb-virtual)\n" + " -m, --milan Use Milan v1.2 mode (default: legacy)\n", + name); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + struct pw_loop *l; + int res = -1; + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "remote", required_argument, NULL, 'r' }, + { "name", required_argument, NULL, 'n' }, + { "milan", no_argument, NULL, 'm' }, + { NULL, 0, NULL, 0 } + }; + int c; + + setlocale(LC_ALL, ""); + pw_init(&argc, &argv); + + data.opt_name = "avb-virtual"; + + while ((c = getopt_long(argc, argv, "hVr:n:m", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(argv[0]); + return 0; + case 'V': + printf("%s\n" + "Compiled with libpipewire %s\n" + "Linked with libpipewire %s\n", + argv[0], + pw_get_headers_version(), + pw_get_library_version()); + return 0; + case 'r': + data.opt_remote = optarg; + break; + case 'n': + data.opt_name = optarg; + break; + case 'm': + data.opt_milan = true; + break; + default: + show_help(argv[0]); + return -1; + } + } + + data.loop = pw_main_loop_new(NULL); + if (data.loop == NULL) { + fprintf(stderr, "can't create main loop: %m\n"); + goto exit; + } + + l = pw_main_loop_get_loop(data.loop); + pw_loop_add_signal(l, SIGINT, do_quit, &data); + pw_loop_add_signal(l, SIGTERM, do_quit, &data); + + data.context = pw_context_new(l, NULL, 0); + if (data.context == NULL) { + fprintf(stderr, "can't create context: %m\n"); + goto exit; + } + + data.core = pw_context_connect(data.context, + pw_properties_new( + PW_KEY_REMOTE_NAME, data.opt_remote, + NULL), + 0); + if (data.core == NULL) { + fprintf(stderr, "can't connect to PipeWire: %m\n"); + goto exit; + } + + pw_core_add_listener(data.core, &data.core_listener, + &core_events, &data); + + /* Initialize the AVB impl */ + data.impl.loop = l; + data.impl.timer_queue = pw_context_get_timer_queue(data.context); + data.impl.context = data.context; + data.impl.core = data.core; + spa_list_init(&data.impl.servers); + + /* Create the virtual AVB server with streams */ + data.server = create_virtual_server(&data); + if (data.server == NULL) { + fprintf(stderr, "can't create virtual AVB server: %m\n"); + goto exit; + } + + fprintf(stdout, "Virtual AVB device running (%s mode). Press Ctrl-C to stop.\n", + data.opt_milan ? "Milan v1.2" : "legacy"); + + pw_main_loop_run(data.loop); + + res = 0; +exit: + if (data.server) + avdecc_server_free(data.server); + 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); + pw_deinit(); + + return res; +}