Tutti: an Italian musical direction. The conductor cues everyone in.
A single binary that asks every audio renderer on your LAN to announce itself, catalogues the answers, and tells you what a Go control-point app would see when it tried to use each device. Output is a directory of JSON + raw artifacts, paste-ready for a bug report and ingested by tutti.dunn.dev for the public corpus.
A UPnP renderer that doesn't show up in your music client is almost always a parser mismatch, not a network problem. The device announces itself fine; the client's library throws it away because the descriptor doesn't match what the parser expected. Different libraries throw away different devices. tutti reproduces what a Go control-point sees on the wire, runs the descriptor through every Go UPnP library it knows about, and records the accept/reject decision with a machine-readable reason code. Full background: tutti.dunn.dev/learn.
Pre-built signed binaries (linux/amd64, linux/arm64, darwin/arm64, windows/amd64) live on the releases page. Per-OS walkthrough with the one-time unquarantine / firewall notes: tutti.dunn.dev/contribute.
From source (Go 1.25+):
git clone https://gitlab.com/dunn.dev/tutti.git
cd tutti
make build# Walk the LAN passively.
tutti capture
# Same, plus a real cast against any UPnP renderer found.
tutti capture --drive
# Validate a capture directory against the schema.
tutti validate ./capture-2026-05-09T143022Z-myhosttutti capture does not touch any device. Adding --drive does an
AVTransport probe against discovered UPnP renderers, serving embedded
test tones from a transient local HTTP server. If a device is already
PLAYING, tutti refuses unless --force is passed. Output lands in
./capture-<timestamp>-<host>/.
Errors name the next action, not just the problem (re-run with a specific interface, open a firewall port, file the failing capture-id). See tutti.dunn.dev/learn/errors/ for the shape and three exemplars.
tutti diff-libs --descriptor http://<DEVICE_IP>:1054/description.xmlHands the same descriptor to every Go control-point library tutti knows about and reports where they diverge. Useful when you have a device that one client sees and another doesn't, and you're trying to isolate which library's parser dropped it.
Per device on your LAN, the capture records:
- SSDP + mDNS announcements (raw and parsed, all audio service types).
- Device descriptor (raw XML + canonical fields).
- Library decisions (
go-upnpcast,huin/goupnp): accept/reject with a machine-parseable reason code. - GetProtocolInfo Sink list with derived counts per format class.
- Drive transcripts (only with
--drive): metadata round-trip recorded on two surfaces (DIDL-Lite envelope vs embedded tags), withtranscript_fileper scenario.
Schema: schema/manifest.v1.json is the
source of truth. The Go validator, the renderer's TypeScript types,
and the CI gate are all derived from it. Full walkthrough of the
capture shape: tutti.dunn.dev/learn/manifest/.
Capture directory layout
capture-2026-05-09T143022Z-andrewdunndev/
├── manifest.json # schema-versioned index
├── ssdp.json # parsed SSDP responses
├── ssdp-raw.txt # original wire dump
├── mdns.json # parsed mDNS records
├── devices/
│ └── eversolo-dmp-a6/ # one dir per device
│ ├── descriptor.xml # raw
│ ├── descriptor.json # parsed, canonical
│ ├── decisions.json # per-library accept/reject + reason
│ ├── protocol-info.json # parsed Sink list
│ ├── protocol-info.txt # raw GetProtocolInfo response
│ └── drive/ # only with --drive
└── notes.md # writable by you
The schema is versioned independently of the binary. Within a schema-major-version (v1.x):
- New fields are added as nullable. Older captures pass validation with the field absent.
- Existing fields' shapes are frozen. Their meanings are frozen.
- Derived fields (counts, classifications) can be added if they're computable from raw artifacts in the same capture, so old captures can be re-derived against new tutti versions.
A breaking change bumps the schema major version. The renderer keeps parsers for every shipped major version. Old captures keep rendering.
tutti validate rejects captures that are technically well-formed
JSON but don't actually capture anything useful:
- A capture with
runstats.ssdp_responses == 0andrunstats.mdns_records == 0is rejected. If your LAN really has zero devices announcing on either protocol, pass--allow-emptyto acknowledge. - A
devices[]entry must have at least one entry indecisions. - A
drive_testwithperformed: truemust have at least one entry inruns[]. - A
redactionsarray must be present unless--no-redactwas set (and the manifest records that flag was set).
tutti does: walk SSDP/mDNS, fetch and parse UPnP descriptors, run accept/reject analysis against Go control-point libraries, drive a real cast when asked, produce a paste-ready bundle.
tutti does not: cast Chromecast or AirPlay 2 streams (surfaced at discovery only); implement Roon Ready, SlimProto, MPD, or any closed/proprietary streaming protocol; modify any device; send anything outside your LAN.
Redaction is enforced by the schema, not by convention: a manifest
without a redactions array fails validation and won't render. tutti
auto-scrubs Subsonic/Navidrome tokens, salts, usernames, Authorization
headers, and client/device LAN IPs at capture time. The device UDN is
preserved on purpose: it's the central piece of parser-bug evidence,
advertised in clear on every multicast.
- Run
tutti capture(ortutti capture --drive). - Review the directory, edit
notes.mdif context helps. tutti validate <dir>self-checks before submitting.- Open an MR adding the directory under
evidence/<vendor>-<model>/captures/. CI runs the same validator. - tutti.dunn.dev picks up the merge and renders a page for the device with your capture in the history.
Anonymous contributions accepted (contributor: null). Submission
walkthrough with templates: tutti.dunn.dev/contribute.
tutti.dunn.dev is the public corpus and the progressive disclosure surface for everything not in this README:
/learn/: protocol references, why streaming is finicky, a 35-stack implementations catalog, the capture shape walkthrough, and the error-message design notes./devices/: every profiled device with library decisions, format-support summary, drive transcripts, capture history./contribute/: per-OS install + capture + validate + submit walkthrough.
Versioned per SemVer. Schema is independent of the binary and bumps only on breaking change.
| Item | State |
|---|---|
| SSDP / mDNS discovery | shipped |
| UPnP descriptor parse | shipped |
| go-upnpcast / huin-goupnp decision trace | shipped (reimpl) |
| GetProtocolInfo capture | shipped |
| Drive test (UPnP AVTransport) | shipped |
| Real library invocation (vs reimpl) | not started |
| AirPlay / Chromecast drive | not started |
reference-impl/python/ contains a stdlib-only Python port of the
wire layer (~100 LoC each, no deps). Two purposes: legibility anchor
for anyone wary of a static binary, and a hack-quick path for one-off
changes. Origin: the narjo-eversolo-upnp case study, which
produced the first device in this corpus.
go-upnpcast and
huin/goupnp for being the
control-point libraries this tool can usefully compare.
alexballas/go2tv and
dweymouth/supersonic for
being the apps that motivated the work.
Plutinosoft Platinum whose
SDK fingerprint shows up in capture #1 as the renderer-side software.
MIT.