# 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 `@`. 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: ```text 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: ```text 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.