21 Commits

Author SHA1 Message Date
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
24 changed files with 637 additions and 269 deletions

2
.gitignore vendored
View File

@@ -131,3 +131,5 @@ dmypy.json
# config, quick test
config.toml
quick.py
.vscode/

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,6 @@
[![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/)
[![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
@@ -55,7 +54,7 @@ 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 +71,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 +113,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 +125,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 +140,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 +299,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
@@ -311,6 +328,14 @@ for example:
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.

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()

354
poetry.lock generated
View File

@@ -1,92 +1,79 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.5 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"
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"
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"
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 = "*"
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.0"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "filelock"
version = "3.16.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
typing = ["typing-extensions (>=4.12.2)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
@@ -98,83 +85,42 @@ files = [
{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"},
]
[[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"
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"
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"
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,52 +128,56 @@ 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"
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"
files = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
{file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"},
{file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"},
]
[package.dependencies]
packaging = ">=24.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
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 = "7.1.3"
version = "7.4.4"
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 = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
]
[package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-randomly"
@@ -254,6 +204,33 @@ files = [
{file = "python_osc-1.8.0-py3-none-any.whl", hash = "sha256:9e2abb2fc9ba2c356f8e951609a03c9c7017bf0bad82cca8490e9b8af9e92a0b"},
]
[[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"
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"
@@ -265,7 +242,80 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[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"
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.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "virtualenv"
version = "20.28.1"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.8"
files = [
{file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"},
{file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[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)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[[package]]
name = "virtualenv-pyenv"
version = "0.5.0"
description = "A virtualenv Python discovery plugin for pyenv-installed interpreters"
optional = false
python-versions = ">=3.8"
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"
content-hash = "03c4b24c8bf12ad78f55f6e99a5001183bbb855bf8ef841dd3522fb79c1f7288"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "xair-api"
version = "2.2.4a0"
version = "2.4.0"
description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
@@ -13,10 +13,11 @@ python-osc = "^1.8.0"
tomli = { version = "^2.0.1", python = "<3.11" }
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
pytest = "^7.4.4"
pytest-randomly = "^3.12.0"
black = "^22.6.0"
isort = "^5.10.1"
ruff = "^0.8.6"
tox = "^4.23.2"
virtualenv-pyenv = "^0.5.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -27,3 +28,108 @@ obs = "scripts:ex_obs"
sends = "scripts:ex_sends"
xair = "scripts:test_xair"
x32 = "scripts:test_x32"
all = "scripts:test_all"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py310,py311,py312
[testenv]
setenv = VIRTUALENV_DISCOVERY=pyenv
allowlist_externals = poetry
commands =
poetry install -v
poetry run pytest tests/
"""
[tool.ruff]
lint.select = [
"E",
"F",
]
lint.ignore = [
"E501",
]
lint.fixable = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"I",
"N",
"Q",
"S",
"T",
"W",
"ANN",
"ARG",
"BLE",
"COM",
"DJ",
"DTZ",
"EM",
"ERA",
"EXE",
"FBT",
"ICN",
"INP",
"ISC",
"NPY",
"PD",
"PGH",
"PIE",
"PL",
"PT",
"PTH",
"PYI",
"RET",
"RSE",
"RUF",
"SIM",
"SLF",
"TCH",
"TID",
"TRY",
"UP",
"YTT",
]
lint.unfixable = []
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",
]
line-length = 88
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py312"
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"E402",
"F401",
]

View File

@@ -21,3 +21,7 @@ def test_xair():
def test_x32():
path = Path.cwd() / "tests" / "x32"
subprocess.run(["pytest", "-v", str(path)])
def test_all():
subprocess.run(["tox"])

View File

@@ -8,7 +8,7 @@ from xair_api import kinds
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)

View File

@@ -8,7 +8,7 @@ from xair_api import kinds
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)

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
@@ -25,16 +26,22 @@ class FxRtn(IFxRtn):
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)}"
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,6 +13,7 @@ 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}")

View File

@@ -1,15 +1,18 @@
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}")
@@ -33,8 +36,8 @@ 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}",
(cls, LINKS_cls),
@@ -47,7 +50,7 @@ class Config(IConfig):
@property
def address(self) -> str:
return f"/config"
return "/config"
@property
def amixenable(self) -> bool:
@@ -105,7 +108,7 @@ class Config(IConfig):
@source.setter
def source(self, val: int):
self.setter(f"source", val)
self.setter("source", val)
@property
def sourcetrim(self) -> float:
@@ -114,7 +117,9 @@ class Config(IConfig):
@sourcetrim.setter
def sourcetrim(self, val: float):
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
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
@@ -140,7 +145,9 @@ class Config(IConfig):
@dimgain.setter
def dimgain(self, val: int):
if not -40 <= val <= 0:
raise XAirRemoteError("expected value in range -40 to 0")
self.logger.warning(
f"dimgain got {val}, expected value in range -40 to 0"
)
self.setter("dimatt", util.lin_set(-40, 0, val))
@property

View File

@@ -1,6 +1,7 @@
import abc
import logging
from .errors import XAirRemoteError
logger = logging.getLogger(__name__)
class IDCA(abc.ABC):
@@ -9,6 +10,7 @@ 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}")
@@ -50,7 +52,7 @@ class DCA(IDCA):
@name.setter
def name(self, val: str):
self.setter("config/name")[0]
self.setter("config/name", val)
@property
def color(self) -> int:

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,6 +13,7 @@ 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}")

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

@@ -16,6 +16,7 @@ class X32KindMap(KindMap):
num_fx: int = 8
num_auxrtn: int = 8
num_matrix: int = 6
num_headamp: int = 127
@dataclass

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,6 +15,7 @@ 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}")
@@ -61,4 +64,4 @@ class LR(ILR):
@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,13 +50,6 @@ 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("_", ".")
def fget(self) -> float:
@@ -65,7 +57,9 @@ def geq_prop(param):
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,6 +15,7 @@ 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}")

View File

@@ -1,7 +1,6 @@
from typing import Optional, Union
from . import util
from .errors import XAirRemoteError
from .meta import geq_prop
"""
@@ -61,7 +60,9 @@ class Preamp:
@usbtrim.setter
def usbtrim(self, val: float):
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
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
@@ -95,7 +96,9 @@ class Preamp:
@highpassfilter.setter
def highpassfilter(self, val: int):
if not 20 <= val <= 400:
raise XAirRemoteError("expected value in range 20 to 400")
self.logger.warning(
f"highpassfilter got {val}, expected value in range 20 to 400"
)
self.setter("hpf", util.log_set(20, 400, val))
@@ -122,7 +125,7 @@ class Gate:
def mode(self, val: str):
opts = ("gate", "exp2", "exp3", "exp4", "duck")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.logger.warning(f"mode got {val}, expected one of {opts}")
self.setter("mode", opts.index(val))
@property
@@ -132,7 +135,9 @@ class Gate:
@threshold.setter
def threshold(self, val: float):
if not -80 <= val <= 0:
raise XAirRemoteError("expected value in range -80.0 to 0.0")
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
@@ -142,7 +147,7 @@ class Gate:
@range.setter
def range(self, val: int):
if not 3 <= val <= 60:
raise XAirRemoteError("expected value in range 3 to 60")
self.logger.warning(f"range got {val}, expected value in range 3 to 60")
self.setter("range", util.lin_set(3, 60, val))
@property
@@ -152,7 +157,7 @@ class Gate:
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.logger.warning(f"attack got {val}, expected value in range 0 to 120")
self.setter("attack", util.lin_set(0, 120, val))
@property
@@ -163,7 +168,9 @@ 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.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
@@ -173,7 +180,7 @@ class Gate:
@release.setter
def release(self, val: int):
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.logger.warning(f"release got {val}, expected value in range 5 to 4000")
self.setter("release", util.log_set(5, 4000, val))
@property
@@ -208,7 +215,9 @@ 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.logger.warning(
f"filterfreq got {val}, expected value in range 20 to 20000"
)
self.setter("filter/f", util.log_set(20, 20000, val))
@@ -235,7 +244,7 @@ class Dyn:
def mode(self, val: str):
opts = ("comp", "exp")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.logger.warning(f"mode got {val}, expected one of {opts}")
self.setter("mode", opts.index(val))
@property
@@ -247,7 +256,7 @@ class Dyn:
def det(self, val: str):
opts = ("peak", "rms")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.logger.warning(f"det got {val}, expected one of {opts}")
self.setter("det", opts.index(val))
@property
@@ -259,7 +268,7 @@ class Dyn:
def env(self, val: str):
opts = ("lin", "log")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.logger.warning(f"env got {val}, expected one of {opts}")
self.setter("env", opts.index(val))
@property
@@ -269,7 +278,9 @@ class Dyn:
@threshold.setter
def threshold(self, val: float):
if not -60 <= val <= 0:
raise XAirRemoteError("expected value in range -60.0 to 0")
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
@@ -288,7 +299,7 @@ class Dyn:
@knee.setter
def knee(self, val: int):
if not 0 <= val <= 5:
raise XAirRemoteError("expected value in range 0 to 5")
self.logger.warning(f"knee got {val}, expected value in range 0 to 5")
self.setter("knee", util.lin_set(0, 5, val))
@property
@@ -298,7 +309,7 @@ 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.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
@@ -308,7 +319,7 @@ class Dyn:
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.logger.warning(f"attack got {val}, expected value in range 0 to 120")
self.setter("attack", util.lin_set(0, 120, val))
@property
@@ -319,7 +330,9 @@ 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.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
@@ -329,7 +342,7 @@ class Dyn:
@release.setter
def release(self, val: int):
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.logger.warning(f"release got {val}, expected value in range 5 to 4000")
self.setter("release", util.log_set(5, 4000, val))
@property
@@ -339,7 +352,7 @@ class Dyn:
@mix.setter
def mix(self, val: int):
if not 0 <= val <= 100:
raise XAirRemoteError("expected value in range 0 to 100")
self.logger.warning(f"mix got {val}, expected value in range 0 to 100")
self.setter("mix", util.lin_set(0, 100, val))
@property
@@ -382,7 +395,9 @@ 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.logger.warning(
f"filterfreq got {val}, expected value in range 20 to 20000"
)
self.setter("filter/f", util.log_set(20, 20000, val))
@@ -462,7 +477,7 @@ class EQ:
def mode(self, val: str):
opts = ("peq", "geq", "teq")
if val not in opts:
raise XAirRemoteError(f"expected one of {opts}")
self.logger.warning(f"mode got {val}, expected one of {opts}")
self.setter("mode", opts.index(val))
class EQBand:
@@ -481,7 +496,7 @@ class EQ:
@type.setter
def type(self, val: int):
self.setter(f"type", val)
self.setter("type", val)
@property
def frequency(self) -> float:
@@ -491,7 +506,9 @@ 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.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
@@ -501,7 +518,9 @@ 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.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
@@ -512,7 +531,9 @@ 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.logger.warning(
f"quality got {val}, expected value in range 0.3 to 10.0"
)
self.setter("q", util.log_set(0.3, 10, val))
@@ -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",
]
@@ -620,7 +641,9 @@ class Automix:
@weight.setter
def weight(self, val: float):
if not -12 <= val <= 12:
raise XAirRemoteError("expected value in range -12.0 to 12.0")
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))
@@ -641,10 +664,10 @@ class Send:
@property
@util.db_from
def level(self):
def level(self) -> float:
return self.getter("level")[0]
@level.setter
@util.db_to
def level(self, 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,6 +13,7 @@ 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}")

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,17 +14,20 @@ 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):
@@ -45,10 +48,6 @@ 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):
@@ -57,6 +56,8 @@ class XAirRemote(abc.ABC):
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")
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
@@ -73,14 +74,11 @@ class XAirRemote(abc.ABC):
conn = tomllib.load(f)
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(
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]}."
)
@@ -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,6 +156,7 @@ 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":
return type(
@@ -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)