Merge branch 'milan-avb-stabilizing-milan-extra' into 'master'

milan-avb: fixing MRP and connection, now stream to official device

See merge request pipewire/pipewire!2848
This commit is contained in:
hackerman-kl 2026-06-05 09:31:46 +02:00
commit 7e638bb971
20 changed files with 1506 additions and 188 deletions

View file

@ -79,4 +79,22 @@ struct avb_packet_aaf {
uint8_t payload[0];
} __attribute__ ((__packed__));
/* IEEE 1722-2016 Table 18: AAF nsr code to rate in Hz, 0 if user/reserved. */
static inline uint32_t avb_aaf_nsr_to_rate(uint8_t nsr)
{
switch (nsr) {
case AVB_AAF_PCM_NSR_8KHZ: return 8000;
case AVB_AAF_PCM_NSR_16KHZ: return 16000;
case AVB_AAF_PCM_NSR_24KHZ: return 24000;
case AVB_AAF_PCM_NSR_32KHZ: return 32000;
case AVB_AAF_PCM_NSR_44_1KHZ: return 44100;
case AVB_AAF_PCM_NSR_48KHZ: return 48000;
case AVB_AAF_PCM_NSR_88_2KHZ: return 88200;
case AVB_AAF_PCM_NSR_96KHZ: return 96000;
case AVB_AAF_PCM_NSR_176_4KHZ: return 176400;
case AVB_AAF_PCM_NSR_192KHZ: return 192000;
default: return 0;
}
}
#endif /* AVB_AAF_H */

View file

@ -53,6 +53,12 @@ static inline uint64_t peer_id_from_entity_id(uint64_t entity_id, uint16_t uniqu
return 0;
}
/* Non-EUI-64 entity_id (MAC|entity_index): high 48 bits are the MAC, swap the
* low 16 for unique_id. EUI-64 (FF:FE marker) rebuilds the stream_id below. */
if (((entity_id >> 24) & 0xFFFFULL) != 0xFFFEULL) {
return (entity_id & 0xFFFFFFFFFFFF0000ULL) | unique_id;
}
return (entity_id & 0xFFFFFF0000000000ULL) |
((entity_id & 0xFFFFFFULL) << 16) |
unique_id;
@ -69,11 +75,17 @@ static inline void clear_stream_binding(struct aecp_aem_stream_input_state_milan
sizeof(stream->stream_in_sta.common.stream.addr));
stream->stream_in_sta.common.stream.vlan_id = AVB_DEFAULT_VLAN;
stream->acmp_sta.talker_entity_id = 0;
stream->stream_in_sta.stream_info_dirty = true;
}
static inline uint64_t stream_talker_entity_id(const struct aecp_aem_stream_input_state_milan_v12 *s)
{
/* Prefer the talker entity_id stashed at BIND_RX (round-trip-safe); deriving it
* from the MSRP stream_id is lossy for non-EUI-64 entity_ids. */
if (s->acmp_sta.talker_entity_id != 0) {
return s->acmp_sta.talker_entity_id;
}
return entity_id_from_peer_id(be64toh(s->stream_in_sta.common.lstream_attr.attr.listener.stream_id));
}
@ -455,6 +467,7 @@ static void binding_save_parameters(struct acmp *acmp,
uint64_t stream_id = htobe64(peer_id_from_entity_id(be64toh(p->talker_guid), ntohs(p->talker_unique_id)));
stream->acmp_sta.controller_entity_id = be64toh(p->controller_guid);
stream->acmp_sta.talker_entity_id = be64toh(p->talker_guid);
stream->stream_in_sta.common.lstream_attr.attr.listener.stream_id = stream_id;
stream->stream_in_sta.common.tastream_attr.attr.talker.stream_id = stream_id;
stream->stream_in_sta.common.tfstream_attr.attr.talker_fail.talker.stream_id = stream_id;
@ -2444,6 +2457,38 @@ void acmp_periodic_milan_v12(struct acmp *acmp, uint64_t now)
stream_out->last_probe_rx_time = 0;
}
}
/* Milan Section 4.3.3.1 / 5.5.3: a settled Listener with no reservation yet
* (SETTLED_NO_RSV) re-evaluates Listener Ready against the Talker Advertise
* registrar each tick. After a bridge convergence delay the TA arrives with no
* fresh ACMP event, so this re-declares Ready and advances to SETTLED_RSV_OK the
* instant the TA is IN the stall self-heals, no controller re-bind needed. */
for (uint16_t desc_index = 0; desc_index < UINT16_MAX; desc_index++) {
struct descriptor *desc;
struct aecp_aem_stream_input_state_milan_v12 *si_m;
struct stream_common *common;
bool ta_in;
desc = server_find_descriptor(acmp->server, AVB_AEM_DESC_STREAM_INPUT,
desc_index);
if (desc == NULL)
break;
si_m = desc->ptr;
if (si_m->acmp_sta.fsm_acmp_state != FSM_ACMP_STATE_MILAN_V12_SETTLED_NO_RSV)
continue;
common = &si_m->stream_in_sta.common;
ta_in = common->tastream_attr.mrp != NULL &&
avb_mrp_attribute_get_registrar_state(common->tastream_attr.mrp) == AVB_MRP_IN;
common->lstream_attr.param = ta_in
? AVB_MSRP_LISTENER_PARAM_READY
: AVB_MSRP_LISTENER_PARAM_ASKING_FAILED;
if (common->lstream_attr.mrp != NULL)
avb_mrp_attribute_join(common->lstream_attr.mrp, now, true);
if (ta_in)
si_m->acmp_sta.fsm_acmp_state = FSM_ACMP_STATE_MILAN_V12_SETTLED_RSV_OK;
}
}
static const char *probing_status_name(uint8_t s)

View file

@ -360,7 +360,11 @@ static void emit_avb_interface_counters(struct aecp *aecp, uint16_t desc_index,
* here, gated by last_counters_emit_ns. */
#define COUNTER_UNSOL_MIN_INTERVAL_NS ((int64_t)SPA_NSEC_PER_SEC)
#define MEDIA_UNLOCK_TIMEOUT_NS ((int64_t)(2 * SPA_NSEC_PER_MSEC))
/* A frame is "missing" only after a gap far longer than event-loop scheduling
* jitter 2 ms (16 PDU periods) is processed-not-arrived jitter, not media
* loss, and spuriously unlocks a healthy stream on a busy software listener.
* 100 ms is ~5x the listener prefill and well above loop jitter. */
#define MEDIA_UNLOCK_TIMEOUT_NS ((int64_t)(100 * SPA_NSEC_PER_MSEC))
static bool counter_rate_limit_elapsed(int64_t now, int64_t last_emit)
{

View file

@ -141,6 +141,11 @@ int handle_cmd_set_clock_source_milan_v12(struct aecp *aecp, int64_t now,
/** Descriptor always keep the network endianness */
dclk_domain->clock_source_index = htons(clock_src_index);
/* milan-avb: apply the new selection to the data plane on the fly —
* (de)activate AAF media-clock recovery on the affected input streams. */
avb_stream_update_clock_source(server);
rc = reply_success(aecp, m, len);
if (rc) {
pw_log_error("Reply failed for set_clock_source\n");

View file

@ -182,10 +182,17 @@ struct aecp_aem_stream_input_state {
* most recent valid PDU; media_locked_state is the current edge. */
int64_t last_frame_rx_ns;
bool media_locked_state;
/* Settle window after a (re)lock: a Listener binding mid-stream behind an
* SRP bridge sees a one-time sequence step as the bridge opens forwarding a
* beat after the talker is already transmitting. Re-seed prev_seq for this
* many post-lock PDUs instead of counting it as SEQ_NUM_MISMATCH. */
uint8_t seq_settle;
};
struct acmp_stream_status_milan_v12 {
uint64_t controller_entity_id;
uint64_t talker_entity_id; /* IEEE 1722.1-2021 Section 7.4.6, BIND_RX_COMMAND talker_guid */
uint32_t acmp_flags;
uint8_t probing_status;
uint8_t acmp_status;

View file

@ -13,6 +13,7 @@
#include "aecp.h"
#include "aecp-aem-types.h"
#include "packets.h"
#include "aaf.h"
/* AVDECC stream_format decoder.
*
@ -23,12 +24,8 @@
* plane plus an `is_audio` shortcut so callers can skip non-media streams
* (CRF). Fields are 0 when not applicable.
*
* NOTE: bit-level decoding inside each subtype is incomplete. Today the
* decoder identifies the subtype reliably and falls back to the historical
* 8 ch / 48 kHz / 24-bit defaults for audio sufficient for current Milan
* builds where every stream_input_0 / stream_output_0 uses one of those
* formats. TODO Section H.1 (AAF) and Section F (IEC 61883-6 AM824) fields once a real
* conformance run needs other rates / channel counts. */
* AAF (Section H.1) decodes each field. IEC 61883-6 (Section F) still uses the
* historical 8 ch / 48 kHz defaults, TODO decode it. */
enum avb_aem_stream_format_kind {
AVB_AEM_STREAM_FORMAT_KIND_UNKNOWN = 0,
AVB_AEM_STREAM_FORMAT_KIND_AAF,
@ -44,6 +41,9 @@ struct avb_aem_stream_format_info {
uint16_t channels;
uint8_t bit_depth;
uint16_t samples_per_frame;
uint8_t format;
uint8_t nsr;
uint8_t sparse;
};
static inline void avb_aem_stream_format_decode(uint64_t fmt_be,
@ -57,15 +57,23 @@ static inline void avb_aem_stream_format_decode(uint64_t fmt_be,
out->channels = 0;
out->bit_depth = 0;
out->samples_per_frame = 0;
out->format = 0;
out->nsr = 0;
out->sparse = 0;
switch (out->subtype) {
case AVB_SUBTYPE_AAF:
/* Section H.1 quadlet: nsr[51:48] format[47:40] bit_depth[39:32]
* channels[31:22] samples_per_frame[21:12]. */
out->kind = AVB_AEM_STREAM_FORMAT_KIND_AAF;
out->is_audio = true;
out->rate = 48000;
out->channels = 8;
out->bit_depth = 24;
out->samples_per_frame = 6;
out->nsr = (uint8_t)((f >> 48) & 0x0F);
out->format = (uint8_t)((f >> 40) & 0xFF);
out->bit_depth = (uint8_t)((f >> 32) & 0xFF);
out->channels = (uint16_t)((f >> 22) & 0x3FF);
out->samples_per_frame = (uint16_t)((f >> 12) & 0x3FF);
out->sparse = AVB_AAF_PCM_SP_NORMAL;
out->rate = avb_aaf_nsr_to_rate(out->nsr);
break;
case AVB_SUBTYPE_61883_IIDC:
out->kind = AVB_AEM_STREAM_FORMAT_KIND_IEC_61883_6;

View file

@ -41,6 +41,10 @@ struct pw_avb *pw_avb_new(struct pw_context *context,
impl->context = context;
impl->loop = pw_context_get_main_loop(context);
/* Acquire an RT loop (data.rt class) so the talker flush timer is paced
* under SCHED_FIFO; main-loop scheduling jitter exceeds the 2ms class-A
* presentation margin and produces late timestamps at the listener. */
impl->data_loop = pw_context_acquire_loop(context, NULL);
impl->timer_queue = pw_context_get_timer_queue(context);
impl->props = props;
impl->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core);
@ -80,6 +84,9 @@ static void impl_free(struct impl *impl)
spa_list_consume(s, &impl->servers, link)
avdecc_server_free(s);
if (impl->data_loop != NULL) {
pw_context_release_loop(impl->context, impl->data_loop);
}
free(impl);
}

View file

@ -5,6 +5,9 @@
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/filter.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/if_link.h>
#include <linux/net_tstamp.h>
#include <limits.h>
#include <net/if.h>
@ -253,11 +256,187 @@ static int raw_transport_setup(struct server *server)
return 0;
}
/* milan-avb: hand-rolled netlink VLAN sub-iface creator.
*
* On I210-class NICs with rx-vlan-filter[fixed]=on the silicon drops
* VLAN-tagged frames whose VID is not registered. Registering happens
* implicitly when a VLAN sub-iface is added via RTM_NEWLINK. We create
* <parent>.<vid> on first use, never delete it (bounded leak: one
* sub-iface per SR class), and bind the listener stream socket to it.
*
* Fall back to PACKET_MR_PROMISC if any step fails (no CAP_NET_ADMIN,
* no 8021q module loaded, etc.). */
#define MILAN_NLALIGN(n) (((n) + 3U) & ~3U)
static int milan_nl_send(int fd, uint16_t mt, uint16_t flags,
uint32_t seq, const void *payload, size_t plen)
{
struct {
struct nlmsghdr nlh;
char body[2048];
} msg = { 0 };
size_t need = NLMSG_HDRLEN + plen;
if (need > sizeof(msg))
return -EMSGSIZE;
msg.nlh.nlmsg_len = need;
msg.nlh.nlmsg_type = mt;
msg.nlh.nlmsg_flags = flags;
msg.nlh.nlmsg_seq = seq;
msg.nlh.nlmsg_pid = 0;
if (plen)
memcpy(msg.body, payload, plen);
if (send(fd, &msg, need, 0) < 0)
return -errno;
return 0;
}
static int milan_nl_recv_ack(int fd, uint32_t seq)
{
char buf[4096];
for (;;) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
size_t off = 0;
if (n < 0)
return -errno;
while (off + NLMSG_HDRLEN <= (size_t)n) {
struct nlmsghdr *h = (struct nlmsghdr *)(buf + off);
if (h->nlmsg_len < NLMSG_HDRLEN ||
off + h->nlmsg_len > (size_t)n)
break;
if (h->nlmsg_type == NLMSG_ERROR && h->nlmsg_seq == seq) {
struct nlmsgerr *e = NLMSG_DATA(h);
return e->error;
}
off += NLMSG_ALIGN(h->nlmsg_len);
}
}
}
static size_t milan_nl_attr(char *p, uint16_t type, const void *val, uint16_t vlen)
{
struct rtattr *r = (struct rtattr *)p;
size_t total;
r->rta_len = RTA_LENGTH(vlen);
r->rta_type = type;
memcpy(RTA_DATA(r), val, vlen);
total = RTA_ALIGN(r->rta_len);
if (total > (size_t)r->rta_len)
memset(p + r->rta_len, 0, total - r->rta_len);
return total;
}
/* Returns ifindex (>0) or -errno. */
static int milan_get_ifindex(const char *name)
{
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
struct ifreq req;
int res, saved;
if (fd < 0)
return -errno;
spa_zero(req);
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", name);
res = ioctl(fd, SIOCGIFINDEX, &req);
saved = -errno;
close(fd);
return res < 0 ? saved : req.ifr_ifindex;
}
/* Ensure <parent>.<vid> exists, is UP, and is a VLAN sub-iface of <parent>.
* Writes the sub-iface name to out and returns 0 on success. */
static int milan_ensure_vlan_iface(const char *parent_ifname, uint16_t vid,
char *out, size_t out_size)
{
int rc, existing, parent_idx, nlfd, err, new_idx;
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
char payload[256];
char *p = payload;
struct ifinfomsg ifi = { 0 };
uint32_t pidx;
char nested[64];
char *np = nested;
const char kind[] = "vlan";
char data[16];
char *dp = data;
uint16_t vidv = vid;
uint32_t seq = 1;
struct ifinfomsg up_ifi = { 0 };
if (vid == 0 || vid >= 4095)
return -EINVAL;
rc = snprintf(out, out_size, "%s.%u", parent_ifname, (unsigned)vid);
if (rc < 0 || (size_t)rc >= out_size)
return -ENAMETOOLONG;
/* If the sub-iface already exists, assume it is what we want and reuse. */
existing = milan_get_ifindex(out);
if (existing > 0)
return 0;
parent_idx = milan_get_ifindex(parent_ifname);
if (parent_idx <= 0)
return parent_idx ? parent_idx : -ENODEV;
nlfd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
if (nlfd < 0)
return -errno;
if (bind(nlfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
int e = -errno; close(nlfd); return e;
}
/* RTM_NEWLINK: ifinfomsg + IFLA_IFNAME + IFLA_LINK + IFLA_LINKINFO{KIND=vlan, DATA{VLAN_ID}} */
ifi.ifi_family = AF_UNSPEC;
ifi.ifi_change = 0xFFFFFFFFu;
memcpy(p, &ifi, sizeof(ifi)); p += sizeof(ifi);
p += milan_nl_attr(p, IFLA_IFNAME, out, (uint16_t)(strlen(out) + 1));
pidx = (uint32_t)parent_idx;
p += milan_nl_attr(p, IFLA_LINK, &pidx, sizeof(pidx));
/* LINKINFO is nested: KIND=vlan, DATA={VLAN_ID=vid} */
np += milan_nl_attr(np, IFLA_INFO_KIND, kind, sizeof(kind));
dp += milan_nl_attr(dp, IFLA_VLAN_ID, &vidv, sizeof(vidv));
np += milan_nl_attr(np, IFLA_INFO_DATA, data, (uint16_t)(dp - data));
p += milan_nl_attr(p, IFLA_LINKINFO, nested, (uint16_t)(np - nested));
err = milan_nl_send(nlfd, RTM_NEWLINK,
NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK,
seq, payload, (size_t)(p - payload));
if (err == 0)
err = milan_nl_recv_ack(nlfd, seq);
if (err != 0 && err != -EEXIST) {
close(nlfd);
return err;
}
/* Bring it UP. */
new_idx = milan_get_ifindex(out);
if (new_idx <= 0) {
close(nlfd);
return new_idx ? new_idx : -ENODEV;
}
up_ifi.ifi_family = AF_UNSPEC;
up_ifi.ifi_index = new_idx;
up_ifi.ifi_flags = IFF_UP;
up_ifi.ifi_change = IFF_UP;
err = milan_nl_send(nlfd, RTM_NEWLINK, NLM_F_REQUEST | NLM_F_ACK,
++seq, &up_ifi, sizeof(up_ifi));
if (err == 0)
err = milan_nl_recv_ack(nlfd, seq);
close(nlfd);
if (err != 0)
return err;
return 0;
}
static int raw_stream_setup_socket(struct server *server, struct stream *stream)
{
int res;
char buf[128];
struct ifreq req;
const char *bind_ifname = server->ifname;
char vlan_ifname[IFNAMSIZ];
bool used_vlan_subiface = false;
spa_autoclose int fd = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, htons(ETH_P_ALL));
if (fd < 0) {
@ -265,8 +444,30 @@ static int raw_stream_setup_socket(struct server *server, struct stream *stream)
return -errno;
}
/* For listener RX: route stream via a VLAN sub-iface so the NIC's
* hardware filter accepts VID-tagged AAF without promisc on parent. */
/* Listener-only: route stream RX via a VLAN sub-iface so the NIC accepts
* VID-tagged AAF without promisc on parent. OUTPUT direction stays on
* the parent because setup_pdu_milan_v12() already inserts a manual
* 802.1Q tag in the PDU header the kernel would add a second tag
* (QinQ) if we bound the talker socket to enp6s0.<vid>. */
if (stream->direction == SPA_DIRECTION_INPUT && stream->vlan_id > 0) {
int e = milan_ensure_vlan_iface(server->ifname,
(uint16_t)stream->vlan_id,
vlan_ifname, sizeof(vlan_ifname));
if (e == 0) {
bind_ifname = vlan_ifname;
used_vlan_subiface = true;
pw_log_info("milan-avb: listener RX via VLAN sub-iface %s (vid %d)",
vlan_ifname, stream->vlan_id);
} else {
pw_log_warn("milan-avb: VLAN sub-iface setup failed (%d), "
"falling back to PACKET_MR_PROMISC on parent", -e);
}
}
spa_zero(req);
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", bind_ifname);
res = ioctl(fd, SIOCGIFINDEX, &req);
if (res < 0) {
pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
@ -279,23 +480,15 @@ static int raw_stream_setup_socket(struct server *server, struct stream *stream)
stream->sock_addr.sll_ifindex = req.ifr_ifindex;
if (stream->direction == SPA_DIRECTION_OUTPUT) {
struct sock_txtime txtime_cfg;
/* CBS/Qav-exclusive: set only the traffic-class priority so the egress
* CBS qdisc shapes this stream. SO_TXTIME (launch-time/ETF) is NOT set --
* CBS and SO_TXTIME cannot coexist on the same queue. */
res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
sizeof(stream->prio));
if (res < 0) {
pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
return -errno;
}
txtime_cfg.clockid = CLOCK_TAI;
txtime_cfg.flags = 0;
res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
sizeof(txtime_cfg));
if (res < 0) {
pw_log_error("setsockopt(SO_TXTIME) failed: %m");
return -errno;
}
} else {
struct packet_mreq mreq;
@ -319,6 +512,18 @@ static int raw_stream_setup_socket(struct server *server, struct stream *stream)
pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
return -errno;
}
/* Fallback: lift promisc only when the VLAN sub-iface path didn't
* take. With the sub-iface, the NIC accepts VID 2 natively. */
if (!used_vlan_subiface) {
spa_zero(mreq);
mreq.mr_ifindex = req.ifr_ifindex;
mreq.mr_type = PACKET_MR_PROMISC;
res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
&mreq, sizeof(struct packet_mreq));
if (res < 0)
pw_log_warn("setsockopt(PACKET_MR_PROMISC) fallback failed: %m");
}
}
return spa_steal_fd(fd);
}

View file

@ -4,6 +4,10 @@
/* SPDX-FileCopyrightText: Copyright © 2025 Simon Gapp <simon.gapp@kebag-logic.com> */
/* SPDX-License-Identifier: MIT */
#include <unistd.h>
#include <pipewire/pipewire.h>
#include "adp.h"
#include "aecp-aem.h"
#include "aecp-aem-types.h"
@ -300,23 +304,26 @@ static void init_descriptor_legacy_avb(struct server *server)
static void init_descriptor_milan_v12(struct server *server)
{
/* name the entity after the hostname so each box shows as pw0/pw1/pw2 */
char hostname[64] = {0};
if (gethostname(hostname, sizeof(hostname) - 1) != 0 || hostname[0] == '\0')
snprintf(hostname, sizeof(hostname), "%s", DSC_STRINGS_0_DEVICE_NAME);
// TODO PERSISTENCE: retrieve the saved buffers.
/**************************************************************************************/
/* IEEE 1722.1-2021, Sec. 7.2.12 - STRINGS Descriptor
* Up to 7 localized strings
*/
es_builder_add_descriptor(server, AVB_AEM_DESC_STRINGS, 0,
sizeof(struct avb_aem_desc_strings),
&(struct avb_aem_desc_strings)
{
.string_0 = DSC_STRINGS_0_DEVICE_NAME,
struct avb_aem_desc_strings strings = {
.string_1 = DSC_STRINGS_1_CONFIGURATION_NAME,
.string_2 = DSC_STRINGS_2_MANUFACTURER_NAME,
.string_3 = DSC_STRINGS_3_GROUP_NAME,
.string_4 = DSC_STRINGS_4_MAINTAINER_0,
.string_5 = DSC_STRINGS_4_MAINTAINER_1,
}
);
};
snprintf(strings.string_0, sizeof(strings.string_0), "%s", hostname);
es_builder_add_descriptor(server, AVB_AEM_DESC_STRINGS, 0,
sizeof(strings), &strings);
/**************************************************************************************/
/* IEEE 1722.1-2021, Sec. 7.2.11 - LOCALE Descriptor */
@ -334,8 +341,7 @@ static void init_descriptor_milan_v12(struct server *server)
/* Milan v1.2, Sec. 5.3.3.1 */
struct avb_entity_config entity_conf = conf_load_entity(server->impl->props);
struct avb_aem_desc_entity entity_desc =
{
struct avb_aem_desc_entity entity = {
.entity_id = htobe64(server->entity_id),
.entity_model_id = htobe64(DSC_ENTITY_MODEL_ID),
.entity_capabilities = htonl(entity_conf.entity_capabilities),
@ -351,21 +357,19 @@ static void init_descriptor_milan_v12(struct server *server)
.available_index = htonl(DSC_ENTITY_MODEL_AVAILABLE_INDEX),
.association_id = htobe64(DSC_ENTITY_MODEL_ASSOCIATION_ID),
.vendor_name_string = htons(entity_conf.vendor_name),
.model_name_string = htons(entity_conf.model_name),
.vendor_name_string = htons(DSC_ENTITY_MODEL_VENDOR_NAME_STRING),
.model_name_string = htons(DSC_ENTITY_MODEL_MODEL_NAME_STRING),
.group_name = DSC_ENTITY_MODEL_GROUP_NAME,
.serial_number = DSC_ENTITY_MODEL_SERIAL_NUMBER,
.configurations_count = htons(DSC_ENTITY_MODEL_CONFIGURATIONS_COUNT),
.current_configuration = htons(DSC_ENTITY_MODEL_CURRENT_CONFIGURATION)
};
memcpy(entity_desc.entity_name, entity_conf.entity_name, sizeof(entity_desc.entity_name));
memcpy(entity_desc.firmware_version, entity_conf.firmware_version, sizeof(entity_desc.firmware_version));
memcpy(entity_desc.group_name, entity_conf.group_name, sizeof(entity_desc.group_name));
memcpy(entity_desc.serial_number, entity_conf.serial_number, sizeof(entity_desc.serial_number));
snprintf(entity.entity_name, sizeof(entity.entity_name), "%s", hostname);
/* firmware_version = the canonical PipeWire library version */
snprintf(entity.firmware_version, sizeof(entity.firmware_version), "%s", pw_get_library_version());
es_builder_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
sizeof(struct avb_aem_desc_entity),
&entity_desc);
sizeof(entity), &entity);
/**************************************************************************************/
/* IEEE 1722.1-2021, Sec. 7.2.2 - CONFIGURATION Descriptor*/
/* Milan v1.2, Sec. 5.3.3.2 */

View file

@ -319,9 +319,11 @@ BUILD_SAMPLING_RATE(DSC_AUDIO_UNIT_SAMPLING_RATE_PULL, DSC_AUDIO_UNIT_SAMPLING_R
#define DSC_STREAM_INPUT_LOCALIZED_DESCRIPTION AVB_AEM_DESC_INVALID
#define DSC_STREAM_INPUT_CLOCK_DOMAIN_INDEX 0
#define DSC_STREAM_INPUT_STREAM_FLAGS (AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE | AVB_AEM_DESC_STREAM_FLAG_CLASS_A)
// To match my talker
// Default current_format = AAF INT32/48k/8ch, to match the 8-channel PipeWire talker
// (pw1) without a per-bind set-format. The supported list below keeps the smaller
// channel counts (1/2/4/6) so 4ch talkers like the DS20 still bind via set-format.
// TODO: Define based on AUDIO_UNIT etc.
#define DSC_STREAM_INPUT_CURRENT_FORMAT 0x0205022001006000ULL
#define DSC_STREAM_INPUT_CURRENT_FORMAT 0x0205022002006000ULL
// TODO: Is 132 here, should be 138 according to spec
#define DSC_STREAM_INPUT_FORMATS_OFFSET (4 + sizeof(struct avb_aem_desc_stream))
@ -342,7 +344,7 @@ BUILD_SAMPLING_RATE(DSC_AUDIO_UNIT_SAMPLING_RATE_PULL, DSC_AUDIO_UNIT_SAMPLING_R
#define DSC_STREAM_INPUT_AVB_INTERFACE_INDEX 0
#define DSC_STREAM_INPUT_BUFFER_LENGTH_IN_NS 2126000
#define DSC_STREAM_INPUT_FORMATS_0 DSC_STREAM_INPUT_CURRENT_FORMAT
#define DSC_STREAM_INPUT_FORMATS_0 0x0205022001006000ULL /* 4ch (DS20) — kept supported */
#define DSC_STREAM_INPUT_FORMATS_1 0x0205022000406000ULL
#define DSC_STREAM_INPUT_FORMATS_2 0x0205022000806000ULL
#define DSC_STREAM_INPUT_FORMATS_3 0x0205022001806000ULL

View file

@ -122,6 +122,15 @@ static struct descriptor *es_buidler_desc_avb_interface(struct server *server,
avb_mrp_attribute_begin(if_ptr->domain_attr.mrp, 0);
avb_mrp_attribute_join(if_ptr->domain_attr.mrp, 0, true);
/* milan-avb: declare VID membership (MVRP) once per interface, held for the
* life of the interface like the SR Domain above NOT per stream, so that
* destroying one stream cannot withdraw the VLAN other streams still need. */
avb_mvrp_attribute_new(server->mvrp, &if_ptr->vlan_attr,
AVB_MVRP_ATTRIBUTE_TYPE_VID);
if_ptr->vlan_attr.attr.vid.vlan = htons(AVB_DEFAULT_VLAN);
avb_mrp_attribute_begin(if_ptr->vlan_attr.mrp, 0);
avb_mrp_attribute_join(if_ptr->vlan_attr.mrp, 0, true);
return desc;
}

View file

@ -0,0 +1,132 @@
/* AVB support */
/* SPDX-FileCopyrightText: Copyright © 2025 Kebag-Logic */
/* SPDX-License-Identifier: MIT */
/* gPTP time read from the NIC PHC (dynamic POSIX clock) mapped onto CLOCK_MONOTONIC_RAW,
* decoupled from the system wall clock so it stays free for NTP. */
#ifndef AVB_GPTP_CLOCK_H
#define AVB_GPTP_CLOCK_H
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <spa/utils/defs.h>
#define AVB_CLOCKFD 3
#define AVB_FD_TO_CLOCKID(fd) ((~(clockid_t)(fd) << 3) | AVB_CLOCKFD)
#define AVB_GPTP_REFRESH_NS (10 * SPA_NSEC_PER_MSEC) /* re-anchor phase/freq ~100 Hz */
#define AVB_GPTP_READ_BRACKET_NS (50 * SPA_NSEC_PER_USEC) /* reject a jittered PHC read */
struct avb_gptp_clock {
int phc_fd;
clockid_t phc_id;
bool ok;
uint64_t base_mono; /* CLOCK_MONOTONIC_RAW ns at last anchor */
uint64_t base_gptp; /* PHC ns at last anchor */
double ratio; /* d(phc)/d(mono) ~ 1.0 (the frequency offset) */
uint64_t last_refresh_mono;
};
/* Resolve ifname -> PHC index via ETHTOOL_GET_TS_INFO, open /dev/ptpN. >=0 = phc_index. */
static inline int avb_gptp_clock_open(struct avb_gptp_clock *c, const char *ifname)
{
struct ethtool_ts_info tsi;
struct ifreq ifr;
char path[32];
int sock;
memset(c, 0, sizeof(*c));
c->phc_fd = -1;
c->ratio = 1.0;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
return -1;
}
memset(&tsi, 0, sizeof(tsi));
tsi.cmd = ETHTOOL_GET_TS_INFO;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
ifr.ifr_data = (void *)&tsi;
if (ioctl(sock, SIOCETHTOOL, &ifr) < 0) {
close(sock);
return -1;
}
close(sock);
if (tsi.phc_index < 0) {
return -1;
}
snprintf(path, sizeof(path), "/dev/ptp%d", tsi.phc_index);
c->phc_fd = open(path, O_RDONLY);
if (c->phc_fd < 0) {
return -1;
}
c->phc_id = AVB_FD_TO_CLOCKID(c->phc_fd);
c->ok = true;
return tsi.phc_index;
}
/* Re-anchor (mono,gptp) and update the frequency ratio. Off the hot loop (~100 Hz). */
static inline void avb_gptp_clock_refresh(struct avb_gptp_clock *c)
{
struct timespec m1, p, m2;
uint64_t mono, gptp;
double r;
if (!c->ok) {
return;
}
if (clock_gettime(CLOCK_MONOTONIC_RAW, &m1) < 0 ||
clock_gettime(c->phc_id, &p) < 0 ||
clock_gettime(CLOCK_MONOTONIC_RAW, &m2) < 0) {
return;
}
if (SPA_TIMESPEC_TO_NSEC(&m2) - SPA_TIMESPEC_TO_NSEC(&m1) > AVB_GPTP_READ_BRACKET_NS) {
return;
}
mono = (SPA_TIMESPEC_TO_NSEC(&m1) + SPA_TIMESPEC_TO_NSEC(&m2)) / 2;
gptp = SPA_TIMESPEC_TO_NSEC(&p);
if (c->base_mono != 0 && mono > c->base_mono) {
r = (double)(gptp - c->base_gptp) / (double)(mono - c->base_mono);
if (r > 0.999 && r < 1.001) {
c->ratio += 0.10 * (r - c->ratio);
}
}
c->base_mono = mono;
c->base_gptp = gptp;
c->last_refresh_mono = mono;
}
/* gPTP time now, in ns; cheap monotonic read + phase/freq map. 0 if no PHC (caller falls back). */
static inline uint64_t avb_gptp_now(struct avb_gptp_clock *c)
{
struct timespec ts;
uint64_t mono;
if (!c->ok) {
return 0;
}
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
mono = SPA_TIMESPEC_TO_NSEC(&ts);
if (c->base_mono == 0 || mono - c->last_refresh_mono > AVB_GPTP_REFRESH_NS) {
avb_gptp_clock_refresh(c);
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
mono = SPA_TIMESPEC_TO_NSEC(&ts);
}
if (c->base_mono == 0) {
return 0;
}
return c->base_gptp + (uint64_t)((double)(mono - c->base_mono) * c->ratio);
}
#endif /* AVB_GPTP_CLOCK_H */

View file

@ -230,14 +230,14 @@ static void gptp_invalidate_state(struct gptp *gptp)
gptp->path_trace_valid;
struct avb_aem_desc_avb_interface *iface;
gptp->data_valid = false;
/* keep last-known GM/data_valid across transient ptp4l query timeouts (see avb_gptp_get_grandmaster_id) */
gptp->data_valid_current = false;
gptp->path_trace_valid = false;
gptp->path_trace_count = 0;
gptp->steps_removed = 0;
gptp->offset_from_master_scaled_ns = 0;
memset(gptp->clock_id, 0, sizeof(gptp->clock_id));
memset(gptp->gm_id, 0, sizeof(gptp->gm_id));
/* gm_id kept (last-known grandmaster) so the ADP does not flap to self on a missed query */
memset(gptp->path_trace, 0, sizeof(gptp->path_trace));
iface = get_avb_interface(gptp);

View file

@ -9,6 +9,8 @@
#include <pipewire/pipewire.h>
#include "gptp-clock.h"
#ifdef __cplusplus
extern "C" {
#endif
@ -38,6 +40,7 @@ struct avb_transport_ops {
struct impl {
struct pw_loop *loop;
struct pw_loop *data_loop; /* RT (SCHED_FIFO) loop for talker egress pacing */
struct pw_timer_queue *timer_queue;
struct pw_context *context;
struct spa_hook context_listener;
@ -104,6 +107,11 @@ struct server {
uint64_t entity_id;
int ifindex;
/* milan-avb: gPTP time read from the NIC PHC (server->ifname), decoupled from the
* system clock. Lazily opened on first use; gclock_tried guards the one-shot open. */
struct avb_gptp_clock gclock;
unsigned gclock_tried:1;
const struct avb_transport_ops *transport;
void *transport_data;

View file

@ -0,0 +1,113 @@
/* AVB support */
/* SPDX-FileCopyrightText: Copyright © 2025 Kebag-Logic */
/* SPDX-License-Identifier: MIT */
/*
* mc-recover.h AAF media-clock recovery estimator (listener side).
*
* Self-contained and pure (no PipeWire/stream deps) so it can be unit-tested
* in isolation. A second-order DLL (spa_dll) recovers the talker media rate
* from the AAF avtp_timestamp progression: each PDU carries a presentation
* time in the talker's gPTP domain, advancing by frames_per_pdu samples. The
* model clock advances by the DLL-corrected period; the phase error against the
* received timestamp drives the DLL. Recovered rate = nominal / corr.
*/
#ifndef AVB_MC_RECOVER_H
#define AVB_MC_RECOVER_H
#include <stdint.h>
#include <stdbool.h>
#include <spa/utils/dll.h>
struct mc_recover {
bool init;
struct spa_dll dll;
double corr; /* DLL output (period multiplier, ~1.0) */
double rate; /* recovered media rate, Hz */
int32_t last_err_ns; /* last phase error (model vs avtp_ts), ns */
uint64_t model_ns; /* model presentation clock (DLL-tracked) */
uint32_t last_avtp_ts; /* previous fed timestamp; model advances by actual PDU count */
uint64_t pdus; /* PDUs since prime */
};
static inline void mc_recover_reset(struct mc_recover *m, double nominal_rate)
{
m->init = false;
m->corr = 1.0;
m->rate = nominal_rate;
m->last_err_ns = 0;
m->model_ns = 0;
m->last_avtp_ts = 0;
m->pdus = 0;
}
/* Feed one PDU's presentation timestamp (low 32 bits of CLOCK_TAI ns). Returns
* the recovered media rate in Hz. nominal_rate/frames_per_pdu/pdu_period_ns
* describe the stream's nominal media clock. */
static inline double mc_recover_update(struct mc_recover *m, uint32_t avtp_ts,
int frames_per_pdu, int nominal_rate, int64_t pdu_period_ns)
{
int32_t err_ns;
double err_samples;
uint64_t step;
int32_t raw_delta;
int n_pdus;
if (!m->init) {
spa_dll_init(&m->dll);
spa_dll_set_bw(&m->dll, SPA_DLL_BW_MIN, frames_per_pdu, nominal_rate);
m->corr = 1.0;
m->rate = nominal_rate;
m->last_err_ns = 0;
m->model_ns = avtp_ts;
m->last_avtp_ts = avtp_ts;
m->pdus = 0;
m->init = true;
return m->rate;
}
/* Advance the model by the ACTUAL number of nominal PDUs elapsed since the
* last fed timestamp (avtp_ts delta rounded to pdu_period), then measure the
* phase error. Using the real PDU count (not a fixed one-per-call) keeps a
* non-1:1 feed dropped or coalesced PDUs from accumulating phase error
* and saturating the loop into a ±ppm hunt. err>0 = talker ahead of model;
* spa_dll returns corr<1, step grows, model catches up (negative feedback);
* recovered rate = nominal*corr. A large jump (>8 PDUs: stream gap, reorder,
* or the bind-transient seed) re-seeds the phase rather than slewing the
* deliberately-slow loop, which otherwise wedges it. */
raw_delta = (int32_t)(avtp_ts - m->last_avtp_ts);
m->last_avtp_ts = avtp_ts;
n_pdus = (int)((double)raw_delta / (double)pdu_period_ns + 0.5);
if (n_pdus < 1)
n_pdus = 1;
if (n_pdus > 8) {
m->model_ns = avtp_ts;
m->last_err_ns = 0;
m->pdus++;
return m->rate;
}
step = (uint64_t)((double)n_pdus * (double)pdu_period_ns / m->corr + 0.5);
m->model_ns += step;
err_ns = (int32_t)(avtp_ts - (uint32_t)m->model_ns);
m->last_err_ns = err_ns;
err_samples = (double)err_ns * (double)nominal_rate / 1e9;
/* bound the response to a single corrupt/late timestamp */
if (err_samples > 128.0)
err_samples = 128.0;
else if (err_samples < -128.0)
err_samples = -128.0;
m->corr = spa_dll_update(&m->dll, err_samples);
/* clamp to ±10 % — far beyond any real media clock; guards 1/corr */
if (m->corr < 0.9)
m->corr = 0.9;
else if (m->corr > 1.1)
m->corr = 1.1;
m->rate = (double)nominal_rate * m->corr;
m->pdus++;
return m->rate;
}
#endif /* AVB_MC_RECOVER_H */

View file

@ -108,11 +108,10 @@ static void mrp_periodic(void *data, uint64_t now)
if (now > mrp->lva_timer.leave_all_timeout) {
/* 802.1Q-2014 Table 10-5 */
/* IEEE 802.1Q-2018 Section 10.7.5.20: own LVA timer => TX path only, no RX_LVA */
mrp->lva_timer.state = FSM_LVA_ACTIVE;
if (mrp->lva_timer.leave_all_timeout > 0) {
mrp->lva_tx_pending = true;
global_event(mrp, now, AVB_MRP_EVENT_RX_LVA);
leave_all = true;
}
}
@ -435,7 +434,6 @@ void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now
break;
case AVB_MRP_EVENT_RX_LV:
case AVB_MRP_EVENT_RX_LVA:
case AVB_MRP_EVENT_TX_LVA:
case AVB_MRP_EVENT_REDECLARE:
switch (state) {
case AVB_MRP_IN:
@ -444,6 +442,9 @@ void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now
break;
}
break;
case AVB_MRP_EVENT_TX_LVA:
/* IEEE 802.1Q-2018 Table 10-4: TX events do not transition the registrar */
break;
case AVB_MRP_EVENT_FLUSH:
switch (state) {
case AVB_MRP_LV:
@ -463,11 +464,10 @@ void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now
default:
break;
}
if (notify) {
mrp_attribute_emit_notify(a, now, notify);
mrp_emit_notify(mrp, now, &a->attr, notify);
}
/* commit registrar_state BEFORE notify: callbacks (e.g. notify_talker ->
* refresh_listener_param) read the registrar state, so they must see the new
* one. Emitting notify first made the Listener latch AskingFailed off a stale
* MT state and never recompute -> SRP bridge refused to forward the stream. */
if (a->registrar_state != state || notify) {
pw_log_debug("REG: attr %p: %s %s %s -> %s notify=%s", a, a->attr.name,
avb_mrp_event_name(event), avb_registrar_state_name(a->registrar_state),
@ -476,6 +476,11 @@ void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now
a->registrar_state = state;
}
if (notify) {
mrp_attribute_emit_notify(a, now, notify);
mrp_emit_notify(mrp, now, &a->attr, notify);
}
state = a->applicant_state;
switch (event) {

View file

@ -58,6 +58,16 @@ static void debug_msrp_talker(const struct avb_packet_msrp_talker *t)
/* IEEE 802.1Q Section 35.2.2.4.4: Listener may declare Ready only once the matching
* Talker Advertise is registered; otherwise it stays in AskingFailed. */
/* Milan v1.2 Section 4.3.3.1: Listener_Ready iff Talker Advertise registrar IN, else AskingFailed */
static void refresh_listener_param(struct stream_common *sc)
{
bool ta_in = sc->tastream_attr.mrp != NULL &&
avb_mrp_attribute_get_registrar_state(sc->tastream_attr.mrp) == AVB_MRP_IN;
sc->lstream_attr.param = ta_in
? AVB_MSRP_LISTENER_PARAM_READY
: AVB_MSRP_LISTENER_PARAM_ASKING_FAILED;
}
static void notify_talker(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
{
struct stream_common *sc;
@ -76,12 +86,8 @@ static void notify_talker(struct msrp *msrp, uint64_t now, struct attr *attr, ui
sc = SPA_CONTAINER_OF(attr->attr, struct stream_common, tastream_attr);
if (sc->stream.direction == SPA_DIRECTION_INPUT) {
if (notify == AVB_MRP_NOTIFY_NEW || notify == AVB_MRP_NOTIFY_JOIN)
sc->lstream_attr.param = AVB_MSRP_LISTENER_PARAM_READY;
else if (notify == AVB_MRP_NOTIFY_LEAVE)
sc->lstream_attr.param = AVB_MSRP_LISTENER_PARAM_ASKING_FAILED;
/* Milan Table 5.10: TA registrar state flips flags_ex.REGISTERING
* in the listener-side GET_STREAM_INFO answer emit an unsol. */
refresh_listener_param(sc);
/* Milan Table 5.10: TA registrar state flips flags_ex.REGISTERING */
avb_aecp_aem_mark_stream_info_dirty(msrp->server,
AVB_AEM_DESC_STREAM_INPUT, sc->stream.index);
}
@ -101,12 +107,12 @@ static void notify_talker_failed(struct msrp *msrp, uint64_t now, struct attr *a
handle_evt_tk_registration_failed(msrp->server->acmp, attr->attr, now);
}
/* Milan Table 5.10: TF registrar state also flips flags_ex.REGISTERING
* on the listener side; emit an unsol when it changes. */
sc = SPA_CONTAINER_OF(attr->attr, struct stream_common, tfstream_attr);
if (sc->stream.direction == SPA_DIRECTION_INPUT)
if (sc->stream.direction == SPA_DIRECTION_INPUT) {
refresh_listener_param(sc);
avb_aecp_aem_mark_stream_info_dirty(msrp->server,
AVB_AEM_DESC_STREAM_INPUT, sc->stream.index);
}
}
static int process_talker(struct msrp *msrp, uint64_t now, uint8_t attr_type,
@ -325,18 +331,22 @@ static int process_domain(struct msrp *msrp, uint64_t now, uint8_t attr_type,
continue;
}
if (msrp->server->avb_mode == AVB_MODE_MILAN_V12)
{
/** Milan V1.2 Section 4.2.7.2.1:
The endstation shall re-adjust the domain according
to the from the MSRPDU received on its interface */
bool mismatch = (a->attr->attr.domain.sr_class_id != d->sr_class_id
|| a->attr->attr.domain.sr_class_priority != d->sr_class_priority
if (msrp->server->avb_mode == AVB_MODE_MILAN_V12) {
/* Milan v1.2 Section 4.2.7.2.1: re-adjust scoped to matching sr_class_id only */
bool mismatch;
if (a->attr->attr.domain.sr_class_id != d->sr_class_id)
continue;
mismatch = (a->attr->attr.domain.sr_class_priority != d->sr_class_priority
|| a->attr->attr.domain.sr_class_vid != d->sr_class_vid);
if (mismatch) {
pw_log_info("Domain mismatch re-adjusting");
a->attr->attr.domain = *d;
pw_log_info("Domain re-adjust sr_class_id=%u prio %u->%u vid %u->%u",
d->sr_class_id, a->attr->attr.domain.sr_class_priority,
d->sr_class_priority, ntohs(a->attr->attr.domain.sr_class_vid),
ntohs(d->sr_class_vid));
a->attr->attr.domain.sr_class_priority = d->sr_class_priority;
a->attr->attr.domain.sr_class_vid = d->sr_class_vid;
avb_mrp_attribute_leave(a->attr->mrp, now);
avb_mrp_attribute_begin(a->attr->mrp, now);
avb_mrp_attribute_join(a->attr->mrp, now, true);

View file

@ -0,0 +1,63 @@
/* AVB support */
/* SPDX-FileCopyrightText: Copyright © 2025 Kebag-Logic */
/* SPDX-License-Identifier: MIT */
/*
* play-loop.h consume-side actuator for the listener.
*
* Pure (no PipeWire deps) so it can be unit-tested like mc-recover.h. Keeps the
* listener ring at a target fill by trimming the output resampler ratio
* (SPA_IO_RateMatch). Same loop as module-rtp's receiver:
* error = target - avail; corr = spa_dll_update(dll, error); rate = ff / corr
* ff = nominal/recovered rate feeds the recovered clock forward; the DLL trims
* the rest. The sign is the trap (same as the old mc_recover bug); test_play_loop
* locks it: converges on the right sign, diverges on the wrong one.
*/
#ifndef AVB_PLAY_LOOP_H
#define AVB_PLAY_LOOP_H
#include <stdbool.h>
#include <spa/utils/defs.h>
#include <spa/utils/dll.h>
struct play_loop {
bool init;
struct spa_dll dll;
double corr; /* DLL output, ~1.0 */
double rate; /* last applied resampler ratio (ff / corr) */
};
static inline void play_loop_reset(struct play_loop *p)
{
p->init = false;
p->corr = 1.0;
p->rate = 1.0;
}
/* One step. error_samples = target - avail; max_error clamps a transient;
* ff_ratio = nominal/recovered rate (1.0 = no feedforward). Returns the ratio
* to apply via pw_stream_set_rate(). */
static inline double play_loop_update(struct play_loop *p, double error_samples,
double max_error, double ff_ratio, unsigned period, unsigned rate)
{
if (!p->init) {
spa_dll_init(&p->dll);
spa_dll_set_bw(&p->dll, SPA_DLL_BW_MIN, period, rate);
p->corr = 1.0;
p->rate = ff_ratio;
p->init = true;
}
error_samples = SPA_CLAMPD(error_samples, -max_error, max_error);
p->corr = spa_dll_update(&p->dll, error_samples);
/* clamp ±10 %, guards 1/corr */
if (p->corr < 0.9)
p->corr = 0.9;
else if (p->corr > 1.1)
p->corr = 1.1;
p->rate = ff_ratio / p->corr;
return p->rate;
}
#endif /* AVB_PLAY_LOOP_H */

File diff suppressed because it is too large Load diff

View file

@ -12,10 +12,15 @@
#include <spa/utils/ringbuffer.h>
#include <spa/param/audio/format.h>
#include <spa/node/io.h>
#include "mc-recover.h"
#include "play-loop.h"
#include <pipewire/pipewire.h>
#define BUFFER_SIZE (1u<<16)
/* milan-avb: the talker ring must hold SEVERAL graph quanta so the burst-fill (one quantum/cycle) and the 125us flush-drain decouple; at 1<<16 the ring equalled ONE quantum so each cycle overwrote unread samples (~40 glitches/s). 1<<18 = 4 quanta. */
#define BUFFER_SIZE (1u<<18)
#define BUFFER_MASK (BUFFER_SIZE-1)
struct stream {
@ -37,7 +42,19 @@ struct stream {
struct spa_source *source;
struct spa_source *flush_timer;
uint64_t flush_last_ns;
/* milan-avb: self-driving timer (the node IS the graph DRIVER); a data-loop one-shot fills io_position->clock, re-arms to drive_next_time, and triggers one graph cycle (pipe-tunnel pattern) so the graph fill and AVTP egress share one clock (no follower-resampler FM). */
struct spa_source *drive_timer;
uint64_t drive_next_time;
bool driving;
uint64_t drive_phc_last;
uint64_t drive_mono_last;
double drive_ratio_ema; /* M2: EMA of PHC/MONOTONIC rate ratio */
/* M2: monotonic AVTP presentation-timestamp accumulator (PHC domain); advances by exactly pdu_period per PDU, slow-leaked toward the live PHC, so the listener's recovered media clock stays jitter-free (per-tick PHC reads inject interpolation jitter, audible FM). */
uint64_t tx_pts;
bool is_crf;
uint64_t next_txtime;
int prio;
int mtt;
int t_uncertainty;
@ -66,6 +83,28 @@ struct stream {
uint64_t format;
uint32_t stride;
struct spa_audio_info info;
/* milan-avb: AAF media-clock recovery (listener / STREAM_INPUT only); active only while the CLOCK_DOMAIN selects the AAF (INPUT_STREAM) source pointing at this stream; recovered from avtp_timestamp deltas (mc-recover.h). */
bool mc_aaf_active;
struct mc_recover mc;
/* milan-avb: actuator I/O areas (set via .io_changed); io_rate_match is the resampler knob, NULL unless the adapter inserted a resampler. */
struct spa_io_rate_match *io_rate_match;
struct spa_io_position *io_position;
/* milan-avb: previous 1 Hz sample for the local consume-rate log. */
uint64_t play_last_consume_tai;
uint64_t play_last_ticks;
uint64_t play_log_last_ns;
bool play_primed;
/* milan-avb: actuator state; servos the ring to play_target (play-loop.h). */
struct play_loop play;
int32_t play_target;
/* milan-avb: optional raw PCM dump for offline analysis (THDN, waveform). */
FILE *raw_dump_fp;
size_t raw_dump_bytes;
};
#include "msrp.h"
@ -81,4 +120,7 @@ int stream_activate(struct stream *stream, uint16_t index, uint64_t now);
int stream_deactivate(struct stream *stream, uint64_t now);
int stream_activate_virtual(struct stream *stream, uint16_t index);
/* milan-avb: re-evaluate each input stream's media-clock recovery against the current CLOCK_DOMAIN selection; call after SET_CLOCK_SOURCE for on-the-fly clock-source switching. */
void avb_stream_update_clock_source(struct server *server);
#endif /* AVB_STREAM_H */