mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-04-09 08:21:08 -04:00
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:
parent
14310e66fe
commit
f5259828b6
2 changed files with 327 additions and 0 deletions
|
|
@ -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
283
src/tools/pw-avb-virtual.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue