28 Commits

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

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

delay for test suites reduced to 0.008

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

aux renamed to auxreturn

lower level methods send, query added to readme.

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

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

View File

@@ -9,7 +9,41 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
## [Unreleased]
- [x]
- [ ]
## [2.1.0] - 2022-11-08
### Added
- delay keyword argument
- bounds checks for vals passed to lin_set/log_set
### Removed
- type checks, prefer duck typing
## [2.0.0] - 2022-11-07
Some support for the X32 mixer has been added using an adapter module but the code related to the XAir api has been left largely untouched.
However, a couple of changes have been made which are breaking, they are as follows:
### Changed
- FX class added to fx module. This now deals with osc addresses that begin with "/fx/". Call it with mixer.fx.
- FxRtn class added to rtn module. This now deals with addresses that begin with "/rtn/". Call it with mixer.fxreturn
- Aux class renamed to AuxRtn in rtn module. Call it with mixer.auxreturn.
These changes were made to better resemble the underlying osc api and to better describe the function of the classes.
### Added
- A small number of X32 tests. More will be added. XAir tests moved into it's own test module.
- XAirRemote lower level section added to README.
- Links to OSC command documentation added to README.
### Removed
- mixer.aux was renamed to mixer.auxreturn
## [1.1.0] - 2022-09-05

View File

@@ -2,7 +2,7 @@
[![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/)
![Tests Status](./tests/MR18.svg?dummy=8484744)
![Tests Status](./tests/xair/MR18.svg?dummy=8484744)
# Xair API
@@ -40,6 +40,9 @@ import xair_api
def main():
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].mix.on = True
@@ -49,21 +52,29 @@ def main():
if __name__ == "__main__":
kind_id = "MR18"
ip = "<ip address>"
main()
```
## API
#### `xair_api.connect(kind_id, ip=ip, delay=delay)`
Currently the following devices are support:
Currently the following devices are supported:
- `XR18`
- `MR18`
- `XR18`
- `XR16`
- `XR12`
The `X32` is partially supported. However, this document covers specifically the `XAir` series.
The following keyword arguments may be passed:
- `ip`: ip address of the mixer
- `port`: mixer port, defaults to 10023 for x32 and 10024 for xair
- `delay`: a delay between each command, defaults to 20ms.
- a note about delay, stability may rely on network connection. For wired connections the delay can be safely reduced.
## API
### XAirRemote class (higher level)
`mixer.lr`
@@ -82,6 +93,10 @@ A Bus tuple containing a class for each output bus channel
A DCA tuple containing a class for each DCA group
`mixer.fx`
An FX tuple containing a class for each FX channel
`mixer.fxsend`
An FXSend tuple containing a class for each FX Send channel
@@ -90,13 +105,9 @@ An FXSend tuple containing a class for each FX Send channel
An FXReturn tuple containing a class for each FX Return channel
`mixer.aux`
`mixer.auxreturn`
A class representing aux channel
`mixer.rtn`
An RTN tuple containing a class for each rtn channel
A class representing auxreturn channel
`mixer.config`
@@ -122,12 +133,12 @@ Contains the subclasses:
Contains the subclasses:
(`Config`, `Mix`, `Group`)
### `Aux`
### `FXRtn`
Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`)
### `Rtn`
### `AuxRtn`
Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`)
@@ -258,12 +269,14 @@ For the subclass `monitor` the following properties are available
- `level`: float, -inf to 10.0
- `source`: int, from 0 to 14
- `chmode` bool
- `busmode` bool
- `dim` bool
- `mono` bool
- `mute` bool
- `dimfpl` bool
- `sourcetrim`: float, from -18.0 to 18.0
- `chmode`: bool
- `busmode`: bool
- `dim`: bool
- `dimgain`: float, from -40.0 to 0.0
- `mono`: bool
- `mute`: bool
- `dimfpl`: bool
for example: `config.monitor.chmode`
@@ -275,6 +288,29 @@ tuple containing a class for each mute group
for example: `config.mute_group[0].on = True`
### XAirRemote class (lower level)
Send an OSC command directly to the mixer
- `send(osc command, value)`
for example:
```python
mixer.send("/ch/01/mix/on", 1)
mixer.send("/bus/2/config/name", "somename")
```
Query the value of a command:
- `query(osc command)`
for example:
```python
print(mixer.query("/ch/01/mix/on"))
```
### `Tests`
Unplug any expensive equipment before running tests.
@@ -290,6 +326,12 @@ To run all tests:
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
## Documentation
[XAir OSC Commands](https://behringer.world/wiki/doku.php?id=x-air_osc)
[X32 OSC Commands](https://wiki.munichmakerlab.de/images/1/17/UNOFFICIAL_X32_OSC_REMOTE_PROTOCOL_%281%29.pdf)
## Special Thanks
[Peter Dikant](https://github.com/peterdikant) for writing the base class

View File

@@ -2,6 +2,9 @@ import xair_api
def main():
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.on = True
@@ -11,7 +14,4 @@ def main():
if __name__ == "__main__":
kind_id = "MR18"
ip = "<ip address>"
main()

185
poetry.lock generated
View File

@@ -1,24 +1,53 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "dev"
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 = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
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."
category = "dev"
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"
@@ -37,9 +66,12 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -48,47 +80,62 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
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."
category = "dev"
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]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
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."
category = "dev"
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"},
]
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
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"
@@ -97,29 +144,38 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
name = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
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)"]
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
[package.extras]
dev = ["pre-commit", "tox"]
@@ -129,28 +185,37 @@ testing = ["pytest", "pytest-benchmark"]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
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 = ["railroad-diagrams", "jinja2"]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
category = "dev"
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"},
]
[package.dependencies]
attrs = ">=19.2.0"
@@ -168,9 +233,12 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.
name = "pytest-randomly"
version = "3.12.0"
description = "Pytest plugin to randomly order tests and control random.seed."
category = "dev"
optional = false
python-versions = ">=3.7"
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"},
]
[package.dependencies]
pytest = "*"
@@ -179,74 +247,25 @@ pytest = "*"
name = "python-osc"
version = "1.8.0"
description = "Open Sound Control server and client implementations in pure Python"
category = "main"
optional = false
python-versions = "*"
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"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "80440f75f4191b46dc73824fbfc4fd2fc1ea4dfbdba08591cabb600a86ae2400"
[metadata.files]
attrs = []
black = []
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pathspec = []
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = []
pytest-randomly = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
]
python-osc = []
tomli = [
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "def96d1658f870a9820fef363ee6a04455f1d895e15a189ea4f39801f168552f"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "xair-api"
version = "1.1.1"
version = "2.2.3"
description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
@@ -12,7 +12,7 @@ python = "^3.10"
python-osc = "^1.8.0"
tomli = { version = "^2.0.1", python = "<3.11" }
[tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
pytest-randomly = "^3.12.0"
black = "^22.6.0"
@@ -23,4 +23,6 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
obs = "examples.xair-obs.__main__:main"
obs = "scripts:ex_obs"
xair = "scripts:test_xair"
x32 = "scripts:test_x32"

17
scripts.py Normal file
View File

@@ -0,0 +1,17 @@
import subprocess
from pathlib import Path
def ex_obs():
path = Path.cwd() / "examples" / "xair_obs" / "."
subprocess.run(["py", str(path)])
def test_xair():
path = Path.cwd() / "tests" / "xair"
subprocess.run(["pytest", "-v", str(path)])
def test_x32():
path = Path.cwd() / "tests" / "x32"
subprocess.run(["pytest", "-v", str(path)])

View File

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

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

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

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

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

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

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

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

View File

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

View File

@@ -1,6 +1,6 @@
import pytest
from tests import data, tests
from tests.xair import data, tests
"""
Not every subclass is tested for every superclass to avoid redundancy.
@@ -106,6 +106,23 @@ class TestSetAndGetLRGEQHigher:
""" STRIP TESTS """
class TestSetAndGetStripMuteHigher:
"""Mute"""
__test__ = True
def setup_class(self):
self.target = getattr(tests, "strip")[data.strip]
@pytest.mark.parametrize(
"param,value",
[("mute", True), ("mute", False)],
)
def test_it_sets_and_gets_strip_mute_bool_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
class TestSetAndGetStripMixHigher:
"""Mix"""
@@ -259,7 +276,7 @@ class TestSetAndGetStripAutomixHigher:
"param,value",
[("group", 0), ("group", 2)],
)
def test_it_sets_and_gets_fxsend_int_params(self, param, value):
def test_it_sets_and_gets_strip_int_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@@ -267,7 +284,7 @@ class TestSetAndGetStripAutomixHigher:
"param,value",
[("weight", -10.5), ("weight", 3.5)],
)
def test_it_sets_and_gets_fxsend_float_params(self, param, value):
def test_it_sets_and_gets_strip_float_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value
@@ -326,7 +343,7 @@ class TestSetAndGetBusDynHigher:
assert getattr(self.target, param) == value
class TestSetAndGetBusDynHigher:
class TestSetAndGetBusEQHigher:
"""EQ"""
__test__ = True
@@ -368,6 +385,6 @@ class TestSetAndGetFXSendGroupHigher:
"param,value",
[("dca", 0), ("dca", 12), ("mute", 3), ("mute", 8)],
)
def test_it_sets_and_gets_bus_bool_params(self, param, value):
def test_it_sets_and_gets_fxsend_int_params(self, param, value):
setattr(self.target, param, value)
assert getattr(self.target, param) == value

40
xair_api/adapter.py Normal file
View File

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

View File

@@ -1,6 +1,7 @@
import abc
from .errors import XAirRemoteError
from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
@@ -12,8 +13,7 @@ class IBus(abc.ABC):
self.index = index + 1
def getter(self, param: str):
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -52,7 +52,8 @@ class Bus(IBus):
Mix,
Group,
)
}
},
"mute": mute_prop(),
},
)
return BUS_cls(remote, index)

View File

@@ -13,8 +13,7 @@ class IConfig(abc.ABC):
self._remote = remote
def getter(self, param: str):
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -57,8 +56,6 @@ class Config(IConfig):
@amixenable.setter
def amixenable(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("amixenable is a bool parameter")
self.setter("amixenable", 1 if val else 0)
@property
@@ -67,8 +64,6 @@ class Config(IConfig):
@amixlock.setter
def amixlock(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("amixlock is a bool parameter")
self.setter("amixlock", 1 if val else 0)
class MuteGroup:
@@ -87,8 +82,6 @@ class Config(IConfig):
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter(f"{self.i}", 1 if val else 0)
class Monitor:
@@ -112,8 +105,6 @@ class Config(IConfig):
@source.setter
def source(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("source is an int parameter")
self.setter(f"source", val)
@property
@@ -122,10 +113,8 @@ class Config(IConfig):
@sourcetrim.setter
def sourcetrim(self, val: float):
if not isinstance(val, float):
raise XAirRemoteError(
"sourcetrim is a float parameter, expected value in range -18 to 18"
)
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
self.setter("sourcetrim", lin_set(-18, 18, val))
@property
@@ -134,8 +123,6 @@ class Config(IConfig):
@chmode.setter
def chmode(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("chmode is a bool parameter")
self.setter("chmode", 1 if val else 0)
@property
@@ -144,8 +131,6 @@ class Config(IConfig):
@busmode.setter
def busmode(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("busmode is a bool parameter")
self.setter("busmode", 1 if val else 0)
@property
@@ -154,10 +139,8 @@ class Config(IConfig):
@dimgain.setter
def dimgain(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"dimgain is an int parameter, expected value in range -40 to 0"
)
if not -40 <= val <= 0:
raise XAirRemoteError("expected value in range -40 to 0")
self.setter("dimatt", lin_set(-40, 0, val))
@property
@@ -166,8 +149,6 @@ class Config(IConfig):
@dim.setter
def dim(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("dim is a bool parameter")
self.setter("dim", 1 if val else 0)
@property
@@ -176,8 +157,6 @@ class Config(IConfig):
@mono.setter
def mono(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("mono is a bool parameter")
self.setter("mono", 1 if val else 0)
@property
@@ -186,8 +165,6 @@ class Config(IConfig):
@mute.setter
def mute(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("mute is a bool parameter")
self.setter("mute", 1 if val else 0)
@property
@@ -196,8 +173,6 @@ class Config(IConfig):
@dimfpl.setter
def dimfpl(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("dimfpl is a bool parameter")
self.setter("dimfpl", 1 if val else 0)

View File

@@ -11,8 +11,7 @@ class IDCA(abc.ABC):
self.index = index + 1
def getter(self, param: str) -> tuple:
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -35,18 +34,22 @@ class DCA(IDCA):
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property
def mute(self) -> bool:
return not self.on
@mute.setter
def mute(self, val: bool):
self.on = not val
@property
def name(self) -> str:
return self.getter("config/name")[0]
@name.setter
def name(self, val: str):
if not isinstance(val, str):
raise XAirRemoteError("name is a str parameter")
self.setter("config/name")[0]
@property
@@ -55,6 +58,4 @@ class DCA(IDCA):
@color.setter
def color(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("color is an int parameter")
self.setter("config/color", val)

View File

@@ -1,6 +1,7 @@
import abc
from .errors import XAirRemoteError
from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
@@ -12,8 +13,7 @@ class IFX(abc.ABC):
self.index = index + 1
def getter(self, param: str):
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -23,6 +23,22 @@ class IFX(abc.ABC):
pass
class FX(IFX):
"""Concrete class for fx"""
@property
def address(self) -> str:
return f"/fx/{self.index}"
@property
def type(self) -> int:
return self.getter("type")[0]
@type.setter
def type(self, val: int):
self.setter("type", val)
class FXSend(IFX):
"""Concrete class for fxsend"""
@@ -44,7 +60,8 @@ class FXSend(IFX):
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote, index)
for _cls in (Config, Mix, Group)
}
},
"mute": mute_prop(),
},
)
return FXSEND_cls(remote, index)
@@ -52,21 +69,3 @@ class FXSend(IFX):
@property
def address(self) -> str:
return f"/fxsend/{self.index}"
class FXReturn(IFX):
"""Concrete class for fxreturn"""
@property
def address(self) -> str:
return f"/fx/{self.index}"
@property
def type(self) -> int:
return self.getter("type")[0]
@type.setter
def type(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("type is an integer parameter")
self.setter("type", val)

View File

@@ -1,18 +1,5 @@
from dataclasses import dataclass
"""
# osc slightly different, interface would need adjusting to support this mixer.
@dataclass
class X32KindMap:
id_: str = "X32"
num_dca: int = 8
num_strip: int = 32
num_bus: int = 16
num_fx: int = 8
num_rtn: int = 6
"""
@dataclass
class KindMap:
@@ -21,14 +8,24 @@ class KindMap:
@dataclass
class MR18KindMap(KindMap):
# note ch 17-18 defined as aux rtn
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
@dataclass
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
num_rtn: int = 4
@dataclass
@@ -38,7 +35,6 @@ class XR16KindMap(KindMap):
num_strip: int = 16
num_bus: int = 4
num_fx: int = 4
num_rtn: int = 4
@dataclass
@@ -48,12 +44,12 @@ class XR12KindMap(KindMap):
num_strip: int = 12
num_bus: int = 2
num_fx: int = 4
num_rtn: int = 4
_kinds = {
"XR18": MR18KindMap(id_="XR18"),
"MR18": MR18KindMap(id_="MR18"),
"X32": X32KindMap(id_="X32"),
"MR18": XR18KindMap(id_="MR18"),
"XR18": XR18KindMap(id_="XR18"),
"XR16": XR16KindMap(id_="XR16"),
"XR12": XR12KindMap(id_="XR12"),
}

View File

@@ -1,18 +1,21 @@
import abc
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
class ILR(abc.ABC):
"""Abstract Base Class for buses"""
def __init__(self, remote):
def __init__(self, remote, index: Optional[int] = None):
self._remote = remote
if index is not None:
self.index = index + 1
def getter(self, param: str):
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -26,7 +29,7 @@ class LR(ILR):
"""Concrete class for buses"""
@classmethod
def make(cls, remote):
def make(cls, remote, index=None):
"""
Factory function for LR
@@ -41,19 +44,20 @@ class LR(ILR):
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote)
)(remote, index)
for _cls in (
Config,
Dyn,
Insert,
GEQ.make(),
EQ.make_sixband(cls, remote),
EQ.make_sixband(cls, remote, index),
Mix,
)
},
"mute": mute_prop(),
},
)
return LR_cls(remote)
return LR_cls(remote, index)
@property
def address(self) -> str:

View File

@@ -9,8 +9,6 @@ def bool_prop(param):
return self.getter(param)[0] == 1
def fset(self, val):
if not isinstance(val, bool):
raise XAirRemoteError(f"{param} is a boolean parameter")
self.setter(param, 1 if val else 0)
return property(fget, fset)
@@ -23,8 +21,6 @@ def string_prop(param):
return self.getter(param)[0]
def fset(self, val):
if not isinstance(val, str):
raise XAirRemoteError(f"{param} is a string parameter")
self.setter(param, val)
return property(fget, fset)
@@ -37,8 +33,6 @@ def int_prop(param):
return int(self.getter(param)[0])
def fset(self, val):
if not isinstance(val, int):
raise XAirRemoteError(f"{param} is an integer parameter")
self.setter(param, val)
return property(fget, fset)
@@ -51,8 +45,6 @@ def float_prop(param):
return round(self.getter(param)[0], 1)
def fset(self, val):
if not isinstance(val, int):
raise XAirRemoteError(f"{param} is a float parameter")
self.setter(param, val)
return property(fget, fset)
@@ -72,6 +64,18 @@ def geq_prop(param):
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.setter(param, lin_set(-15, 15, val))
return property(fget, fset)
def mute_prop():
def fget(self):
return not self.mix.on
def fset(self, val):
self.mix.on = not val
return property(fget, fset)

View File

@@ -2,6 +2,7 @@ import abc
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
@@ -14,8 +15,7 @@ class IRtn(abc.ABC):
self.index = index + 1
def getter(self, param: str):
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -25,26 +25,24 @@ class IRtn(abc.ABC):
pass
class Aux(IRtn):
class AuxRtn(IRtn):
"""Concrete class for aux"""
@classmethod
def make(cls, remote):
def make(cls, remote, index=None):
"""
Factory function for aux
Factory function for auxrtn
Creates a mixin of shared subclasses, sets them as class attributes.
Returns an Aux class of a kind.
Returns an AuxRtn class of a kind.
"""
AUX_cls = type(
f"Aux{remote.kind}",
AUXRTN_cls = type(
f"AuxRtn{remote.kind}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote)
)(remote, index)
for _cls in (
Config,
Preamp,
@@ -52,35 +50,34 @@ class Aux(IRtn):
Mix,
Group,
)
}
},
"mute": mute_prop(),
},
)
return AUX_cls(remote)
return AUXRTN_cls(remote, index)
@property
def address(self):
return "/rtn/aux"
class Rtn(IRtn):
class FxRtn(IRtn):
"""Concrete class for rtn"""
@classmethod
def make(cls, remote, index):
"""
Factory function for rtn
Factory function for fxrtn
Creates a mixin of shared subclasses, sets them as class attributes.
Returns an Rtn class of a kind.
Returns an FxRtn class of a kind.
"""
RTN_cls = type(
f"Rtn{remote.kind.id_}",
FXRTN_cls = type(
f"FxRtn{remote.kind}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote, index)
for _cls in (
Config,
@@ -89,10 +86,11 @@ class Rtn(IRtn):
Mix,
Group,
)
}
},
"mute": mute_prop(),
},
)
return RTN_cls(remote, index)
return FXRTN_cls(remote, index)
@property
def address(self):

View File

@@ -5,7 +5,7 @@ from .meta import geq_prop
from .util import _get_fader_val, _set_fader_val, lin_get, lin_set, log_get, log_set
"""
Classes shared by /ch, /rtn, /rt/aux, /bus, /fxsend, /lr
Classes shared by /ch, /rtn, /rtn/aux, /bus, /fxsend, /lr
"""
@@ -21,8 +21,6 @@ class Config:
@name.setter
def name(self, val: str):
if not isinstance(val, str):
raise XAirRemoteError("name is a string parameter")
self.setter("name", val)
@property
@@ -31,8 +29,6 @@ class Config:
@color.setter
def color(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("color is an int parameter")
self.setter("color", val)
@property
@@ -41,8 +37,6 @@ class Config:
@inputsource.setter
def inputsource(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("inputsource is an int parameter")
self.setter("insrc", val)
@property
@@ -51,8 +45,6 @@ class Config:
@usbreturn.setter
def usbreturn(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("usbreturn is an int parameter")
self.setter("rtnsrc", val)
@@ -68,10 +60,8 @@ class Preamp:
@usbtrim.setter
def usbtrim(self, val: float):
if not isinstance(val, float):
raise XAirRemoteError(
"usbtrim is a float parameter, expected value in range -18 to 18"
)
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
self.setter("rtntrim", lin_set(-18, 18, val))
@property
@@ -80,8 +70,6 @@ class Preamp:
@usbinput.setter
def usbinput(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("rtnsw is a bool parameter")
self.setter("rtnsw", 1 if val else 0)
@property
@@ -90,8 +78,6 @@ class Preamp:
@invert.setter
def invert(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("invert is a bool parameter")
self.setter("invert", 1 if val else 0)
@property
@@ -100,8 +86,6 @@ class Preamp:
@highpasson.setter
def highpasson(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("hpon is a bool parameter")
self.setter("hpon", 1 if val else 0)
@property
@@ -110,8 +94,8 @@ class Preamp:
@highpassfilter.setter
def highpassfilter(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("highpassfilter is an int parameter")
if not 20 <= val <= 400:
raise XAirRemoteError("expected value in range 20 to 400")
self.setter("hpf", log_set(20, 400, val))
@@ -127,8 +111,6 @@ class Gate:
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property
@@ -139,8 +121,8 @@ class Gate:
@mode.setter
def mode(self, val: str):
opts = ("gate", "exp2", "exp3", "exp4", "duck")
if not isinstance(val, str) and val not in opts:
raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("mode", opts.index(val))
@property
@@ -149,10 +131,8 @@ class Gate:
@threshold.setter
def threshold(self, val: float):
if not isinstance(val, float):
raise XAirRemoteError(
"threshold is a float parameter, expected value in range -80 to 0"
)
if not -80 <= val <= 0:
raise XAirRemoteError("expected value in range -80.0 to 0.0")
self.setter("thr", lin_set(-80, 0, val))
@property
@@ -161,10 +141,8 @@ class Gate:
@range.setter
def range(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"range is an int parameter, expected value in range 3 to 60"
)
if not 3 <= val <= 60:
raise XAirRemoteError("expected value in range 3 to 60")
self.setter("range", lin_set(3, 60, val))
@property
@@ -173,10 +151,8 @@ class Gate:
@attack.setter
def attack(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"attack is an int parameter, expected value in range 0 to 120"
)
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.setter("attack", lin_set(0, 120, val))
@property
@@ -186,6 +162,8 @@ class Gate:
@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", log_set(0.02, 2000, val))
@property
@@ -194,10 +172,8 @@ class Gate:
@release.setter
def release(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"release is an int parameter, expected value in range 5 to 4000"
)
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.setter("release", log_set(5, 4000, val))
@property
@@ -206,8 +182,6 @@ class Gate:
@keysource.setter
def keysource(self, val):
if not isinstance(val, int):
raise XAirRemoteError("keysource is an int parameter")
self.setter("keysrc", val)
@property
@@ -216,8 +190,6 @@ class Gate:
@filteron.setter
def filteron(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("filteron is a boolean parameter")
self.setter("filter/on", 1 if val else 0)
@property
@@ -226,8 +198,6 @@ class Gate:
@filtertype.setter
def filtertype(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("filtertype is an int parameter")
self.setter("filter/type", val)
@property
@@ -237,6 +207,8 @@ class Gate:
@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", log_set(20, 20000, val))
@@ -252,8 +224,6 @@ class Dyn:
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property
@@ -264,8 +234,8 @@ class Dyn:
@mode.setter
def mode(self, val: str):
opts = ("comp", "exp")
if not isinstance(val, str) and val not in opts:
raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("mode", opts.index(val))
@property
@@ -276,8 +246,8 @@ class Dyn:
@det.setter
def det(self, val: str):
opts = ("peak", "rms")
if not isinstance(val, str) and val not in opts:
raise XAirRemoteError(f"det is a string parameter, expected one of {opts}")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("det", opts.index(val))
@property
@@ -288,8 +258,8 @@ class Dyn:
@env.setter
def env(self, val: str):
opts = ("lin", "log")
if not isinstance(val, str) and val not in opts:
raise XAirRemoteError(f"env is a string parameter, expected one of {opts}")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("env", opts.index(val))
@property
@@ -298,10 +268,8 @@ class Dyn:
@threshold.setter
def threshold(self, val: float):
if not isinstance(val, float):
raise XAirRemoteError(
"threshold is a float parameter, expected value in range -80 to 0"
)
if not -60 <= val <= 0:
raise XAirRemoteError("expected value in range -60.0 to 0")
self.setter("thr", lin_set(-60, 0, val))
@property
@@ -311,8 +279,6 @@ class Dyn:
@ratio.setter
def ratio(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("ratio is an int parameter")
self.setter("ratio", val)
@property
@@ -321,10 +287,8 @@ class Dyn:
@knee.setter
def knee(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"knee is an int parameter, expected value in range 0 to 5"
)
if not 0 <= val <= 5:
raise XAirRemoteError("expected value in range 0 to 5")
self.setter("knee", lin_set(0, 5, val))
@property
@@ -333,6 +297,8 @@ class Dyn:
@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", lin_set(0, 24, val))
@property
@@ -341,6 +307,8 @@ class Dyn:
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.setter("attack", lin_set(0, 120, val))
@property
@@ -350,6 +318,8 @@ class Dyn:
@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", log_set(0.02, 2000, val))
@property
@@ -358,10 +328,8 @@ class Dyn:
@release.setter
def release(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"release is an int parameter, expected value in range 5 to 4000"
)
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.setter("release", log_set(5, 4000, val))
@property
@@ -370,10 +338,8 @@ class Dyn:
@mix.setter
def mix(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError(
"mix is an int parameter, expected value in range 0 to 5"
)
if not 0 <= val <= 100:
raise XAirRemoteError("expected value in range 0 to 100")
self.setter("mix", lin_set(0, 100, val))
@property
@@ -382,8 +348,6 @@ class Dyn:
@keysource.setter
def keysource(self, val):
if not isinstance(val, int):
raise XAirRemoteError("keysource is an int parameter")
self.setter("keysrc", val)
@property
@@ -392,8 +356,6 @@ class Dyn:
@auto.setter
def auto(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("auto is a boolean parameter")
self.setter("auto", 1 if val else 0)
@property
@@ -402,8 +364,6 @@ class Dyn:
@filteron.setter
def filteron(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("filteron is a boolean parameter")
self.setter("filter/on", 1 if val else 0)
@property
@@ -412,8 +372,6 @@ class Dyn:
@filtertype.setter
def filtertype(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("filtertype is an int parameter")
self.setter("filter/type", val)
@property
@@ -423,6 +381,8 @@ class Dyn:
@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", log_set(20, 20000, val))
@@ -438,8 +398,6 @@ class Insert:
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property
@@ -448,8 +406,6 @@ class Insert:
@sel.setter
def sel(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("sel is an int parameter")
self.setter("sel", val)
@@ -495,8 +451,6 @@ class EQ:
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property
@@ -507,8 +461,8 @@ class EQ:
@mode.setter
def mode(self, val: str):
opts = ("peq", "geq", "teq")
if not isinstance(val, str) and val not in opts:
raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.setter("mode", opts.index(val))
class EQBand:
@@ -530,8 +484,6 @@ class EQ:
@type.setter
def type(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("type is an int parameter")
self.setter(f"type", val)
@property
@@ -541,6 +493,8 @@ class EQ:
@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", log_set(20, 20000, val))
@property
@@ -549,6 +503,8 @@ class EQ:
@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", lin_set(-15, 15, val))
@property
@@ -558,6 +514,8 @@ class EQ:
@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", log_set(0.3, 10, val))
@@ -600,8 +558,6 @@ class Mix:
@on.setter
def on(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0)
@property
@@ -619,8 +575,6 @@ class Mix:
@lr.setter
def lr(self, val: bool):
if not isinstance(val, bool):
raise XAirRemoteError("lr is a boolean parameter")
self.setter("lr", 1 if val else 0)
@@ -636,8 +590,6 @@ class Group:
@dca.setter
def dca(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("dca is an int parameter")
self.setter("dca", val)
@property
@@ -646,8 +598,6 @@ class Group:
@mute.setter
def mute(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("mute is an int parameter")
self.setter("mute", val)
@@ -663,8 +613,6 @@ class Automix:
@group.setter
def group(self, val: int):
if not isinstance(val, int):
raise XAirRemoteError("group is an int parameter")
self.setter("group", val)
@property
@@ -673,8 +621,6 @@ class Automix:
@weight.setter
def weight(self, val: float):
if not isinstance(val, float):
raise XAirRemoteError(
"weight is a float parameter, expected value in range -12 to 12"
)
if not -12 <= val <= 12:
raise XAirRemoteError("expected value in range -12.0 to 12.0")
self.setter("weight", lin_set(-12, 12, val))

View File

@@ -1,6 +1,7 @@
import abc
from .errors import XAirRemoteError
from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
@@ -12,8 +13,7 @@ class IStrip(abc.ABC):
self.index = index + 1
def getter(self, param: str) -> tuple:
self._remote.send(f"{self.address}/{param}")
return self._remote.info_response
return self._remote.query(f"{self.address}/{param}")
def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val)
@@ -35,6 +35,7 @@ class Strip(IStrip):
Returns a Strip class of a kind.
"""
STRIP_cls = type(
f"Strip{remote.kind}",
(cls,),
@@ -55,6 +56,7 @@ class Strip(IStrip):
Automix,
)
},
"mute": mute_prop(),
},
)
return STRIP_cls(remote, index)

View File

@@ -1,8 +1,9 @@
import abc
import logging
import threading
import time
from pathlib import Path
from typing import Optional
from typing import Optional, Union
try:
import tomllib
@@ -13,15 +14,15 @@ from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_message_builder import OscMessageBuilder
from pythonosc.osc_server import BlockingOSCUDPServer
from . import kinds
from . import adapter, kinds
from .bus import Bus
from .config import Config
from .dca import DCA
from .errors import XAirRemoteError
from .fx import FXReturn, FXSend
from .fx import FX, FXSend
from .kinds import KindMap
from .lr import LR
from .rtn import Aux, Rtn
from .rtn import AuxRtn, FxRtn
from .strip import Strip
@@ -30,15 +31,12 @@ class OSCClientServer(BlockingOSCUDPServer):
super().__init__(("", 0), dispatcher)
self.xr_address = address
def send_message(self, address: str, value: str):
def send_message(self, address: str, vals: Optional[Union[str, list]]):
builder = OscMessageBuilder(address=address)
if value is None:
values = list()
elif isinstance(value, list):
values = value
else:
values = [value]
for val in values:
vals = vals if vals is not None else []
if not isinstance(vals, list):
vals = [vals]
for val in vals:
builder.add_arg(val)
msg = builder.build()
self.socket.sendto(msg.dgram, self.xr_address)
@@ -47,21 +45,20 @@ 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
_WAIT_TIME = 0.025
_REFRESH_TIMEOUT = 5
XAIR_PORT = 10024
info_response = []
_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"] or self.XAIR_PORT
if not (self.xair_ip and self.xair_port):
raise XAirRemoteError("No valid ip or password detected")
self.xair_port = kwargs["port"]
self._delay = kwargs["delay"]
if not self.xair_ip:
raise XAirRemoteError("No valid ip detected")
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
def __enter__(self):
@@ -79,26 +76,32 @@ class XAirRemote(abc.ABC):
def validate_connection(self):
self.send("/xinfo")
time.sleep(self._CONNECT_TIMEOUT)
if len(self.info_response) > 0:
print(f"Successfully connected to {self.info_response[2]}.")
else:
print(
"Error: Failed to setup OSC connection to mixer. Please check for correct ip address."
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]}."
)
@property
def info_response(self):
return self._info_response
def run_server(self):
self.server.serve_forever()
def msg_handler(self, addr, *data):
self.info_response = data[:]
self.logger.debug(f"received: {addr} {data if data else ''}")
self._info_response = data[:]
def send(self, address: str, param: Optional[str] = None):
self.server.send_message(address, param)
time.sleep(self._WAIT_TIME)
def send(self, addr: str, param: Optional[str] = None):
self.logger.debug(f"sending: {addr} {param if param is not None else ''}")
self.server.send_message(addr, param)
def _query(self, address):
def query(self, address):
self.send(address)
time.sleep(self._WAIT_TIME)
time.sleep(self._delay)
return self.info_response
def __exit__(self, exc_type, exc_value, exc_tr):
@@ -112,8 +115,26 @@ def _make_remote(kind: KindMap) -> XAirRemote:
The returned class will subclass XAirRemote.
"""
def init(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": None}
def init_x32(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": 10023, "delay": 0.02}
kwargs = defaultkwargs | kwargs
XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind
self.mainst = adapter.MainStereo.make(self)
self.mainmono = adapter.MainMono.make(self)
self.matrix = tuple(
adapter.Matrix.make(self, i) for i in range(kind.num_matrix)
)
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
self.bus = tuple(adapter.Bus.make(self, i) for i in range(kind.num_bus))
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
self.fx = tuple(FX(self, i) for i in range(kind.num_fx))
self.fxreturn = tuple(adapter.FxRtn.make(self, i) for i in range(kind.num_fx))
self.auxin = tuple(adapter.AuxRtn.make(self, i) for i in range(kind.num_auxrtn))
self.config = Config.make(self)
def init_xair(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": 10024, "delay": 0.02}
kwargs = defaultkwargs | kwargs
XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind
@@ -121,17 +142,25 @@ def _make_remote(kind: KindMap) -> XAirRemote:
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
self.bus = tuple(Bus.make(self, i) for i in range(kind.num_bus))
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
self.fx = tuple(FX(self, i) for i in range(kind.num_fx))
self.fxsend = tuple(FXSend.make(self, i) for i in range(kind.num_fx))
self.fxreturn = tuple(FXReturn(self, i) for i in range(kind.num_fx))
self.fxreturn = tuple(FxRtn.make(self, i) for i in range(kind.num_fx))
self.auxreturn = AuxRtn.make(self)
self.config = Config.make(self)
self.aux = Aux.make(self)
self.rtn = tuple(Rtn.make(self, i) for i in range(kind.num_rtn))
if kind.id_ == "X32":
return type(
f"XAirRemote{kind}",
(XAirRemote,),
{
"__init__": init,
"__init__": init_x32,
},
)
return type(
f"XAirRemote{kind}",
(XAirRemote,),
{
"__init__": init_xair,
},
)