|
|
@@ -8,14 +8,247 @@ file.
|
|
|
Write what a Dante device *does*, in our own words (message purpose, timing,
|
|
|
field meanings) — not how Inferno's code does it.
|
|
|
|
|
|
-## Discovery (mDNS)
|
|
|
+> **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.
|
|
|
|
|
|
-TBD
|
|
|
+## Big picture
|
|
|
|
|
|
-## Control protocol (routing / subscriptions)
|
|
|
+A Dante device exposes several independent UDP services and advertises them
|
|
|
+over mDNS. Discovery and control are separate from the audio itself:
|
|
|
|
|
|
-TBD
|
|
|
+- **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**.
|
|
|
|
|
|
-## Clocking (PTPv1)
|
|
|
+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**.
|
|
|
|
|
|
-TBD
|
|
|
+### 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:
|
|
|
+
|
|
|
+```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.
|