tools: add pw-avb-virtual for virtual AVB graph nodes

Add a standalone tool that creates virtual AVB talker/listener endpoints
visible in the PipeWire graph (e.g. Helvum). Uses the loopback transport
so no AVB hardware or network access is needed.

The sink node consumes audio silently, the source produces silence.
Supports --milan flag for Milan v1.2 mode and --name for custom node
name prefix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian F.K. Schaller 2026-04-07 17:39:22 -04:00 committed by Wim Taymans
parent 14310e66fe
commit f5259828b6
2 changed files with 327 additions and 0 deletions

View file

@ -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',

283
src/tools/pw-avb-virtual.c Normal file
View file

@ -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 <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <getopt.h>
#include <locale.h>
#include <spa/utils/result.h>
#include <pipewire/pipewire.h>
#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;
}