mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-06-13 14:33:03 -04:00
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:
commit
7e638bb971
20 changed files with 1506 additions and 188 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
132
src/modules/module-avb/gptp-clock.h
Normal file
132
src/modules/module-avb/gptp-clock.h
Normal 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 */
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
113
src/modules/module-avb/mc-recover.h
Normal file
113
src/modules/module-avb/mc-recover.h
Normal 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 */
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
63
src/modules/module-avb/play-loop.h
Normal file
63
src/modules/module-avb/play-loop.h
Normal 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
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue