46 Commits

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
35 changed files with 1317 additions and 760 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.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,8 +9,43 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
## [Unreleased]
- [x] Send class mixed into Strip, AuxRtn, FxRtn. May now be accessed with {Class}.send
- [x] Sends example added
- [ ]
## [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

View File

@@ -1,7 +1,7 @@
[![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)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
[![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/xair/MR18.svg?dummy=8484744)
# Xair API
@@ -40,22 +40,22 @@ import xair_api
def main():
kind_id = "XR18"
ip = "<ip address>"
kind_id = 'XR18'
ip = '<ip address>'
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
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__':
main()
```
#### `xair_api.connect(kind_id, ip=ip, delay=delay)`
#### `xair_api.connect(kind_id, ip=ip, delay=0.02, connect_timeout=2)`
Currently the following devices are supported:
@@ -72,6 +72,7 @@ The following keyword arguments may be passed:
- `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
@@ -113,6 +114,10 @@ A class representing auxreturn channel
A class representing the main config settings
`mixer.headamp`
A class representing the channel preamps (phantom power/gain).
### `LR`
Contains the subclasses:
@@ -121,7 +126,7 @@ Contains the subclasses:
### `Strip`
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`
@@ -136,12 +141,19 @@ Contains the subclasses:
### `FXRtn`
Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`)
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`, `Send`)
### `AuxRtn`
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`
@@ -288,6 +300,12 @@ tuple containing a class for each mute group
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
@@ -297,8 +315,8 @@ Send an OSC command directly to the mixer
for example:
```python
mixer.send("/ch/01/mix/on", 1)
mixer.send("/bus/2/config/name", "somename")
mixer.send('/ch/01/mix/on', 1)
mixer.send('/bus/2/config/name', 'somename')
```
Query the value of a command:
@@ -308,19 +326,27 @@ Query the value of a command:
for example:
```python
print(mixer.query("/ch/01/mix/on"))
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`
Unplug any expensive equipment before running tests.
Save your current settings to a snapshot first.
Install [poetry](https://python-poetry.org/docs/#installation) and then:
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:
`pytest -v`.
Unplug any expensive equipment and save your current settings to a snapshot first.
## License

View File

@@ -2,16 +2,16 @@ import xair_api
def main():
kind_id = "XR18"
ip = "<ip address>"
kind_id = 'XR18'
ip = '<ip address>'
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
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__':
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

@@ -6,7 +6,7 @@ logging.basicConfig(level=logging.DEBUG)
def main():
with xair_api.connect("XR18", ip="mixer.local") as mixer:
with xair_api.connect('XR18', ip='mixer.local') as mixer:
for send in mixer.strip[0].send:
send.level = -22.8
@@ -20,5 +20,5 @@ def main():
print(mixer.fxreturn[0].send[0].level)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@@ -19,7 +19,7 @@ password = "mystrongpass"
## 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

View File

@@ -1,13 +1,48 @@
import obsws_python as obs
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):
def __init__(self, mixer, stop_event):
self._mixer = mixer
self._client = obs.EventClient()
self._client.callback.register(self.on_current_program_scene_changed)
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
@@ -17,28 +52,32 @@ class Observer:
def on_current_program_scene_changed(self, data):
scene = data.scene_name
print(f"Switched to scene {scene}")
print(f'Switched to scene {scene}')
match scene:
case "START":
print("Toggling strip 01 on")
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")
case 'BRB':
print('Setting strip 08 fader')
self._mixer.strip[7].mix.fader = -12.8
case "END":
print("Settings strip 02 color")
case 'END':
print('Settings strip 02 color')
self._mixer.strip[1].config.color = 8
case "LIVE":
case 'LIVE':
self._mixer.config.mute_group[0].on = True
print(f"Mute Group 1 is {self._mixer.config.mute_group[0].on}")
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:
with Observer(mixer):
while _ := input("Press <Enter> to exit\n"):
pass
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__":
if __name__ == '__main__':
main()

470
poetry.lock generated
View File

@@ -1,180 +1,132 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.5"
files = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
[package.extras]
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "black"
version = "22.8.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.6.2"
files = [
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
]
[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"
name = "cachetools"
version = "5.5.0"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
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]]
name = "colorama"
version = "0.4.5"
version = "0.4.6"
description = "Cross-platform colored terminal text."
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.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
{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]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = "*"
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.6.1,<4.0"
files = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
optional = false
python-versions = "*"
files = [
{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"},
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "packaging"
version = "21.3"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
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]]
name = "pluggy"
version = "1.0.0"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
@@ -182,62 +134,70 @@ dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "pyenv-inspect"
version = "0.4.0"
description = "An auxiliary library for the virtualenv-pyenv and tox-pyenv-redux plugins"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
{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]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
name = "pyproject-api"
version = "1.8.0"
description = "API to interact with the python pyproject.toml based projects"
optional = false
python-versions = ">=3.6.8"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
{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]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
packaging = ">=24.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
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]]
name = "pytest"
version = "8.3.4"
description = "pytest: simple powerful testing with Python"
optional = false
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]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-randomly"
version = "3.12.0"
version = "3.16.0"
description = "Pytest plugin to randomly order tests and control random.seed."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
{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]
@@ -245,27 +205,167 @@ pytest = "*"
[[package]]
name = "python-osc"
version = "1.8.0"
version = "1.9.3"
description = "Open Sound Control server and client implementations in pure Python"
optional = false
python-versions = "*"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "python-osc-1.8.0.tar.gz", hash = "sha256:2f8c187c68d239960fb2eddcb5346a62a9b35e64f2de045b3e5e509f475ca73d"},
{file = "python_osc-1.8.0-py3-none-any.whl", hash = "sha256:9e2abb2fc9ba2c356f8e951609a03c9c7017bf0bad82cca8490e9b8af9e92a0b"},
{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]]
name = "tomli"
version = "2.0.1"
version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
{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]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "def96d1658f870a9820fef363ee6a04455f1d895e15a189ea4f39801f168552f"
lock-version = "2.1"
python-versions = ">=3.10"
content-hash = "dbba803b0ac29e3120c3f302260b600a8ef58bfac91c41a58f242804f17b89ba"

View File

@@ -1,29 +1,138 @@
[tool.poetry]
[project]
name = "xair-api"
version = "2.2.4a0"
version = "2.4.3"
description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
license = { text = "MIT" }
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]
python = "^3.10"
python-osc = "^1.8.0"
tomli = { version = "^2.0.1", python = "<3.11" }
[tool.poetry.requires-plugins]
poethepoet = "^0.35.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
pytest-randomly = "^3.12.0"
black = "^22.6.0"
isort = "^5.10.1"
pytest = "^8.3.4"
pytest-randomly = "^3.16.0"
ruff = "^0.8.6"
tox = "^4.23.2"
virtualenv-pyenv = "^0.5.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
obs = "scripts:ex_obs"
sends = "scripts:ex_sends"
xair = "scripts:test_xair"
x32 = "scripts:test_x32"
[tool.poe.tasks]
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"]

View File

@@ -1,23 +1,35 @@
import os
import subprocess
import sys
from pathlib import Path
def ex_obs():
path = Path.cwd() / "examples" / "xair_obs" / "."
subprocess.run([sys.executable, str(path)])
subprocess.run(['tox', 'r', '-e', 'obs'])
def ex_sends():
path = Path.cwd() / "examples" / "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():
path = Path.cwd() / "tests" / "xair"
subprocess.run(["pytest", "-v", str(path)])
subprocess.run(['tox'], env=os.environ.copy() | {'TEST_MODULE': 'xair'})
def test_x32():
path = Path.cwd() / "tests" / "x32"
subprocess.run(["pytest", "-v", str(path)])
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

@@ -5,10 +5,10 @@ from dataclasses import dataclass
import xair_api
from xair_api import kinds
kind_id = "X32"
ip = "x32.local"
kind_id = 'X32'
ip = 'x32.local'
tests = xair_api.connect(kind_id, ip=ip, delay=0.008)
tests = xair_api.connect(kind_id, ip=ip)
kind = kinds.get(kind_id)
@@ -30,7 +30,7 @@ data = Data()
def setup_module():
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
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()

View File

@@ -11,11 +11,11 @@ class TestSetAndGetStripMuteHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")[data.strip]
self.target = getattr(tests, 'strip')[data.strip]
@pytest.mark.parametrize(
"param,value",
[("mute", True), ("mute", False)],
'param,value',
[('mute', True), ('mute', False)],
)
def test_it_sets_and_gets_strip_mute_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -28,12 +28,12 @@ class TestSetAndGetStripMixHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")
self.target = getattr(self.target[data.strip], "mix")
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'mix')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False)],
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_strip_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -49,12 +49,12 @@ class TestSetAndGetBusConfigHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "bus")
self.target = getattr(self.target[data.bus], "config")
self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], 'config')
@pytest.mark.parametrize(
"param,value",
[("color", 0), ("color", 15)],
'param,value',
[('color', 0), ('color', 15)],
)
def test_it_sets_and_gets_bus_int_params(self, param, value):
setattr(self.target, param, value)
@@ -70,12 +70,12 @@ class TestSetAndGetAuxInPreampHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "auxin")
self.target = getattr(self.target[data.auxrtn], "preamp")
self.target = getattr(tests, 'auxin')
self.target = getattr(self.target[data.auxrtn], 'preamp')
@pytest.mark.parametrize(
"param,value",
[("invert", True), ("invert", False)],
'param,value',
[('invert', True), ('invert', False)],
)
def test_it_sets_and_gets_auxrtn_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -91,12 +91,12 @@ class TestSetAndGetFXReturnEQHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "fxreturn")
self.target = getattr(self.target[data.fx], "eq")
self.target = getattr(tests, 'fxreturn')
self.target = getattr(self.target[data.fx], 'eq')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False)],
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_fxrtn_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -112,12 +112,12 @@ class TestSetAndGetMatrixDynHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "matrix")
self.target = getattr(self.target[data.matrix], "dyn")
self.target = getattr(tests, 'matrix')
self.target = getattr(self.target[data.matrix], 'dyn')
@pytest.mark.parametrize(
"param,value",
[("mode", "comp"), ("mode", "exp")],
'param,value',
[('mode', 'comp'), ('mode', 'exp')],
)
def test_it_sets_and_gets_matrix_string_params(self, param, value):
setattr(self.target, param, value)
@@ -133,11 +133,11 @@ class TestSetAndGetMainStereoInsertHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "mainst")
self.target = getattr(tests, 'mainst')
@pytest.mark.parametrize(
"param,value",
[("mode", "comp"), ("mode", "exp")],
'param,value',
[('mode', 'comp'), ('mode', 'exp')],
)
def test_it_sets_and_gets_mainst_string_params(self, param, value):
setattr(self.target, param, value)

View File

@@ -5,10 +5,10 @@ from dataclasses import dataclass
import xair_api
from xair_api import kinds
kind_id = "MR18"
ip = "mixer.local"
kind_id = 'MR18'
ip = 'mixer.local'
tests = xair_api.connect(kind_id, ip=ip, delay=0.008)
tests = xair_api.connect(kind_id, ip=ip)
kind = kinds.get(kind_id)
@@ -28,7 +28,7 @@ data = Data()
def setup_module():
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
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()

View File

@@ -19,20 +19,20 @@ class TestSetAndGetLRMixHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "lr")
self.target = getattr(self.target, "mix")
self.target = getattr(tests, 'lr')
self.target = getattr(self.target, 'mix')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False)],
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_lr_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("fader", -80.6), ("fader", -67.0)],
'param,value',
[('fader', -80.6), ('fader', -67.0)],
)
def test_it_sets_and_gets_lr_float_params(self, param, value):
setattr(self.target, param, value)
@@ -45,10 +45,10 @@ class TestSetAndGetLRConfigHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "lr")
self.target = getattr(self.target, "config")
self.target = getattr(tests, 'lr')
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):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@@ -60,20 +60,20 @@ class TestSetAndGetLRInsertHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "lr")
self.target = getattr(self.target, "insert")
self.target = getattr(tests, 'lr')
self.target = getattr(self.target, 'insert')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False)],
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_lr_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("sel", 0), ("sel", 4)],
'param,value',
[('sel', 0), ('sel', 4)],
)
def test_it_sets_and_gets_lr_int_params(self, param, value):
setattr(self.target, param, value)
@@ -86,16 +86,16 @@ class TestSetAndGetLRGEQHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "lr")
self.target = getattr(self.target, "geq")
self.target = getattr(tests, 'lr')
self.target = getattr(self.target, 'geq')
@pytest.mark.parametrize(
"param,value",
'param,value',
[
("slider_20", -13.5),
("slider_20", 5.5),
("slider_6k3", -8.5),
("slider_6k3", 8.5),
('slider_20', -13.5),
('slider_20', 5.5),
('slider_6k3', -8.5),
('slider_6k3', 8.5),
],
)
def test_it_sets_and_gets_lr_int_params(self, param, value):
@@ -112,11 +112,11 @@ class TestSetAndGetStripMuteHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")[data.strip]
self.target = getattr(tests, 'strip')[data.strip]
@pytest.mark.parametrize(
"param,value",
[("mute", True), ("mute", False)],
'param,value',
[('mute', True), ('mute', False)],
)
def test_it_sets_and_gets_strip_mute_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -129,12 +129,12 @@ class TestSetAndGetStripMixHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")
self.target = getattr(self.target[data.strip], "mix")
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'mix')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False), ("lr", True), ("lr", False)],
'param,value',
[('on', True), ('on', False), ('lr', True), ('lr', False)],
)
def test_it_sets_and_gets_strip_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -147,16 +147,16 @@ class TestSetAndGetStripPreampHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")
self.target = getattr(self.target[data.strip], "preamp")
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'preamp')
@pytest.mark.parametrize(
"param,value",
'param,value',
[
("highpasson", True),
("highpasson", False),
("usbinput", True),
("usbinput", False),
('highpasson', True),
('highpasson', False),
('usbinput', True),
('usbinput', False),
],
)
def test_it_sets_and_gets_strip_bool_params(self, param, value):
@@ -164,16 +164,16 @@ class TestSetAndGetStripPreampHigher:
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("highpassfilter", 20), ("highpassfilter", 399)],
'param,value',
[('highpassfilter', 20), ('highpassfilter', 399)],
)
def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("usbtrim", -16.5), ("usbtrim", 5.5)],
'param,value',
[('usbtrim', -16.5), ('usbtrim', 5.5)],
)
def test_it_sets_and_gets_strip_float_params(self, param, value):
setattr(self.target, param, value)
@@ -186,12 +186,12 @@ class TestSetAndGetStripConfigHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")
self.target = getattr(self.target[data.strip], "config")
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'config')
@pytest.mark.parametrize(
"param,value",
[("inputsource", 0), ("inputsource", 18), ("usbreturn", 3), ("usbreturn", 12)],
'param,value',
[('inputsource', 0), ('inputsource', 18), ('usbreturn', 3), ('usbreturn', 12)],
)
def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value)
@@ -204,18 +204,18 @@ class TestSetAndGetStripGateHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")
self.target = getattr(self.target[data.strip], "gate")
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'gate')
@pytest.mark.parametrize(
"param,value",
'param,value',
[
("on", True),
("on", False),
("invert", True),
("invert", False),
("filteron", True),
("filteron", False),
('on', True),
('on', False),
('invert', True),
('invert', False),
('filteron', True),
('filteron', False),
],
)
def test_it_sets_and_gets_strip_bool_params(self, param, value):
@@ -223,16 +223,16 @@ class TestSetAndGetStripGateHigher:
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
'param,value',
[
("range", 11),
("range", 48),
("attack", 5),
("attack", 110),
("release", 360),
("release", 2505),
("filtertype", 0),
("filtertype", 8),
('range', 11),
('range', 48),
('attack', 5),
('attack', 110),
('release', 360),
('release', 2505),
('filtertype', 0),
('filtertype', 8),
],
)
def test_it_sets_and_gets_strip_int_params(self, param, value):
@@ -240,22 +240,22 @@ class TestSetAndGetStripGateHigher:
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("mode", "exp2"), ("mode", "duck")],
'param,value',
[('mode', 'exp2'), ('mode', 'duck')],
)
def test_it_sets_and_gets_strip_string_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
'param,value',
[
("threshold", -80.0),
("threshold", 0.0),
("hold", 355),
("hold", 63.2),
("filterfreq", 37.2),
("filterfreq", 12765),
('threshold', -80.0),
('threshold', 0.0),
('hold', 355),
('hold', 63.2),
('filterfreq', 37.2),
('filterfreq', 12765),
],
)
def test_it_sets_and_gets_strip_float_params(self, param, value):
@@ -269,20 +269,20 @@ class TestSetAndGetStripAutomixHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")
self.target = getattr(self.target[data.strip], "automix")
self.target = getattr(tests, 'strip')
self.target = getattr(self.target[data.strip], 'automix')
@pytest.mark.parametrize(
"param,value",
[("group", 0), ("group", 2)],
'param,value',
[('group', 0), ('group', 2)],
)
def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("weight", -10.5), ("weight", 3.5)],
'param,value',
[('weight', -10.5), ('weight', 3.5)],
)
def test_it_sets_and_gets_strip_float_params(self, param, value):
setattr(self.target, param, value)
@@ -298,12 +298,12 @@ class TestSetAndGetBusConfigHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "bus")
self.target = getattr(self.target[data.bus], "config")
self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], 'config')
@pytest.mark.parametrize(
"param,value",
[("color", 0), ("color", 15)],
'param,value',
[('color', 0), ('color', 15)],
)
def test_it_sets_and_gets_bus_bool_params(self, param, value):
setattr(self.target, param, value)
@@ -316,26 +316,26 @@ class TestSetAndGetBusDynHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "bus")
self.target = getattr(self.target[data.bus], "dyn")
self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], 'dyn')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False)],
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_bus_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
'param,value',
[
("mode", "comp"),
("mode", "exp"),
("env", "lin"),
("env", "log"),
("det", "peak"),
("det", "rms"),
('mode', 'comp'),
('mode', 'exp'),
('env', 'lin'),
('env', 'log'),
('det', 'peak'),
('det', 'rms'),
],
)
def test_it_sets_and_gets_bus_string_params(self, param, value):
@@ -349,20 +349,20 @@ class TestSetAndGetBusEQHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "bus")
self.target = getattr(self.target[data.bus], "eq")
self.target = getattr(tests, 'bus')
self.target = getattr(self.target[data.bus], 'eq')
@pytest.mark.parametrize(
"param,value",
[("on", True), ("on", False)],
'param,value',
[('on', True), ('on', False)],
)
def test_it_sets_and_gets_bus_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@pytest.mark.parametrize(
"param,value",
[("mode", "peq"), ("mode", "geq"), ("mode", "teq")],
'param,value',
[('mode', 'peq'), ('mode', 'geq'), ('mode', 'teq')],
)
def test_it_sets_and_gets_bus_string_params(self, param, value):
setattr(self.target, param, value)
@@ -378,12 +378,12 @@ class TestSetAndGetFXSendGroupHigher:
__test__ = True
def setup_class(self):
self.target = getattr(tests, "fxsend")
self.target = getattr(self.target[data.fx], "group")
self.target = getattr(tests, 'fxsend')
self.target = getattr(self.target[data.fx], 'group')
@pytest.mark.parametrize(
"param,value",
[("dca", 0), ("dca", 12), ("mute", 3), ("mute", 8)],
'param,value',
[('dca', 0), ('dca', 12), ('mute', 3), ('mute', 8)],
)
def test_it_sets_and_gets_fxsend_int_params(self, param, value):
setattr(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
_ALL__ = ["connect"]
_ALL__ = ['connect']

View File

@@ -1,4 +1,5 @@
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
@@ -7,34 +8,40 @@ from .rtn import FxRtn as IFxRtn
class Bus(IBus):
@property
def address(self):
return f"/bus/{str(self.index).zfill(2)}"
return f'/bus/{str(self.index).zfill(2)}'
class AuxRtn(IAuxRtn):
@property
def address(self):
return f"/auxin/{str(self.index).zfill(2)}"
return f'/auxin/{str(self.index).zfill(2)}'
class FxRtn(IFxRtn):
@property
def address(self):
return f"/fxrtn/{str(self.index).zfill(2)}"
return f'/fxrtn/{str(self.index).zfill(2)}'
class MainStereo(ILR):
@property
def address(self) -> str:
return f"/main/st"
return '/main/st'
class MainMono(ILR):
@property
def address(self) -> str:
return f"/main/m"
return '/main/m'
class Matrix(ILR):
@property
def address(self) -> str:
return f"/mtx/{str(self.index).zfill(2)}"
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,8 +1,10 @@
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):
@@ -11,12 +13,13 @@ class IBus(abc.ABC):
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}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -36,12 +39,12 @@ class Bus(IBus):
Returns a Bus class of a kind.
"""
BUS_cls = type(
f"Bus{remote.kind}",
f'Bus{remote.kind}',
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index)
for _cls in (
Config,
@@ -53,11 +56,11 @@ class Bus(IBus):
Group,
)
},
"mute": mute_prop(),
'mute': mute_prop(),
},
)
return BUS_cls(remote, index)
@property
def address(self) -> str:
return f"/bus/{self.index}"
return f'/bus/{self.index}'

View File

@@ -1,21 +1,24 @@
import abc
import logging
from . import kinds, util
from .errors import XAirRemoteError
from .meta import bool_prop
logger = logging.getLogger(__name__)
class IConfig(abc.ABC):
"""Abstract Base Class for config"""
def __init__(self, remote):
self._remote = remote
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str):
return self._remote.query(f"{self.address}/{param}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -33,37 +36,37 @@ class Config(IConfig):
Returns a Config class of a kind.
"""
LINKS_cls = _make_links_mixins[remote.kind.id_]
MUTEGROUP_cls = type(f"MuteGroup", (Config.MuteGroup, cls), {})
MONITOR_cls = type(f"ConfigMonitor", (Config.Monitor, cls), {})
MUTEGROUP_cls = type('MuteGroup', (Config.MuteGroup, cls), {})
MONITOR_cls = type('ConfigMonitor', (Config.Monitor, cls), {})
CONFIG_cls = type(
f"Config{remote.kind}",
f'Config{remote.kind}',
(cls, LINKS_cls),
{
"mute_group": tuple(MUTEGROUP_cls(remote, i) for i in range(4)),
"monitor": MONITOR_cls(remote),
'mute_group': tuple(MUTEGROUP_cls(remote, i) for i in range(4)),
'monitor': MONITOR_cls(remote),
},
)
return CONFIG_cls(remote)
@property
def address(self) -> str:
return f"/config"
return '/config'
@property
def amixenable(self) -> bool:
return self.getter("mute")[0] == 1
return self.getter('mute')[0] == 1
@amixenable.setter
def amixenable(self, val: bool):
self.setter("amixenable", 1 if val else 0)
self.setter('amixenable', 1 if val else 0)
@property
def amixlock(self) -> bool:
return self.getter("amixlock")[0] == 1
return self.getter('amixlock')[0] == 1
@amixlock.setter
def amixlock(self, val: bool):
self.setter("amixlock", 1 if val else 0)
self.setter('amixlock', 1 if val else 0)
class MuteGroup:
def __init__(self, remote, i):
@@ -73,124 +76,128 @@ class Config(IConfig):
@property
def address(self) -> str:
root = super(Config.MuteGroup, self).address
return f"{root}/mute"
return f'{root}/mute'
@property
def on(self) -> bool:
return self.getter(f"{self.i}")[0] == 1
return self.getter(f'{self.i}')[0] == 1
@on.setter
def on(self, val: bool):
self.setter(f"{self.i}", 1 if val else 0)
self.setter(f'{self.i}', 1 if val else 0)
class Monitor:
@property
def address(self) -> str:
root = super(Config.Monitor, self).address
return f"{root}/solo"
return f'{root}/solo'
@property
@util.db_from
def level(self) -> float:
return self.getter("level")[0]
return self.getter('level')[0]
@level.setter
@util.db_to
def level(self, val: float):
self.setter("level", val)
self.setter('level', val)
@property
def source(self) -> int:
return int(self.getter("source")[0])
return int(self.getter('source')[0])
@source.setter
def source(self, val: int):
self.setter(f"source", val)
self.setter('source', val)
@property
def sourcetrim(self) -> float:
return round(util.lin_get(-18, 18, self.getter("sourcetrim")[0]), 1)
return round(util.lin_get(-18, 18, self.getter('sourcetrim')[0]), 1)
@sourcetrim.setter
def sourcetrim(self, val: float):
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
self.setter("sourcetrim", util.lin_set(-18, 18, val))
self.logger.warning(
f'sourcetrim got {val}, expected value in range -18.0 to 18.0'
)
self.setter('sourcetrim', util.lin_set(-18, 18, val))
@property
def chmode(self) -> bool:
return self.getter("chmode")[0] == 1
return self.getter('chmode')[0] == 1
@chmode.setter
def chmode(self, val: bool):
self.setter("chmode", 1 if val else 0)
self.setter('chmode', 1 if val else 0)
@property
def busmode(self) -> bool:
return self.getter("busmode")[0] == 1
return self.getter('busmode')[0] == 1
@busmode.setter
def busmode(self, val: bool):
self.setter("busmode", 1 if val else 0)
self.setter('busmode', 1 if val else 0)
@property
def dimgain(self) -> int:
return int(util.lin_get(-40, 0, self.getter("dimatt")[0]))
return int(util.lin_get(-40, 0, self.getter('dimatt')[0]))
@dimgain.setter
def dimgain(self, val: int):
if not -40 <= val <= 0:
raise XAirRemoteError("expected value in range -40 to 0")
self.setter("dimatt", util.lin_set(-40, 0, val))
self.logger.warning(
f'dimgain got {val}, expected value in range -40 to 0'
)
self.setter('dimatt', util.lin_set(-40, 0, val))
@property
def dim(self) -> bool:
return self.getter("dim")[0] == 1
return self.getter('dim')[0] == 1
@dim.setter
def dim(self, val: bool):
self.setter("dim", 1 if val else 0)
self.setter('dim', 1 if val else 0)
@property
def mono(self) -> bool:
return self.getter("mono")[0] == 1
return self.getter('mono')[0] == 1
@mono.setter
def mono(self, val: bool):
self.setter("mono", 1 if val else 0)
self.setter('mono', 1 if val else 0)
@property
def mute(self) -> bool:
return self.getter("mute")[0] == 1
return self.getter('mute')[0] == 1
@mute.setter
def mute(self, val: bool):
self.setter("mute", 1 if val else 0)
self.setter('mute', 1 if val else 0)
@property
def dimfpl(self) -> bool:
return self.getter("dimfpl")[0] == 1
return self.getter('dimfpl')[0] == 1
@dimfpl.setter
def dimfpl(self, val: bool):
self.setter("dimfpl", 1 if val else 0)
self.setter('dimfpl', 1 if val else 0)
def _make_links_mixin(kind):
"""Creates a links mixin"""
return type(
f"Links{kind}",
f'Links{kind}',
(),
{
"link_eq": bool_prop("linkcfg/eq"),
"link_dyn": bool_prop("linkcfg/dyn"),
"link_fader_mute": bool_prop("linkcfg/fdrmute"),
'link_eq': bool_prop('linkcfg/eq'),
'link_dyn': bool_prop('linkcfg/dyn'),
'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)
},
**{
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)
},
},

View File

@@ -1,6 +1,7 @@
import abc
import logging
from .errors import XAirRemoteError
logger = logging.getLogger(__name__)
class IDCA(abc.ABC):
@@ -9,12 +10,13 @@ class IDCA(abc.ABC):
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) -> tuple:
return self._remote.query(f"{self.address}/{param}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -26,15 +28,15 @@ class DCA(IDCA):
@property
def address(self) -> str:
return f"/dca/{self.index}"
return f'/dca/{self.index}'
@property
def on(self) -> bool:
return self.getter("on")[0] == 1
return self.getter('on')[0] == 1
@on.setter
def on(self, val: bool):
self.setter("on", 1 if val else 0)
self.setter('on', 1 if val else 0)
@property
def mute(self) -> bool:
@@ -46,16 +48,16 @@ class DCA(IDCA):
@property
def name(self) -> str:
return self.getter("config/name")[0]
return self.getter('config/name')[0]
@name.setter
def name(self, val: str):
self.setter("config/name")[0]
self.setter('config/name', val)
@property
def color(self) -> int:
return self.getter("config/color")[0]
return self.getter('config/color')[0]
@color.setter
def color(self, val: int):
self.setter("config/color", val)
self.setter('config/color', val)

View File

@@ -1,2 +1,14 @@
class XAirRemoteError(Exception):
"""Base error class for XAIR Remote."""
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,8 +1,10 @@
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):
@@ -11,12 +13,13 @@ class IFX(abc.ABC):
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}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -28,15 +31,15 @@ class FX(IFX):
@property
def address(self) -> str:
return f"/fx/{self.index}"
return f'/fx/{self.index}'
@property
def type(self) -> int:
return self.getter("type")[0]
return self.getter('type')[0]
@type.setter
def type(self, val: int):
self.setter("type", val)
self.setter('type', val)
class FXSend(IFX):
@@ -52,20 +55,20 @@ class FXSend(IFX):
Returns an FXSend class of a kind.
"""
FXSEND_cls = type(
f"FXSend{remote.kind}",
f'FXSend{remote.kind}',
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index)
for _cls in (Config, Mix, Group)
},
"mute": mute_prop(),
'mute': mute_prop(),
},
)
return FXSEND_cls(remote, index)
@property
def address(self) -> str:
return f"/fxsend/{self.index}"
return f'/fxsend/{self.index}'

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,45 +1,44 @@
from dataclasses import dataclass
@dataclass
@dataclass(frozen=True)
class KindMap:
id_: str
def __str__(self) -> str:
return self.id_
@dataclass
@dataclass(frozen=True)
class X32KindMap(KindMap):
id_: str
num_dca: int = 8
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
@dataclass(frozen=True)
class XR18KindMap(KindMap):
# note ch 17-18 defined as aux return
id_: str
num_dca: int = 4
num_strip: int = 16
num_bus: int = 6
num_fx: int = 4
@dataclass
@dataclass(frozen=True)
class XR16KindMap(KindMap):
id_: str
num_dca: int = 4
num_strip: int = 16
num_bus: int = 4
num_fx: int = 4
@dataclass
@dataclass(frozen=True)
class XR12KindMap(KindMap):
id_: str
num_dca: int = 4
num_strip: int = 12
num_bus: int = 2
@@ -47,11 +46,11 @@ class XR12KindMap(KindMap):
_kinds = {
"X32": X32KindMap(id_="X32"),
"MR18": XR18KindMap(id_="MR18"),
"XR18": XR18KindMap(id_="XR18"),
"XR16": XR16KindMap(id_="XR16"),
"XR12": XR12KindMap(id_="XR12"),
'X32': X32KindMap(id_='X32'),
'MR18': XR18KindMap(id_='MR18'),
'XR18': XR18KindMap(id_='XR18'),
'XR16': XR16KindMap(id_='XR16'),
'XR12': XR12KindMap(id_='XR12'),
}
@@ -59,4 +58,4 @@ def get(kind_id):
return _kinds[kind_id]
all = list(kind for kind in _kinds.values())
all = list(_kinds.values())

View File

@@ -1,9 +1,11 @@
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):
@@ -13,12 +15,13 @@ class ILR(abc.ABC):
self._remote = remote
if index is not None:
self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str):
return self._remote.query(f"{self.address}/{param}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -38,12 +41,12 @@ class LR(ILR):
Returns an LR class of a kind.
"""
LR_cls = type(
f"LR{remote.kind}",
f'LR{remote.kind}',
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index)
for _cls in (
Config,
@@ -54,11 +57,11 @@ class LR(ILR):
Mix,
)
},
"mute": mute_prop(),
'mute': mute_prop(),
},
)
return LR_cls(remote, index)
@property
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
@@ -51,21 +50,16 @@ def float_prop(param):
def geq_prop(param):
# fmt: off
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("_", ".")
param = param.replace('_', '.')
def fget(self) -> float:
return round(lin_get(-15, 15, self.getter(param)[0]), 1)
def fset(self, val):
if not -15 <= val <= 15:
raise XAirRemoteError("expected value in range -15.0 to 15.0")
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))
return property(fget, fset)

View File

@@ -1,21 +1,11 @@
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,
Send,
)
from .shared import EQ, Config, Group, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IRtn(abc.ABC):
@@ -25,12 +15,13 @@ class IRtn(abc.ABC):
self._remote = remote
if index is not None:
self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str):
return self._remote.query(f"{self.address}/{param}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -50,12 +41,12 @@ class AuxRtn(IRtn):
Returns an AuxRtn class of a kind.
"""
AUXRTN_cls = type(
f"AuxRtn{remote.kind}",
f'AuxRtn{remote.kind}',
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index)
for _cls in (
Config,
@@ -65,18 +56,18 @@ class AuxRtn(IRtn):
Group,
)
},
"send": tuple(
'send': tuple(
Send.make(cls, i, remote)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(),
'mute': mute_prop(),
},
)
return AUXRTN_cls(remote, index)
@property
def address(self):
return "/rtn/aux"
return '/rtn/aux'
class FxRtn(IRtn):
@@ -92,12 +83,12 @@ class FxRtn(IRtn):
Returns an FxRtn class of a kind.
"""
FXRTN_cls = type(
f"FxRtn{remote.kind}",
f'FxRtn{remote.kind}',
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index)
for _cls in (
Config,
@@ -107,15 +98,15 @@ class FxRtn(IRtn):
Group,
)
},
"send": tuple(
'send': tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(),
'mute': mute_prop(),
},
)
return FXRTN_cls(remote, index)
@property
def address(self):
return f"/rtn/{self.index}"
return f'/rtn/{self.index}'

View File

@@ -1,7 +1,6 @@
from typing import Optional, Union
from . import util
from .errors import XAirRemoteError
from .meta import geq_prop
"""
@@ -13,457 +12,473 @@ class Config:
@property
def address(self) -> str:
root = super(Config, self).address
return f"{root}/config"
return f'{root}/config'
@property
def name(self) -> str:
return self.getter("name")[0]
return self.getter('name')[0]
@name.setter
def name(self, val: str):
self.setter("name", val)
self.setter('name', val)
@property
def color(self) -> int:
return self.getter("color")[0]
return self.getter('color')[0]
@color.setter
def color(self, val: int):
self.setter("color", val)
self.setter('color', val)
@property
def inputsource(self) -> int:
return self.getter("insrc")[0]
return self.getter('insrc')[0]
@inputsource.setter
def inputsource(self, val: int):
self.setter("insrc", val)
self.setter('insrc', val)
@property
def usbreturn(self) -> int:
return self.getter("rtnsrc")[0]
return self.getter('rtnsrc')[0]
@usbreturn.setter
def usbreturn(self, val: int):
self.setter("rtnsrc", val)
self.setter('rtnsrc', val)
class Preamp:
@property
def address(self) -> str:
root = super(Preamp, self).address
return f"{root}/preamp"
return f'{root}/preamp'
@property
def usbtrim(self) -> float:
return round(util.lin_get(-18, 18, self.getter("rtntrim")[0]), 1)
return round(util.lin_get(-18, 18, self.getter('rtntrim')[0]), 1)
@usbtrim.setter
def usbtrim(self, val: float):
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
self.setter("rtntrim", util.lin_set(-18, 18, val))
self.logger.warning(
f'usbtrim got {val}, expected value in range -18.0 to 18.0'
)
self.setter('rtntrim', util.lin_set(-18, 18, val))
@property
def usbinput(self) -> bool:
return self.getter("rtnsw")[0] == 1
return self.getter('rtnsw')[0] == 1
@usbinput.setter
def usbinput(self, val: bool):
self.setter("rtnsw", 1 if val else 0)
self.setter('rtnsw', 1 if val else 0)
@property
def invert(self) -> bool:
return self.getter("invert")[0] == 1
return self.getter('invert')[0] == 1
@invert.setter
def invert(self, val: bool):
self.setter("invert", 1 if val else 0)
self.setter('invert', 1 if val else 0)
@property
def highpasson(self) -> bool:
return self.getter("hpon")[0] == 1
return self.getter('hpon')[0] == 1
@highpasson.setter
def highpasson(self, val: bool):
self.setter("hpon", 1 if val else 0)
self.setter('hpon', 1 if val else 0)
@property
def highpassfilter(self) -> int:
return int(util.log_get(20, 400, self.getter("hpf")[0]))
return int(util.log_get(20, 400, self.getter('hpf')[0]))
@highpassfilter.setter
def highpassfilter(self, val: int):
if not 20 <= val <= 400:
raise XAirRemoteError("expected value in range 20 to 400")
self.setter("hpf", util.log_set(20, 400, val))
self.logger.warning(
f'highpassfilter got {val}, expected value in range 20 to 400'
)
self.setter('hpf', util.log_set(20, 400, val))
class Gate:
@property
def address(self) -> str:
root = super(Gate, self).address
return f"{root}/gate"
return f'{root}/gate'
@property
def on(self) -> bool:
return self.getter("on")[0] == 1
return self.getter('on')[0] == 1
@on.setter
def on(self, val: bool):
self.setter("on", 1 if val else 0)
self.setter('on', 1 if val else 0)
@property
def mode(self) -> str:
opts = ("gate", "exp2", "exp3", "exp4", "duck")
return opts[self.getter("mode")[0]]
opts = ('exp2', 'exp3', 'exp4', 'gate', 'duck')
return opts[self.getter('mode')[0]]
@mode.setter
def mode(self, val: str):
opts = ("gate", "exp2", "exp3", "exp4", "duck")
opts = ('exp2', 'exp3', 'exp4', 'gate', 'duck')
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("mode", opts.index(val))
self.logger.warning(f'mode got {val}, expected one of {opts}')
self.setter('mode', opts.index(val))
@property
def threshold(self) -> float:
return round(util.lin_get(-80, 0, self.getter("thr")[0]), 1)
return round(util.lin_get(-80, 0, self.getter('thr')[0]), 1)
@threshold.setter
def threshold(self, val: float):
if not -80 <= val <= 0:
raise XAirRemoteError("expected value in range -80.0 to 0.0")
self.setter("thr", util.lin_set(-80, 0, val))
self.logger.warning(
f'threshold got {val}, expected value in range -80.0 to 0.0'
)
self.setter('thr', util.lin_set(-80, 0, val))
@property
def range(self) -> int:
return int(util.lin_get(3, 60, self.getter("range")[0]))
return int(util.lin_get(3, 60, self.getter('range')[0]))
@range.setter
def range(self, val: int):
if not 3 <= val <= 60:
raise XAirRemoteError("expected value in range 3 to 60")
self.setter("range", util.lin_set(3, 60, val))
self.logger.warning(f'range got {val}, expected value in range 3 to 60')
self.setter('range', util.lin_set(3, 60, val))
@property
def attack(self) -> int:
return int(util.lin_get(0, 120, self.getter("attack")[0]))
return int(util.lin_get(0, 120, self.getter('attack')[0]))
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.setter("attack", util.lin_set(0, 120, val))
self.logger.warning(f'attack got {val}, expected value in range 0 to 120')
self.setter('attack', util.lin_set(0, 120, val))
@property
def hold(self) -> Union[float, int]:
val = util.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)
@hold.setter
def hold(self, val: float):
if not 0.02 <= val <= 2000:
raise XAirRemoteError("expected value in range 0.02 to 2000.0")
self.setter("hold", util.log_set(0.02, 2000, val))
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
def release(self) -> int:
return int(util.log_get(5, 4000, self.getter("release")[0]))
return int(util.log_get(5, 4000, self.getter('release')[0]))
@release.setter
def release(self, val: int):
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.setter("release", util.log_set(5, 4000, val))
self.logger.warning(f'release got {val}, expected value in range 5 to 4000')
self.setter('release', util.log_set(5, 4000, val))
@property
def keysource(self):
return self.getter("keysrc")[0]
return self.getter('keysrc')[0]
@keysource.setter
def keysource(self, val):
self.setter("keysrc", val)
self.setter('keysrc', val)
@property
def filteron(self):
return self.getter("filter/on")[0] == 1
return self.getter('filter/on')[0] == 1
@filteron.setter
def filteron(self, val: bool):
self.setter("filter/on", 1 if val else 0)
self.setter('filter/on', 1 if val else 0)
@property
def filtertype(self) -> int:
return int(self.getter("filter/type")[0])
return int(self.getter('filter/type')[0])
@filtertype.setter
def filtertype(self, val: int):
self.setter("filter/type", val)
self.setter('filter/type', val)
@property
def filterfreq(self) -> Union[float, int]:
retval = util.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)
@filterfreq.setter
def filterfreq(self, val: Union[float, int]):
if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20 to 20000")
self.setter("filter/f", util.log_set(20, 20000, val))
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:
@property
def address(self) -> str:
root = super(Dyn, self).address
return f"{root}/dyn"
return f'{root}/dyn'
@property
def on(self) -> bool:
return self.getter("on")[0] == 1
return self.getter('on')[0] == 1
@on.setter
def on(self, val: bool):
self.setter("on", 1 if val else 0)
self.setter('on', 1 if val else 0)
@property
def mode(self) -> str:
opts = ("comp", "exp")
return opts[self.getter("mode")[0]]
opts = ('comp', 'exp')
return opts[self.getter('mode')[0]]
@mode.setter
def mode(self, val: str):
opts = ("comp", "exp")
opts = ('comp', 'exp')
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("mode", opts.index(val))
self.logger.warning(f'mode got {val}, expected one of {opts}')
self.setter('mode', opts.index(val))
@property
def det(self) -> str:
opts = ("peak", "rms")
return opts[self.getter("det")[0]]
opts = ('peak', 'rms')
return opts[self.getter('det')[0]]
@det.setter
def det(self, val: str):
opts = ("peak", "rms")
opts = ('peak', 'rms')
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("det", opts.index(val))
self.logger.warning(f'det got {val}, expected one of {opts}')
self.setter('det', opts.index(val))
@property
def env(self) -> str:
opts = ("lin", "log")
return opts[self.getter("env")[0]]
opts = ('lin', 'log')
return opts[self.getter('env')[0]]
@env.setter
def env(self, val: str):
opts = ("lin", "log")
opts = ('lin', 'log')
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("env", opts.index(val))
self.logger.warning(f'env got {val}, expected one of {opts}')
self.setter('env', opts.index(val))
@property
def threshold(self) -> float:
return round(util.lin_get(-60, 0, self.getter("thr")[0]), 1)
return round(util.lin_get(-60, 0, self.getter('thr')[0]), 1)
@threshold.setter
def threshold(self, val: float):
if not -60 <= val <= 0:
raise XAirRemoteError("expected value in range -60.0 to 0")
self.setter("thr", util.lin_set(-60, 0, val))
self.logger.warning(
f'threshold got {val}, expected value in range -60.0 to 0'
)
self.setter('thr', util.lin_set(-60, 0, val))
@property
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)
return opts[self.getter("ratio")[0]]
return opts[self.getter('ratio')[0]]
@ratio.setter
def ratio(self, val: int):
self.setter("ratio", val)
self.setter('ratio', val)
@property
def knee(self) -> int:
return int(util.lin_get(0, 5, self.getter("knee")[0]))
return int(util.lin_get(0, 5, self.getter('knee')[0]))
@knee.setter
def knee(self, val: int):
if not 0 <= val <= 5:
raise XAirRemoteError("expected value in range 0 to 5")
self.setter("knee", util.lin_set(0, 5, val))
self.logger.warning(f'knee got {val}, expected value in range 0 to 5')
self.setter('knee', util.lin_set(0, 5, val))
@property
def mgain(self) -> float:
return round(util.lin_get(0, 24, self.getter("mgain")[0]), 1)
return round(util.lin_get(0, 24, self.getter('mgain')[0]), 1)
@mgain.setter
def mgain(self, val: float):
if not 0 <= val <= 24:
raise XAirRemoteError("expected value in range 0.0 to 24.0")
self.setter("mgain", util.lin_set(0, 24, val))
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
def attack(self) -> int:
return int(util.lin_get(0, 120, self.getter("attack")[0]))
return int(util.lin_get(0, 120, self.getter('attack')[0]))
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.setter("attack", util.lin_set(0, 120, val))
self.logger.warning(f'attack got {val}, expected value in range 0 to 120')
self.setter('attack', util.lin_set(0, 120, val))
@property
def hold(self) -> Union[float, int]:
val = util.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)
@hold.setter
def hold(self, val: float):
if not 0.02 <= val <= 2000:
raise XAirRemoteError("expected value in range 0.02 to 2000.0")
self.setter("hold", util.log_set(0.02, 2000, val))
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
def release(self) -> int:
return int(util.log_get(5, 4000, self.getter("release")[0]))
return int(util.log_get(5, 4000, self.getter('release')[0]))
@release.setter
def release(self, val: int):
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.setter("release", util.log_set(5, 4000, val))
self.logger.warning(f'release got {val}, expected value in range 5 to 4000')
self.setter('release', util.log_set(5, 4000, val))
@property
def mix(self) -> int:
return int(util.lin_get(0, 100, self.getter("mix")[0]))
return int(util.lin_get(0, 100, self.getter('mix')[0]))
@mix.setter
def mix(self, val: int):
if not 0 <= val <= 100:
raise XAirRemoteError("expected value in range 0 to 100")
self.setter("mix", util.lin_set(0, 100, val))
self.logger.warning(f'mix got {val}, expected value in range 0 to 100')
self.setter('mix', util.lin_set(0, 100, val))
@property
def keysource(self):
return self.getter("keysrc")[0]
return self.getter('keysrc')[0]
@keysource.setter
def keysource(self, val):
self.setter("keysrc", val)
self.setter('keysrc', val)
@property
def auto(self) -> bool:
return self.getter("auto")[0] == 1
return self.getter('auto')[0] == 1
@auto.setter
def auto(self, val: bool):
self.setter("auto", 1 if val else 0)
self.setter('auto', 1 if val else 0)
@property
def filteron(self):
return self.getter("filter/on")[0] == 1
return self.getter('filter/on')[0] == 1
@filteron.setter
def filteron(self, val: bool):
self.setter("filter/on", 1 if val else 0)
self.setter('filter/on', 1 if val else 0)
@property
def filtertype(self) -> int:
return int(self.getter("filter/type")[0])
return int(self.getter('filter/type')[0])
@filtertype.setter
def filtertype(self, val: int):
self.setter("filter/type", val)
self.setter('filter/type', val)
@property
def filterfreq(self) -> Union[float, int]:
retval = util.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)
@filterfreq.setter
def filterfreq(self, val: Union[float, int]):
if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20 to 20000")
self.setter("filter/f", util.log_set(20, 20000, val))
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:
@property
def address(self) -> str:
root = super(Insert, self).address
return f"{root}/insert"
return f'{root}/insert'
@property
def on(self) -> bool:
return self.getter("on")[0] == 1
return self.getter('on')[0] == 1
@on.setter
def on(self, val: bool):
self.setter("on", 1 if val else 0)
self.setter('on', 1 if val else 0)
@property
def sel(self) -> int:
return self.getter("sel")[0]
return self.getter('sel')[0]
@sel.setter
def sel(self, val: int):
self.setter("sel", val)
self.setter('sel', val)
class EQ:
@classmethod
def make_fourband(cls, _cls, remote, index=None):
EQBand_cls = type("EQBand", (EQ.EQBand, _cls), {})
EQBand_cls = type('EQBand', (EQ.EQBand, _cls), {})
return type(
"EQ",
'EQ',
(cls,),
{
"low": EQBand_cls(1, remote, index),
"lomid": EQBand_cls(2, remote, index),
"himid": EQBand_cls(3, remote, index),
"high": EQBand_cls(4, remote, index),
'low': EQBand_cls(1, remote, index),
'lomid': EQBand_cls(2, remote, index),
'himid': EQBand_cls(3, remote, index),
'high': EQBand_cls(4, remote, index),
},
)
@classmethod
def make_sixband(cls, _cls, remote, index=None):
EQBand_cls = type("EQBand", (EQ.EQBand, _cls), {})
EQBand_cls = type('EQBand', (EQ.EQBand, _cls), {})
return type(
"EQ",
'EQ',
(cls,),
{
"low": EQBand_cls(1, remote, index),
"low2": EQBand_cls(2, remote, index),
"lomid": EQBand_cls(3, remote, index),
"himid": EQBand_cls(4, remote, index),
"high2": EQBand_cls(5, remote, index),
"high": EQBand_cls(6, remote, index),
'low': EQBand_cls(1, remote, index),
'low2': EQBand_cls(2, remote, index),
'lomid': EQBand_cls(3, remote, index),
'himid': EQBand_cls(4, remote, index),
'high2': EQBand_cls(5, remote, index),
'high': EQBand_cls(6, remote, index),
},
)
@property
def address(self) -> str:
root = super(EQ, self).address
return f"{root}/eq"
return f'{root}/eq'
@property
def on(self) -> bool:
return self.getter("on")[0] == 1
return self.getter('on')[0] == 1
@on.setter
def on(self, val: bool):
self.setter("on", 1 if val else 0)
self.setter('on', 1 if val else 0)
@property
def mode(self) -> str:
opts = ("peq", "geq", "teq")
return opts[self.getter("mode")[0]]
opts = ('peq', 'geq', 'teq')
return opts[self.getter('mode')[0]]
@mode.setter
def mode(self, val: str):
opts = ("peq", "geq", "teq")
opts = ('peq', 'geq', 'teq')
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("mode", opts.index(val))
self.logger.warning(f'mode got {val}, expected one of {opts}')
self.setter('mode', opts.index(val))
class EQBand:
def __init__(self, i, remote, index):
@@ -473,47 +488,53 @@ class EQ:
@property
def address(self) -> str:
root = super(EQ.EQBand, self).address
return f"{root}/eq/{self.i}"
return f'{root}/eq/{self.i}'
@property
def type(self) -> int:
return int(self.getter("type")[0])
return int(self.getter('type')[0])
@type.setter
def type(self, val: int):
self.setter(f"type", val)
self.setter('type', val)
@property
def frequency(self) -> float:
retval = util.log_get(20, 20000, self.getter("f")[0])
retval = util.log_get(20, 20000, self.getter('f')[0])
return round(retval, 1)
@frequency.setter
def frequency(self, val: float):
if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20.0 to 20000.0")
self.setter("f", util.log_set(20, 20000, val))
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
def gain(self) -> float:
return round(util.lin_get(-15, 15, self.getter("g")[0]), 1)
return round(util.lin_get(-15, 15, self.getter('g')[0]), 1)
@gain.setter
def gain(self, val: float):
if not -15 <= val <= 15:
raise XAirRemoteError("expected value in range -15.0 to 15.0")
self.setter("g", util.lin_set(-15, 15, val))
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
def quality(self) -> float:
retval = util.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)
@quality.setter
def quality(self, val: float):
if not 0.3 <= val <= 10:
raise XAirRemoteError("expected value in range 0.3 to 10.0")
self.setter("q", util.log_set(0.3, 10, val))
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:
@@ -528,7 +549,7 @@ class GEQ:
f"slider_{param}": geq_prop(param)
for param in [
"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",
"10k", "12k5", "16k", "20k",
]
@@ -540,88 +561,90 @@ class GEQ:
@property
def address(self) -> str:
root = super(GEQ, self).address
return f"{root}/geq"
return f'{root}/geq'
class Mix:
@property
def address(self) -> str:
root = super(Mix, self).address
return f"{root}/mix"
return f'{root}/mix'
@property
def on(self) -> bool:
return self.getter("on")[0] == 1
return self.getter('on')[0] == 1
@on.setter
def on(self, val: bool):
self.setter("on", 1 if val else 0)
self.setter('on', 1 if val else 0)
@property
@util.db_from
def fader(self) -> float:
return self.getter("fader")[0]
return self.getter('fader')[0]
@fader.setter
@util.db_to
def fader(self, val: float):
self.setter("fader", val)
self.setter('fader', val)
@property
def lr(self) -> bool:
return self.getter("lr")[0] == 1
return self.getter('lr')[0] == 1
@lr.setter
def lr(self, val: bool):
self.setter("lr", 1 if val else 0)
self.setter('lr', 1 if val else 0)
class Group:
@property
def address(self) -> str:
root = super(Group, self).address
return f"{root}/grp"
return f'{root}/grp'
@property
def dca(self) -> int:
return self.getter("dca")[0]
return self.getter('dca')[0]
@dca.setter
def dca(self, val: int):
self.setter("dca", val)
self.setter('dca', val)
@property
def mute(self) -> int:
return self.getter("mute")[0]
return self.getter('mute')[0]
@mute.setter
def mute(self, val: int):
self.setter("mute", val)
self.setter('mute', val)
class Automix:
@property
def address(self) -> str:
root = super(Automix, self).address
return f"{root}/automix"
return f'{root}/automix'
@property
def group(self) -> int:
return self.getter("group")[0]
return self.getter('group')[0]
@group.setter
def group(self, val: int):
self.setter("group", val)
self.setter('group', val)
@property
def weight(self) -> float:
return round(util.lin_get(-12, 12, self.getter("weight")[0]), 1)
return round(util.lin_get(-12, 12, self.getter('weight')[0]), 1)
@weight.setter
def weight(self, val: float):
if not -12 <= val <= 12:
raise XAirRemoteError("expected value in range -12.0 to 12.0")
self.setter("weight", util.lin_set(-12, 12, val))
self.logger.warning(
f'weight got {val}, expected value in range -12.0 to 12.0'
)
self.setter('weight', util.lin_set(-12, 12, val))
class Send:
@@ -631,20 +654,20 @@ class Send:
@classmethod
def make(cls, _cls, i, remote, index=None):
SEND_cls = type("Send", (cls, _cls), {})
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)}"
return f'{root}/mix/{str(self.i).zfill(2)}'
@property
@util.db_from
def level(self):
return self.getter("level")[0]
def level(self) -> float:
return self.getter('level')[0]
@level.setter
@util.db_to
def level(self, val):
self.setter("level", val)
def level(self, val: float):
self.setter('level', val)

View File

@@ -1,20 +1,10 @@
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,
Send,
)
from .shared import EQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IStrip(abc.ABC):
@@ -23,12 +13,13 @@ class IStrip(abc.ABC):
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) -> tuple:
return self._remote.query(f"{self.address}/{param}")
return self._remote.query(f'{self.address}/{param}')
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
def address(self):
@@ -49,12 +40,12 @@ class Strip(IStrip):
"""
STRIP_cls = type(
f"Strip{remote.kind}",
f'Strip{remote.kind}',
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
)(remote, index)
for _cls in (
Config,
@@ -68,15 +59,15 @@ class Strip(IStrip):
Automix,
)
},
"send": tuple(
'send': tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(),
'mute': mute_prop(),
},
)
return STRIP_cls(remote, index)
@property
def address(self) -> str:
return f"/ch/{str(self.index).zfill(2)}"
return f'/ch/{str(self.index).zfill(2)}'

View File

@@ -1,6 +1,35 @@
import functools
import time
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):
return min + (max - min) * val

View File

@@ -14,21 +14,24 @@ from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_message_builder import OscMessageBuilder
from pythonosc.osc_server import BlockingOSCUDPServer
from . import adapter, kinds
from . import adapter, kinds, util
from .bus import Bus
from .config import Config
from .dca import DCA
from .errors import XAirRemoteError
from .errors import XAirRemoteConnectionTimeoutError, XAirRemoteError
from .fx import FX, FXSend
from .headamp import HeadAmp
from .kinds import KindMap
from .lr import LR
from .rtn import AuxRtn, FxRtn
from .strip import Strip
logger = logging.getLogger(__name__)
class OSCClientServer(BlockingOSCUDPServer):
def __init__(self, address: str, dispatcher: Dispatcher):
super().__init__(("", 0), dispatcher)
super().__init__(('', 0), dispatcher)
self.xr_address = address
def send_message(self, address: str, vals: Optional[Union[str, list]]):
@@ -45,20 +48,18 @@ class OSCClientServer(BlockingOSCUDPServer):
class XAirRemote(abc.ABC):
"""Handles the communication with the mixer via the OSC protocol"""
logger = logging.getLogger("xair.xairremote")
_CONNECT_TIMEOUT = 0.5
_info_response = []
def __init__(self, **kwargs):
dispatcher = Dispatcher()
dispatcher.set_default_handler(self.msg_handler)
self.xair_ip = kwargs["ip"] or self._ip_from_toml()
self.xair_port = kwargs["port"]
self._delay = kwargs["delay"]
self.xair_ip = kwargs['ip'] or self._ip_from_toml()
self.xair_port = kwargs['port']
self._delay = kwargs['delay']
self.connect_timeout = kwargs['connect_timeout']
self.logger = logger.getChild(self.__class__.__name__)
if not self.xair_ip:
raise XAirRemoteError("No valid ip detected")
raise XAirRemoteError('No valid ip detected')
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
def __enter__(self):
@@ -68,20 +69,17 @@ class XAirRemote(abc.ABC):
return self
def _ip_from_toml(self) -> str:
filepath = Path.cwd() / "config.toml"
with open(filepath, "rb") as f:
filepath = Path.cwd() / 'config.toml'
with open(filepath, 'rb') as f:
conn = tomllib.load(f)
return conn["connection"].get("ip")
return conn['connection'].get('ip')
@util.timeout
def validate_connection(self):
self.send("/xinfo")
time.sleep(self._CONNECT_TIMEOUT)
if not self.info_response:
raise XAirRemoteError(
"Failed to setup OSC connection to mixer. Please check for correct ip address."
)
print(
f"Successfully connected to {self.info_response[2]} at {self.info_response[0]}."
if not self.query('/xinfo'):
raise XAirRemoteConnectionTimeoutError(self.xair_ip, self.xair_port)
self.logger.info(
f'Successfully connected to {self.info_response[2]} at {self.info_response[0]}.'
)
@property
@@ -116,7 +114,12 @@ def _make_remote(kind: KindMap) -> XAirRemote:
"""
def init_x32(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": 10023, "delay": 0.02}
defaultkwargs = {
'ip': None,
'port': 10023,
'delay': 0.02,
'connect_timeout': 2,
}
kwargs = defaultkwargs | kwargs
XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind
@@ -132,9 +135,15 @@ def _make_remote(kind: KindMap) -> XAirRemote:
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}
defaultkwargs = {
'ip': None,
'port': 10024,
'delay': 0.02,
'connect_timeout': 2,
}
kwargs = defaultkwargs | kwargs
XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind
@@ -147,20 +156,21 @@ def _make_remote(kind: KindMap) -> XAirRemote:
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.headamp = tuple(HeadAmp(self, i) for i in range(kind.num_strip))
if kind.id_ == "X32":
if kind.id_ == 'X32':
return type(
f"XAirRemote{kind}",
f'XAirRemote{kind}',
(XAirRemote,),
{
"__init__": init_x32,
'__init__': init_x32,
},
)
return type(
f"XAirRemote{kind}",
f'XAirRemote{kind}',
(XAirRemote,),
{
"__init__": init_xair,
'__init__': init_xair,
},
)
@@ -174,9 +184,10 @@ def request_remote_obj(kind_id: str, *args, **kwargs) -> XAirRemote:
Returns a reference to an XAirRemote class of a kind
"""
XAIRREMOTE_cls = None
try:
XAIRREMOTE_cls = _remotes[kind_id]
except ValueError as e:
raise SystemExit(e)
except KeyError as e:
raise XAirRemoteError(f"Unknown mixer kind '{kind_id}'") from e
return XAIRREMOTE_cls(*args, **kwargs)