dante-protocol-notes.md 13 KB

Dante protocol notes (clean-room)

Behavioural notes on how Dante devices behave on the wire, written from understanding gained by reading reference/inferno — not translated or copied from it. See the licensing rule in CLAUDE.md before adding to this file.

Write what a Dante device does, in our own words (message purpose, timing, field meanings) — not how Inferno's code does it.

Status: first pass, derived from reading Inferno (an unofficial, reverse-engineered implementation) plus the public dante-aes67-relay.js gist. Dante is proprietary and undocumented; everything here is reverse-engineered and partly guessed. Confirm every value against a real packet capture before implementing. Byte-exact struct layouts are deliberately not transcribed here — capture them fresh when writing the code, so the implementation is genuinely independent.

Big picture

A Dante device exposes several independent UDP services and advertises them over mDNS. Discovery and control are separate from the audio itself:

  • Discovery — mDNS/DNS-SD advertises the device and each of its channels.
  • Control (ARC) — a controller (e.g. Dante Controller) or a peer device queries capabilities, channel lists, and routing (which TX channel feeds which RX channel) over a request/response UDP protocol.
  • Flow setup — before audio moves, a receiver asks a transmitter to open an audio "flow" (a bundle of channels to a destination socket), which must then be kept alive.
  • Media transport — the actual PCM audio, unicast or multicast, in Dante's own lightweight framing (close to, but not, AES67 RTP).
  • Clocking — PTP keeps every device sample-accurate. Legacy Dante uses PTPv1; AES67 mode uses PTPv2.

Names Inferno uses (worth knowing because captures/tools use them too): the control protocol is ARC, device discovery/announce is CMC, and the media clock control protocol is dbcp.

UDP ports (standard, as seen on hardware)

Service Port Role
ARC (control / routing) 4440 capability + channel queries, subscriptions
CMC (device announce) 8800 device-advertisement request/response
Flow control (dbcp) 4455 open/update/stop audio flows
Info / multicast 8700 channel-change notifications, info requests
mDNS 5353 discovery
Media (RTP-ish) 5004 (default) PCM audio; actual port is negotiated

Multiple instances on one IP shift the first four ports by an offset (Dante Controller and peers learn the real ports from mDNS TXT / CMC, so they don't have to be the standard ones). Media source ports in particular are OS-allocated and only knowable from the flow-setup exchange.

All control messages are big-endian.

Discovery (mDNS / DNS-SD)

Everything is under _udp.local. A device publishes several service types:

  • _netaudio-arc._udp — the device's control endpoint. Points at the ARC port. TXT keys include arcp_vers / arcp_min (ARC protocol version and minimum), router_vers, router_info (board name), mf (manufacturer), model. Advertised with a long TTL (~4500 s).
  • _netaudio-cmc._udp — the device announce endpoint (CMC port). TXT keys include id (the 8-byte factory device ID as hex — typically the MAC padded to 8 bytes), process (process ID, distinguishes multiple instances on one IP), cmcp_vers/cmcp_min, server_vers, a channels bitfield, mf, model.
  • _netaudio-chan._udp — one instance per TX channel. The service name is <channelname>@<hostname>. Points at the flow-control port. TXT keys describe how to receive that channel: txtvers, dbcp1 (flow-control start code, e.g. 0x1102), dbcp, id (1-based channel index), rate (sample rate), pcm (bytes-per-sample + PCM type), enc/en (bit depth), latency_ns, fpp (frames-per-packet range advertised as max,min), nchan (max channels per flow), and default on the factory-name copy. A channel with a user-assigned friendly name is advertised twice (factory name and friendly name).
  • _netaudio-bund._udp — a multicast bundle (a pre-made multicast flow other devices can join). TXT keys: id, nchan, latency_ns, fpp, rate, enc, and crucially a.0 (destination multicast IP) and p.0 (destination port) — so a receiver can subscribe to a multicast without a unicast flow-setup handshake.

So a controller learns the whole routing picture from mDNS alone: which devices exist, what channels each transmits, and how to reach them.

Inferno also publishes an in-addr.local record with an _inferno-response-origin TXT to reserve a multicast address; that part is Inferno-specific bookkeeping, not core Dante behaviour.

Control protocol (ARC — routing / subscriptions)

Request/response over UDP on the ARC port (4440). Every message starts with a 10-byte header followed by a content blob:

u16 start_code     # protocol/version marker
u16 total_length   # header + content
u16 seqnum         # echoed in the response, matches replies to requests
u16 opcode1        # which operation
u16 opcode2        # in a request: 0; in a response: status (1 = OK)
... content ...

The responder echoes start_code, seqnum, and opcode1, and sets opcode2 to a status: 1 = OK. A response code of 0x8112 means "more results exist" — large lists (channel enumerations) are paginated: the request carries a 1-based start index and the reply signals whether to ask again. Other non-1 codes are errors.

Operations (opcode1 values observed)

opcode1 Operation Purpose
0x1000 channel & flow counts capability probe: TX/RX channel counts, max channels per flow, max TX/RX flows, feature flags (supports TX rename, supports TX multicast)
0x1002 / 0x1003 device names board name, revision, friendly + factory hostname
0x2000 get TX channels enumerate transmit channels (id, name, format)
0x2010 TX channel friendly names friendly-name overrides for TX channels
0x2013 rename TX channel set a TX channel's friendly name
0x3000 get RX channels enumerate receive channels incl. subscription status
0x3001 rename RX channel set an RX channel's friendly name
0x3010 set channel subscriptions the routing operation — see below
0x2200 query TX flows list active transmit flows
0x2201 create multicast TX flow make a new multicast flow
0x2202 delete multicast TX flow tear one down
0x3200 query RX flows list active receive flows

The subscription (routing) model

Routing in Dante is expressed from the receiver's side. To route "channel X of device Y" into local RX channel Z, the controller sends a set channel subscriptions (0x3010) request naming, per subscription:

  • the local RX channel id,
  • the TX channel name (string), and
  • the TX (source) hostname (string).

i.e. subscriptions are by name, not by IP/port — the device then resolves the name via mDNS and negotiates the actual flow itself. When you later enumerate RX channels (0x3000), each carries a subscription status word (distinct values for "subscribed & receiving", "pending / unresolved", etc.).

Channel formats are described by a common descriptor carrying sample rate, bit depth, and a PCM type code (e.g. L16/L24-style). These reappear in mDNS TXT (rate, enc, pcm) and in flow setup — they must all agree or flow setup fails with a sample-rate-mismatch error.

CMC — device announce (port 8800)

A short request (0x1001, "request device advertisement") elicits an advertisement carrying the process id, the 8-byte factory device id, the device's IP, and the info-request port. This is how a peer bootstraps from "I saw you in mDNS" to "here's your identity and where to send info requests."

Multicast info / channel-change (port 8700)

A wider 32-byte header (start code, total length, seqnum, process id, 8-byte factory device id, 8-byte vendor string, 8-byte opcode, then content). The notable message is a channel-change notification: start code 0xffff, a fixed opcode, and content that is a bitmask of channel indices that changed (so listeners know to re-query rather than poll). Used for pushing "something about my channels changed" to interested parties.

Flow setup (dbcp — port 4455)

Before audio flows unicast, the receiver asks the transmitter to open a flow. Same 10-byte req/resp header as ARC, but the start code is the dbcp1 value from mDNS (e.g. 0x1102). Operations (in opcode1):

  • 0x0100 request flow — "send me these channels." Content includes sample rate, bit depth, frames-per-packet (fpp), the ordered list of TX channel ids wanted (0 = gap/unused slot), and the receiver's socket (address + port) and flow name. The reply returns a 6-byte flow handle.
  • 0x0102 update flow — change the channel list of an existing flow (by handle).
  • 0x0101 stop flow — tear down a flow (by handle).

Flows expire without keepalives — a flow that goes quiet is dropped and further packets to it return a "flow not found / expired" error. So a receiver must periodically refresh. Known error codes (in opcode2 of the reply):

code meaning
0x0103 flow expired / not found (missing keepalives)
0x0315 too many TX flows
0x0301 sample-rate mismatch

Multicast flows are simpler for the receiver: the transmitter advertises the bundle via _netaudio-bund mDNS (with destination IP/port in TXT), so a receiver can just join the multicast group instead of doing a unicast request-flow handshake.

Media transport (the audio itself)

Dante media packets are UDP carrying PCM, deliberately close to AES67 RTP but with a lighter, Dante-specific header. From the public relay gist, a Dante multicast audio packet looks like:

offset 0  : u8   channel count
offset 1  : u32  timestamp, seconds (big-endian)
offset 5  : u16  (reserved, seen as 0)
offset 7  : u16  media timestamp — sub-second sample offset (big-endian)
offset 9  : PCM samples, interleaved, big-endian (L16 / L24 / L32)

That is: strip the first 9 bytes of the UDP payload and the rest is raw interleaved PCM. The RTP timestamp an AES67 receiver expects is reconstructable as seconds * sample_rate + media_offset (mod 2^32).

Practical consequences for us:

  • AES67 interop path (phase 1): we already speak AES67 RTP on 5004. A Dante device with AES67 mode enabled emits standard RTP we can consume directly — no Dante control protocol needed. This is the fastest route to "hears Dante audio."
  • Native Dante path (phase 2): to talk to non-AES67 Dante devices we'd need to (a) advertise/answer mDNS as above, (b) implement enough ARC to be discoverable and subscribable, (c) do dbcp flow setup + keepalives, and (d) translate the 9-byte-header media framing to/from our I2S engine. The media conversion itself is cheap; the control/discovery surface is the work.

Dante multicast-only AES67 in Dante gear is locked to 48 kHz; native Dante flows support other rates negotiated via the descriptors above.

Clocking (PTP)

  • Legacy / default Dante: PTPv1 (IEEE 1588-2002). This is the blocker noted in CLAUDE.md — the forked AES67 stack does PTPv2 against the P4's hardware-timestamp path, and PTPv1 differs on the wire (message formats, no two-step/native support in the same code path).
  • AES67 mode: PTPv2 (IEEE 1588-2008, AES67 media profile). If the Dante gear can be put in AES67 mode, our existing PTPv2 works and PTPv1 is not needed for phase 1.
  • A device can be a PTP follower (slaves to a grandmaster) or, harder, a leader/grandmaster. Inferno relies on an external PTP daemon (Statime) as a separate OS process — an option we do not have on ESP-IDF, so any PTPv1 work must live inside the firmware's existing PTP task (see CLAUDE.md "No multi-process model").

Open questions / to verify against captures

  • Exact byte layout of ARC channel/flow descriptors (offsets, the several still-unknown fields) — re-derive from captures when implementing.
  • pcm_type code values ↔ AES67 codec names (L16 / L24 / L32 / AM824).
  • How subscription-status words map to Dante Controller's coloured states.
  • Whether our device must answer CMC/ARC to merely receive multicast, or only to be routable from Dante Controller. (Likely the latter — multicast receive may be possible from mDNS bundle info alone.)
  • PTPv1 message specifics and how badly they diverge from the existing v2 path.