87 Commits
main ... dev

Author SHA1 Message Date
7fbc7612c3 add pre-commit config 2026-03-21 13:29:38 +00:00
3111508b38 Merge pull request #14 from onyx-and-iris/dependabot/pip/filelock-3.20.3
Bump filelock from 3.20.1 to 3.20.3
2026-03-16 01:45:23 +00:00
dependabot[bot]
c27d68b009 Bump filelock from 3.20.1 to 3.20.3
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.20.1 to 3.20.3.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.20.1...3.20.3)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 01:44:53 +00:00
c39a3fc87d Merge pull request #13 from onyx-and-iris/dependabot/pip/virtualenv-20.36.1
Bump virtualenv from 20.28.1 to 20.36.1
2026-03-16 01:43:42 +00:00
dependabot[bot]
6cc3bdff96 Bump virtualenv from 20.28.1 to 20.36.1
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.28.1 to 20.36.1.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.28.1...20.36.1)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.36.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 01:38:07 +00:00
669aba4cc4 fixes bug with {Gate}.mode() setting incorrect values.
patch bump
2026-02-04 06:10:40 +00:00
ce4fccdc85 remove unnecessary assignment 2026-02-02 04:18:23 +00:00
deb298537f add ruff + publish actions 2026-02-01 22:27:10 +00:00
f963997f35 fixes bug with {EQ}.quality, it should use an invertic log scale.
patch bump
2026-02-01 21:39:19 +00:00
39f3a9dd98 Merge pull request #12 from onyx-and-iris/dependabot/pip/filelock-3.20.1
Bump filelock from 3.16.1 to 3.20.1
2026-01-08 10:38:46 +00:00
dependabot[bot]
6cccfd0f56 Bump filelock from 3.16.1 to 3.20.1
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.16.1 to 3.20.1.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.16.1...3.20.1)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-08 10:37:59 +00:00
05df111e69 upd .gitignore 2025-03-26 03:01:22 +00:00
2b91863337 add py13 to tox envlist
test_xair and test_x32 now run tests with tox

upd Tests section in README
2025-02-13 11:01:44 +00:00
cf4a1d295f upd python-requires 2025-01-16 20:22:31 +00:00
b5b5633577 add poetry badge 2025-01-16 15:08:41 +00:00
7b6e70028b upd obs example 2025-01-16 14:49:41 +00:00
fd4a228086 re-run through ruff formatter 2025-01-15 11:10:45 +00:00
b45abf2492 reformat snippets in readme 2025-01-15 10:57:39 +00:00
53ad8fdff5 re-run through ruff formatter 2025-01-15 10:54:52 +00:00
5a988e8d37 remake pyproject with poetry 2 2025-01-15 10:54:36 +00:00
1ada889135 add tox env for obs example
add headamp script to poe tasks
2025-01-15 10:22:46 +00:00
7ede9b1ef3 freeze dataclasses 2025-01-15 10:21:22 +00:00
01d5d9801c add poethepoet to poetry.requires-plugins 2025-01-12 12:50:03 +00:00
a558e7daf7 move example/test scripts into poe.tasks 2025-01-12 11:47:07 +00:00
e5aec24751 update ruff configuration 2025-01-05 08:34:01 +00:00
3010b44b09 minor version bump
add 2.4.0 section to CHANGELOG

upd README with HeadAmp class

closes #10
2025-01-04 18:59:01 +00:00
f26de42b89 add headamp class 2025-01-03 10:19:06 +00:00
6bdd4a0040 Merge pull request #9 from onyx-and-iris/dependabot/pip/black-24.3.0
Bump black from 22.8.0 to 24.3.0
2024-03-28 14:59:43 +00:00
dependabot[bot]
b53ed46014 Bump black from 22.8.0 to 24.3.0
Bumps [black](https://github.com/psf/black) from 22.8.0 to 24.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.8.0...24.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-28 14:59:15 +00:00
caaf2689ff upd docstring 2024-02-16 12:53:34 +00:00
7e7aa1b4de 2.3.2 section added to CHANGELOG
patch bump
2024-02-16 12:50:54 +00:00
2dc096e306 connect_timeout kwarg added to README
Errors section added to README
2024-02-16 12:37:15 +00:00
ed397e57aa XAirRemoteConnectionTimeoutError added to errors 2024-02-16 12:26:24 +00:00
718ecbd982 timeout decorator func added to util 2024-02-16 12:26:00 +00:00
69cabb3db0 add configurable kwarg connect_timeout 2024-02-16 12:25:41 +00:00
6a2df6352d add tox tests 2024-02-15 23:54:31 +00:00
9c1fa36aed upd pytest dep version 2024-02-15 18:47:11 +00:00
3a70a4c578 upd docs 2024-02-15 18:35:34 +00:00
8b1b2c7f79 log value given as well as value expected 2024-02-15 16:58:18 +00:00
1e5e458105 log OOB as warnings
patch bump

closes #8
2024-02-15 15:19:05 +00:00
e05460e998 implement module level loggers
class loggers are now child loggers

minor version bump

closes #7
2024-02-15 15:15:30 +00:00
d27824d1cf should an incorrect kind be passed to entry point, raise XAirRemoteError
remove the print statement

patch bump
2024-02-15 13:05:54 +00:00
764195a452 remove unused opts dict in geq_prop 2024-02-14 22:47:12 +00:00
b295fee6e1 lint fixes
fix {DCA}.name setter

removed unused imports

patch bump
2024-02-14 22:06:28 +00:00
06be2f2831 fix date 2024-02-14 21:39:58 +00:00
2d0c0f91f0 upd CHANGELOG
bump to 2.2.4
2024-02-14 21:38:56 +00:00
6e017b4afc add poetry script sends
bump to 2.2.4a0
2024-02-08 21:38:30 +00:00
85664c8465 swap i, remote order 2024-02-08 18:15:18 +00:00
a3473d5922 swap i, remote order for consistency. 2024-02-08 17:54:27 +00:00
e9ef113b5c upd __init__ methods 2024-02-08 17:41:56 +00:00
56ec9a17c0 print back level send values in sends example 2024-02-08 17:41:37 +00:00
9a7d98d58b fix docstrings
remove pass
2024-02-08 17:40:49 +00:00
f3cf215a76 upd decorator func names 2024-02-08 15:27:15 +00:00
a62a46d61a upd docs 2024-02-08 13:46:49 +00:00
5eeaff2371 now using sys.executable, fixes issue with pyenv-win 2024-02-08 13:46:26 +00:00
c2cf2fe523 sends example added.
obs example updated to reflect updates to obsws
2024-02-08 13:45:55 +00:00
265c26eb67 import util as namespace 2024-02-08 13:44:55 +00:00
467b769ea4 from_db, to_db decorator functions added to util.
_get_{fader,level}_val, _set_{fader,level}_val removed
2024-02-08 13:43:49 +00:00
27d0811091 Send class added to shared module
Send mixed into Strip, AuxRtn, FxRtn classes.

addresses #4
2024-02-08 13:43:15 +00:00
df2d158618 patch bump
closes #6
2024-02-03 13:04:25 +00:00
035c8d6507 move delay into {XAirRemote}.query()
update getters
2024-02-03 13:03:49 +00:00
cab3888946 upd lock file 2023-08-27 19:14:32 +01:00
eddfb89fa9 add group dev (dev-dependencies deprecated) 2023-08-27 19:13:20 +01:00
a1062e92b5 upd test badge 2022-11-16 15:52:34 +00:00
ac382c4c32 mute prop moved into meta
patch bump
2022-11-16 15:51:26 +00:00
onyx-and-iris
a09b07e1c2 mute prop added to dca class
mute tests added

patch bump
2022-11-10 12:10:01 +00:00
onyx-and-iris
e7d38bb9d7 minor ver bump
added mute property to bus,fx,lr,rtn,strip
2022-11-08 22:10:36 +00:00
onyx-and-iris
2255da4e53 minor ver bump
2.1.0 added to changelog
2022-11-08 17:07:42 +00:00
onyx-and-iris
6ab7d03a11 update changelog 2022-11-08 16:54:57 +00:00
onyx-and-iris
87217f6f9c remove type checks, prefer duck typing.
keep bounds checks for any vals passed to lin_set/log_set
2022-11-08 16:54:23 +00:00
onyx-and-iris
fb8d3dee75 add missing parameters in config.monitor 2022-11-08 16:52:00 +00:00
onyx-and-iris
4973eb3215 reword docstring 2022-11-08 16:51:39 +00:00
onyx-and-iris
65a817ed87 update changelog 2022-11-08 15:12:03 +00:00
onyx-and-iris
0b20ac953f delay kwarg added.
delay added to README

delay for test suites reduced to 0.008

main stereo added to adapter tests
2022-11-08 15:09:20 +00:00
onyx-and-iris
7015383b98 fix logging bug
patch bump
2022-11-07 21:05:27 +00:00
onyx-and-iris
981f4b57f8 add ex_obs to scripts 2022-11-07 20:06:56 +00:00
onyx-and-iris
2732e56a86 patch bump 2022-11-07 19:57:26 +00:00
onyx-and-iris
315dc64feb rename xair obs dir
upd poetry script
2022-11-07 19:50:46 +00:00
onyx-and-iris
4a36e4a2ce major ver bump
CHANGELOG updated to reflect changes
2022-11-07 18:36:59 +00:00
onyx-and-iris
858275beda md fix 2022-11-07 17:43:43 +00:00
onyx-and-iris
d6fe34aef4 test badge fixed
fx added to higher classes section in readme.

aux renamed to auxreturn

lower level methods send, query added to readme.

documentation links for OSC commands added.
2022-11-07 17:29:35 +00:00
onyx-and-iris
0606c8d107 info_response property added to base clase. 2022-11-07 17:27:41 +00:00
onyx-and-iris
78cd0b489a add poetry scripts for test suites 2022-11-07 15:23:20 +00:00
onyx-and-iris
6944ba5128 minor refactors 2022-11-07 15:22:59 +00:00
onyx-and-iris
e4dc4d0b13 add tests.x32 module 2022-11-07 15:22:29 +00:00
onyx-and-iris
079d1b308d move xair tests into tests.xair package 2022-11-07 15:21:51 +00:00
onyx-and-iris
a69734b738 adapter module added
factory method for x32 added

logging module added for tracing OSC requests/reponses.
2022-11-07 11:08:56 +00:00
40 changed files with 2066 additions and 1105 deletions

53
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Publish to PyPI
on:
release:
types: [published]
push:
tags:
- 'v*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Poetry
run: |
pip install poetry==2.3.1
poetry --version
- name: Build package
run: |
poetry install --only-root
poetry build
- uses: actions/upload-artifact@v4
with:
name: dist
path: ./dist
pypi-publish:
needs: build
name: Upload release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/xair-api
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: ./dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./dist

19
.github/workflows/ruff.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Ruff
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
with:
args: 'format --check --diff'

6
.gitignore vendored
View File

@@ -130,4 +130,8 @@ dmypy.json
# config, quick test # config, quick test
config.toml config.toml
quick.py test-*.py
tools/*
!tools/README.md
.vscode/

13
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,13 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/python-poetry/poetry
rev: '2.3.2'
hooks:
- id: poetry-check
- id: poetry-lock

View File

@@ -9,62 +9,138 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
## [Unreleased] ## [Unreleased]
- [x] - [ ]
## [2.4.0] - 2025-01-04
### Added
- HeadAmp class to all kinds for enabling phantom power and setting preamp gain.
- headamp example.
## [2.3.2] - 2024-02-16
### Added
- Configurable kwarg `connect_timeout` added. Defaults to 2 seconds.
- New error class `XAirRemoteConnectionTimeoutError`. Raised if a connection validation times out.
- timeout kwarg + Errors section added to README.
## [2.3.1] - 2024-02-15
### Changed
- Module level loggers implemented
- class loggers are now child loggers
- Passing an incorrect kind_id to the entry point now raises an XAirRemoteError.
- Passing a value out of bounds to a setter now logs a warning instead of raising an exception.
- Send class added to README.
## [2.2.4] - 2024-02-14
### Added
- Send class mixed into Strip, AuxRtn, FxRtn. May now be accessed with {Class}.send
- Sends example added
### Changed
- delay kwarg now applies to getters. See [Issue #6](https://github.com/onyx-and-iris/xair-api-python/issues/6).
## [2.2.0] - 2022-11-08
### Added
- mute prop to Bus, FX, LR, RTN, Strip classes.
## [2.1.0] - 2022-11-08
### Added
- delay keyword argument
- bounds checks for vals passed to lin_set/log_set
### Removed
- type checks, prefer duck typing
## [2.0.0] - 2022-11-07
Some support for the X32 mixer has been added using an adapter module but the code related to the XAir api has been left largely untouched.
However, a couple of changes have been made which are breaking, they are as follows:
### Changed
- FX class added to fx module. This now deals with osc addresses that begin with "/fx/". Call it with mixer.fx.
- FxRtn class added to rtn module. This now deals with addresses that begin with "/rtn/". Call it with mixer.fxreturn
- Aux class renamed to AuxRtn in rtn module. Call it with mixer.auxreturn.
These changes were made to better resemble the underlying osc api and to better describe the function of the classes.
### Added
- A small number of X32 tests. More will be added. XAir tests moved into it's own test module.
- XAirRemote lower level section added to README.
- Links to OSC command documentation added to README.
### Removed
- mixer.aux was renamed to mixer.auxreturn
## [1.1.0] - 2022-09-05 ## [1.1.0] - 2022-09-05
### Added ### Added
- tomli/tomllib compatibility layer to support python 3.10 - tomli/tomllib compatibility layer to support python 3.10
## [1.0.2] - 2022-08-07 ## [1.0.2] - 2022-08-07
### Added ### Added
- now packaged with poetry - now packaged with poetry
- package added to pypi - package added to pypi
- pypi, isort badges added to readme - pypi, isort badges added to readme
### Changed ### Changed
- package renamed to xair-api - package renamed to xair-api
- now using tomllib for config, requires python 3.11 - now using tomllib for config, requires python 3.11
- readme, example updated. - readme, example updated.
- imports isorted. - imports isorted.
## [0.1.0] - 2022-05-01 ## [0.1.0] - 2022-05-01
### Added ### Added
- kind maps for "XR16", "XR12" added. - kind maps for "XR16", "XR12" added.
- get() added to kind module. - get() added to kind module.
- pre-commit.ps1 added for use with git hook. - pre-commit.ps1 added for use with git hook.
- tests passed badge added to readme. - tests passed badge added to readme.
### Changed ### Changed
- readme updated to reflect changes. - readme updated to reflect changes.
### Fixed ### Fixed
- link to clone repo fixed in readme. - link to clone repo fixed in readme.
- unit tests migrated from nose to pytest since nose will not be supported from python 3.10 onwards. - unit tests migrated from nose to pytest since nose will not be supported from python 3.10 onwards.
## [0.0.1] - 2022-04-05 ## [0.0.1] - 2022-04-05
### Added ### Added
- \_query() added to base class, allows testing a single parameter. - \_query() added to base class, allows testing a single parameter.
- Interface entry point defined. - Interface entry point defined.
- Kind map for XR18/MR18 added - Kind map for XR18/MR18 added
- Higher level classes (lr, strip, bus, fxsend, aux, rtn) implemented - Higher level classes (lr, strip, bus, fxsend, aux, rtn) implemented
- Subclass mixin implemented (shared classes) - Subclass mixin implemented (shared classes)
- meta module added - meta module added
- util module added, mostly functions that perform math operations. - util module added, mostly functions that perform math operations.
- readme initial commit. - readme initial commit.
### Changed ### Changed
- base class now supports context manager. - base class now supports context manager.
- load ip from ini - load ip from ini
- unit tests initial commit. tests for shared classes added. - unit tests initial commit. tests for shared classes added.

286
README.md
View File

@@ -1,8 +1,8 @@
[![PyPI version](https://badge.fury.io/py/xair-api.svg)](https://badge.fury.io/py/xair-api) [![PyPI version](https://badge.fury.io/py/xair-api.svg)](https://badge.fury.io/py/xair-api)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/xair-api-python/blob/dev/LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/xair-api-python/blob/dev/LICENSE)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
![Tests Status](./tests/MR18.svg?dummy=8484744) ![Tests Status](./tests/xair/MR18.svg?dummy=8484744)
# Xair API # Xair API
@@ -12,7 +12,7 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Prerequisites ## Prerequisites
- Python 3.10 or greater - Python 3.10 or greater
## Installation ## Installation
@@ -40,30 +40,42 @@ import xair_api
def main(): def main():
kind_id = 'XR18'
ip = '<ip address>'
with xair_api.connect(kind_id, ip=ip) as mixer: with xair_api.connect(kind_id, ip=ip) as mixer:
mixer.strip[8].config.name = "sm7b" mixer.strip[8].config.name = 'sm7b'
mixer.strip[8].mix.on = True mixer.strip[8].mix.on = True
print( print(
f"strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].mix.on}" f'strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].mix.on}'
) )
if __name__ == "__main__": if __name__ == '__main__':
kind_id = "MR18"
ip = "<ip address>"
main() main()
``` ```
#### `xair_api.connect(kind_id, ip=ip, delay=0.02, connect_timeout=2)`
Currently the following devices are supported:
- `MR18`
- `XR18`
- `XR16`
- `XR12`
The `X32` is partially supported. However, this document covers specifically the `XAir` series.
The following keyword arguments may be passed:
- `ip`: ip address of the mixer
- `port`: mixer port, defaults to 10023 for x32 and 10024 for xair
- `delay`: a delay between each command (applies to the getters). Defaults to 20ms.
- a note about delay, stability may rely on network connection. For wired connections the delay can be safely reduced.
- `connect_timeout`: amount of time to wait for a validated connection. Defaults to 2s.
## API ## API
Currently the following devices are support:
- `XR18`
- `MR18`
- `XR16`
- `XR12`
### XAirRemote class (higher level) ### XAirRemote class (higher level)
`mixer.lr` `mixer.lr`
@@ -82,6 +94,10 @@ A Bus tuple containing a class for each output bus channel
A DCA tuple containing a class for each DCA group A DCA tuple containing a class for each DCA group
`mixer.fx`
An FX tuple containing a class for each FX channel
`mixer.fxsend` `mixer.fxsend`
An FXSend tuple containing a class for each FX Send channel An FXSend tuple containing a class for each FX Send channel
@@ -90,18 +106,18 @@ An FXSend tuple containing a class for each FX Send channel
An FXReturn tuple containing a class for each FX Return channel An FXReturn tuple containing a class for each FX Return channel
`mixer.aux` `mixer.auxreturn`
A class representing aux channel A class representing auxreturn channel
`mixer.rtn`
An RTN tuple containing a class for each rtn channel
`mixer.config` `mixer.config`
A class representing the main config settings A class representing the main config settings
`mixer.headamp`
A class representing the channel preamps (phantom power/gain).
### `LR` ### `LR`
Contains the subclasses: Contains the subclasses:
@@ -110,7 +126,7 @@ Contains the subclasses:
### `Strip` ### `Strip`
Contains the subclasses: Contains the subclasses:
(`Config`, `Preamp`, `Gate`, `Dyn`, `Insert`, `GEQ`, `EQ`, `Mix`, `Group`, `Automix`) (`Config`, `Preamp`, `Gate`, `Dyn`, `Insert`, `GEQ`, `EQ`, `Mix`, `Group`, `Automix`, `Send`)
### `Bus` ### `Bus`
@@ -122,15 +138,22 @@ Contains the subclasses:
Contains the subclasses: Contains the subclasses:
(`Config`, `Mix`, `Group`) (`Config`, `Mix`, `Group`)
### `Aux` ### `FXRtn`
Contains the subclasses: Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`) (`Config`, `Preamp`, `EQ`, `Mix`, `Group`, `Send`)
### `Rtn` ### `AuxRtn`
Contains the subclasses: Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`) (`Config`, `Preamp`, `EQ`, `Mix`, `Group`, `Send`)
### `HeadAmp`
The following properties are available:
- `gain`: float, from -12.0 to 60.0
- `phantom`: bool
### `Subclasses` ### `Subclasses`
@@ -138,132 +161,134 @@ For each subclass the corresponding properties are available.
`Config` `Config`
- `name`: string - `name`: string
- `color`: int, from 0, 16 - `color`: int, from 0, 16
- `inputsource`: int - `inputsource`: int
- `usbreturn`: int - `usbreturn`: int
`Preamp` `Preamp`
- `on`: bool - `on`: bool
- `usbtrim`: float, from -18.0 to 18.0 - `usbtrim`: float, from -18.0 to 18.0
- `usbinput`: bool - `usbinput`: bool
- `invert`: bool - `invert`: bool
- `highpasson`: bool - `highpasson`: bool
- `highpassfilter`: int, from 20 to 400 - `highpassfilter`: int, from 20 to 400
`Gate` `Gate`
- `on`: bool - `on`: bool
- `mode`: str, one of ('gate', 'exp2', 'exp3', 'exp4', 'duck') - `mode`: str, one of ('gate', 'exp2', 'exp3', 'exp4', 'duck')
- `threshold`: float, from -80.0 to 0.0 - `threshold`: float, from -80.0 to 0.0
- `range`: int, from 3 to 60 - `range`: int, from 3 to 60
- `attack`: int, from 0 to 120 - `attack`: int, from 0 to 120
- `hold`: float, from 0.02 to 2000 - `hold`: float, from 0.02 to 2000
- `release`: int, from 5 to 4000 - `release`: int, from 5 to 4000
- `keysource`, from 0 to 22 - `keysource`, from 0 to 22
- `filteron`: bool - `filteron`: bool
- `filtertype`: int, from 0 to 8 - `filtertype`: int, from 0 to 8
- `filterfreq`: float, from 20 to 20000 - `filterfreq`: float, from 20 to 20000
`Dyn` `Dyn`
- `on`: bool - `on`: bool
- `mode`: str, one of ('comp', 'exp') - `mode`: str, one of ('comp', 'exp')
- `det`: str, one of ('peak', 'rms') - `det`: str, one of ('peak', 'rms')
- `env`: str, one of ('lin', 'log') - `env`: str, one of ('lin', 'log')
- `threshold`: float, from -60.0 to 0.0 - `threshold`: float, from -60.0 to 0.0
- `ratio`: int, from 0 to 11 - `ratio`: int, from 0 to 11
- `knee`: int, from 0 to 5 - `knee`: int, from 0 to 5
- `mgain`: float, from 0.0 to 24.0 - `mgain`: float, from 0.0 to 24.0
- `attack`: int, from 0 to 120 - `attack`: int, from 0 to 120
- `hold`: float, from 0.02 to 2000 - `hold`: float, from 0.02 to 2000
- `release`: int, from 5 to 4000 - `release`: int, from 5 to 4000
- `mix`: int, from 0 to 100 - `mix`: int, from 0 to 100
- `keysource`: int, from 0 to 22 - `keysource`: int, from 0 to 22
- `auto`: bool - `auto`: bool
- `filteron`: bool - `filteron`: bool
- `filtertype`: int, from 0 to 8 - `filtertype`: int, from 0 to 8
- `filterfreq`: float, from 20 to 20000 - `filterfreq`: float, from 20 to 20000
`Insert` `Insert`
- `on`: bool - `on`: bool
- `sel`: int - `sel`: int
`GEQ` `GEQ`
The following method names preceded by `slider_` The following method names preceded by `slider_`
- `20`, `25`, `31_5`, `40`, `50`, `63`, `80`, `100`, `125`, `160`, - `20`, `25`, `31_5`, `40`, `50`, `63`, `80`, `100`, `125`, `160`,
- `200`, `250`, `315`, `400`, `500`, `630`, `800`, `1k`, `1k25`, `1k6`, `2k`, - `200`, `250`, `315`, `400`, `500`, `630`, `800`, `1k`, `1k25`, `1k6`, `2k`,
- `2k5`, `3k15`, `4k`, `5k`, `6k3`, `8k`, `10k`, `12k5`, `16k`, `20k`: float, from -15.0 to 15.0 - `2k5`, `3k15`, `4k`, `5k`, `6k3`, `8k`, `10k`, `12k5`, `16k`, `20k`: float, from -15.0 to 15.0
for example: `slider_20`, `slider_6k3` etc.. for example: `slider_20`, `slider_6k3` etc..
`EQ` `EQ`
- `on`: bool - `on`: bool
- `mode`: str, one of ('peq', 'geq', 'teq') - `mode`: str, one of ('peq', 'geq', 'teq')
For the subclasses: `low`, `low2`, `lomid`, `himid`, `high2`, `high` the following properties are available: For the subclasses: `low`, `low2`, `lomid`, `himid`, `high2`, `high` the following properties are available:
- `type`: int, from 0 to 5 - `type`: int, from 0 to 5
- `frequency`: float, from 20.0 to 20000.0 - `frequency`: float, from 20.0 to 20000.0
- `gain`: float, -15.0 to 15.0 - `gain`: float, -15.0 to 15.0
- `quality`: float, from 0.3 to 10.0 - `quality`: float, from 0.3 to 10.0
for example: `eq.low2.type` for example: `eq.low2.type`
`Mix` `Mix`
- `on`: bool - `on`: bool
- `fader`: float, -inf, to 10.0 - `fader`: float, -inf, to 10.0
- `lr`: bool - `lr`: bool
`Group` `Group`
- `dca`: int, from 0 to 15 - `dca`: int, from 0 to 15
- `mute`: int, from 0 to 15 - `mute`: int, from 0 to 15
`Automix` `Automix`
- `group`: int, from 0 to 2 - `group`: int, from 0 to 2
- `weight`: float, from -12.0 to 12.0 - `weight`: float, from -12.0 to 12.0
### `DCA` ### `DCA`
- `on`: bool - `on`: bool
- `name`: str - `name`: str
- `color`: int, from 0 to 15 - `color`: int, from 0 to 15
### `Config` ### `Config`
The following method names preceded by `chlink` The following method names preceded by `chlink`
- `1_2`, `3_4`, `5_6`, `7_8`, `9_10`, `11_12`, `13_14`, `15_16` - `1_2`, `3_4`, `5_6`, `7_8`, `9_10`, `11_12`, `13_14`, `15_16`
The following method names preceded by `buslink` The following method names preceded by `buslink`
- `1_2`, `3_4`, `5_6` - `1_2`, `3_4`, `5_6`
for example: `chlink1_2`, `buslink5_6` etc.. for example: `chlink1_2`, `buslink5_6` etc..
- `link_eq`: bool - `link_eq`: bool
- `link_dyn`: bool - `link_dyn`: bool
- `link_fader_mute`: bool - `link_fader_mute`: bool
- `amixenable`: bool - `amixenable`: bool
- `amixlock`: bool - `amixlock`: bool
For the subclass `monitor` the following properties are available For the subclass `monitor` the following properties are available
- `level`: float, -inf to 10.0 - `level`: float, -inf to 10.0
- `source`: int, from 0 to 14 - `source`: int, from 0 to 14
- `chmode` bool - `sourcetrim`: float, from -18.0 to 18.0
- `busmode` bool - `chmode`: bool
- `dim` bool - `busmode`: bool
- `mono` bool - `dim`: bool
- `mute` bool - `dimgain`: float, from -40.0 to 0.0
- `dimfpl` bool - `mono`: bool
- `mute`: bool
- `dimfpl`: bool
for example: `config.monitor.chmode` for example: `config.monitor.chmode`
@@ -271,25 +296,68 @@ for example: `config.monitor.chmode`
tuple containing a class for each mute group tuple containing a class for each mute group
- `on`: bool, from 0 to 3 - `on`: bool, from 0 to 3
for example: `config.mute_group[0].on = True` for example: `config.mute_group[0].on = True`
### `Send`
- `level`: float, -inf to 10.0
for example: `mixer.strip[10].send[3].level = -16.5`
### XAirRemote class (lower level)
Send an OSC command directly to the mixer
- `send(osc command, value)`
for example:
```python
mixer.send('/ch/01/mix/on', 1)
mixer.send('/bus/2/config/name', 'somename')
```
Query the value of a command:
- `query(osc command)`
for example:
```python
print(mixer.query('/ch/01/mix/on'))
```
### Errors
- `errors.XAirRemoteError`: Base error class for XAIR Remote.
- `errors.XAirRemoteConnectionTimeoutError`:Exception raised when a connection attempt times out.
- The following attributes are available:
- `ip`: IP of the mixer.
- `port`: Port of the mixer.
### `Tests` ### `Tests`
Unplug any expensive equipment before running tests. Install [poetry](https://python-poetry.org/docs/#installation) and then:
Save your current settings to a snapshot first.
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/xair-api-python#installation) ```powershell
poetry poe test-xair
poetry poe test-x32
```
To run all tests: Unplug any expensive equipment and save your current settings to a snapshot first.
`pytest -v`.
## License ## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
## Documentation
[XAir OSC Commands](https://behringer.world/wiki/doku.php?id=x-air_osc)
[X32 OSC Commands](https://wiki.munichmakerlab.de/images/1/17/UNOFFICIAL_X32_OSC_REMOTE_PROTOCOL_%281%29.pdf)
## Special Thanks ## Special Thanks
[Peter Dikant](https://github.com/peterdikant) for writing the base class [Peter Dikant](https://github.com/peterdikant) for writing the base class

View File

@@ -2,16 +2,16 @@ import xair_api
def main(): def main():
kind_id = 'XR18'
ip = '<ip address>'
with xair_api.connect(kind_id, ip=ip) as mixer: with xair_api.connect(kind_id, ip=ip) as mixer:
mixer.strip[8].config.name = "sm7b" mixer.strip[8].config.name = 'sm7b'
mixer.strip[8].config.on = True mixer.strip[8].config.on = True
print( print(
f"strip 09 ({mixer.strip[8].config.name}) has been set to {mixer.strip[8].config.on}" f'strip 09 ({mixer.strip[8].config.name}) has been set to {mixer.strip[8].config.on}'
) )
if __name__ == "__main__": if __name__ == '__main__':
kind_id = "MR18"
ip = "<ip address>"
main() main()

View File

@@ -0,0 +1,20 @@
# Warning this script enables the phantom power for strip 09
import logging
import xair_api
logging.basicConfig(level=logging.DEBUG)
def main():
with xair_api.connect('XR18', ip='mixer.local') as mixer:
mixer.headamp[8].phantom = True
for i in range(-12, -6):
mixer.headamp[8].gain = i
print(mixer.headamp[8].gain)
input('Press Enter to continue...')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,24 @@
import logging
import xair_api
logging.basicConfig(level=logging.DEBUG)
def main():
with xair_api.connect('XR18', ip='mixer.local') as mixer:
for send in mixer.strip[0].send:
send.level = -22.8
mixer.strip[15].send[0].level = -16.5
print(mixer.strip[15].send[0].level)
mixer.auxreturn.send[0].level = -15.5
print(mixer.auxreturn.send[0].level)
mixer.fxreturn[0].send[0].level = -14.5
print(mixer.fxreturn[0].send[0].level)
if __name__ == '__main__':
main()

View File

@@ -1,39 +0,0 @@
import obsws_python as obs
import xair_api
class Observer:
def __init__(self, mixer):
self._mixer = mixer
self._cl = obs.EventClient()
self._cl.callback.register(self.on_current_program_scene_changed)
def on_current_program_scene_changed(self, data):
scene = data.scene_name
print(f"Switched to scene {scene}")
match scene:
case "START":
print("Toggling strip 01 on")
self._mixer.strip[0].mix.on = not self._mixer.strip[0].mix.on
case "BRB":
print("Setting strip 08 fader")
self._mixer.strip[7].mix.fader = -12.8
case "END":
print("Settings strip 02 color")
self._mixer.strip[1].config.color = 8
case "LIVE":
self._mixer.config.mute_group[0].on = True
print(f"Mute Group 1 is {self._mixer.config.mute_group[0].on}")
def main():
with xair_api.connect("MR18", ip="mixer.local") as mixer:
Observer(mixer)
while cmd := input("<Enter> to exit\n"):
if not cmd:
break
if __name__ == "__main__":
main()

View File

@@ -19,7 +19,7 @@ password = "mystrongpass"
## Use ## Use
Change the xair ip argument from `mixer.local` to the ip of your xair mixer. Run the code and switch between scenes in OBS. Change the xair ip argument from `mixer.local` to the ip of your xair mixer. Run the code and switch between scenes in OBS. Closing OBS will end the script.
## Notes ## Notes

View File

@@ -0,0 +1,83 @@
import threading
from logging import config
import obsws_python as obsws
import xair_api
config.dictConfig(
{
'version': 1,
'formatters': {
'standard': {
'format': '%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s'
}
},
'handlers': {
'stream': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'standard',
}
},
'loggers': {
'xair_api.xair': {
'handlers': ['stream'],
'level': 'DEBUG',
'propagate': False,
}
},
'root': {'handlers': ['stream'], 'level': 'WARNING'},
}
)
class Observer:
def __init__(self, mixer, stop_event):
self._mixer = mixer
self._stop_event = stop_event
self._client = obsws.EventClient()
self._client.callback.register(
(
self.on_current_program_scene_changed,
self.on_exit_started,
)
)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self._client.disconnect()
def on_current_program_scene_changed(self, data):
scene = data.scene_name
print(f'Switched to scene {scene}')
match scene:
case 'START':
print('Toggling strip 01 on')
self._mixer.strip[0].mix.on = not self._mixer.strip[0].mix.on
case 'BRB':
print('Setting strip 08 fader')
self._mixer.strip[7].mix.fader = -12.8
case 'END':
print('Settings strip 02 color')
self._mixer.strip[1].config.color = 8
case 'LIVE':
self._mixer.config.mute_group[0].on = True
print(f'Mute Group 1 is {self._mixer.config.mute_group[0].on}')
def on_exit_started(self, _):
self._stop_event.set()
def main():
with xair_api.connect('MR18', ip='mixer.local') as mixer:
stop_event = threading.Event()
with Observer(mixer, stop_event):
stop_event.wait()
if __name__ == '__main__':
main()

475
poetry.lock generated
View File

@@ -1,252 +1,371 @@
[[package]] # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
[[package]] [[package]]
name = "black" name = "cachetools"
version = "22.8.0" version = "5.5.0"
description = "The uncompromising code formatter." description = "Extensible memoizing collections and decorators"
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
]
[package.dependencies] [[package]]
colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "chardet"
version = "5.2.0"
description = "Universal encoding detector for Python 3"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
{file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.5" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "distlib"
version = "0.3.9"
description = "Distribution utilities"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "filelock"
version = "3.20.3"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
{file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "1.1.1" version = "2.0.0"
description = "iniconfig: brain-dead simple config-ini parsing" description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = ">=3.7"
groups = ["dev"]
[[package]] files = [
name = "isort" {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
version = "5.10.1" {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
description = "A Python utility / library to sort Python imports." ]
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.3" version = "24.2"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.8"
groups = ["dev"]
[package.dependencies] files = [
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
[[package]] ]
name = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "2.5.2" version = "4.3.6"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras] [package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.11.2)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.5.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras] [package.extras]
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "py" name = "pyenv-inspect"
version = "1.11.0" version = "0.4.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities" description = "An auxiliary library for the virtualenv-pyenv and tox-pyenv-redux plugins"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pyenv-inspect-0.4.0.tar.gz", hash = "sha256:ec429d1d81b67ab0b08a0408414722a79d24fd1845a5b264267e44e19d8d60f0"},
{file = "pyenv_inspect-0.4.0-py3-none-any.whl", hash = "sha256:618683ae7d3e6db14778d58aa0fc6b3170180d944669b5d35a8aa4fb7db550d2"},
]
[[package]] [[package]]
name = "pyparsing" name = "pyproject-api"
version = "3.0.9" version = "1.8.0"
description = "pyparsing module - Classes and methods to define and execute parsing grammars" description = "API to interact with the python pyproject.toml based projects"
category = "dev"
optional = false optional = false
python-versions = ">=3.6.8" python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"},
{file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"},
]
[package.dependencies]
packaging = ">=24.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
diagrams = ["railroad-diagrams", "jinja2"] docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"]
testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.1.3" version = "8.3.4"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
]
[package.dependencies] [package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""} colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*" iniconfig = "*"
packaging = "*" packaging = "*"
pluggy = ">=0.12,<2.0" pluggy = ">=1.5,<2"
py = ">=1.8.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""}
tomli = ">=1.0.0"
[package.extras] [package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]] [[package]]
name = "pytest-randomly" name = "pytest-randomly"
version = "3.12.0" version = "3.16.0"
description = "Pytest plugin to randomly order tests and control random.seed." description = "Pytest plugin to randomly order tests and control random.seed."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"},
{file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"},
]
[package.dependencies] [package.dependencies]
pytest = "*" pytest = "*"
[[package]] [[package]]
name = "python-osc" name = "python-osc"
version = "1.8.0" version = "1.9.3"
description = "Open Sound Control server and client implementations in pure Python" description = "Open Sound Control server and client implementations in pure Python"
category = "main"
optional = false optional = false
python-versions = "*" python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "python_osc-1.9.3-py3-none-any.whl", hash = "sha256:7def2075be72f07bae5a4c1a55cc7d907b247f4a5d910f3159ed30ac2b1f17cc"},
{file = "python_osc-1.9.3.tar.gz", hash = "sha256:bd0fa40def43ce509894709feb0e18f02192aca192c5e6c8fe2ba69e58f21794"},
]
[[package]]
name = "ruff"
version = "0.8.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"},
{file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"},
{file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"},
{file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"},
{file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"},
{file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"},
{file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"},
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.2.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
name = "tox"
version = "4.23.2"
description = "tox is a generic virtualenv management and test command line tool"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"},
{file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"},
]
[package.dependencies]
cachetools = ">=5.5"
chardet = ">=5.2"
colorama = ">=0.4.6"
filelock = ">=3.16.1"
packaging = ">=24.1"
platformdirs = ">=4.3.6"
pluggy = ">=1.5"
pyproject-api = ">=1.8"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""}
virtualenv = ">=20.26.6"
[package.extras]
test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version == \"3.10\""
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "virtualenv"
version = "20.36.1"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"},
{file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}
platformdirs = ">=3.9.1,<5"
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""]
[[package]]
name = "virtualenv-pyenv"
version = "0.5.0"
description = "A virtualenv Python discovery plugin for pyenv-installed interpreters"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "virtualenv-pyenv-0.5.0.tar.gz", hash = "sha256:7b0e5fe3dfbdf484f4cf9b01e1f98111e398db6942237910f666356e6293597f"},
{file = "virtualenv_pyenv-0.5.0-py3-none-any.whl", hash = "sha256:21750247e36c55b3c547cfdeb08f51a3867fe7129922991a4f9c96980c0a4a5d"},
]
[package.dependencies]
pyenv-inspect = ">=0.4,<0.5"
virtualenv = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "2.1"
python-versions = "^3.10" python-versions = ">=3.10"
content-hash = "80440f75f4191b46dc73824fbfc4fd2fc1ea4dfbdba08591cabb600a86ae2400" content-hash = "dbba803b0ac29e3120c3f302260b600a8ef58bfac91c41a58f242804f17b89ba"
[metadata.files]
attrs = []
black = []
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pathspec = []
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = []
pytest-randomly = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
]
python-osc = []
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]

View File

@@ -1,26 +1,138 @@
[tool.poetry] [project]
name = "xair-api" name = "xair-api"
version = "1.1.1" version = "2.4.3"
description = "Remote control Behringer X-Air | Midas MR mixers through OSC" description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
license = "MIT" license = { text = "MIT" }
readme = "README.md" readme = "README.md"
repository = "https://github.com/onyx-and-iris/xair-api-python" requires-python = ">=3.10"
dependencies = [
"python-osc (>=1.9.3,<2.0.0)",
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
]
[tool.poetry.dependencies] [tool.poetry.requires-plugins]
python = "^3.10" poethepoet = "^0.35.0"
python-osc = "^1.8.0"
tomli = { version = "^2.0.1", python = "<3.11" }
[tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.1.2" pytest = "^8.3.4"
pytest-randomly = "^3.12.0" pytest-randomly = "^3.16.0"
black = "^22.6.0" ruff = "^0.8.6"
isort = "^5.10.1" tox = "^4.23.2"
virtualenv-pyenv = "^0.5.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts] [tool.poe.tasks]
obs = "examples.xair-obs.__main__:main" obs.script = "scripts:ex_obs"
sends.script = "scripts:ex_sends"
headamp.script = "scripts:ex_headamp"
test-xair.script = "scripts:test_xair"
test-x32.script = "scripts:test_x32"
test-all.script = "scripts:test_all"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py310,py311,py312,py313
[testenv]
passenv =
TEST_MODULE
setenv = VIRTUALENV_DISCOVERY=pyenv
allowlist_externals = poetry
commands_pre =
poetry install --no-interaction --no-root
commands =
poetry run pytest tests/{env:TEST_MODULE}
[testenv:obs]
setenv = VIRTUALENV_DISCOVERY=pyenv
allowlist_externals = poetry
deps = obsws-python
commands_pre =
poetry install --no-interaction --no-root --without dev
commands =
poetry run python examples/xair_obs
"""
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
# Same as Black.
line-length = 88
indent-width = 4
# Assume Python 3.10
target-version = "py310"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Unlike Black, use single quotes for strings.
quote-style = "single"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = false
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402", "F401"]

35
scripts.py Normal file
View File

@@ -0,0 +1,35 @@
import os
import subprocess
import sys
from pathlib import Path
def ex_obs():
subprocess.run(['tox', 'r', '-e', 'obs'])
def ex_sends():
path = Path.cwd() / 'examples' / 'sends' / '.'
subprocess.run([sys.executable, str(path)])
def ex_headamp():
path = Path.cwd() / 'examples' / 'headamp' / '.'
subprocess.run([sys.executable, str(path)])
def test_xair():
subprocess.run(['tox'], env=os.environ.copy() | {'TEST_MODULE': 'xair'})
def test_x32():
path = Path.cwd() / 'tools' / 'x32.exe'
proc = subprocess.Popen([str(path), '-i', 'x32.local'])
subprocess.run(['tox'], env=os.environ.copy() | {'TEST_MODULE': 'x32'})
proc.terminate()
def test_all():
steps = [test_xair, test_x32]
for step in steps:
step()

View File

@@ -1,40 +0,0 @@
import sys
import threading
from dataclasses import dataclass
import xair_api
from xair_api import kinds
kind_id = "MR18"
ip = "mixer.local"
tests = xair_api.connect(kind_id, ip=ip)
kind = kinds.get(kind_id)
@dataclass
class Data:
"""bounds data to map tests to a kind"""
name: str = kind.id_
dca: int = kind.num_dca - 1
strip: int = kind.num_strip - 1
bus: int = kind.num_bus - 1
fx: int = kind.num_fx - 1
rtn: int = kind.num_rtn - 1
data = Data()
def setup_module():
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
tests.worker = threading.Thread(target=tests.run_server)
tests.worker.daemon = True
tests.worker.start()
tests.validate_connection()
def teardown_module():
tests.server.shutdown()

41
tests/x32/__init__.py Normal file
View File

@@ -0,0 +1,41 @@
import sys
import threading
from dataclasses import dataclass
import xair_api
from xair_api import kinds
kind_id = 'X32'
ip = 'x32.local'
tests = xair_api.connect(kind_id, ip=ip)
kind = kinds.get(kind_id)
@dataclass
class Data:
"""bounds test data to a kind"""
name: str = kind.id_
dca: int = kind.num_dca - 1
strip: int = kind.num_strip - 1
bus: int = kind.num_bus - 1
fx: int = kind.num_fx - 1
auxrtn: int = kind.num_auxrtn - 1
matrix: int = kind.num_matrix - 1
data = Data()
def setup_module():
print(f'\nRunning tests for kind [{data.name}]\n', file=sys.stdout)
tests.worker = threading.Thread(target=tests.run_server)
tests.worker.daemon = True
tests.worker.start()
tests.validate_connection()
def teardown_module():
tests.server.shutdown()

144
tests/x32/test_adapter.py Normal file
View File

@@ -0,0 +1,144 @@
import pytest
from tests.x32 import data, tests
""" STRIP TESTS """
class TestSetAndGetStripMuteHigher:
"""Mute"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'strip')[data.strip]
@pytest.mark.parametrize(
'param,value',
[('mute', True), ('mute', False)],
)
def test_it_sets_and_gets_strip_mute_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
class TestSetAndGetStripMixHigher:
"""Mix"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'mix')
@pytest.mark.parametrize(
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_strip_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
""" BUS TESTS """
class TestSetAndGetBusConfigHigher:
"""Config"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], 'config')
@pytest.mark.parametrize(
'param,value',
[('color', 0), ('color', 15)],
)
def test_it_sets_and_gets_bus_int_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
""" AUXIN TESTS """
class TestSetAndGetAuxInPreampHigher:
"""Preamp"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'auxin')
self.target = getattr(self.target[data.auxrtn], 'preamp')
@pytest.mark.parametrize(
'param,value',
[('invert', True), ('invert', False)],
)
def test_it_sets_and_gets_auxrtn_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
""" FX RETURN TESTS """
class TestSetAndGetFXReturnEQHigher:
"""EQ"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'fxreturn')
self.target = getattr(self.target[data.fx], 'eq')
@pytest.mark.parametrize(
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_fxrtn_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
""" MATRIX TESTS """
class TestSetAndGetMatrixDynHigher:
"""Dyn"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'matrix')
self.target = getattr(self.target[data.matrix], 'dyn')
@pytest.mark.parametrize(
'param,value',
[('mode', 'comp'), ('mode', 'exp')],
)
def test_it_sets_and_gets_matrix_string_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
""" MAIN STEREO TESTS """
class TestSetAndGetMainStereoInsertHigher:
"""Insert"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'mainst')
@pytest.mark.parametrize(
'param,value',
[('mode', 'comp'), ('mode', 'exp')],
)
def test_it_sets_and_gets_mainst_string_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 67"><title>tests: 67</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">67</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">67</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 77"><title>tests: 77</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">77</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">77</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

39
tests/xair/__init__.py Normal file
View File

@@ -0,0 +1,39 @@
import sys
import threading
from dataclasses import dataclass
import xair_api
from xair_api import kinds
kind_id = 'MR18'
ip = 'mixer.local'
tests = xair_api.connect(kind_id, ip=ip)
kind = kinds.get(kind_id)
@dataclass
class Data:
"""bounds test data to a kind"""
name: str = kind.id_
dca: int = kind.num_dca - 1
strip: int = kind.num_strip - 1
bus: int = kind.num_bus - 1
fx: int = kind.num_fx - 1
data = Data()
def setup_module():
print(f'\nRunning tests for kind [{data.name}]\n', file=sys.stdout)
tests.worker = threading.Thread(target=tests.run_server)
tests.worker.daemon = True
tests.worker.start()
tests.validate_connection()
def teardown_module():
tests.server.shutdown()

View File

@@ -1,6 +1,7 @@
Function RunTests { Function RunTests {
$coverage = "./tests/pytest_coverage.log" "Running tests in directory $PSScriptRoot" | Write-Host
$run_tests = "pytest -v --capture=tee-sys --junitxml=./tests/.coverage.xml" $coverage = Join-Path $PSScriptRoot "pytest_coverage.log"
$run_tests = "pytest -v $PSScriptRoot --capture=tee-sys --junitxml=$(Join-Path $PSScriptRoot ".coverage.xml")"
$match_pattern = "^=|^\s*$|^Running|^Using|^plugins|^collecting|^tests" $match_pattern = "^=|^\s*$|^Running|^Using|^plugins|^collecting|^tests"
if ( Test-Path $coverage ) { Clear-Content $coverage } if ( Test-Path $coverage ) { Clear-Content $coverage }
@@ -13,7 +14,7 @@ Function RunTests {
} }
Write-Output "$(Get-TimeStamp)" | Out-File $coverage -Append Write-Output "$(Get-TimeStamp)" | Out-File $coverage -Append
Invoke-Expression "genbadge tests -t 90 -i ./tests/.coverage.xml -o ./tests/$kind.svg" Invoke-Expression "genbadge tests -t 90 -i $(Join-Path $PSScriptRoot ".coverage.xml") -o $(Join-Path $PSScriptRoot "$kind.svg")"
} }
Function Get-TimeStamp { Function Get-TimeStamp {

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from tests import data, tests from tests.xair import data, tests
""" """
Not every subclass is tested for every superclass to avoid redundancy. Not every subclass is tested for every superclass to avoid redundancy.
@@ -19,20 +19,20 @@ class TestSetAndGetLRMixHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "lr") self.target = getattr(tests, 'lr')
self.target = getattr(self.target, "mix") self.target = getattr(self.target, 'mix')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("on", True), ("on", False)], [('on', True), ('on', False)],
) )
def test_it_sets_and_gets_lr_bool_params(self, param, value): def test_it_sets_and_gets_lr_bool_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("fader", -80.6), ("fader", -67.0)], [('fader', -80.6), ('fader', -67.0)],
) )
def test_it_sets_and_gets_lr_float_params(self, param, value): def test_it_sets_and_gets_lr_float_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -45,10 +45,10 @@ class TestSetAndGetLRConfigHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "lr") self.target = getattr(tests, 'lr')
self.target = getattr(self.target, "config") self.target = getattr(self.target, 'config')
@pytest.mark.parametrize("param,value", [("name", "test0"), ("name", "test1")]) @pytest.mark.parametrize('param,value', [('name', 'test0'), ('name', 'test1')])
def test_it_sets_and_gets_lr_string_params(self, param, value): def test_it_sets_and_gets_lr_string_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@@ -60,20 +60,20 @@ class TestSetAndGetLRInsertHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "lr") self.target = getattr(tests, 'lr')
self.target = getattr(self.target, "insert") self.target = getattr(self.target, 'insert')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("on", True), ("on", False)], [('on', True), ('on', False)],
) )
def test_it_sets_and_gets_lr_bool_params(self, param, value): def test_it_sets_and_gets_lr_bool_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("sel", 0), ("sel", 4)], [('sel', 0), ('sel', 4)],
) )
def test_it_sets_and_gets_lr_int_params(self, param, value): def test_it_sets_and_gets_lr_int_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -86,16 +86,16 @@ class TestSetAndGetLRGEQHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "lr") self.target = getattr(tests, 'lr')
self.target = getattr(self.target, "geq") self.target = getattr(self.target, 'geq')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[ [
("slider_20", -13.5), ('slider_20', -13.5),
("slider_20", 5.5), ('slider_20', 5.5),
("slider_6k3", -8.5), ('slider_6k3', -8.5),
("slider_6k3", 8.5), ('slider_6k3', 8.5),
], ],
) )
def test_it_sets_and_gets_lr_int_params(self, param, value): def test_it_sets_and_gets_lr_int_params(self, param, value):
@@ -106,18 +106,35 @@ class TestSetAndGetLRGEQHigher:
""" STRIP TESTS """ """ STRIP TESTS """
class TestSetAndGetStripMuteHigher:
"""Mute"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, 'strip')[data.strip]
@pytest.mark.parametrize(
'param,value',
[('mute', True), ('mute', False)],
)
def test_it_sets_and_gets_strip_mute_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
class TestSetAndGetStripMixHigher: class TestSetAndGetStripMixHigher:
"""Mix""" """Mix"""
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "strip") self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], "mix") self.target = getattr(self.target[data.strip], 'mix')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("on", True), ("on", False), ("lr", True), ("lr", False)], [('on', True), ('on', False), ('lr', True), ('lr', False)],
) )
def test_it_sets_and_gets_strip_bool_params(self, param, value): def test_it_sets_and_gets_strip_bool_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -130,16 +147,16 @@ class TestSetAndGetStripPreampHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "strip") self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], "preamp") self.target = getattr(self.target[data.strip], 'preamp')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[ [
("highpasson", True), ('highpasson', True),
("highpasson", False), ('highpasson', False),
("usbinput", True), ('usbinput', True),
("usbinput", False), ('usbinput', False),
], ],
) )
def test_it_sets_and_gets_strip_bool_params(self, param, value): def test_it_sets_and_gets_strip_bool_params(self, param, value):
@@ -147,16 +164,16 @@ class TestSetAndGetStripPreampHigher:
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("highpassfilter", 20), ("highpassfilter", 399)], [('highpassfilter', 20), ('highpassfilter', 399)],
) )
def test_it_sets_and_gets_strip_int_params(self, param, value): def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("usbtrim", -16.5), ("usbtrim", 5.5)], [('usbtrim', -16.5), ('usbtrim', 5.5)],
) )
def test_it_sets_and_gets_strip_float_params(self, param, value): def test_it_sets_and_gets_strip_float_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -169,12 +186,12 @@ class TestSetAndGetStripConfigHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "strip") self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], "config") self.target = getattr(self.target[data.strip], 'config')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("inputsource", 0), ("inputsource", 18), ("usbreturn", 3), ("usbreturn", 12)], [('inputsource', 0), ('inputsource', 18), ('usbreturn', 3), ('usbreturn', 12)],
) )
def test_it_sets_and_gets_strip_int_params(self, param, value): def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -187,18 +204,18 @@ class TestSetAndGetStripGateHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "strip") self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], "gate") self.target = getattr(self.target[data.strip], 'gate')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[ [
("on", True), ('on', True),
("on", False), ('on', False),
("invert", True), ('invert', True),
("invert", False), ('invert', False),
("filteron", True), ('filteron', True),
("filteron", False), ('filteron', False),
], ],
) )
def test_it_sets_and_gets_strip_bool_params(self, param, value): def test_it_sets_and_gets_strip_bool_params(self, param, value):
@@ -206,16 +223,16 @@ class TestSetAndGetStripGateHigher:
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[ [
("range", 11), ('range', 11),
("range", 48), ('range', 48),
("attack", 5), ('attack', 5),
("attack", 110), ('attack', 110),
("release", 360), ('release', 360),
("release", 2505), ('release', 2505),
("filtertype", 0), ('filtertype', 0),
("filtertype", 8), ('filtertype', 8),
], ],
) )
def test_it_sets_and_gets_strip_int_params(self, param, value): def test_it_sets_and_gets_strip_int_params(self, param, value):
@@ -223,22 +240,22 @@ class TestSetAndGetStripGateHigher:
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("mode", "exp2"), ("mode", "duck")], [('mode', 'exp2'), ('mode', 'duck')],
) )
def test_it_sets_and_gets_strip_string_params(self, param, value): def test_it_sets_and_gets_strip_string_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[ [
("threshold", -80.0), ('threshold', -80.0),
("threshold", 0.0), ('threshold', 0.0),
("hold", 355), ('hold', 355),
("hold", 63.2), ('hold', 63.2),
("filterfreq", 37.2), ('filterfreq', 37.2),
("filterfreq", 12765), ('filterfreq', 12765),
], ],
) )
def test_it_sets_and_gets_strip_float_params(self, param, value): def test_it_sets_and_gets_strip_float_params(self, param, value):
@@ -252,22 +269,22 @@ class TestSetAndGetStripAutomixHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "strip") self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], "automix") self.target = getattr(self.target[data.strip], 'automix')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("group", 0), ("group", 2)], [('group', 0), ('group', 2)],
) )
def test_it_sets_and_gets_fxsend_int_params(self, param, value): def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("weight", -10.5), ("weight", 3.5)], [('weight', -10.5), ('weight', 3.5)],
) )
def test_it_sets_and_gets_fxsend_float_params(self, param, value): def test_it_sets_and_gets_strip_float_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@@ -281,12 +298,12 @@ class TestSetAndGetBusConfigHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "bus") self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], "config") self.target = getattr(self.target[data.bus], 'config')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("color", 0), ("color", 15)], [('color', 0), ('color', 15)],
) )
def test_it_sets_and_gets_bus_bool_params(self, param, value): def test_it_sets_and_gets_bus_bool_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -299,26 +316,26 @@ class TestSetAndGetBusDynHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "bus") self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], "dyn") self.target = getattr(self.target[data.bus], 'dyn')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("on", True), ("on", False)], [('on', True), ('on', False)],
) )
def test_it_sets_and_gets_bus_bool_params(self, param, value): def test_it_sets_and_gets_bus_bool_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[ [
("mode", "comp"), ('mode', 'comp'),
("mode", "exp"), ('mode', 'exp'),
("env", "lin"), ('env', 'lin'),
("env", "log"), ('env', 'log'),
("det", "peak"), ('det', 'peak'),
("det", "rms"), ('det', 'rms'),
], ],
) )
def test_it_sets_and_gets_bus_string_params(self, param, value): def test_it_sets_and_gets_bus_string_params(self, param, value):
@@ -326,26 +343,26 @@ class TestSetAndGetBusDynHigher:
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
class TestSetAndGetBusDynHigher: class TestSetAndGetBusEQHigher:
"""EQ""" """EQ"""
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "bus") self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], "eq") self.target = getattr(self.target[data.bus], 'eq')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("on", True), ("on", False)], [('on', True), ('on', False)],
) )
def test_it_sets_and_gets_bus_bool_params(self, param, value): def test_it_sets_and_gets_bus_bool_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("mode", "peq"), ("mode", "geq"), ("mode", "teq")], [('mode', 'peq'), ('mode', 'geq'), ('mode', 'teq')],
) )
def test_it_sets_and_gets_bus_string_params(self, param, value): def test_it_sets_and_gets_bus_string_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
@@ -361,13 +378,13 @@ class TestSetAndGetFXSendGroupHigher:
__test__ = True __test__ = True
def setup_class(self): def setup_class(self):
self.target = getattr(tests, "fxsend") self.target = getattr(tests, 'fxsend')
self.target = getattr(self.target[data.fx], "group") self.target = getattr(self.target[data.fx], 'group')
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,value", 'param,value',
[("dca", 0), ("dca", 12), ("mute", 3), ("mute", 8)], [('dca', 0), ('dca', 12), ('mute', 3), ('mute', 8)],
) )
def test_it_sets_and_gets_bus_bool_params(self, param, value): def test_it_sets_and_gets_fxsend_int_params(self, param, value):
setattr(self.target, param, value) setattr(self.target, param, value)
assert getattr(self.target, param) == value assert getattr(self.target, param) == value

3
tools/README.md Normal file
View File

@@ -0,0 +1,3 @@
# X32 Tests
Download the [x32 Emulator](https://sites.google.com/site/patrickmaillot/x32#h.p_rE4IH0Luimc0) and place it in this directory in order to run the x32 tests.

View File

@@ -1,3 +1,3 @@
from .xair import request_remote_obj as connect from .xair import request_remote_obj as connect
_ALL__ = ["connect"] _ALL__ = ['connect']

47
xair_api/adapter.py Normal file
View File

@@ -0,0 +1,47 @@
from .bus import Bus as IBus
from .headamp import HeadAmp as IHeadAmp
from .lr import LR as ILR
from .rtn import AuxRtn as IAuxRtn
from .rtn import FxRtn as IFxRtn
class Bus(IBus):
@property
def address(self):
return f'/bus/{str(self.index).zfill(2)}'
class AuxRtn(IAuxRtn):
@property
def address(self):
return f'/auxin/{str(self.index).zfill(2)}'
class FxRtn(IFxRtn):
@property
def address(self):
return f'/fxrtn/{str(self.index).zfill(2)}'
class MainStereo(ILR):
@property
def address(self) -> str:
return '/main/st'
class MainMono(ILR):
@property
def address(self) -> str:
return '/main/m'
class Matrix(ILR):
@property
def address(self) -> str:
return f'/mtx/{str(self.index).zfill(2)}'
class HeadAmp(IHeadAmp):
@property
def address(self):
return f'/headamp/{str(self.index).zfill(3)}'

View File

@@ -1,7 +1,10 @@
import abc import abc
import logging
from .errors import XAirRemoteError from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, GEQ, Config, Dyn, Group, Insert, Mix
logger = logging.getLogger(__name__)
class IBus(abc.ABC): class IBus(abc.ABC):
@@ -10,13 +13,13 @@ class IBus(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
@@ -36,12 +39,12 @@ class Bus(IBus):
Returns a Bus class of a kind. Returns a Bus class of a kind.
""" """
BUS_cls = type( BUS_cls = type(
f"Bus{remote.kind}", f'Bus{remote.kind}',
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {} f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in ( for _cls in (
Config, Config,
@@ -52,11 +55,12 @@ class Bus(IBus):
Mix, Mix,
Group, Group,
) )
} },
'mute': mute_prop(),
}, },
) )
return BUS_cls(remote, index) return BUS_cls(remote, index)
@property @property
def address(self) -> str: def address(self) -> str:
return f"/bus/{self.index}" return f'/bus/{self.index}'

View File

@@ -1,9 +1,10 @@
import abc import abc
import logging
from . import kinds from . import kinds, util
from .errors import XAirRemoteError
from .meta import bool_prop from .meta import bool_prop
from .util import _get_level_val, _set_level_val, lin_get, lin_set
logger = logging.getLogger(__name__)
class IConfig(abc.ABC): class IConfig(abc.ABC):
@@ -11,13 +12,13 @@ class IConfig(abc.ABC):
def __init__(self, remote): def __init__(self, remote):
self._remote = remote self._remote = remote
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
@@ -35,41 +36,37 @@ class Config(IConfig):
Returns a Config class of a kind. Returns a Config class of a kind.
""" """
LINKS_cls = _make_links_mixins[remote.kind.id_] LINKS_cls = _make_links_mixins[remote.kind.id_]
MUTEGROUP_cls = type(f"MuteGroup", (Config.MuteGroup, cls), {}) MUTEGROUP_cls = type('MuteGroup', (Config.MuteGroup, cls), {})
MONITOR_cls = type(f"ConfigMonitor", (Config.Monitor, cls), {}) MONITOR_cls = type('ConfigMonitor', (Config.Monitor, cls), {})
CONFIG_cls = type( CONFIG_cls = type(
f"Config{remote.kind}", f'Config{remote.kind}',
(cls, LINKS_cls), (cls, LINKS_cls),
{ {
"mute_group": tuple(MUTEGROUP_cls(remote, i) for i in range(4)), 'mute_group': tuple(MUTEGROUP_cls(remote, i) for i in range(4)),
"monitor": MONITOR_cls(remote), 'monitor': MONITOR_cls(remote),
}, },
) )
return CONFIG_cls(remote) return CONFIG_cls(remote)
@property @property
def address(self) -> str: def address(self) -> str:
return f"/config" return '/config'
@property @property
def amixenable(self) -> bool: def amixenable(self) -> bool:
return self.getter("mute")[0] == 1 return self.getter('mute')[0] == 1
@amixenable.setter @amixenable.setter
def amixenable(self, val: bool): def amixenable(self, val: bool):
if not isinstance(val, bool): self.setter('amixenable', 1 if val else 0)
raise XAirRemoteError("amixenable is a bool parameter")
self.setter("amixenable", 1 if val else 0)
@property @property
def amixlock(self) -> bool: def amixlock(self) -> bool:
return self.getter("amixlock")[0] == 1 return self.getter('amixlock')[0] == 1
@amixlock.setter @amixlock.setter
def amixlock(self, val: bool): def amixlock(self, val: bool):
if not isinstance(val, bool): self.setter('amixlock', 1 if val else 0)
raise XAirRemoteError("amixlock is a bool parameter")
self.setter("amixlock", 1 if val else 0)
class MuteGroup: class MuteGroup:
def __init__(self, remote, i): def __init__(self, remote, i):
@@ -79,143 +76,128 @@ class Config(IConfig):
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Config.MuteGroup, self).address root = super(Config.MuteGroup, self).address
return f"{root}/mute" return f'{root}/mute'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter(f"{self.i}")[0] == 1 return self.getter(f'{self.i}')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter(f'{self.i}', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter(f"{self.i}", 1 if val else 0)
class Monitor: class Monitor:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Config.Monitor, self).address root = super(Config.Monitor, self).address
return f"{root}/solo" return f'{root}/solo'
@property @property
@util.db_from
def level(self) -> float: def level(self) -> float:
retval = self.getter("level")[0] return self.getter('level')[0]
return _get_level_val(retval)
@level.setter @level.setter
@util.db_to
def level(self, val: float): def level(self, val: float):
_set_level_val(self, val) self.setter('level', val)
@property @property
def source(self) -> int: def source(self) -> int:
return int(self.getter("source")[0]) return int(self.getter('source')[0])
@source.setter @source.setter
def source(self, val: int): def source(self, val: int):
if not isinstance(val, int): self.setter('source', val)
raise XAirRemoteError("source is an int parameter")
self.setter(f"source", val)
@property @property
def sourcetrim(self) -> float: def sourcetrim(self) -> float:
return round(lin_get(-18, 18, self.getter("sourcetrim")[0]), 1) return round(util.lin_get(-18, 18, self.getter('sourcetrim')[0]), 1)
@sourcetrim.setter @sourcetrim.setter
def sourcetrim(self, val: float): def sourcetrim(self, val: float):
if not isinstance(val, float): if not -18 <= val <= 18:
raise XAirRemoteError( self.logger.warning(
"sourcetrim is a float parameter, expected value in range -18 to 18" f'sourcetrim got {val}, expected value in range -18.0 to 18.0'
) )
self.setter("sourcetrim", lin_set(-18, 18, val)) self.setter('sourcetrim', util.lin_set(-18, 18, val))
@property @property
def chmode(self) -> bool: def chmode(self) -> bool:
return self.getter("chmode")[0] == 1 return self.getter('chmode')[0] == 1
@chmode.setter @chmode.setter
def chmode(self, val: bool): def chmode(self, val: bool):
if not isinstance(val, bool): self.setter('chmode', 1 if val else 0)
raise XAirRemoteError("chmode is a bool parameter")
self.setter("chmode", 1 if val else 0)
@property @property
def busmode(self) -> bool: def busmode(self) -> bool:
return self.getter("busmode")[0] == 1 return self.getter('busmode')[0] == 1
@busmode.setter @busmode.setter
def busmode(self, val: bool): def busmode(self, val: bool):
if not isinstance(val, bool): self.setter('busmode', 1 if val else 0)
raise XAirRemoteError("busmode is a bool parameter")
self.setter("busmode", 1 if val else 0)
@property @property
def dimgain(self) -> int: def dimgain(self) -> int:
return int(lin_get(-40, 0, self.getter("dimatt")[0])) return int(util.lin_get(-40, 0, self.getter('dimatt')[0]))
@dimgain.setter @dimgain.setter
def dimgain(self, val: int): def dimgain(self, val: int):
if not isinstance(val, int): if not -40 <= val <= 0:
raise XAirRemoteError( self.logger.warning(
"dimgain is an int parameter, expected value in range -40 to 0" f'dimgain got {val}, expected value in range -40 to 0'
) )
self.setter("dimatt", lin_set(-40, 0, val)) self.setter('dimatt', util.lin_set(-40, 0, val))
@property @property
def dim(self) -> bool: def dim(self) -> bool:
return self.getter("dim")[0] == 1 return self.getter('dim')[0] == 1
@dim.setter @dim.setter
def dim(self, val: bool): def dim(self, val: bool):
if not isinstance(val, bool): self.setter('dim', 1 if val else 0)
raise XAirRemoteError("dim is a bool parameter")
self.setter("dim", 1 if val else 0)
@property @property
def mono(self) -> bool: def mono(self) -> bool:
return self.getter("mono")[0] == 1 return self.getter('mono')[0] == 1
@mono.setter @mono.setter
def mono(self, val: bool): def mono(self, val: bool):
if not isinstance(val, bool): self.setter('mono', 1 if val else 0)
raise XAirRemoteError("mono is a bool parameter")
self.setter("mono", 1 if val else 0)
@property @property
def mute(self) -> bool: def mute(self) -> bool:
return self.getter("mute")[0] == 1 return self.getter('mute')[0] == 1
@mute.setter @mute.setter
def mute(self, val: bool): def mute(self, val: bool):
if not isinstance(val, bool): self.setter('mute', 1 if val else 0)
raise XAirRemoteError("mute is a bool parameter")
self.setter("mute", 1 if val else 0)
@property @property
def dimfpl(self) -> bool: def dimfpl(self) -> bool:
return self.getter("dimfpl")[0] == 1 return self.getter('dimfpl')[0] == 1
@dimfpl.setter @dimfpl.setter
def dimfpl(self, val: bool): def dimfpl(self, val: bool):
if not isinstance(val, bool): self.setter('dimfpl', 1 if val else 0)
raise XAirRemoteError("dimfpl is a bool parameter")
self.setter("dimfpl", 1 if val else 0)
def _make_links_mixin(kind): def _make_links_mixin(kind):
"""Creates a links mixin""" """Creates a links mixin"""
return type( return type(
f"Links{kind}", f'Links{kind}',
(), (),
{ {
"link_eq": bool_prop("linkcfg/eq"), 'link_eq': bool_prop('linkcfg/eq'),
"link_dyn": bool_prop("linkcfg/dyn"), 'link_dyn': bool_prop('linkcfg/dyn'),
"link_fader_mute": bool_prop("linkcfg/fdrmute"), 'link_fader_mute': bool_prop('linkcfg/fdrmute'),
**{ **{
f"chlink{i}_{i+1}": bool_prop(f"chlink/{i}-{i+1}") f'chlink{i}_{i+1}': bool_prop(f'chlink/{i}-{i+1}')
for i in range(1, kind.num_strip, 2) for i in range(1, kind.num_strip, 2)
}, },
**{ **{
f"buslink{i}_{i+1}": bool_prop(f"buslink/{i}-{i+1}") f'buslink{i}_{i+1}': bool_prop(f'buslink/{i}-{i+1}')
for i in range(1, kind.num_bus, 2) for i in range(1, kind.num_bus, 2)
}, },
}, },

View File

@@ -1,6 +1,7 @@
import abc import abc
import logging
from .errors import XAirRemoteError logger = logging.getLogger(__name__)
class IDCA(abc.ABC): class IDCA(abc.ABC):
@@ -9,13 +10,13 @@ class IDCA(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str) -> tuple: def getter(self, param: str) -> tuple:
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
@@ -27,34 +28,36 @@ class DCA(IDCA):
@property @property
def address(self) -> str: def address(self) -> str:
return f"/dca/{self.index}" return f'/dca/{self.index}'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter("on")[0] == 1 return self.getter('on')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter('on', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) @property
def mute(self) -> bool:
return not self.on
@mute.setter
def mute(self, val: bool):
self.on = not val
@property @property
def name(self) -> str: def name(self) -> str:
return self.getter("config/name")[0] return self.getter('config/name')[0]
@name.setter @name.setter
def name(self, val: str): def name(self, val: str):
if not isinstance(val, str): self.setter('config/name', val)
raise XAirRemoteError("name is a str parameter")
self.setter("config/name")[0]
@property @property
def color(self) -> int: def color(self) -> int:
return self.getter("config/color")[0] return self.getter('config/color')[0]
@color.setter @color.setter
def color(self, val: int): def color(self, val: int):
if not isinstance(val, int): self.setter('config/color', val)
raise XAirRemoteError("color is an int parameter")
self.setter("config/color", val)

View File

@@ -1,4 +1,14 @@
class XAirRemoteError(Exception): class XAirRemoteError(Exception):
"""Base error class for XAIR Remote.""" """Base error class for XAIR Remote."""
pass
class XAirRemoteConnectionTimeoutError(XAirRemoteError):
"""Exception raised when a connection attempt times out"""
def __init__(self, ip, port):
self.ip = ip
self.port = port
super().__init__(
f'Timeout attempting to connect to mixer at {self.ip}:{self.port}'
)

View File

@@ -1,7 +1,10 @@
import abc import abc
import logging
from .errors import XAirRemoteError from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import Config, Group, Mix
logger = logging.getLogger(__name__)
class IFX(abc.ABC): class IFX(abc.ABC):
@@ -10,19 +13,35 @@ class IFX(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
pass pass
class FX(IFX):
"""Concrete class for fx"""
@property
def address(self) -> str:
return f'/fx/{self.index}'
@property
def type(self) -> int:
return self.getter('type')[0]
@type.setter
def type(self, val: int):
self.setter('type', val)
class FXSend(IFX): class FXSend(IFX):
"""Concrete class for fxsend""" """Concrete class for fxsend"""
@@ -36,37 +55,20 @@ class FXSend(IFX):
Returns an FXSend class of a kind. Returns an FXSend class of a kind.
""" """
FXSEND_cls = type( FXSEND_cls = type(
f"FXSend{remote.kind}", f'FXSend{remote.kind}',
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {} f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in (Config, Mix, Group) for _cls in (Config, Mix, Group)
} },
'mute': mute_prop(),
}, },
) )
return FXSEND_cls(remote, index) return FXSEND_cls(remote, index)
@property @property
def address(self) -> str: def address(self) -> str:
return f"/fxsend/{self.index}" return f'/fxsend/{self.index}'
class FXReturn(IFX):
"""Concrete class for fxreturn"""
@property
def address(self) -> str:
return f"/fx/{self.index}"
@property
def type(self) -> int:
return self.getter("type")[0]
@type.setter
def type(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("type is an integer parameter")
self.setter("type", val)

49
xair_api/headamp.py Normal file
View File

@@ -0,0 +1,49 @@
import abc
import logging
from . import util
logger = logging.getLogger(__name__)
class IHeadAmp(abc.ABC):
"""Abstract Base Class for headamps"""
def __init__(self, remote, index: int):
self._remote = remote
self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str):
return self._remote.query(f'{self.address}/{param}')
def setter(self, param: str, val: int):
self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod
def address(self):
pass
class HeadAmp(IHeadAmp):
"""Concrete class for headamps"""
@property
def address(self):
return f'/headamp/{str(self.index).zfill(2)}'
@property
def gain(self):
return round(util.lin_get(-12, 60, self.getter('gain')[0]), 1)
@gain.setter
def gain(self, val):
self.setter('gain', util.lin_set(-12, 60, val))
@property
def phantom(self):
return self.getter('phantom')[0] == 1
@phantom.setter
def phantom(self, val):
self.setter('phantom', 1 if val else 0)

View File

@@ -1,61 +1,56 @@
from dataclasses import dataclass from dataclasses import dataclass
"""
# osc slightly different, interface would need adjusting to support this mixer.
@dataclass @dataclass(frozen=True)
class X32KindMap:
id_: str = "X32"
num_dca: int = 8
num_strip: int = 32
num_bus: int = 16
num_fx: int = 8
num_rtn: int = 6
"""
@dataclass
class KindMap: class KindMap:
id_: str
def __str__(self) -> str: def __str__(self) -> str:
return self.id_ return self.id_
@dataclass @dataclass(frozen=True)
class MR18KindMap(KindMap): class X32KindMap(KindMap):
# note ch 17-18 defined as aux rtn num_dca: int = 8
id_: str num_strip: int = 32
num_bus: int = 16
num_fx: int = 8
num_auxrtn: int = 8
num_matrix: int = 6
num_headamp: int = 127
@dataclass(frozen=True)
class XR18KindMap(KindMap):
# note ch 17-18 defined as aux return
num_dca: int = 4 num_dca: int = 4
num_strip: int = 16 num_strip: int = 16
num_bus: int = 6 num_bus: int = 6
num_fx: int = 4 num_fx: int = 4
num_rtn: int = 4
@dataclass @dataclass(frozen=True)
class XR16KindMap(KindMap): class XR16KindMap(KindMap):
id_: str
num_dca: int = 4 num_dca: int = 4
num_strip: int = 16 num_strip: int = 16
num_bus: int = 4 num_bus: int = 4
num_fx: int = 4 num_fx: int = 4
num_rtn: int = 4
@dataclass @dataclass(frozen=True)
class XR12KindMap(KindMap): class XR12KindMap(KindMap):
id_: str
num_dca: int = 4 num_dca: int = 4
num_strip: int = 12 num_strip: int = 12
num_bus: int = 2 num_bus: int = 2
num_fx: int = 4 num_fx: int = 4
num_rtn: int = 4
_kinds = { _kinds = {
"XR18": MR18KindMap(id_="XR18"), 'X32': X32KindMap(id_='X32'),
"MR18": MR18KindMap(id_="MR18"), 'MR18': XR18KindMap(id_='MR18'),
"XR16": XR16KindMap(id_="XR16"), 'XR18': XR18KindMap(id_='XR18'),
"XR12": XR12KindMap(id_="XR12"), 'XR16': XR16KindMap(id_='XR16'),
'XR12': XR12KindMap(id_='XR12'),
} }
@@ -63,4 +58,4 @@ def get(kind_id):
return _kinds[kind_id] return _kinds[kind_id]
all = list(kind for kind in _kinds.values()) all = list(_kinds.values())

View File

@@ -1,21 +1,27 @@
import abc import abc
import logging
from typing import Optional
from .errors import XAirRemoteError from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, GEQ, Config, Dyn, Insert, Mix
logger = logging.getLogger(__name__)
class ILR(abc.ABC): class ILR(abc.ABC):
"""Abstract Base Class for buses""" """Abstract Base Class for lr"""
def __init__(self, remote): def __init__(self, remote, index: Optional[int] = None):
self._remote = remote self._remote = remote
if index is not None:
self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
@@ -23,10 +29,10 @@ class ILR(abc.ABC):
class LR(ILR): class LR(ILR):
"""Concrete class for buses""" """Concrete class for lr"""
@classmethod @classmethod
def make(cls, remote): def make(cls, remote, index=None):
""" """
Factory function for LR Factory function for LR
@@ -35,26 +41,27 @@ class LR(ILR):
Returns an LR class of a kind. Returns an LR class of a kind.
""" """
LR_cls = type( LR_cls = type(
f"LR{remote.kind}", f'LR{remote.kind}',
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {} f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote) )(remote, index)
for _cls in ( for _cls in (
Config, Config,
Dyn, Dyn,
Insert, Insert,
GEQ.make(), GEQ.make(),
EQ.make_sixband(cls, remote), EQ.make_sixband(cls, remote, index),
Mix, Mix,
) )
}, },
'mute': mute_prop(),
}, },
) )
return LR_cls(remote) return LR_cls(remote, index)
@property @property
def address(self) -> str: def address(self) -> str:
return f"/lr" return '/lr'

View File

@@ -1,4 +1,3 @@
from .errors import XAirRemoteError
from .util import lin_get, lin_set from .util import lin_get, lin_set
@@ -9,8 +8,6 @@ def bool_prop(param):
return self.getter(param)[0] == 1 return self.getter(param)[0] == 1
def fset(self, val): def fset(self, val):
if not isinstance(val, bool):
raise XAirRemoteError(f"{param} is a boolean parameter")
self.setter(param, 1 if val else 0) self.setter(param, 1 if val else 0)
return property(fget, fset) return property(fget, fset)
@@ -23,8 +20,6 @@ def string_prop(param):
return self.getter(param)[0] return self.getter(param)[0]
def fset(self, val): def fset(self, val):
if not isinstance(val, str):
raise XAirRemoteError(f"{param} is a string parameter")
self.setter(param, val) self.setter(param, val)
return property(fget, fset) return property(fget, fset)
@@ -37,8 +32,6 @@ def int_prop(param):
return int(self.getter(param)[0]) return int(self.getter(param)[0])
def fset(self, val): def fset(self, val):
if not isinstance(val, int):
raise XAirRemoteError(f"{param} is an integer parameter")
self.setter(param, val) self.setter(param, val)
return property(fget, fset) return property(fget, fset)
@@ -51,27 +44,32 @@ def float_prop(param):
return round(self.getter(param)[0], 1) return round(self.getter(param)[0], 1)
def fset(self, val): def fset(self, val):
if not isinstance(val, int):
raise XAirRemoteError(f"{param} is a float parameter")
self.setter(param, val) self.setter(param, val)
return property(fget, fset) return property(fget, fset)
def geq_prop(param): def geq_prop(param):
# fmt: off param = param.replace('_', '.')
opts = {
"1k": 1000, "1k25": 1250, "1k6": 1600, "2k": 2000, "3k15": 3150, "4k": 4000,
"5k": 5000, "6k3": 6300, "8k": 8000, "10k": 10000, "12k5": 12500, "16k": 16000,
"20k": 20000,
}
# fmt: on
param = param.replace("_", ".")
def fget(self) -> float: def fget(self) -> float:
return round(lin_get(-15, 15, self.getter(param)[0]), 1) return round(lin_get(-15, 15, self.getter(param)[0]), 1)
def fset(self, val): def fset(self, val):
if not -15 <= val <= 15:
self.logger.warning(
f'slider_{param} got {val}, expected value in range -15.0 to 15.0'
)
self.setter(param, lin_set(-15, 15, val)) self.setter(param, lin_set(-15, 15, val))
return property(fget, fset) return property(fget, fset)
def mute_prop():
def fget(self):
return not self.mix.on
def fset(self, val):
self.mix.on = not val
return property(fget, fset)

View File

@@ -1,50 +1,53 @@
import abc import abc
import logging
from typing import Optional from typing import Optional
from .errors import XAirRemoteError from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, Config, Group, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IRtn(abc.ABC): class IRtn(abc.ABC):
"""Abstract Base Class for aux""" """Abstract Base Class for rtn"""
def __init__(self, remote, index: Optional[int] = None): def __init__(self, remote, index: Optional[int] = None):
self._remote = remote self._remote = remote
if index is not None: if index is not None:
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
pass pass
class Aux(IRtn): class AuxRtn(IRtn):
"""Concrete class for aux""" """Concrete class for auxrtn"""
@classmethod @classmethod
def make(cls, remote): def make(cls, remote, index=None):
""" """
Factory function for aux Factory function for auxrtn
Creates a mixin of shared subclasses, sets them as class attributes. Creates a mixin of shared subclasses, sets them as class attributes.
Returns an Aux class of a kind. Returns an AuxRtn class of a kind.
""" """
AUX_cls = type( AUXRTN_cls = type(
f"Aux{remote.kind}", f'AuxRtn{remote.kind}',
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {} f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote) )(remote, index)
for _cls in ( for _cls in (
Config, Config,
Preamp, Preamp,
@@ -52,35 +55,40 @@ class Aux(IRtn):
Mix, Mix,
Group, Group,
) )
} },
'send': tuple(
Send.make(cls, i, remote)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
'mute': mute_prop(),
}, },
) )
return AUX_cls(remote) return AUXRTN_cls(remote, index)
@property @property
def address(self): def address(self):
return "/rtn/aux" return '/rtn/aux'
class Rtn(IRtn): class FxRtn(IRtn):
"""Concrete class for rtn""" """Concrete class for fxrtn"""
@classmethod @classmethod
def make(cls, remote, index): def make(cls, remote, index):
""" """
Factory function for rtn Factory function for fxrtn
Creates a mixin of shared subclasses, sets them as class attributes. Creates a mixin of shared subclasses, sets them as class attributes.
Returns an Rtn class of a kind. Returns an FxRtn class of a kind.
""" """
RTN_cls = type( FXRTN_cls = type(
f"Rtn{remote.kind.id_}", f'FxRtn{remote.kind}',
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {} f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in ( for _cls in (
Config, Config,
@@ -89,11 +97,16 @@ class Rtn(IRtn):
Mix, Mix,
Group, Group,
) )
} },
'send': tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
'mute': mute_prop(),
}, },
) )
return RTN_cls(remote, index) return FXRTN_cls(remote, index)
@property @property
def address(self): def address(self):
return f"/rtn/{self.index}" return f'/rtn/{self.index}'

View File

@@ -1,11 +1,10 @@
from typing import Union from typing import Optional, Union
from .errors import XAirRemoteError from . import util
from .meta import geq_prop from .meta import geq_prop
from .util import _get_fader_val, _set_fader_val, lin_get, lin_set, log_get, log_set
""" """
Classes shared by /ch, /rtn, /rt/aux, /bus, /fxsend, /lr Classes shared by /ch, /rtn, /rtn/aux, /bus, /fxsend, /lr
""" """
@@ -13,552 +12,529 @@ class Config:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Config, self).address root = super(Config, self).address
return f"{root}/config" return f'{root}/config'
@property @property
def name(self) -> str: def name(self) -> str:
return self.getter("name")[0] return self.getter('name')[0]
@name.setter @name.setter
def name(self, val: str): def name(self, val: str):
if not isinstance(val, str): self.setter('name', val)
raise XAirRemoteError("name is a string parameter")
self.setter("name", val)
@property @property
def color(self) -> int: def color(self) -> int:
return self.getter("color")[0] return self.getter('color')[0]
@color.setter @color.setter
def color(self, val: int): def color(self, val: int):
if not isinstance(val, int): self.setter('color', val)
raise XAirRemoteError("color is an int parameter")
self.setter("color", val)
@property @property
def inputsource(self) -> int: def inputsource(self) -> int:
return self.getter("insrc")[0] return self.getter('insrc')[0]
@inputsource.setter @inputsource.setter
def inputsource(self, val: int): def inputsource(self, val: int):
if not isinstance(val, int): self.setter('insrc', val)
raise XAirRemoteError("inputsource is an int parameter")
self.setter("insrc", val)
@property @property
def usbreturn(self) -> int: def usbreturn(self) -> int:
return self.getter("rtnsrc")[0] return self.getter('rtnsrc')[0]
@usbreturn.setter @usbreturn.setter
def usbreturn(self, val: int): def usbreturn(self, val: int):
if not isinstance(val, int): self.setter('rtnsrc', val)
raise XAirRemoteError("usbreturn is an int parameter")
self.setter("rtnsrc", val)
class Preamp: class Preamp:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Preamp, self).address root = super(Preamp, self).address
return f"{root}/preamp" return f'{root}/preamp'
@property @property
def usbtrim(self) -> float: def usbtrim(self) -> float:
return round(lin_get(-18, 18, self.getter("rtntrim")[0]), 1) return round(util.lin_get(-18, 18, self.getter('rtntrim')[0]), 1)
@usbtrim.setter @usbtrim.setter
def usbtrim(self, val: float): def usbtrim(self, val: float):
if not isinstance(val, float): if not -18 <= val <= 18:
raise XAirRemoteError( self.logger.warning(
"usbtrim is a float parameter, expected value in range -18 to 18" f'usbtrim got {val}, expected value in range -18.0 to 18.0'
) )
self.setter("rtntrim", lin_set(-18, 18, val)) self.setter('rtntrim', util.lin_set(-18, 18, val))
@property @property
def usbinput(self) -> bool: def usbinput(self) -> bool:
return self.getter("rtnsw")[0] == 1 return self.getter('rtnsw')[0] == 1
@usbinput.setter @usbinput.setter
def usbinput(self, val: bool): def usbinput(self, val: bool):
if not isinstance(val, bool): self.setter('rtnsw', 1 if val else 0)
raise XAirRemoteError("rtnsw is a bool parameter")
self.setter("rtnsw", 1 if val else 0)
@property @property
def invert(self) -> bool: def invert(self) -> bool:
return self.getter("invert")[0] == 1 return self.getter('invert')[0] == 1
@invert.setter @invert.setter
def invert(self, val: bool): def invert(self, val: bool):
if not isinstance(val, bool): self.setter('invert', 1 if val else 0)
raise XAirRemoteError("invert is a bool parameter")
self.setter("invert", 1 if val else 0)
@property @property
def highpasson(self) -> bool: def highpasson(self) -> bool:
return self.getter("hpon")[0] == 1 return self.getter('hpon')[0] == 1
@highpasson.setter @highpasson.setter
def highpasson(self, val: bool): def highpasson(self, val: bool):
if not isinstance(val, bool): self.setter('hpon', 1 if val else 0)
raise XAirRemoteError("hpon is a bool parameter")
self.setter("hpon", 1 if val else 0)
@property @property
def highpassfilter(self) -> int: def highpassfilter(self) -> int:
return int(log_get(20, 400, self.getter("hpf")[0])) return int(util.log_get(20, 400, self.getter('hpf')[0]))
@highpassfilter.setter @highpassfilter.setter
def highpassfilter(self, val: int): def highpassfilter(self, val: int):
if not isinstance(val, int): if not 20 <= val <= 400:
raise XAirRemoteError("highpassfilter is an int parameter") self.logger.warning(
self.setter("hpf", log_set(20, 400, val)) f'highpassfilter got {val}, expected value in range 20 to 400'
)
self.setter('hpf', util.log_set(20, 400, val))
class Gate: class Gate:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Gate, self).address root = super(Gate, self).address
return f"{root}/gate" return f'{root}/gate'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter("on")[0] == 1 return self.getter('on')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter('on', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property @property
def mode(self) -> str: def mode(self) -> str:
opts = ("gate", "exp2", "exp3", "exp4", "duck") opts = ('exp2', 'exp3', 'exp4', 'gate', 'duck')
return opts[self.getter("mode")[0]] return opts[self.getter('mode')[0]]
@mode.setter @mode.setter
def mode(self, val: str): def mode(self, val: str):
opts = ("gate", "exp2", "exp3", "exp4", "duck") opts = ('exp2', 'exp3', 'exp4', 'gate', 'duck')
if not isinstance(val, str) and val not in opts: if val not in opts:
raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}") self.logger.warning(f'mode got {val}, expected one of {opts}')
self.setter("mode", opts.index(val)) self.setter('mode', opts.index(val))
@property @property
def threshold(self) -> float: def threshold(self) -> float:
return round(lin_get(-80, 0, self.getter("thr")[0]), 1) return round(util.lin_get(-80, 0, self.getter('thr')[0]), 1)
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
if not isinstance(val, float): if not -80 <= val <= 0:
raise XAirRemoteError( self.logger.warning(
"threshold is a float parameter, expected value in range -80 to 0" f'threshold got {val}, expected value in range -80.0 to 0.0'
) )
self.setter("thr", lin_set(-80, 0, val)) self.setter('thr', util.lin_set(-80, 0, val))
@property @property
def range(self) -> int: def range(self) -> int:
return int(lin_get(3, 60, self.getter("range")[0])) return int(util.lin_get(3, 60, self.getter('range')[0]))
@range.setter @range.setter
def range(self, val: int): def range(self, val: int):
if not isinstance(val, int): if not 3 <= val <= 60:
raise XAirRemoteError( self.logger.warning(f'range got {val}, expected value in range 3 to 60')
"range is an int parameter, expected value in range 3 to 60" self.setter('range', util.lin_set(3, 60, val))
)
self.setter("range", lin_set(3, 60, val))
@property @property
def attack(self) -> int: def attack(self) -> int:
return int(lin_get(0, 120, self.getter("attack")[0])) return int(util.lin_get(0, 120, self.getter('attack')[0]))
@attack.setter @attack.setter
def attack(self, val: int): def attack(self, val: int):
if not isinstance(val, int): if not 0 <= val <= 120:
raise XAirRemoteError( self.logger.warning(f'attack got {val}, expected value in range 0 to 120')
"attack is an int parameter, expected value in range 0 to 120" self.setter('attack', util.lin_set(0, 120, val))
)
self.setter("attack", lin_set(0, 120, val))
@property @property
def hold(self) -> Union[float, int]: def hold(self) -> Union[float, int]:
val = log_get(0.02, 2000, self.getter("hold")[0]) val = util.log_get(0.02, 2000, self.getter('hold')[0])
return round(val, 1) if val < 100 else int(val) return round(val, 1) if val < 100 else int(val)
@hold.setter @hold.setter
def hold(self, val: float): def hold(self, val: float):
self.setter("hold", log_set(0.02, 2000, val)) if not 0.02 <= val <= 2000:
self.logger.warning(
f'hold got {val}, expected value in range 0.02 to 2000.0'
)
self.setter('hold', util.log_set(0.02, 2000, val))
@property @property
def release(self) -> int: def release(self) -> int:
return int(log_get(5, 4000, self.getter("release")[0])) return int(util.log_get(5, 4000, self.getter('release')[0]))
@release.setter @release.setter
def release(self, val: int): def release(self, val: int):
if not isinstance(val, int): if not 5 <= val <= 4000:
raise XAirRemoteError( self.logger.warning(f'release got {val}, expected value in range 5 to 4000')
"release is an int parameter, expected value in range 5 to 4000" self.setter('release', util.log_set(5, 4000, val))
)
self.setter("release", log_set(5, 4000, val))
@property @property
def keysource(self): def keysource(self):
return self.getter("keysrc")[0] return self.getter('keysrc')[0]
@keysource.setter @keysource.setter
def keysource(self, val): def keysource(self, val):
if not isinstance(val, int): self.setter('keysrc', val)
raise XAirRemoteError("keysource is an int parameter")
self.setter("keysrc", val)
@property @property
def filteron(self): def filteron(self):
return self.getter("filter/on")[0] == 1 return self.getter('filter/on')[0] == 1
@filteron.setter @filteron.setter
def filteron(self, val: bool): def filteron(self, val: bool):
if not isinstance(val, bool): self.setter('filter/on', 1 if val else 0)
raise XAirRemoteError("filteron is a boolean parameter")
self.setter("filter/on", 1 if val else 0)
@property @property
def filtertype(self) -> int: def filtertype(self) -> int:
return int(self.getter("filter/type")[0]) return int(self.getter('filter/type')[0])
@filtertype.setter @filtertype.setter
def filtertype(self, val: int): def filtertype(self, val: int):
if not isinstance(val, int): self.setter('filter/type', val)
raise XAirRemoteError("filtertype is an int parameter")
self.setter("filter/type", val)
@property @property
def filterfreq(self) -> Union[float, int]: def filterfreq(self) -> Union[float, int]:
retval = log_get(20, 20000, self.getter("filter/f")[0]) retval = util.log_get(20, 20000, self.getter('filter/f')[0])
return int(retval) if retval > 1000 else round(retval, 1) return int(retval) if retval > 1000 else round(retval, 1)
@filterfreq.setter @filterfreq.setter
def filterfreq(self, val: Union[float, int]): def filterfreq(self, val: Union[float, int]):
self.setter("filter/f", log_set(20, 20000, val)) if not 20 <= val <= 20000:
self.logger.warning(
f'filterfreq got {val}, expected value in range 20 to 20000'
)
self.setter('filter/f', util.log_set(20, 20000, val))
class Dyn: class Dyn:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Dyn, self).address root = super(Dyn, self).address
return f"{root}/dyn" return f'{root}/dyn'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter("on")[0] == 1 return self.getter('on')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter('on', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property @property
def mode(self) -> str: def mode(self) -> str:
opts = ("comp", "exp") opts = ('comp', 'exp')
return opts[self.getter("mode")[0]] return opts[self.getter('mode')[0]]
@mode.setter @mode.setter
def mode(self, val: str): def mode(self, val: str):
opts = ("comp", "exp") opts = ('comp', 'exp')
if not isinstance(val, str) and val not in opts: if val not in opts:
raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}") self.logger.warning(f'mode got {val}, expected one of {opts}')
self.setter("mode", opts.index(val)) self.setter('mode', opts.index(val))
@property @property
def det(self) -> str: def det(self) -> str:
opts = ("peak", "rms") opts = ('peak', 'rms')
return opts[self.getter("det")[0]] return opts[self.getter('det')[0]]
@det.setter @det.setter
def det(self, val: str): def det(self, val: str):
opts = ("peak", "rms") opts = ('peak', 'rms')
if not isinstance(val, str) and val not in opts: if val not in opts:
raise XAirRemoteError(f"det is a string parameter, expected one of {opts}") self.logger.warning(f'det got {val}, expected one of {opts}')
self.setter("det", opts.index(val)) self.setter('det', opts.index(val))
@property @property
def env(self) -> str: def env(self) -> str:
opts = ("lin", "log") opts = ('lin', 'log')
return opts[self.getter("env")[0]] return opts[self.getter('env')[0]]
@env.setter @env.setter
def env(self, val: str): def env(self, val: str):
opts = ("lin", "log") opts = ('lin', 'log')
if not isinstance(val, str) and val not in opts: if val not in opts:
raise XAirRemoteError(f"env is a string parameter, expected one of {opts}") self.logger.warning(f'env got {val}, expected one of {opts}')
self.setter("env", opts.index(val)) self.setter('env', opts.index(val))
@property @property
def threshold(self) -> float: def threshold(self) -> float:
return round(lin_get(-60, 0, self.getter("thr")[0]), 1) return round(util.lin_get(-60, 0, self.getter('thr')[0]), 1)
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
if not isinstance(val, float): if not -60 <= val <= 0:
raise XAirRemoteError( self.logger.warning(
"threshold is a float parameter, expected value in range -80 to 0" f'threshold got {val}, expected value in range -60.0 to 0'
) )
self.setter("thr", lin_set(-60, 0, val)) self.setter('thr', util.lin_set(-60, 0, val))
@property @property
def ratio(self) -> Union[float, int]: def ratio(self) -> Union[float, int]:
opts = (1.1, 1.3, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10, 20, 100) opts = (1.1, 1.3, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10, 20, 100)
return opts[self.getter("ratio")[0]] return opts[self.getter('ratio')[0]]
@ratio.setter @ratio.setter
def ratio(self, val: int): def ratio(self, val: int):
if not isinstance(val, int): self.setter('ratio', val)
raise XAirRemoteError("ratio is an int parameter")
self.setter("ratio", val)
@property @property
def knee(self) -> int: def knee(self) -> int:
return int(lin_get(0, 5, self.getter("knee")[0])) return int(util.lin_get(0, 5, self.getter('knee')[0]))
@knee.setter @knee.setter
def knee(self, val: int): def knee(self, val: int):
if not isinstance(val, int): if not 0 <= val <= 5:
raise XAirRemoteError( self.logger.warning(f'knee got {val}, expected value in range 0 to 5')
"knee is an int parameter, expected value in range 0 to 5" self.setter('knee', util.lin_set(0, 5, val))
)
self.setter("knee", lin_set(0, 5, val))
@property @property
def mgain(self) -> float: def mgain(self) -> float:
return round(lin_get(0, 24, self.getter("mgain")[0]), 1) return round(util.lin_get(0, 24, self.getter('mgain')[0]), 1)
@mgain.setter @mgain.setter
def mgain(self, val: float): def mgain(self, val: float):
self.setter("mgain", lin_set(0, 24, val)) if not 0 <= val <= 24:
self.logger.warning(f'mgain got {val}, expected value in range 0.0 to 24.0')
self.setter('mgain', util.lin_set(0, 24, val))
@property @property
def attack(self) -> int: def attack(self) -> int:
return int(lin_get(0, 120, self.getter("attack")[0])) return int(util.lin_get(0, 120, self.getter('attack')[0]))
@attack.setter @attack.setter
def attack(self, val: int): def attack(self, val: int):
self.setter("attack", lin_set(0, 120, val)) if not 0 <= val <= 120:
self.logger.warning(f'attack got {val}, expected value in range 0 to 120')
self.setter('attack', util.lin_set(0, 120, val))
@property @property
def hold(self) -> Union[float, int]: def hold(self) -> Union[float, int]:
val = log_get(0.02, 2000, self.getter("hold")[0]) val = util.log_get(0.02, 2000, self.getter('hold')[0])
return round(val, 1) if val < 100 else int(val) return round(val, 1) if val < 100 else int(val)
@hold.setter @hold.setter
def hold(self, val: float): def hold(self, val: float):
self.setter("hold", log_set(0.02, 2000, val)) if not 0.02 <= val <= 2000:
self.logger.warning(
f'hold got {val}, expected value in range 0.02 to 2000.0'
)
self.setter('hold', util.log_set(0.02, 2000, val))
@property @property
def release(self) -> int: def release(self) -> int:
return int(log_get(5, 4000, self.getter("release")[0])) return int(util.log_get(5, 4000, self.getter('release')[0]))
@release.setter @release.setter
def release(self, val: int): def release(self, val: int):
if not isinstance(val, int): if not 5 <= val <= 4000:
raise XAirRemoteError( self.logger.warning(f'release got {val}, expected value in range 5 to 4000')
"release is an int parameter, expected value in range 5 to 4000" self.setter('release', util.log_set(5, 4000, val))
)
self.setter("release", log_set(5, 4000, val))
@property @property
def mix(self) -> int: def mix(self) -> int:
return int(lin_get(0, 100, self.getter("mix")[0])) return int(util.lin_get(0, 100, self.getter('mix')[0]))
@mix.setter @mix.setter
def mix(self, val: int): def mix(self, val: int):
if not isinstance(val, int): if not 0 <= val <= 100:
raise XAirRemoteError( self.logger.warning(f'mix got {val}, expected value in range 0 to 100')
"mix is an int parameter, expected value in range 0 to 5" self.setter('mix', util.lin_set(0, 100, val))
)
self.setter("mix", lin_set(0, 100, val))
@property @property
def keysource(self): def keysource(self):
return self.getter("keysrc")[0] return self.getter('keysrc')[0]
@keysource.setter @keysource.setter
def keysource(self, val): def keysource(self, val):
if not isinstance(val, int): self.setter('keysrc', val)
raise XAirRemoteError("keysource is an int parameter")
self.setter("keysrc", val)
@property @property
def auto(self) -> bool: def auto(self) -> bool:
return self.getter("auto")[0] == 1 return self.getter('auto')[0] == 1
@auto.setter @auto.setter
def auto(self, val: bool): def auto(self, val: bool):
if not isinstance(val, bool): self.setter('auto', 1 if val else 0)
raise XAirRemoteError("auto is a boolean parameter")
self.setter("auto", 1 if val else 0)
@property @property
def filteron(self): def filteron(self):
return self.getter("filter/on")[0] == 1 return self.getter('filter/on')[0] == 1
@filteron.setter @filteron.setter
def filteron(self, val: bool): def filteron(self, val: bool):
if not isinstance(val, bool): self.setter('filter/on', 1 if val else 0)
raise XAirRemoteError("filteron is a boolean parameter")
self.setter("filter/on", 1 if val else 0)
@property @property
def filtertype(self) -> int: def filtertype(self) -> int:
return int(self.getter("filter/type")[0]) return int(self.getter('filter/type')[0])
@filtertype.setter @filtertype.setter
def filtertype(self, val: int): def filtertype(self, val: int):
if not isinstance(val, int): self.setter('filter/type', val)
raise XAirRemoteError("filtertype is an int parameter")
self.setter("filter/type", val)
@property @property
def filterfreq(self) -> Union[float, int]: def filterfreq(self) -> Union[float, int]:
retval = log_get(20, 20000, self.getter("filter/f")[0]) retval = util.log_get(20, 20000, self.getter('filter/f')[0])
return int(retval) if retval > 1000 else round(retval, 1) return int(retval) if retval > 1000 else round(retval, 1)
@filterfreq.setter @filterfreq.setter
def filterfreq(self, val: Union[float, int]): def filterfreq(self, val: Union[float, int]):
self.setter("filter/f", log_set(20, 20000, val)) if not 20 <= val <= 20000:
self.logger.warning(
f'filterfreq got {val}, expected value in range 20 to 20000'
)
self.setter('filter/f', util.log_set(20, 20000, val))
class Insert: class Insert:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Insert, self).address root = super(Insert, self).address
return f"{root}/insert" return f'{root}/insert'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter("on")[0] == 1 return self.getter('on')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter('on', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property @property
def sel(self) -> int: def sel(self) -> int:
return self.getter("sel")[0] return self.getter('sel')[0]
@sel.setter @sel.setter
def sel(self, val: int): def sel(self, val: int):
if not isinstance(val, int): self.setter('sel', val)
raise XAirRemoteError("sel is an int parameter")
self.setter("sel", val)
class EQ: class EQ:
@classmethod @classmethod
def make_fourband(cls, _cls, remote, index=None): def make_fourband(cls, _cls, remote, index=None):
EQBand_cls = type("EQBand", (EQ.EQBand, _cls), {}) EQBand_cls = type('EQBand', (EQ.EQBand, _cls), {})
return type( return type(
"EQ", 'EQ',
(cls,), (cls,),
{ {
"low": EQBand_cls(1, remote, index), 'low': EQBand_cls(1, remote, index),
"lomid": EQBand_cls(2, remote, index), 'lomid': EQBand_cls(2, remote, index),
"himid": EQBand_cls(3, remote, index), 'himid': EQBand_cls(3, remote, index),
"high": EQBand_cls(4, remote, index), 'high': EQBand_cls(4, remote, index),
}, },
) )
@classmethod @classmethod
def make_sixband(cls, _cls, remote, index=None): def make_sixband(cls, _cls, remote, index=None):
EQBand_cls = type("EQBand", (EQ.EQBand, _cls), {}) EQBand_cls = type('EQBand', (EQ.EQBand, _cls), {})
return type( return type(
"EQ", 'EQ',
(cls,), (cls,),
{ {
"low": EQBand_cls(1, remote, index), 'low': EQBand_cls(1, remote, index),
"low2": EQBand_cls(2, remote, index), 'low2': EQBand_cls(2, remote, index),
"lomid": EQBand_cls(3, remote, index), 'lomid': EQBand_cls(3, remote, index),
"himid": EQBand_cls(4, remote, index), 'himid': EQBand_cls(4, remote, index),
"high2": EQBand_cls(5, remote, index), 'high2': EQBand_cls(5, remote, index),
"high": EQBand_cls(6, remote, index), 'high': EQBand_cls(6, remote, index),
}, },
) )
@property @property
def address(self) -> str: def address(self) -> str:
root = super(EQ, self).address root = super(EQ, self).address
return f"{root}/eq" return f'{root}/eq'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter("on")[0] == 1 return self.getter('on')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter('on', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property @property
def mode(self) -> str: def mode(self) -> str:
opts = ("peq", "geq", "teq") opts = ('peq', 'geq', 'teq')
return opts[self.getter("mode")[0]] return opts[self.getter('mode')[0]]
@mode.setter @mode.setter
def mode(self, val: str): def mode(self, val: str):
opts = ("peq", "geq", "teq") opts = ('peq', 'geq', 'teq')
if not isinstance(val, str) and val not in opts: if val not in opts:
raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}") self.logger.warning(f'mode got {val}, expected one of {opts}')
self.setter("mode", opts.index(val)) self.setter('mode', opts.index(val))
class EQBand: class EQBand:
def __init__(self, i, remote, index): def __init__(self, i, remote, index):
if index is None: super(EQ.EQBand, self).__init__(remote, index)
super(EQ.EQBand, self).__init__(remote)
else:
super(EQ.EQBand, self).__init__(remote, index)
self.i = i self.i = i
@property @property
def address(self) -> str: def address(self) -> str:
root = super(EQ.EQBand, self).address root = super(EQ.EQBand, self).address
return f"{root}/eq/{self.i}" return f'{root}/eq/{self.i}'
@property @property
def type(self) -> int: def type(self) -> int:
return int(self.getter("type")[0]) return int(self.getter('type')[0])
@type.setter @type.setter
def type(self, val: int): def type(self, val: int):
if not isinstance(val, int): self.setter('type', val)
raise XAirRemoteError("type is an int parameter")
self.setter(f"type", val)
@property @property
def frequency(self) -> float: def frequency(self) -> float:
retval = log_get(20, 20000, self.getter("f")[0]) retval = util.log_get(20, 20000, self.getter('f')[0])
return round(retval, 1) return round(retval, 1)
@frequency.setter @frequency.setter
def frequency(self, val: float): def frequency(self, val: float):
self.setter("f", log_set(20, 20000, val)) if not 20 <= val <= 20000:
self.logger.warning(
f'frequency got {val}, expected value in range 20.0 to 20000.0'
)
self.setter('f', util.log_set(20, 20000, val))
@property @property
def gain(self) -> float: def gain(self) -> float:
return round(lin_get(-15, 15, self.getter("g")[0]), 1) return round(util.lin_get(-15, 15, self.getter('g')[0]), 1)
@gain.setter @gain.setter
def gain(self, val: float): def gain(self, val: float):
self.setter("g", lin_set(-15, 15, val)) if not -15 <= val <= 15:
self.logger.warning(
f'gain got {val}, expected value in range -15.0 to 15.0'
)
self.setter('g', util.lin_set(-15, 15, val))
@property @property
def quality(self) -> float: def quality(self) -> float:
retval = log_get(0.3, 10, self.getter("q")[0]) retval = util.log_get(0.3, 10, 1.0 - self.getter('q')[0])
return round(retval, 1) return round(retval, 1)
@quality.setter @quality.setter
def quality(self, val: float): def quality(self, val: float):
self.setter("q", log_set(0.3, 10, val)) if not 0.3 <= val <= 10:
self.logger.warning(
f'quality got {val}, expected value in range 0.3 to 10.0'
)
self.setter('q', 1.0 - util.log_set(0.3, 10, val))
class GEQ: class GEQ:
@@ -573,7 +549,7 @@ class GEQ:
f"slider_{param}": geq_prop(param) f"slider_{param}": geq_prop(param)
for param in [ for param in [
"20", "25", "31_5", "40", "50", "63", "80", "100", "125", "20", "25", "31_5", "40", "50", "63", "80", "100", "125",
"160", "200", "250", "315" "400", "500", "630", "800", "1k", "160", "200", "250", "315", "400", "500", "630", "800", "1k",
"1k25", "1k6", "2k", "2k5", "3k15", "4k", "5k", "6k3", "8k", "1k25", "1k6", "2k", "2k5", "3k15", "4k", "5k", "6k3", "8k",
"10k", "12k5", "16k", "20k", "10k", "12k5", "16k", "20k",
] ]
@@ -585,96 +561,113 @@ class GEQ:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(GEQ, self).address root = super(GEQ, self).address
return f"{root}/geq" return f'{root}/geq'
class Mix: class Mix:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Mix, self).address root = super(Mix, self).address
return f"{root}/mix" return f'{root}/mix'
@property @property
def on(self) -> bool: def on(self) -> bool:
return self.getter("on")[0] == 1 return self.getter('on')[0] == 1
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): self.setter('on', 1 if val else 0)
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property @property
@util.db_from
def fader(self) -> float: def fader(self) -> float:
retval = self.getter("fader")[0] return self.getter('fader')[0]
return _get_fader_val(retval)
@fader.setter @fader.setter
@util.db_to
def fader(self, val: float): def fader(self, val: float):
_set_fader_val(self, val) self.setter('fader', val)
@property @property
def lr(self) -> bool: def lr(self) -> bool:
return self.getter("lr")[0] == 1 return self.getter('lr')[0] == 1
@lr.setter @lr.setter
def lr(self, val: bool): def lr(self, val: bool):
if not isinstance(val, bool): self.setter('lr', 1 if val else 0)
raise XAirRemoteError("lr is a boolean parameter")
self.setter("lr", 1 if val else 0)
class Group: class Group:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Group, self).address root = super(Group, self).address
return f"{root}/grp" return f'{root}/grp'
@property @property
def dca(self) -> int: def dca(self) -> int:
return self.getter("dca")[0] return self.getter('dca')[0]
@dca.setter @dca.setter
def dca(self, val: int): def dca(self, val: int):
if not isinstance(val, int): self.setter('dca', val)
raise XAirRemoteError("dca is an int parameter")
self.setter("dca", val)
@property @property
def mute(self) -> int: def mute(self) -> int:
return self.getter("mute")[0] return self.getter('mute')[0]
@mute.setter @mute.setter
def mute(self, val: int): def mute(self, val: int):
if not isinstance(val, int): self.setter('mute', val)
raise XAirRemoteError("mute is an int parameter")
self.setter("mute", val)
class Automix: class Automix:
@property @property
def address(self) -> str: def address(self) -> str:
root = super(Automix, self).address root = super(Automix, self).address
return f"{root}/automix" return f'{root}/automix'
@property @property
def group(self) -> int: def group(self) -> int:
return self.getter("group")[0] return self.getter('group')[0]
@group.setter @group.setter
def group(self, val: int): def group(self, val: int):
if not isinstance(val, int): self.setter('group', val)
raise XAirRemoteError("group is an int parameter")
self.setter("group", val)
@property @property
def weight(self) -> float: def weight(self) -> float:
return round(lin_get(-12, 12, self.getter("weight")[0]), 1) return round(util.lin_get(-12, 12, self.getter('weight')[0]), 1)
@weight.setter @weight.setter
def weight(self, val: float): def weight(self, val: float):
if not isinstance(val, float): if not -12 <= val <= 12:
raise XAirRemoteError( self.logger.warning(
"weight is a float parameter, expected value in range -12 to 12" f'weight got {val}, expected value in range -12.0 to 12.0'
) )
self.setter("weight", lin_set(-12, 12, val)) self.setter('weight', util.lin_set(-12, 12, val))
class Send:
def __init__(self, i, remote, index: Optional[int] = None):
super(Send, self).__init__(remote, index)
self.i = i + 1
@classmethod
def make(cls, _cls, i, remote, index=None):
SEND_cls = type('Send', (cls, _cls), {})
return SEND_cls(i, remote, index)
@property
def address(self) -> str:
root = super(Send, self).address
return f'{root}/mix/{str(self.i).zfill(2)}'
@property
@util.db_from
def level(self) -> float:
return self.getter('level')[0]
@level.setter
@util.db_to
def level(self, val: float):
self.setter('level', val)

View File

@@ -1,7 +1,10 @@
import abc import abc
import logging
from .errors import XAirRemoteError from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IStrip(abc.ABC): class IStrip(abc.ABC):
@@ -10,13 +13,13 @@ class IStrip(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str) -> tuple: def getter(self, param: str) -> tuple:
self._remote.send(f"{self.address}/{param}") return self._remote.query(f'{self.address}/{param}')
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f'{self.address}/{param}', val)
@abc.abstractmethod @abc.abstractmethod
def address(self): def address(self):
@@ -35,13 +38,14 @@ class Strip(IStrip):
Returns a Strip class of a kind. Returns a Strip class of a kind.
""" """
STRIP_cls = type( STRIP_cls = type(
f"Strip{remote.kind}", f'Strip{remote.kind}',
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {} f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in ( for _cls in (
Config, Config,
@@ -55,10 +59,15 @@ class Strip(IStrip):
Automix, Automix,
) )
}, },
'send': tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
'mute': mute_prop(),
}, },
) )
return STRIP_cls(remote, index) return STRIP_cls(remote, index)
@property @property
def address(self) -> str: def address(self) -> str:
return f"/ch/{str(self.index).zfill(2)}" return f'/ch/{str(self.index).zfill(2)}'

View File

@@ -1,5 +1,35 @@
import functools
import time
from math import exp, log from math import exp, log
from .errors import XAirRemoteConnectionTimeoutError
def timeout(func):
"""
Times out the validate_connection function once time elapsed exceeds remote.connect_timeout.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
remote, *_ = args
err = None
start = time.time()
while time.time() < start + remote.connect_timeout:
try:
func(*args, **kwargs)
remote.logger.debug(f'login time: {round(time.time() - start, 2)}')
err = None
break
except XAirRemoteConnectionTimeoutError as e:
err = e
continue
if err:
raise err
return wrapper
def lin_get(min, max, val): def lin_get(min, max, val):
return min + (max - min) * val return min + (max - min) * val
@@ -17,61 +47,48 @@ def log_set(min, max, val):
return log(val / min) / log(max / min) return log(val / min) / log(max / min)
def _get_fader_val(retval): def db_from(func):
if retval >= 1: """fader|level converter for getters"""
return 10
elif retval >= 0.5: @functools.wraps(func)
return round((40 * retval) - 30, 1) def wrapper(*args, **kwargs):
elif retval >= 0.25: retval = func(*args, **kwargs)
return round((80 * retval) - 50, 1)
elif retval >= 0.0625: if retval >= 1:
return round((160 * retval) - 70, 1) return 10
elif retval >= 0: elif retval >= 0.5:
return round((480 * retval) - 90, 1) return round((40 * retval) - 30, 1)
else: elif retval >= 0.25:
return -90 return round((80 * retval) - 50, 1)
elif retval >= 0.0625:
return round((160 * retval) - 70, 1)
elif retval >= 0:
return round((480 * retval) - 90, 1)
else:
return -90
return wrapper
def _set_fader_val(self, val): def db_to(func):
if val >= 10: """fader|level converter for setters"""
self.setter("fader", 1)
elif val >= -10:
self.setter("fader", (val + 30) / 40)
elif val >= -30:
self.setter("fader", (val + 50) / 80)
elif val >= -60:
self.setter("fader", (val + 70) / 160)
elif val >= -90:
self.setter("fader", (val + 90) / 480)
else:
self.setter("fader", 0)
@functools.wraps(func)
def wrapper(*args, **kwargs):
param, val = args
if val >= 10:
val = 1
elif val >= -10:
val = (val + 30) / 40
elif val >= -30:
val = (val + 50) / 80
elif val >= -60:
val = (val + 70) / 160
elif val >= -90:
val = (val + 90) / 480
else:
val = 0
def _get_level_val(retval): func(param, val, **kwargs)
if retval >= 1:
return 10
elif retval >= 0.5:
return round((40 * retval) - 30, 1)
elif retval >= 0.25:
return round((80 * retval) - 50, 1)
elif retval >= 0.0625:
return round((160 * retval) - 70, 1)
elif retval >= 0:
return round((480 * retval) - 90, 1)
else:
return -90
return wrapper
def _set_level_val(self, val):
if val >= 10:
self.setter("level", 1)
elif val >= -10:
self.setter("level", (val + 30) / 40)
elif val >= -30:
self.setter("level", (val + 50) / 80)
elif val >= -60:
self.setter("level", (val + 70) / 160)
elif val >= -90:
self.setter("level", (val + 90) / 480)
else:
self.setter("level", 0)

View File

@@ -1,8 +1,9 @@
import abc import abc
import logging
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, Union
try: try:
import tomllib import tomllib
@@ -13,32 +14,32 @@ from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_message_builder import OscMessageBuilder from pythonosc.osc_message_builder import OscMessageBuilder
from pythonosc.osc_server import BlockingOSCUDPServer from pythonosc.osc_server import BlockingOSCUDPServer
from . import kinds from . import adapter, kinds, util
from .bus import Bus from .bus import Bus
from .config import Config from .config import Config
from .dca import DCA from .dca import DCA
from .errors import XAirRemoteError from .errors import XAirRemoteConnectionTimeoutError, XAirRemoteError
from .fx import FXReturn, FXSend from .fx import FX, FXSend
from .headamp import HeadAmp
from .kinds import KindMap from .kinds import KindMap
from .lr import LR from .lr import LR
from .rtn import Aux, Rtn from .rtn import AuxRtn, FxRtn
from .strip import Strip from .strip import Strip
logger = logging.getLogger(__name__)
class OSCClientServer(BlockingOSCUDPServer): class OSCClientServer(BlockingOSCUDPServer):
def __init__(self, address: str, dispatcher: Dispatcher): def __init__(self, address: str, dispatcher: Dispatcher):
super().__init__(("", 0), dispatcher) super().__init__(('', 0), dispatcher)
self.xr_address = address self.xr_address = address
def send_message(self, address: str, value: str): def send_message(self, address: str, vals: Optional[Union[str, list]]):
builder = OscMessageBuilder(address=address) builder = OscMessageBuilder(address=address)
if value is None: vals = vals if vals is not None else []
values = list() if not isinstance(vals, list):
elif isinstance(value, list): vals = [vals]
values = value for val in vals:
else:
values = [value]
for val in values:
builder.add_arg(val) builder.add_arg(val)
msg = builder.build() msg = builder.build()
self.socket.sendto(msg.dgram, self.xr_address) self.socket.sendto(msg.dgram, self.xr_address)
@@ -47,21 +48,18 @@ class OSCClientServer(BlockingOSCUDPServer):
class XAirRemote(abc.ABC): class XAirRemote(abc.ABC):
"""Handles the communication with the mixer via the OSC protocol""" """Handles the communication with the mixer via the OSC protocol"""
_CONNECT_TIMEOUT = 0.5 _info_response = []
_WAIT_TIME = 0.025
_REFRESH_TIMEOUT = 5
XAIR_PORT = 10024
info_response = []
def __init__(self, **kwargs): def __init__(self, **kwargs):
dispatcher = Dispatcher() dispatcher = Dispatcher()
dispatcher.set_default_handler(self.msg_handler) dispatcher.set_default_handler(self.msg_handler)
self.xair_ip = kwargs["ip"] or self._ip_from_toml() self.xair_ip = kwargs['ip'] or self._ip_from_toml()
self.xair_port = kwargs["port"] or self.XAIR_PORT self.xair_port = kwargs['port']
if not (self.xair_ip and self.xair_port): self._delay = kwargs['delay']
raise XAirRemoteError("No valid ip or password detected") self.connect_timeout = kwargs['connect_timeout']
self.logger = logger.getChild(self.__class__.__name__)
if not self.xair_ip:
raise XAirRemoteError('No valid ip detected')
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher) self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
def __enter__(self): def __enter__(self):
@@ -71,34 +69,37 @@ class XAirRemote(abc.ABC):
return self return self
def _ip_from_toml(self) -> str: def _ip_from_toml(self) -> str:
filepath = Path.cwd() / "config.toml" filepath = Path.cwd() / 'config.toml'
with open(filepath, "rb") as f: with open(filepath, 'rb') as f:
conn = tomllib.load(f) conn = tomllib.load(f)
return conn["connection"].get("ip") return conn['connection'].get('ip')
@util.timeout
def validate_connection(self): def validate_connection(self):
self.send("/xinfo") if not self.query('/xinfo'):
time.sleep(self._CONNECT_TIMEOUT) raise XAirRemoteConnectionTimeoutError(self.xair_ip, self.xair_port)
if len(self.info_response) > 0: self.logger.info(
print(f"Successfully connected to {self.info_response[2]}.") f'Successfully connected to {self.info_response[2]} at {self.info_response[0]}.'
else: )
print(
"Error: Failed to setup OSC connection to mixer. Please check for correct ip address." @property
) def info_response(self):
return self._info_response
def run_server(self): def run_server(self):
self.server.serve_forever() self.server.serve_forever()
def msg_handler(self, addr, *data): def msg_handler(self, addr, *data):
self.info_response = data[:] self.logger.debug(f"received: {addr} {data if data else ''}")
self._info_response = data[:]
def send(self, address: str, param: Optional[str] = None): def send(self, addr: str, param: Optional[str] = None):
self.server.send_message(address, param) self.logger.debug(f"sending: {addr} {param if param is not None else ''}")
time.sleep(self._WAIT_TIME) self.server.send_message(addr, param)
def _query(self, address): def query(self, address):
self.send(address) self.send(address)
time.sleep(self._WAIT_TIME) time.sleep(self._delay)
return self.info_response return self.info_response
def __exit__(self, exc_type, exc_value, exc_tr): def __exit__(self, exc_type, exc_value, exc_tr):
@@ -112,8 +113,37 @@ def _make_remote(kind: KindMap) -> XAirRemote:
The returned class will subclass XAirRemote. The returned class will subclass XAirRemote.
""" """
def init(self, *args, **kwargs): def init_x32(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": None} defaultkwargs = {
'ip': None,
'port': 10023,
'delay': 0.02,
'connect_timeout': 2,
}
kwargs = defaultkwargs | kwargs
XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind
self.mainst = adapter.MainStereo.make(self)
self.mainmono = adapter.MainMono.make(self)
self.matrix = tuple(
adapter.Matrix.make(self, i) for i in range(kind.num_matrix)
)
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
self.bus = tuple(adapter.Bus.make(self, i) for i in range(kind.num_bus))
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
self.fx = tuple(FX(self, i) for i in range(kind.num_fx))
self.fxreturn = tuple(adapter.FxRtn.make(self, i) for i in range(kind.num_fx))
self.auxin = tuple(adapter.AuxRtn.make(self, i) for i in range(kind.num_auxrtn))
self.config = Config.make(self)
self.headamp = tuple(adapter.HeadAmp(self, i) for i in range(kind.num_headamp))
def init_xair(self, *args, **kwargs):
defaultkwargs = {
'ip': None,
'port': 10024,
'delay': 0.02,
'connect_timeout': 2,
}
kwargs = defaultkwargs | kwargs kwargs = defaultkwargs | kwargs
XAirRemote.__init__(self, *args, **kwargs) XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind self.kind = kind
@@ -121,17 +151,26 @@ def _make_remote(kind: KindMap) -> XAirRemote:
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip)) self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
self.bus = tuple(Bus.make(self, i) for i in range(kind.num_bus)) self.bus = tuple(Bus.make(self, i) for i in range(kind.num_bus))
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca)) self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
self.fx = tuple(FX(self, i) for i in range(kind.num_fx))
self.fxsend = tuple(FXSend.make(self, i) for i in range(kind.num_fx)) self.fxsend = tuple(FXSend.make(self, i) for i in range(kind.num_fx))
self.fxreturn = tuple(FXReturn(self, i) for i in range(kind.num_fx)) self.fxreturn = tuple(FxRtn.make(self, i) for i in range(kind.num_fx))
self.auxreturn = AuxRtn.make(self)
self.config = Config.make(self) self.config = Config.make(self)
self.aux = Aux.make(self) self.headamp = tuple(HeadAmp(self, i) for i in range(kind.num_strip))
self.rtn = tuple(Rtn.make(self, i) for i in range(kind.num_rtn))
if kind.id_ == 'X32':
return type(
f'XAirRemote{kind}',
(XAirRemote,),
{
'__init__': init_x32,
},
)
return type( return type(
f"XAirRemote{kind}", f'XAirRemote{kind}',
(XAirRemote,), (XAirRemote,),
{ {
"__init__": init, '__init__': init_xair,
}, },
) )
@@ -145,9 +184,10 @@ def request_remote_obj(kind_id: str, *args, **kwargs) -> XAirRemote:
Returns a reference to an XAirRemote class of a kind Returns a reference to an XAirRemote class of a kind
""" """
XAIRREMOTE_cls = None XAIRREMOTE_cls = None
try: try:
XAIRREMOTE_cls = _remotes[kind_id] XAIRREMOTE_cls = _remotes[kind_id]
except ValueError as e: except KeyError as e:
raise SystemExit(e) raise XAirRemoteError(f"Unknown mixer kind '{kind_id}'") from e
return XAIRREMOTE_cls(*args, **kwargs) return XAIRREMOTE_cls(*args, **kwargs)