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.jsgist. 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.
A Dante device exposes several independent UDP services and advertises them over mDNS. Discovery and control are separate from the audio itself:
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.
| 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.
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.localrecord with an_inferno-response-originTXT to reserve a multicast address; that part is Inferno-specific bookkeeping, not core Dante behaviour.
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.
| 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 |
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:
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.
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."
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.
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.
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:
Dante multicast-only AES67 in Dante gear is locked to 48 kHz; native Dante flows support other rates negotiated via the descriptors above.
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).CLAUDE.md "No multi-process model").pcm_type code values ↔ AES67 codec names (L16 / L24 / L32 / AM824).