package renamed to xair-api

now packaged with poetry and added to pypi

using tomllib, requires python 3.11

readme, changelog updated to reflect changes

major version bump
This commit is contained in:
onyx-and-iris 2022-08-07 23:55:51 +01:00
parent e8d23562f1
commit f8c6659fd8
26 changed files with 522 additions and 239 deletions

View File

@ -5,11 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Before any major/minor/patch patch is released all test units will be run to verify they pass. Before any major/minor/patch patch is released all unit tests will be run to verify they pass.
## [Unreleased] ## [Unreleased]
- [ ] Create a stable branch. - [x]
## [1.0.2] - 2022-08-07
### Added
- now packaged with poetry
- package added to pypi
- pypi, isort badges added to readme
### Changed
- package renamed to xair-api
- now using tomllib for config, requires python 3.11
- readme, example updated.
- imports isorted.
## [0.1.0] - 2022-05-01 ## [0.1.0] - 2022-05-01

View File

@ -1,8 +1,10 @@
[![PyPI version](https://badge.fury.io/py/xair-api.svg)](https://badge.fury.io/py/xair-api)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/xair-api-python/blob/dev/LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/xair-api-python/blob/dev/LICENSE)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![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/MR18.svg?dummy=8484744)
# Mair Remote # Xair API
This package offers a python interface for the [Behringer XAir](https://www.behringer.com/series.html?category=R-BEHRINGER-XAIRSERIES), [Midas MR](https://www.midasconsoles.com/catalog.html?catalog=Category&category=C-MIDAS-MIXERS-DIGITALSTAGEBOXMIXERS) series of digital rack mixers. I only have access to an MR18 for testing so if there is an error in the kind maps feel free to raise an issue or PR. This package offers a python interface for the [Behringer XAir](https://www.behringer.com/series.html?category=R-BEHRINGER-XAIRSERIES), [Midas MR](https://www.midasconsoles.com/catalog.html?catalog=Category&category=C-MIDAS-MIXERS-DIGITALSTAGEBOXMIXERS) series of digital rack mixers. I only have access to an MR18 for testing so if there is an error in the kind maps feel free to raise an issue or PR.
@ -10,54 +12,45 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Prerequisites ## Prerequisites
- Python 3.9+ - Python 3.11 or greater
## Installation ## Installation
``` ```
git clone https://github.com/onyx-and-iris/xair-api-python pip install xair-api
cd xair-api-python
```
Just the interface:
```
pip install .
```
With development dependencies:
```
pip install -e .['development']
``` ```
## Usage ## Usage
### Connection ### Connection
An ini file named config.ini, placed into the current working directory of your code may be used to configure the mixers ip. It's contents should resemble: A toml file named config.toml, placed into the current working directory of your code may be used to configure the mixers ip. A valid `config.toml` may resemble:
``` ```toml
[connection] [connection]
ip=<ip address> ip=192.168.0.100
``` ```
Alternatively you may state it explicitly as an argument to mair.connect() Alternatively you may pass it as a keyword argument.
### Example 1 ### Example
```python ```python
import mair import xair_api
def main(): def main():
with mair.connect(kind_id, ip=ip) as mixer: with xair_api.connect(kind_id, ip=ip) as mixer:
mixer.strip[8].config.name = 'sm7b' mixer.strip[8].config.name = "sm7b"
mixer.strip[8].config.on = True mixer.strip[8].config.on = True
print(f'strip 09 ({mixer.strip[8].config.name}) has been set to {mixer.strip[8].config.on}') print(
f"strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].config.on}"
)
if __name__ == '__main__':
kind_id = 'MR18' if __name__ == "__main__":
ip = '<ip address>' kind_id = "MR18"
ip = "192.168.0.100"
main() main()
``` ```
@ -71,7 +64,7 @@ Currently the following devices are support:
- `XR16` - `XR16`
- `XR12` - `XR12`
### MAirRemote (higher level) ### XAirRemote class (higher level)
`mixer.lr` `mixer.lr`
@ -277,8 +270,7 @@ for example: `config.monitor.chmode`
### `Tests` ### `Tests`
People plug expensive equipment into these mixers, the unit tests adjust parameter values such as gain sliders etc. My advice is Unplug any/all expensive equipment before running any tests.
to unplug all equipment from the mixer before running these tests. No tests alter phantom power state.
Save your current settings to a snapshot first. Save your current settings to a snapshot first.
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/xair-api-python#installation) First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/xair-api-python#installation)

View File

@ -1,8 +1,8 @@
import mair import xair_api
def main(): def main():
with mair.connect(kind_id, ip=ip) as mixer: with xair_api.connect(kind_id, ip=ip) as mixer:
mixer.strip[8].config.name = "sm7b" mixer.strip[8].config.name = "sm7b"
mixer.strip[8].config.on = True mixer.strip[8].config.on = True
print( print(

2
config.toml Normal file
View File

@ -0,0 +1,2 @@
[connection]
ip=172.16.0.100

View File

@ -1,3 +0,0 @@
from .mair import connect
_ALL__ = ['connect']

View File

@ -1,4 +0,0 @@
class MAirRemoteError(Exception):
"""Base error class for MAIR Remote."""
pass

291
poetry.lock generated Normal file
View File

@ -0,0 +1,291 @@
[[package]]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "black"
version = "22.6.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
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.*"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[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"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pathspec"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[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"
[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)"]
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
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"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[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"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
version = "7.1.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
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"
[package.dependencies]
pytest = "*"
[[package]]
name = "python-osc"
version = "1.8.0"
description = "Open Sound Control server and client implementations in pure Python"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
[metadata]
lock-version = "1.1"
python-versions = "^3.11"
content-hash = "ba3fcf3e019a33e98eb7f8ba760c0bbda4017819a6c45b50f2ec7e0e31ac127c"
[metadata.files]
atomicwrites = []
attrs = []
black = [
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
{file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
{file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
{file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
{file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
{file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
{file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
{file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
{file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
{file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
{file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
]
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 = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
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 = [
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
]
pytest-randomly = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
]
python-osc = []
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]

22
pyproject.toml Normal file
View File

@ -0,0 +1,22 @@
[tool.poetry]
name = "xair-api"
version = "1.0.2"
description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/onyx-and-iris/xair-api-python"
[tool.poetry.dependencies]
python = "^3.11"
python-osc = "^1.8.0"
[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
pytest-randomly = "^3.12.0"
black = "^22.6.0"
isort = "^5.10.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,10 +0,0 @@
from setuptools import setup
setup(
name="mair_remote",
version="0.1.0",
description="MAIR Remote Python API",
packages=["mair"],
install_requires=["python-osc"],
extras_require={"development": ["pytest", "pytest-randomly", "genbadge[tests]"]},
)

View File

@ -1,13 +1,14 @@
import mair import sys
from mair import kinds
import threading import threading
from dataclasses import dataclass from dataclasses import dataclass
import sys
import xair_api
from xair_api import kinds
kind_id = "MR18" kind_id = "MR18"
ip = "mixer.local" ip = "mixer.local"
tests = mair.connect(kind_id, ip=ip) tests = xair_api.connect(kind_id, ip=ip)
kind = kinds.get(kind_id) kind = kinds.get(kind_id)

View File

@ -11,7 +11,7 @@ Function RunTests {
$line | Tee-Object -FilePath $coverage -Append $line | Tee-Object -FilePath $coverage -Append
} }
} }
Write-Output "$(Get-TimeStamp)" | Out-file $coverage -Append Write-Output "$(Get-TimeStamp)" | Out-File $coverage -Append
Invoke-Expression "genbadge tests -t 90 -i ./tests/.coverage.xml -o ./tests/$kind.svg" Invoke-Expression "genbadge tests -t 90 -i ./tests/.coverage.xml -o ./tests/$kind.svg"
} }
@ -23,7 +23,7 @@ Function Get-TimeStamp {
} }
if ($MyInvocation.InvocationName -ne ".") { if ($MyInvocation.InvocationName -ne ".") {
Invoke-Expression ".\venv\Scripts\Activate.ps1" Invoke-Expression ".\.venv\Scripts\Activate.ps1"
RunTests RunTests

View File

@ -1,5 +1,6 @@
import pytest import pytest
from tests import tests, data
from tests import data, tests
""" """
Not every subclass is tested for every superclass to avoid redundancy. Not every subclass is tested for every superclass to avoid redundancy.

3
xair_api/__init__.py Normal file
View File

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

View File

@ -1,17 +1,7 @@
import abc import abc
from .errors import MAirRemoteError
from .shared import ( from .errors import XAirRemoteError
Config, from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
Preamp,
Gate,
Dyn,
Insert,
EQ,
GEQ,
Mix,
Group,
Automix,
)
class IBus(abc.ABC): class IBus(abc.ABC):
@ -46,12 +36,12 @@ class Bus(IBus):
Returns a Bus class of a kind. Returns a Bus class of a kind.
""" """
BUS_cls = type( BUS_cls = type(
f"Bus{remote.kind.id_}", f"Bus{remote.kind}",
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {} f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in ( for _cls in (
Config, Config,

View File

@ -1,6 +1,7 @@
import abc import abc
from .errors import MAirRemoteError
from . import kinds from . import kinds
from .errors import XAirRemoteError
from .meta import bool_prop from .meta import bool_prop
from .util import _get_level_val, _set_level_val, lin_get, lin_set from .util import _get_level_val, _set_level_val, lin_get, lin_set
@ -36,7 +37,7 @@ class Config(IConfig):
LINKS_cls = _make_links_mixins[remote.kind.id_] LINKS_cls = _make_links_mixins[remote.kind.id_]
MONITOR_cls = type(f"ConfigMonitor", (Config.Monitor, cls), {}) MONITOR_cls = type(f"ConfigMonitor", (Config.Monitor, cls), {})
CONFIG_cls = type( CONFIG_cls = type(
f"Config{remote.kind.id_}", f"Config{remote.kind}",
(cls, LINKS_cls), (cls, LINKS_cls),
{"monitor": MONITOR_cls(remote)}, {"monitor": MONITOR_cls(remote)},
) )
@ -53,7 +54,7 @@ class Config(IConfig):
@amixenable.setter @amixenable.setter
def amixenable(self, val: bool): def amixenable(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("amixenable is a bool parameter") raise XAirRemoteError("amixenable is a bool parameter")
self.setter("amixenable", 1 if val else 0) self.setter("amixenable", 1 if val else 0)
@property @property
@ -63,7 +64,7 @@ class Config(IConfig):
@amixlock.setter @amixlock.setter
def amixlock(self, val: bool): def amixlock(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("amixlock is a bool parameter") raise XAirRemoteError("amixlock is a bool parameter")
self.setter("amixlock", 1 if val else 0) self.setter("amixlock", 1 if val else 0)
@property @property
@ -73,7 +74,7 @@ class Config(IConfig):
@mute_group.setter @mute_group.setter
def mute_group(self, val: bool): def mute_group(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("mute_group is a bool parameter") raise XAirRemoteError("mute_group is a bool parameter")
self.setter("mute", 1 if val else 0) self.setter("mute", 1 if val else 0)
class Monitor: class Monitor:
@ -98,7 +99,7 @@ class Config(IConfig):
@source.setter @source.setter
def source(self, val: int): def source(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("source is an int parameter") raise XAirRemoteError("source is an int parameter")
self.setter(f"source", val) self.setter(f"source", val)
@property @property
@ -108,7 +109,7 @@ class Config(IConfig):
@sourcetrim.setter @sourcetrim.setter
def sourcetrim(self, val: float): def sourcetrim(self, val: float):
if not isinstance(val, float): if not isinstance(val, float):
raise MAirRemoteError( raise XAirRemoteError(
"sourcetrim is a float parameter, expected value in range -18 to 18" "sourcetrim is a float parameter, expected value in range -18 to 18"
) )
self.setter("sourcetrim", lin_set(-18, 18, val)) self.setter("sourcetrim", lin_set(-18, 18, val))
@ -120,7 +121,7 @@ class Config(IConfig):
@chmode.setter @chmode.setter
def chmode(self, val: bool): def chmode(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("chmode is a bool parameter") raise XAirRemoteError("chmode is a bool parameter")
self.setter("chmode", 1 if val else 0) self.setter("chmode", 1 if val else 0)
@property @property
@ -130,7 +131,7 @@ class Config(IConfig):
@busmode.setter @busmode.setter
def busmode(self, val: bool): def busmode(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("busmode is a bool parameter") raise XAirRemoteError("busmode is a bool parameter")
self.setter("busmode", 1 if val else 0) self.setter("busmode", 1 if val else 0)
@property @property
@ -140,7 +141,7 @@ class Config(IConfig):
@dimgain.setter @dimgain.setter
def dimgain(self, val: int): def dimgain(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"dimgain is an int parameter, expected value in range -40 to 0" "dimgain is an int parameter, expected value in range -40 to 0"
) )
self.setter("dimatt", lin_set(-40, 0, val)) self.setter("dimatt", lin_set(-40, 0, val))
@ -152,7 +153,7 @@ class Config(IConfig):
@dim.setter @dim.setter
def dim(self, val: bool): def dim(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("dim is a bool parameter") raise XAirRemoteError("dim is a bool parameter")
self.setter("dim", 1 if val else 0) self.setter("dim", 1 if val else 0)
@property @property
@ -162,7 +163,7 @@ class Config(IConfig):
@mono.setter @mono.setter
def mono(self, val: bool): def mono(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("mono is a bool parameter") raise XAirRemoteError("mono is a bool parameter")
self.setter("mono", 1 if val else 0) self.setter("mono", 1 if val else 0)
@property @property
@ -172,7 +173,7 @@ class Config(IConfig):
@mute.setter @mute.setter
def mute(self, val: bool): def mute(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("mute is a bool parameter") raise XAirRemoteError("mute is a bool parameter")
self.setter("mute", 1 if val else 0) self.setter("mute", 1 if val else 0)
@property @property
@ -182,14 +183,14 @@ class Config(IConfig):
@dimfpl.setter @dimfpl.setter
def dimfpl(self, val: bool): def dimfpl(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("dimfpl is a bool parameter") raise XAirRemoteError("dimfpl is a bool parameter")
self.setter("dimfpl", 1 if val else 0) self.setter("dimfpl", 1 if val else 0)
def _make_links_mixin(kind): def _make_links_mixin(kind):
"""Creates a links mixin""" """Creates a links mixin"""
return type( return type(
f"Links{kind.id_}", f"Links{kind}",
(), (),
{ {
"link_eq": bool_prop("linkcfg/eq"), "link_eq": bool_prop("linkcfg/eq"),

View File

@ -1,5 +1,6 @@
import abc import abc
from .errors import MAirRemoteError
from .errors import XAirRemoteError
class IDCA(abc.ABC): class IDCA(abc.ABC):
@ -35,7 +36,7 @@ class DCA(IDCA):
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("on is a boolean parameter") raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@ -45,7 +46,7 @@ class DCA(IDCA):
@name.setter @name.setter
def name(self, val: str): def name(self, val: str):
if not isinstance(val, str): if not isinstance(val, str):
raise MAirRemoteError("name is a str parameter") raise XAirRemoteError("name is a str parameter")
self.setter("config/name")[0] self.setter("config/name")[0]
@property @property
@ -55,5 +56,5 @@ class DCA(IDCA):
@color.setter @color.setter
def color(self, val: int): def color(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("color is an int parameter") raise XAirRemoteError("color is an int parameter")
self.setter("config/color", val) self.setter("config/color", val)

4
xair_api/errors.py Normal file
View File

@ -0,0 +1,4 @@
class XAirRemoteError(Exception):
"""Base error class for XAIR Remote."""
pass

View File

@ -1,17 +1,7 @@
import abc import abc
from .errors import MAirRemoteError
from .shared import ( from .errors import XAirRemoteError
Config, from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
Preamp,
Gate,
Dyn,
Insert,
EQ,
GEQ,
Mix,
Group,
Automix,
)
class IFX(abc.ABC): class IFX(abc.ABC):
@ -46,12 +36,12 @@ class FXSend(IFX):
Returns an FXSend class of a kind. Returns an FXSend class of a kind.
""" """
FXSEND_cls = type( FXSEND_cls = type(
f"FXSend{remote.kind.id_}", f"FXSend{remote.kind}",
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {} f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in (Config, Mix, Group) for _cls in (Config, Mix, Group)
} }
@ -78,5 +68,5 @@ class FXReturn(IFX):
@type.setter @type.setter
def type(self, val: int): def type(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("type is an integer parameter") raise XAirRemoteError("type is an integer parameter")
self.setter("type", val) self.setter("type", val)

View File

@ -15,7 +15,13 @@ class X32KindMap:
@dataclass @dataclass
class MR18KindMap: class KindMap:
def __str__(self) -> str:
return self.id_.capitalize()
@dataclass
class MR18KindMap(KindMap):
# note ch 17-18 defined as aux rtn # note ch 17-18 defined as aux rtn
id_: str id_: str
num_dca: int = 4 num_dca: int = 4
@ -26,7 +32,7 @@ class MR18KindMap:
@dataclass @dataclass
class XR16KindMap: class XR16KindMap(KindMap):
id_: str id_: str
num_dca: int = 4 num_dca: int = 4
num_strip: int = 16 num_strip: int = 16
@ -36,7 +42,7 @@ class XR16KindMap:
@dataclass @dataclass
class XR12KindMap: class XR12KindMap(KindMap):
id_: str id_: str
num_dca: int = 4 num_dca: int = 4
num_strip: int = 12 num_strip: int = 12

View File

@ -1,17 +1,7 @@
import abc import abc
from .errors import MAirRemoteError
from .shared import ( from .errors import XAirRemoteError
Config, from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
Preamp,
Gate,
Dyn,
Insert,
EQ,
GEQ,
Mix,
Group,
Automix,
)
class ILR(abc.ABC): class ILR(abc.ABC):
@ -45,12 +35,12 @@ class LR(ILR):
Returns an LR class of a kind. Returns an LR class of a kind.
""" """
LR_cls = type( LR_cls = type(
f"LR{remote.kind.id_}", f"LR{remote.kind}",
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {} f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote) )(remote)
for _cls in ( for _cls in (
Config, Config,

View File

@ -1,4 +1,4 @@
from .errors import MAirRemoteError from .errors import XAirRemoteError
from .util import lin_get, lin_set from .util import lin_get, lin_set
@ -10,7 +10,7 @@ def bool_prop(param):
def fset(self, val): def fset(self, val):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError(f"{param} is a boolean parameter") raise XAirRemoteError(f"{param} is a boolean parameter")
self.setter(param, 1 if val else 0) self.setter(param, 1 if val else 0)
return property(fget, fset) return property(fget, fset)
@ -24,7 +24,7 @@ def string_prop(param):
def fset(self, val): def fset(self, val):
if not isinstance(val, str): if not isinstance(val, str):
raise MAirRemoteError(f"{param} is a string parameter") raise XAirRemoteError(f"{param} is a string parameter")
self.setter(param, val) self.setter(param, val)
return property(fget, fset) return property(fget, fset)
@ -38,7 +38,7 @@ def int_prop(param):
def fset(self, val): def fset(self, val):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError(f"{param} is an integer parameter") raise XAirRemoteError(f"{param} is an integer parameter")
self.setter(param, val) self.setter(param, val)
return property(fget, fset) return property(fget, fset)
@ -52,7 +52,7 @@ def float_prop(param):
def fset(self, val): def fset(self, val):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError(f"{param} is a float parameter") raise XAirRemoteError(f"{param} is a float parameter")
self.setter(param, val) self.setter(param, val)
return property(fget, fset) return property(fget, fset)

View File

@ -1,18 +1,8 @@
import abc import abc
from typing import Optional from typing import Optional
from .errors import MAirRemoteError
from .shared import ( from .errors import XAirRemoteError
Config, from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
Preamp,
Gate,
Dyn,
Insert,
EQ,
GEQ,
Mix,
Group,
Automix,
)
class IRtn(abc.ABC): class IRtn(abc.ABC):
@ -48,12 +38,12 @@ class Aux(IRtn):
Returns an Aux class of a kind. Returns an Aux class of a kind.
""" """
AUX_cls = type( AUX_cls = type(
f"Aux{remote.kind.id_}", f"Aux{remote.kind}",
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {} f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote) )(remote)
for _cls in ( for _cls in (
Config, Config,

View File

@ -1,8 +1,8 @@
from typing import Union from typing import Union
from .errors import MAirRemoteError from .errors import XAirRemoteError
from .util import lin_get, lin_set, log_get, log_set, _get_fader_val, _set_fader_val
from .meta import geq_prop from .meta import geq_prop
from .util import _get_fader_val, _set_fader_val, lin_get, lin_set, log_get, log_set
""" """
Classes shared by /ch, /rtn, /rt/aux, /bus, /fxsend, /lr Classes shared by /ch, /rtn, /rt/aux, /bus, /fxsend, /lr
@ -22,7 +22,7 @@ class Config:
@name.setter @name.setter
def name(self, val: str): def name(self, val: str):
if not isinstance(val, str): if not isinstance(val, str):
raise MAirRemoteError("name is a string parameter") raise XAirRemoteError("name is a string parameter")
self.setter("name", val) self.setter("name", val)
@property @property
@ -32,7 +32,7 @@ class Config:
@color.setter @color.setter
def color(self, val: int): def color(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("color is an int parameter") raise XAirRemoteError("color is an int parameter")
self.setter("color", val) self.setter("color", val)
@property @property
@ -42,7 +42,7 @@ class Config:
@inputsource.setter @inputsource.setter
def inputsource(self, val: int): def inputsource(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("inputsource is an int parameter") raise XAirRemoteError("inputsource is an int parameter")
self.setter("insrc", val) self.setter("insrc", val)
@property @property
@ -52,7 +52,7 @@ class Config:
@usbreturn.setter @usbreturn.setter
def usbreturn(self, val: int): def usbreturn(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("usbreturn is an int parameter") raise XAirRemoteError("usbreturn is an int parameter")
self.setter("rtnsrc", val) self.setter("rtnsrc", val)
@ -69,7 +69,7 @@ class Preamp:
@usbtrim.setter @usbtrim.setter
def usbtrim(self, val: float): def usbtrim(self, val: float):
if not isinstance(val, float): if not isinstance(val, float):
raise MAirRemoteError( raise XAirRemoteError(
"usbtrim is a float parameter, expected value in range -18 to 18" "usbtrim is a float parameter, expected value in range -18 to 18"
) )
self.setter("rtntrim", lin_set(-18, 18, val)) self.setter("rtntrim", lin_set(-18, 18, val))
@ -81,7 +81,7 @@ class Preamp:
@usbinput.setter @usbinput.setter
def usbinput(self, val: bool): def usbinput(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("rtnsw is a bool parameter") raise XAirRemoteError("rtnsw is a bool parameter")
self.setter("rtnsw", 1 if val else 0) self.setter("rtnsw", 1 if val else 0)
@property @property
@ -91,7 +91,7 @@ class Preamp:
@invert.setter @invert.setter
def invert(self, val: bool): def invert(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("invert is a bool parameter") raise XAirRemoteError("invert is a bool parameter")
self.setter("invert", 1 if val else 0) self.setter("invert", 1 if val else 0)
@property @property
@ -101,7 +101,7 @@ class Preamp:
@highpasson.setter @highpasson.setter
def highpasson(self, val: bool): def highpasson(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("hpon is a bool parameter") raise XAirRemoteError("hpon is a bool parameter")
self.setter("hpon", 1 if val else 0) self.setter("hpon", 1 if val else 0)
@property @property
@ -111,7 +111,7 @@ class Preamp:
@highpassfilter.setter @highpassfilter.setter
def highpassfilter(self, val: int): def highpassfilter(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("highpassfilter is an int parameter") raise XAirRemoteError("highpassfilter is an int parameter")
self.setter("hpf", log_set(20, 400, val)) self.setter("hpf", log_set(20, 400, val))
@ -128,7 +128,7 @@ class Gate:
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("on is a boolean parameter") raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@ -140,7 +140,7 @@ class Gate:
def mode(self, val: str): def mode(self, val: str):
opts = ("gate", "exp2", "exp3", "exp4", "duck") opts = ("gate", "exp2", "exp3", "exp4", "duck")
if not isinstance(val, str) and val not in opts: if not isinstance(val, str) and val not in opts:
raise MAirRemoteError(f"mode is a string parameter, expected one of {opts}") raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}")
self.setter("mode", opts.index(val)) self.setter("mode", opts.index(val))
@property @property
@ -150,7 +150,7 @@ class Gate:
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
if not isinstance(val, float): if not isinstance(val, float):
raise MAirRemoteError( raise XAirRemoteError(
"threshold is a float parameter, expected value in range -80 to 0" "threshold is a float parameter, expected value in range -80 to 0"
) )
self.setter("thr", lin_set(-80, 0, val)) self.setter("thr", lin_set(-80, 0, val))
@ -162,7 +162,7 @@ class Gate:
@range.setter @range.setter
def range(self, val: int): def range(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"range is an int parameter, expected value in range 3 to 60" "range is an int parameter, expected value in range 3 to 60"
) )
self.setter("range", lin_set(3, 60, val)) self.setter("range", lin_set(3, 60, val))
@ -174,7 +174,7 @@ class Gate:
@attack.setter @attack.setter
def attack(self, val: int): def attack(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"attack is an int parameter, expected value in range 0 to 120" "attack is an int parameter, expected value in range 0 to 120"
) )
self.setter("attack", lin_set(0, 120, val)) self.setter("attack", lin_set(0, 120, val))
@ -195,7 +195,7 @@ class Gate:
@release.setter @release.setter
def release(self, val: int): def release(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"release is an int parameter, expected value in range 5 to 4000" "release is an int parameter, expected value in range 5 to 4000"
) )
self.setter("release", log_set(5, 4000, val)) self.setter("release", log_set(5, 4000, val))
@ -207,7 +207,7 @@ class Gate:
@keysource.setter @keysource.setter
def keysource(self, val): def keysource(self, val):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("keysource is an int parameter") raise XAirRemoteError("keysource is an int parameter")
self.setter("keysrc", val) self.setter("keysrc", val)
@property @property
@ -217,7 +217,7 @@ class Gate:
@filteron.setter @filteron.setter
def filteron(self, val: bool): def filteron(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("filteron is a boolean parameter") raise XAirRemoteError("filteron is a boolean parameter")
self.setter("filter/on", 1 if val else 0) self.setter("filter/on", 1 if val else 0)
@property @property
@ -227,7 +227,7 @@ class Gate:
@filtertype.setter @filtertype.setter
def filtertype(self, val: int): def filtertype(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("filtertype is an int parameter") raise XAirRemoteError("filtertype is an int parameter")
self.setter("filter/type", val) self.setter("filter/type", val)
@property @property
@ -253,7 +253,7 @@ class Dyn:
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("on is a boolean parameter") raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@ -265,7 +265,7 @@ class Dyn:
def mode(self, val: str): def mode(self, val: str):
opts = ("comp", "exp") opts = ("comp", "exp")
if not isinstance(val, str) and val not in opts: if not isinstance(val, str) and val not in opts:
raise MAirRemoteError(f"mode is a string parameter, expected one of {opts}") raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}")
self.setter("mode", opts.index(val)) self.setter("mode", opts.index(val))
@property @property
@ -277,7 +277,7 @@ class Dyn:
def det(self, val: str): def det(self, val: str):
opts = ("peak", "rms") opts = ("peak", "rms")
if not isinstance(val, str) and val not in opts: if not isinstance(val, str) and val not in opts:
raise MAirRemoteError(f"det is a string parameter, expected one of {opts}") raise XAirRemoteError(f"det is a string parameter, expected one of {opts}")
self.setter("det", opts.index(val)) self.setter("det", opts.index(val))
@property @property
@ -289,7 +289,7 @@ class Dyn:
def env(self, val: str): def env(self, val: str):
opts = ("lin", "log") opts = ("lin", "log")
if not isinstance(val, str) and val not in opts: if not isinstance(val, str) and val not in opts:
raise MAirRemoteError(f"env is a string parameter, expected one of {opts}") raise XAirRemoteError(f"env is a string parameter, expected one of {opts}")
self.setter("env", opts.index(val)) self.setter("env", opts.index(val))
@property @property
@ -299,7 +299,7 @@ class Dyn:
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
if not isinstance(val, float): if not isinstance(val, float):
raise MAirRemoteError( raise XAirRemoteError(
"threshold is a float parameter, expected value in range -80 to 0" "threshold is a float parameter, expected value in range -80 to 0"
) )
self.setter("thr", lin_set(-60, 0, val)) self.setter("thr", lin_set(-60, 0, val))
@ -312,7 +312,7 @@ class Dyn:
@ratio.setter @ratio.setter
def ratio(self, val: int): def ratio(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("ratio is an int parameter") raise XAirRemoteError("ratio is an int parameter")
self.setter("ratio", val) self.setter("ratio", val)
@property @property
@ -322,7 +322,7 @@ class Dyn:
@knee.setter @knee.setter
def knee(self, val: int): def knee(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"knee is an int parameter, expected value in range 0 to 5" "knee is an int parameter, expected value in range 0 to 5"
) )
self.setter("knee", lin_set(0, 5, val)) self.setter("knee", lin_set(0, 5, val))
@ -359,7 +359,7 @@ class Dyn:
@release.setter @release.setter
def release(self, val: int): def release(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"release is an int parameter, expected value in range 5 to 4000" "release is an int parameter, expected value in range 5 to 4000"
) )
self.setter("release", log_set(5, 4000, val)) self.setter("release", log_set(5, 4000, val))
@ -371,7 +371,7 @@ class Dyn:
@mix.setter @mix.setter
def mix(self, val: int): def mix(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError( raise XAirRemoteError(
"mix is an int parameter, expected value in range 0 to 5" "mix is an int parameter, expected value in range 0 to 5"
) )
self.setter("mix", lin_set(0, 100, val)) self.setter("mix", lin_set(0, 100, val))
@ -383,7 +383,7 @@ class Dyn:
@keysource.setter @keysource.setter
def keysource(self, val): def keysource(self, val):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("keysource is an int parameter") raise XAirRemoteError("keysource is an int parameter")
self.setter("keysrc", val) self.setter("keysrc", val)
@property @property
@ -393,7 +393,7 @@ class Dyn:
@auto.setter @auto.setter
def auto(self, val: bool): def auto(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("auto is a boolean parameter") raise XAirRemoteError("auto is a boolean parameter")
self.setter("auto", 1 if val else 0) self.setter("auto", 1 if val else 0)
@property @property
@ -403,7 +403,7 @@ class Dyn:
@filteron.setter @filteron.setter
def filteron(self, val: bool): def filteron(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("filteron is a boolean parameter") raise XAirRemoteError("filteron is a boolean parameter")
self.setter("filter/on", 1 if val else 0) self.setter("filter/on", 1 if val else 0)
@property @property
@ -413,7 +413,7 @@ class Dyn:
@filtertype.setter @filtertype.setter
def filtertype(self, val: int): def filtertype(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("filtertype is an int parameter") raise XAirRemoteError("filtertype is an int parameter")
self.setter("filter/type", val) self.setter("filter/type", val)
@property @property
@ -439,7 +439,7 @@ class Insert:
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("on is a boolean parameter") raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@ -449,7 +449,7 @@ class Insert:
@sel.setter @sel.setter
def sel(self, val: int): def sel(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("sel is an int parameter") raise XAirRemoteError("sel is an int parameter")
self.setter("sel", val) self.setter("sel", val)
@ -496,7 +496,7 @@ class EQ:
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("on is a boolean parameter") raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@ -508,7 +508,7 @@ class EQ:
def mode(self, val: str): def mode(self, val: str):
opts = ("peq", "geq", "teq") opts = ("peq", "geq", "teq")
if not isinstance(val, str) and val not in opts: if not isinstance(val, str) and val not in opts:
raise MAirRemoteError(f"mode is a string parameter, expected one of {opts}") raise XAirRemoteError(f"mode is a string parameter, expected one of {opts}")
self.setter("mode", opts.index(val)) self.setter("mode", opts.index(val))
class EQBand: class EQBand:
@ -531,7 +531,7 @@ class EQ:
@type.setter @type.setter
def type(self, val: int): def type(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("type is an int parameter") raise XAirRemoteError("type is an int parameter")
self.setter(f"type", val) self.setter(f"type", val)
@property @property
@ -601,7 +601,7 @@ class Mix:
@on.setter @on.setter
def on(self, val: bool): def on(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("on is a boolean parameter") raise XAirRemoteError("on is a boolean parameter")
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@ -620,7 +620,7 @@ class Mix:
@lr.setter @lr.setter
def lr(self, val: bool): def lr(self, val: bool):
if not isinstance(val, bool): if not isinstance(val, bool):
raise MAirRemoteError("lr is a boolean parameter") raise XAirRemoteError("lr is a boolean parameter")
self.setter("lr", 1 if val else 0) self.setter("lr", 1 if val else 0)
@ -637,7 +637,7 @@ class Group:
@dca.setter @dca.setter
def dca(self, val: int): def dca(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("dca is an int parameter") raise XAirRemoteError("dca is an int parameter")
self.setter("dca", val) self.setter("dca", val)
@property @property
@ -647,7 +647,7 @@ class Group:
@mute.setter @mute.setter
def mute(self, val: int): def mute(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("mute is an int parameter") raise XAirRemoteError("mute is an int parameter")
self.setter("mute", val) self.setter("mute", val)
@ -664,7 +664,7 @@ class Automix:
@group.setter @group.setter
def group(self, val: int): def group(self, val: int):
if not isinstance(val, int): if not isinstance(val, int):
raise MAirRemoteError("group is an int parameter") raise XAirRemoteError("group is an int parameter")
self.setter("group", val) self.setter("group", val)
@property @property
@ -674,7 +674,7 @@ class Automix:
@weight.setter @weight.setter
def weight(self, val: float): def weight(self, val: float):
if not isinstance(val, float): if not isinstance(val, float):
raise MAirRemoteError( raise XAirRemoteError(
"weight is a float parameter, expected value in range -12 to 12" "weight is a float parameter, expected value in range -12 to 12"
) )
self.setter("weight", lin_set(-12, 12, val)) self.setter("weight", lin_set(-12, 12, val))

View File

@ -1,17 +1,7 @@
import abc import abc
from .errors import MAirRemoteError
from .shared import ( from .errors import XAirRemoteError
Config, from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
Preamp,
Gate,
Dyn,
Insert,
EQ,
GEQ,
Mix,
Group,
Automix,
)
class IStrip(abc.ABC): class IStrip(abc.ABC):
@ -46,12 +36,12 @@ class Strip(IStrip):
Returns a Strip class of a kind. Returns a Strip class of a kind.
""" """
STRIP_cls = type( STRIP_cls = type(
f"Strip{remote.kind.id_}", f"Strip{remote.kind}",
(cls,), (cls,),
{ {
**{ **{
_cls.__name__.lower(): type( _cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {} f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
)(remote, index) )(remote, index)
for _cls in ( for _cls in (
Config, Config,

View File

@ -1,4 +1,4 @@
from math import log, exp from math import exp, log
def lin_get(min, max, val): def lin_get(min, max, val):

View File

@ -1,29 +1,33 @@
import abc import abc
import time
import threading import threading
from pythonosc.dispatcher import Dispatcher import time
from pythonosc.osc_server import BlockingOSCUDPServer
from pythonosc.osc_message_builder import OscMessageBuilder
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from typing import Union from typing import Optional, Self
import tomllib
from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_message_builder import OscMessageBuilder
from pythonosc.osc_server import BlockingOSCUDPServer
from . import kinds from . import kinds
from .lr import LR
from .strip import Strip
from .bus import Bus from .bus import Bus
from .dca import DCA
from .fx import FXSend, FXReturn
from .config import Config from .config import Config
from .dca import DCA
from .errors import XAirRemoteError
from .fx import FXReturn, FXSend
from .kinds import KindMap
from .lr import LR
from .rtn import Aux, Rtn from .rtn import Aux, Rtn
from .strip import Strip
class OSCClientServer(BlockingOSCUDPServer): class OSCClientServer(BlockingOSCUDPServer):
def __init__(self, address, dispatcher): def __init__(self, address: str, dispatcher: Dispatcher):
super().__init__(("", 0), dispatcher) super().__init__(("", 0), dispatcher)
self.xr_address = address self.xr_address = address
def send_message(self, address, value): def send_message(self, address: str, value: str):
builder = OscMessageBuilder(address=address) builder = OscMessageBuilder(address=address)
if value is None: if value is None:
values = list() values = list()
@ -37,10 +41,8 @@ class OSCClientServer(BlockingOSCUDPServer):
self.socket.sendto(msg.dgram, self.xr_address) self.socket.sendto(msg.dgram, self.xr_address)
class MAirRemote(abc.ABC): class XAirRemote(abc.ABC):
""" """Handles the communication with the M-Air mixer via the OSC protocol"""
Handles the communication with the M-Air mixer via the OSC protocol
"""
_CONNECT_TIMEOUT = 0.5 _CONNECT_TIMEOUT = 0.5
_WAIT_TIME = 0.025 _WAIT_TIME = 0.025
@ -53,23 +55,23 @@ class MAirRemote(abc.ABC):
def __init__(self, **kwargs): def __init__(self, **kwargs):
dispatcher = Dispatcher() dispatcher = Dispatcher()
dispatcher.set_default_handler(self.msg_handler) dispatcher.set_default_handler(self.msg_handler)
self.xair_ip = kwargs["ip"] or self._ip_from_ini() self.xair_ip = kwargs["ip"] or self._ip_from_toml()
self.xair_port = kwargs["port"] or self.XAIR_PORT 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.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher) self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
def __enter__(self): def __enter__(self) -> Self:
self.worker = threading.Thread(target=self.run_server) self.worker = threading.Thread(target=self.run_server, daemon=True)
self.worker.daemon = True
self.worker.start() self.worker.start()
self.validate_connection() self.validate_connection()
return self return self
def _ip_from_ini(self): def _ip_from_toml(self) -> str:
ini_path = Path.cwd() / "config.ini" filepath = Path.cwd() / "config.toml"
parser = ConfigParser() with open(filepath, "rb") as f:
if not parser.read(ini_path): conn = tomllib.load(f)
print("Could not read config file") return conn["connection"].get("ip")
return parser["connection"].get("ip")
def validate_connection(self): def validate_connection(self):
self.send("/xinfo") self.send("/xinfo")
@ -87,7 +89,7 @@ class MAirRemote(abc.ABC):
def msg_handler(self, addr, *data): def msg_handler(self, addr, *data):
self.info_response = data[:] self.info_response = data[:]
def send(self, address, param=None): def send(self, address: str, param: Optional[str] = None):
self.server.send_message(address, param) self.server.send_message(address, param)
time.sleep(self._WAIT_TIME) time.sleep(self._WAIT_TIME)
@ -100,7 +102,7 @@ class MAirRemote(abc.ABC):
self.server.shutdown() self.server.shutdown()
def _make_remote(kind: kinds.MR18KindMap) -> MAirRemote: def _make_remote(kind: KindMap) -> XAirRemote:
""" """
Creates a new MAIR remote class. Creates a new MAIR remote class.
@ -110,7 +112,7 @@ def _make_remote(kind: kinds.MR18KindMap) -> MAirRemote:
def init(self, *args, **kwargs): def init(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": None} defaultkwargs = {"ip": None, "port": None}
kwargs = defaultkwargs | kwargs kwargs = defaultkwargs | kwargs
MAirRemote.__init__(self, *args, **kwargs) XAirRemote.__init__(self, *args, **kwargs)
self.kind = kind self.kind = kind
self.lr = LR.make(self) self.lr = LR.make(self)
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip)) self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
@ -123,8 +125,8 @@ def _make_remote(kind: kinds.MR18KindMap) -> MAirRemote:
self.rtn = tuple(Rtn.make(self, i) for i in range(kind.num_rtn)) self.rtn = tuple(Rtn.make(self, i) for i in range(kind.num_rtn))
return type( return type(
f"MAirRemote{kind.id_}", f"MAirRemote{kind}",
(MAirRemote,), (XAirRemote,),
{ {
"__init__": init, "__init__": init,
}, },
@ -134,6 +136,15 @@ def _make_remote(kind: kinds.MR18KindMap) -> MAirRemote:
_remotes = {kind.id_: _make_remote(kind) for kind in kinds.all} _remotes = {kind.id_: _make_remote(kind) for kind in kinds.all}
def connect(kind_id: str, *args, **kwargs): def request_remote_obj(kind_id: str, *args, **kwargs) -> XAirRemote:
"""
Interface entry point. Wraps factory expression and handles errors
Returns a reference to an XAirRemote class of a kind
"""
MAIRREMOTE_cls = None
try:
MAIRREMOTE_cls = _remotes[kind_id] MAIRREMOTE_cls = _remotes[kind_id]
except ValueError as e:
raise SystemExit(e)
return MAIRREMOTE_cls(*args, **kwargs) return MAIRREMOTE_cls(*args, **kwargs)