Metadata-Version: 2.5
Name: dc-xair-cli
Version: 2.4.4
Summary: CLI to control Behringer X-Air | Midas MR mixers through OSC
Project-URL: Homepage, https://github.com/dallascrilley/xair-cli
Project-URL: Repository, https://github.com/dallascrilley/xair-cli
Project-URL: Issues, https://github.com/dallascrilley/xair-cli/issues
Project-URL: Changelog, https://github.com/dallascrilley/xair-cli/blob/main/xair-cli/CHANGELOG.md
Author: Dallas Crilley
License: MIT
License-File: LICENSE
License-File: NOTICE
Keywords: audio,behringer,cli,midas,mixer,mr18,osc,x-air,x32,xair,xr18
Requires-Python: >=3.10
Requires-Dist: click<9.0.0,>=8.0.0
Requires-Dist: python-osc<2.0.0,>=1.9.3
Requires-Dist: tomli<3.0,>=2.0.1; python_version < '3.11'
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: mypy>=1.10.0; extra == 'dev'
Requires-Dist: pyinstaller>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Description-Content-Type: text/markdown

# xair-cli

Control Behringer X-Air and Midas MR mixers from the command line.

I wanted my mixer scriptable, so this talks OSC to the console directly instead
of driving the X AIR Edit GUI. Faders, mutes, sends, scenes, and soundcheck
routines all become shell commands you can commit to a repo.

```console
$ xair discover
192.168.1.50     XR18   "Main Room"

$ xair --ip 192.168.1.50 --json health
{"status": "ok", "ip": "192.168.1.50", "name": "Main Room", "model": "XR18", "version": "1.18", "connect": "ok", "info": "ok"}
```

## Features

- Faders, mutes, pans, EQ, dynamics, preamp gain, and sends
- Multi-channel targets: ranges (`1-4`), lists (`1,3,5`), or `all`
- Scene load, save, and list
- Mixer discovery over UDP broadcast
- Batch scripts from a file or stdin
- Three output modes: human-readable, JSON, and `key=value`
- Shell completion for bash, zsh, and fish

## Supported mixers

- Behringer XR18, XR16, XR12
- Midas MR18
- Behringer X32

## Installation

From source (repo root). This is the only path that gives you the build this
README describes:

```bash
./setup.sh           # creates ./.venv if needed, installs the package into it
./setup.sh --dev     # same, plus the test/lint/build extras
source .venv/bin/activate
```

`setup.sh` installs into an already-activated virtualenv if there is one,
otherwise into `./.venv`. It uses `uv` when available and `pip` when not, and
prints which environment and installer it used. Set `PYTHON=/path/to/python3.x`
to choose the interpreter for the virtualenv it creates.

Or manually:

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e "./xair-cli"
```

**Why not `pip install dc-xair-cli` right now.** The package exists on PyPI,
but the published wheel labeled 2.4.3 was built from 2.4.1-era code: it
predates the negative-value parsing, the misplaced-flag guards, and the
`--dry-run` safety work this README documents. Worst of the differences:
`xair --dry-run discover` from that wheel puts a real UDP broadcast on the
wire, which this README promises never happens. It also omits the `docs`
command and ships without LICENSE and NOTICE. You can tell the builds apart:
the stale wheel prints `2.4.1` from `xair --version`; a source build prints
the current version. PyPI filenames are immutable, so the fix will arrive as
a release newer than 2.4.3; until one appears there, install from source. I
will remove this caveat when that release lands.

## Quick start

```bash
# Discover mixers on the network
xair discover

# Show mixer info
xair --ip 192.168.1.50 info

# Control channels
xair --ip 192.168.1.50 ch 1 fader -6
xair --ip 192.168.1.50 ch 1-4 mute on

# Load a scene
xair --ip 192.168.1.50 scene load 1
```

See [QUICKSTART.md](https://github.com/dallascrilley/xair-cli/blob/main/QUICKSTART.md)
for more examples.

## Configuration

Create a config file to avoid passing `--ip` every time:

```bash
xair config init
```

Edit `~/.config/xair/config.toml`:

```toml
[connection]
ip = "192.168.1.50"
model = "XR18"
```

Or set environment variables:

```bash
export XAIR_IP="192.168.1.50"
export XAIR_MODEL="XR18"
```

## Commands

| Command      | Description                             |
| ------------ | --------------------------------------- |
| `info`       | Show mixer model, IP, and firmware       |
| `discover`   | Find mixers on the network               |
| `health`     | Run a read-only check of the connection  |
| `status`     | Show channel overview                    |
| `ch`         | Control input channels                   |
| `bus`        | Control output buses                     |
| `lr`         | Control main L/R output                  |
| `dca`        | Control DCA groups                       |
| `fx`         | Control FX slots                         |
| `rtn`        | Control FX returns                       |
| `scene`      | Manage scenes (load/save/list)           |
| `raw`        | Send raw OSC commands                    |
| `meters`     | Show channel levels                      |
| `batch`      | Execute commands from file               |
| `config`     | Manage configuration                     |
| `completion` | Generate shell completions               |
| `docs`       | Create or update documentation templates |

`xair health` exits non-zero when the mixer cannot be reached, so it works as a
readiness check in scripts:

```bash
xair --json health --details          # timings for connect and info
xair health --auto-discover           # find the mixer first, then check it
```

## Channel properties

| Property  | Description                 | Range         |
| --------- | --------------------------- | ------------- |
| `fader`   | Level in dB                 | -90 to +10    |
| `mute`    | Mute state                  | on/off/toggle |
| `name`    | Channel name                | string        |
| `gain`    | Preamp gain (channels only) | 0-60 dB       |
| `phantom` | 48V phantom power           | on/off        |
| `pan`     | Pan position                | -100 to +100  |
| `eq`      | EQ enable                   | on/off        |
| `gate`    | Gate enable                 | on/off        |
| `comp`    | Compressor enable           | on/off        |
| `send.N`  | Send level to bus N         | -90 to +10 dB |

Not every channel type has every property. DCA groups expose `mute`, `name`,
and `color` only; use `xair raw` for anything a typed command does not cover.

## Negative and relative values

A leading dash is a value, not an option. Absolute negatives work verbatim:

```bash
xair ch 1 fader -6      # Set fader to -6 dB
xair ch 1 pan -50       # Pan halfway left
xair ch 1 send.1 -10    # Set send to bus 1 to -10 dB
```

Use `+` or `--` for changes relative to the current value:

```bash
xair ch 1 fader +3      # 3 dB louder than it is now
xair ch 1 fader --6     # 6 dB quieter than it is now (double dash)
xair bus 1-4 fader --3  # 3 dB quieter on every bus in the range
```

So `-6` means "set to -6 dB" and `--6` means "turn down by 6 dB".

## Dry run

`--dry-run` prints the OSC address and value each command would send and exits
without contacting a mixer. It needs no IP and no network:

```console
$ xair --dry-run ch 1 fader -6
Dry-run mode: nothing is sent to XR18 (no IP configured)
[dry-run] would query /xinfo
[dry-run] would query /ch/01/mix/fader
[dry-run] would send  /ch/01/mix/fader 0.6
fader = -6.0 dB
```

Commands that would otherwise reach the network are previewed too, so
`--dry-run` never puts a packet on the wire:

```console
$ xair --dry-run discover
DRY RUN: would broadcast discovery on port 10024
```

Preview lines go to stderr, so stdout carries only the command's own output:
under `--json` that is one JSON document per affected target, in dry-run and
live alike.

```console
$ xair --json --dry-run ch 1-2 fader -6 2>/dev/null
{"fader": -6.0, "unit": "dB"}
{"fader": -6.0, "unit": "dB"}
```

Values read back in dry-run are placeholders; there is no mixer to ask.

## Output formats

```bash
xair ch 1 fader              # Human readable
xair --json ch 1 fader       # JSON output
xair --plain ch 1 fader      # key=value pairs
```

## Shell completion

```bash
# Bash
xair completion bash >> ~/.bashrc

# Zsh
xair completion zsh >> ~/.zshrc

# Fish
xair completion fish > ~/.config/fish/completions/xair.fish
```

## Repository layout

The Python package lives one level down, in `xair-cli/`, so the repository root
holds the workspace and the package directory holds the distribution:

```
.                       repository root (docs, setup.sh, CI workflows)
└── xair-cli/           the dc-xair-cli package root
    ├── pyproject.toml  build config, entry point, pytest settings
    ├── src/xair/       CLI layer (Click commands, context, output)
    ├── lib/xair_api/   vendored OSC mixer API (see NOTICE)
    └── tests/          pytest suite
```

Two consequences worth knowing before you clone:

- Test commands run from `xair-cli/`, not the repository root. `./setup.sh`
  installs from there for you and can be run from anywhere.
- `README.md`, `LICENSE`, and `NOTICE` exist at the root and again in
  `xair-cli/`, kept byte-identical. The build backend only sees the inner copies
  and puts them in the published package, while GitHub renders the outer ones.
  Edit both.

## Development

```bash
cd xair-cli
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

pytest                       # test suite
ruff check . && ruff format . --check
mypy .
```

CI runs the same four commands plus `python -m build` on every pull request.

## Documentation and policies

- [CONTRIBUTING.md](https://github.com/dallascrilley/xair-cli/blob/main/CONTRIBUTING.md)
  - development setup, the checks CI runs, and how to open a pull request.
- [SECURITY.md](https://github.com/dallascrilley/xair-cli/blob/main/SECURITY.md)
  - how to report a vulnerability privately, and what is in scope.
- [QUICKSTART.md](https://github.com/dallascrilley/xair-cli/blob/main/QUICKSTART.md)
  - a longer worked example than the section above.
- [xair-cli/CHANGELOG.md](https://github.com/dallascrilley/xair-cli/blob/main/xair-cli/CHANGELOG.md)
  - the release history.
- [docs/](https://github.com/dallascrilley/xair-cli/tree/main/docs)
  - `docs/archive/` holds superseded planning notes, kept for reference and not
    maintained.

## Non-goals

- No GUI. This is a command-line tool, and `xair raw` is the escape hatch for
  anything the typed commands do not cover.
- No mixer emulator. The test suite uses a mock mixer plus the `--dry-run`
  transport, so it proves argument parsing, dispatch, OSC address construction,
  and output formatting. It does not prove behavior against real hardware.
- Shell completions are static. They do not reflect the connected mixer's state.
- Discovery relies on UDP broadcast, so it will not cross subnets.

## Third-party code

`xair-cli/lib/xair_api/` is a vendored and modified copy of
[xair-api](https://github.com/onyx-and-iris/xair-api-python) by Onyx and Iris,
used under the MIT License. See
[NOTICE](https://github.com/dallascrilley/xair-cli/blob/main/NOTICE) for
attribution and the copyright lines it preserves.

## License

MIT. See [LICENSE](https://github.com/dallascrilley/xair-cli/blob/main/LICENSE).
