Metadata-Version: 2.5
Name: netaudio
Version: 0.3.3
Summary: CLI for managing network audio devices
Project-URL: Repository, https://github.com/chris-ritsen/network-audio-controller
Author-email: Christopher Ritsen <chris.ritsen@gmail.com>
License-Expression: Unlicense
License-File: LICENSE
Keywords: audinate,audio,cli,dante,network
Requires-Python: >=3.9
Requires-Dist: click>=8.1.7
Requires-Dist: dbus-fast>=2.0.0; sys_platform == 'linux'
Requires-Dist: ifaddr>=0.2.0
Requires-Dist: pywin32>=306; sys_platform == 'win32'
Requires-Dist: pyyaml>=6.0
Requires-Dist: redis>=5.0.0
Requires-Dist: rich>=13.0.0
Requires-Dist: segno>=1.6.6
Requires-Dist: sqlitedict>=1.7.0
Requires-Dist: tomli>=1.0.0; python_version < '3.11'
Requires-Dist: typer>=0.15.0
Requires-Dist: typing-extensions>=4.0.0
Requires-Dist: zeroconf>=0.38.3
Provides-Extra: capture
Requires-Dist: redis>=5.0.0; extra == 'capture'
Provides-Extra: dbus
Requires-Dist: dbus-fast>=2.0.0; extra == 'dbus'
Description-Content-Type: text/markdown


### Description

This is a python program for controlling Dante network audio devices (and
possibly others in the future).  It's early, so expect things to break or
switches to change.  Use this at your own risk; it's not ready for anything
other than a test environment and could make the devices behave unexpectedly.
The first goal is to do everything that Dante Controller can do that would be
useful for control of the devices from a command-line interface or within
scripts.

For more information, check out the [gearspace discussion](https://gearspace.com/board/music-computers/1221989-dante-routing-without-dante-controller-possible.html).

### Features

#### Current

- AVIO input/output gain control
- Add/remove subscriptions
- Browser interface served by the daemon
- CLI
- Cross-platform foreground daemon plus installable boot service
- Device lock/unlock through the native Rust protocol core
- Display active subscriptions, Rx and Tx channels, devices names and
  addresses, subscription status
- JSON output
- Set device latency, sample rate, encoding
- Set/reset channel names, device names
- mDNS device discovery

### Installation

To install from PyPI:

```bash
uv tool install netaudio
```

Or with pip/pipx:

```bash
pip install netaudio
```

To install from a clone (requires Python 3.9+ and a Rust toolchain, since the
native core is compiled from source):

```bash
uv sync
uv run netaudio
```

#### Arch Linux

To install from AUR, build the package with
[aur/netaudio](https://aur.archlinux.org/packages/netaudio).

### Usage

Run `netaudio` if installed globally, or `uv run netaudio` from a clone.

#### Subscription status

Subscription JSON keeps the raw 16-bit subscription `status.code`, separate
`rx_channel_status.code`, and any additional `status_message` warnings. A
`connected` subscription can carry warnings; it does not prove audio delivery.
Managed responses retain `ddm_status`, `ddm_status_message` and `ddm_summary`
separately. Their semantic state comes from the shared Rust definitions, while
severity follows the managed summary. A null managed status remains unknown,
with its available message, summary and channel metadata preserved. GraphQL
errors remain full objects in daemon status and leave inventory degraded;
unrelated API failures are not treated as successful queries.

Numeric definitions and identifier classification live in `netaudio-core`.
The Python client requires native ABI 6 for these functions. Source checkouts
must rebuild the native core after this update. Subscription definitions no
longer come from the optional local label file.

The [status observation fixture](tests/fixtures/subscription/README.md) records
the evidence scope: a synthetic 0x0000–0x00ff sweep at receiver health 0x0101,
observed through one DDM deployment on 2026-09-05. `observed_summary` is populated
for recognized values in the observed receiver contexts. `interpretation`
identifies observed pairs, unverified receiver contexts, unknown numeric values,
or code 1 requiring receiver context. Code 1 resolves to DYNAMIC at health
0x0101 and UNRESOLVED at health 0x0000; other contexts remain unresolved.
Higher values keep all bits and remain unknown. The sweep does not establish a
complete receiver-health precedence rule or naturally occurring hardware faults.

#### Refreshing discovery

With the daemon running, `netaudio discovery refresh` requests service discovery
again without clearing the current inventory. To query one address directly, use
`netaudio discovery refresh --address 192.168.1.50`. HTTP clients can send
`POST /discovery/refresh` with `{}` or `{"address":"192.168.1.50"}`.
Replies arrive through the normal discovery and event stream. A successful
request does not mean a device answered. Direct mDNS queries do not guarantee
discovery across routers. The daemon honors its configured local interface.

Interface selection also binds direct-device control sockets to that interface's
IPv4 address. A missing interface or unavailable address is an error; NetAudio
does not silently send through another source address. Low-level Python callers
can select a source explicitly with `CoreClient(..., local_ip="192.0.2.10")`.
Leaving `local_ip` unset permits the operating system to select the source.
Native callers must use the matching ABI 6 header and library.

#### Selecting devices and channels

Device commands select devices with the same global filters: `-n/--name`
(glob), `-s/--server-name` (glob), `-m/--mac`, and `--host` (IP address).
Commands that act on one device report `device not found` or
`multiple devices matched` when the filters do not narrow to exactly one.
`-h` is an alias for `--help` everywhere.

```bash
netaudio -n avio-usb-1 device show
netaudio -n avio-usb-1 flow list
netaudio --host 192.168.1.50 lock set 1234
```

Channels are written as `tx:1`, `rx:1`, `tx:NAME`, `rx:NAME`, or a bare
channel name. A bare name searches both directions and is rejected when it
matches both a transmitter and a receiver channel.

```bash
netaudio -n avio-usb-1 channel name rx:1
netaudio -n avio-usb-1 channel name rx:1 vocal-in
netaudio -n avio-usb-1 channel gain tx:1 3
netaudio subscription add --tx tx:1@stagebox --rx rx:1@avio-usb-1
netaudio subscription add --tx 1@stagebox --rx 1@avio-usb-1
netaudio subscription remove --rx rx:1@avio-usb-1
```

With `--tx` and `--rx` the direction is implied, so `1@DEVICE` is accepted as
shorthand for `tx:1@DEVICE` and `rx:1@DEVICE` respectively.

Presets are stored in the preset directory (`presets/` next to
`config.toml`, or `preset_directory` in `config.toml`); `preset save NAME`,
`preset show NAME`, and `preset load NAME` use it unless given an explicit
`.xml` path, and `preset list` shows what is saved there.

The app's **Presets** page downloads presets as XML and opens XML files for
review before applying them. Select devices from the current inventory view;
missing entries must be explicitly skipped. Routing and transmitter names are
included by default when saving; audio and single-interface network settings
are optional. Loading supports receiver subscriptions, transmitter names,
sample rate, encoding, latency, preferred leader and network configuration—not
a full device backup. Receiver names, AES67 settings and multicast flows are
not restored. Applying may interrupt audio or connectivity; the app stops after
an unsuccessful or unverified change, reports partial results, and never
automatically rolls back or reboots devices.

```bash
netaudio -n 'avio-*' preset save stage
netaudio preset list
netaudio config show
```

#### Latency configuration and monitoring

Read the complete device-wide latency state for one device:

```bash
netaudio -n avio-aes3-1 device config latency
```

The output distinguishes active, configured, and default values, the
device-reported minimum/maximum range, and the latency options produced by
filtering the standard option set through that range. JSON, YAML, and XML
output include both milliseconds and the original nanosecond values. An
active or configured value remains visible when it is inside the reported
range but absent from the ordinary option list, or even outside the reported
range.

Set latency in milliseconds and require matching active readback:

```bash
netaudio -n avio-aes3-1 device config latency 2
```

`netaudio -n DEVICE device show` keeps three different layers separate:
device-wide configuration, each receiver flow's latency setting and frames per
packet, and receiver-flow current/average/peak measurements. Flow settings and
live measurements are not treated as aliases of the device-wide value.

#### Channel and flow status JSON

Channel-status reads follow every continuation page and return one merged
record list with `page_capacity`, `page_count`, and `total_record_count`.
Channel records carry `channel_number`, `media_type`, and
`media_local_channel_id`; flow records carry `global_flow_id`, `media_type`,
`media_local_flow_id`, and `transmitter_channel_ids_by_slot`.

Modern ARC channel-status media type codes are `3` for audio, `4` for video,
and `5` for ancillary data. The ancillary label is a causal black-box finding:
Dante Controller classified devices publishing code `5` with its ancillary
capability filter. The finding establishes Controller's interpretation of the
field; the tested devices were synthetic, so it does not establish which
physical device families publish ancillary channels.

For the packet-observed frontends, an exact mDNS `arcp_vers` of `2.8.15`
selects protocol `0x280f`; the existing `2.8.9` frontend uses `0x2809`.
Unrecognized or missing versions fail closed instead of selecting a presumed
protocol. Enrolled DDM devices use the separately observed `0x2809` managed
transport contract.

#### Dante Domain Manager devices

When the daemon's merged inventory marks a device as enrolled, ordinary device
commands automatically use DDM. Unenrolled devices continue to use their local
ARC/settings services. A device that has both a local address and enrolled DDM
metadata still uses DDM, so commands do not accidentally bypass domain policy.

The guided login discovers DDM servers over mDNS when `--url` is omitted,
authenticates, reads the visible domains, prompts when there is more than one
choice, and saves the first context as the default:

```bash
netaudio ddm login --username operator
netaudio ddm context list
netaudio ddm context use studio-main
```

An existing Managed API credential can be used without a password prompt:

```bash
netaudio ddm login --url https://ddm.example/graphql --server-profile studio \
  --credential-file ~/.config/netaudio/studio.credential
```

Each context binds one server profile, one credential file, and one domain ID.
The resulting `config.toml` uses this shape:

```toml
[ddm]
default_context = "studio-main"

[ddm.servers.studio]
url = "https://ddm.example/graphql"
credential_file = "credentials/studio.credential"

[ddm.contexts.studio-main]
server = "studio"
domain_id = "0123456789abcdef0123456789abcdef"
domain_name = "Main Studio"
```

Use `netaudio --context CONTEXT ...` or `NETAUDIO_CONTEXT` for a one-command
override. The daemon polls every configured server, while each managed device
record retains its originating server profile, context, domain ID, and device
ID. This keeps devices distinct when separate sites reuse names or IP address
ranges and ensures credentials are sent only to the configured server.

The URL is used as configured; there are no separate certificate, hostname, or
internal-port settings. Low-level Managed API access is grouped by intent and
resource so it does not overwhelm the ordinary DDM commands:

```bash
netaudio ddm api read domains
netaudio ddm api write device set-name --device-id DEVICE_ID --name NAME
netaudio ddm api schema
```

Use the normal device commands for ordinary control. `ddm api write` exposes
schema-derived administrative mutations and should be used deliberately.

The documented GraphQL API supplies inventory and device-name, preferred-leader,
and subscription changes. Capture-derived, version-scoped Controller-service
support supplies Identify and native ARC/settings requests. The normal CLI paths
currently cover device/channel/flow status, device and channel names, latency,
sample rate, encoding, gain, AES67, clock state/subdomain, network interface
status/configuration, subscriptions, receiver port ranges, and the observed
modern flow inventory/delete form. Reads were exercised against enrolled AVIO
input and output adapters; mutation paths retain their normal readback and
capability checks.

Operations without an established managed request and completion model fail
closed instead of trying the unmanaged device address. These currently include
lock/unlock, reboot and factory reset, capability/log exports, modern multicast
flow creation, channel-name reset, and the `0x2729` transmitter-channel-capability
query. Sample-rate pull-up is implemented through the managed settings envelope,
but the tested AVIO family did not publish a response and is reported as
unavailable.

Run tests:

```bash
uv lock --check
uv run --python 3.9 --no-project python -m compileall -q packages/netaudio/src/netaudio
cargo test --manifest-path packages/netaudio-core/Cargo.toml
uv run pytest -q
```

Lint and format:

```bash
uv run ruff check .
uv run ruff format .
```

#### Network configuration

`netaudio device config interface` reads active and configured settings for both
primary and secondary interfaces. Primary settings can be changed with `dhcp`
or `static --ip ADDRESS --netmask MASK [--gateway ADDRESS] [--dns ADDRESS]`.
Secondary settings are currently read-only; secondary writes are rejected.

`netaudio device config redundancy` reads Dante Redundancy. Add `switched`,
`redundant`, or `split_redundant` to request a supported mode. Support is scoped
to the observed A32 Switched/Redundant and AD4D Switched/Split/Redundant variants;
unrecognized modes and network protocols are unavailable.

The device's Network Config panel provides the same controls and distinguishes
active values from configured values awaiting reboot. Changes use fresh
preflight and configured-value readback. An uncertain result is reported without
automatically retrying or rebooting. Network changes can interrupt connectivity;
reboot the device separately when ready.

#### Browser interface

The daemon serves a browser interface from its own HTTP port, so no extra
process or build step is involved. Start the daemon and open the address it
reports:

```bash
netaudio daemon start
netaudio daemon web --open
```

The page is a single-page application backed entirely by the daemon HTTP API and
its `/events` stream, so device, subscription, metering, and Shure state update
live without polling. It uses real browser history routing, so every view,
device, and device tab is a linkable address. It follows Dante Controller's
layout and vocabulary so it is immediately familiar:

- Routing: the full network subscription matrix, Dante receivers across and Dante
  transmitters down by default, with devices of up to 16 channels expanded initially.
  Click a device intersection to expand or collapse its channels, click a channel
  intersection to change its subscription, and filter
  either axis by device or channel name. The matrix is canvas-rendered and stays
  responsive across tens of thousands of cells; pending changes, subscribed,
  warning, and error states are drawn as in Dante Controller.
- Device Info, Clock Status, Network Status: the network-view tables with the
  columns Dante Controller operators expect. Every table lets columns be hidden,
  shown, and dragged into a new order, remembered per table in the browser.
- Device view: Receive, Transmit, Metering, Status, Device config, Network
  config, AES67 config, Transmit flows, Device lock, and Domain tabs. Receivers
  subscribe through a searchable picker; channels can be renamed and gains set
  where the device supports it.
- Metering: live meters for every declared channel, drawn on canvas with peak
  hold. Both metering protocols are surfaced: detailed per-channel levels
  streamed after an explicit start, and the passive signal-presence records a
  device already broadcasts.
- Flows: transmit flow inventory per device, with multicast flow creation and
  deletion.
- Domains: Dante Domain Manager status, domains, and the managed inventory.
- Shure: discovered receivers, channel state, transmitter and battery detail,
  and live meter values.
- Events: the daemon event stream, held in memory only and excluding meter
  samples.

Use Search to find a device or view. On phones, Routing presents receiving
channels as touch-sized controls with source selection in a bounded dialog. Larger
screens offer the crosspoint grid or channel list; grid headers show subscription
indicators even when a connection is off-screen. Network views are directly
accessible through the workspace tabs; the Tools menu provides subscriptions,
presets, domains, Shure, and settings without occupying the workspace.
Phones use a single view selector containing every page.
Each inventory view starts with focused columns; all additional fields remain
available through Columns. Device pages provide a device switcher and consistent
section navigation. See [interface principles](DESIGN.md) for the app-wide model.

Offline devices are excluded from browser inventory lists and search.
Device details, tables, notices, and error text can always be selected and copied.

The client uses Preact, Tailwind CSS, daisyUI components, and Lucide icons.
Assets are bundled: end users need neither Node nor an external CDN.
For UI development, run `npm ci` and `npm run build:webapp` after editing
components or styles. Edit CSS sources in `scripts/webapp/`, not the generated
`app.css`. Run `npm run test:webapp` for model/render tests and
`npx playwright test` for fixture-backed phone and desktop interactions
(install test browsers with `npx playwright install chromium webkit`).

Device mutations use the same verified-write paths as the CLI, so a control that
reports success has been read back from the device. The daemon binds every
interface; restrict access at the network layer if that is not wanted.

### Documentation

- [Examples](https://github.com/chris-ritsen/network-audio-controller/wiki/Examples)
- [Technical details](https://github.com/chris-ritsen/network-audio-controller/wiki/Technical-details)
- [Testing](https://github.com/chris-ritsen/network-audio-controller/wiki/Testing)
