24 Commits

Author SHA1 Message Date
6a2df6352d add tox tests 2024-02-15 23:54:31 +00:00
9c1fa36aed upd pytest dep version 2024-02-15 18:47:11 +00:00
3a70a4c578 upd docs 2024-02-15 18:35:34 +00:00
8b1b2c7f79 log value given as well as value expected 2024-02-15 16:58:18 +00:00
1e5e458105 log OOB as warnings
patch bump

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

minor version bump

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

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

removed unused imports

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

addresses #4
2024-02-08 13:43:15 +00:00
22 changed files with 625 additions and 386 deletions

View File

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

188
README.md
View File

@@ -12,7 +12,7 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Prerequisites
- Python 3.10 or greater
- Python 3.10 or greater
## Installation
@@ -59,19 +59,19 @@ if __name__ == "__main__":
Currently the following devices are supported:
- `MR18`
- `XR18`
- `XR16`
- `XR12`
- `MR18`
- `XR18`
- `XR16`
- `XR12`
The `X32` is partially supported. However, this document covers specifically the `XAir` series.
The following keyword arguments may be passed:
- `ip`: ip address of the mixer
- `port`: mixer port, defaults to 10023 for x32 and 10024 for xair
- `delay`: a delay between each command, defaults to 20ms.
- a note about delay, stability may rely on network connection. For wired connections the delay can be safely reduced.
- `ip`: ip address of the mixer
- `port`: mixer port, defaults to 10023 for x32 and 10024 for xair
- `delay`: a delay between each command (applies to the getters). Defaults to 20ms.
- a note about delay, stability may rely on network connection. For wired connections the delay can be safely reduced.
## API
@@ -121,7 +121,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 +136,12 @@ 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`)
### `Subclasses`
@@ -149,134 +149,134 @@ For each subclass the corresponding properties are available.
`Config`
- `name`: string
- `color`: int, from 0, 16
- `inputsource`: int
- `usbreturn`: int
- `name`: string
- `color`: int, from 0, 16
- `inputsource`: int
- `usbreturn`: int
`Preamp`
- `on`: bool
- `usbtrim`: float, from -18.0 to 18.0
- `usbinput`: bool
- `invert`: bool
- `highpasson`: bool
- `highpassfilter`: int, from 20 to 400
- `on`: bool
- `usbtrim`: float, from -18.0 to 18.0
- `usbinput`: bool
- `invert`: bool
- `highpasson`: bool
- `highpassfilter`: int, from 20 to 400
`Gate`
- `on`: bool
- `mode`: str, one of ('gate', 'exp2', 'exp3', 'exp4', 'duck')
- `threshold`: float, from -80.0 to 0.0
- `range`: int, from 3 to 60
- `attack`: int, from 0 to 120
- `hold`: float, from 0.02 to 2000
- `release`: int, from 5 to 4000
- `keysource`, from 0 to 22
- `filteron`: bool
- `filtertype`: int, from 0 to 8
- `filterfreq`: float, from 20 to 20000
- `on`: bool
- `mode`: str, one of ('gate', 'exp2', 'exp3', 'exp4', 'duck')
- `threshold`: float, from -80.0 to 0.0
- `range`: int, from 3 to 60
- `attack`: int, from 0 to 120
- `hold`: float, from 0.02 to 2000
- `release`: int, from 5 to 4000
- `keysource`, from 0 to 22
- `filteron`: bool
- `filtertype`: int, from 0 to 8
- `filterfreq`: float, from 20 to 20000
`Dyn`
- `on`: bool
- `mode`: str, one of ('comp', 'exp')
- `det`: str, one of ('peak', 'rms')
- `env`: str, one of ('lin', 'log')
- `threshold`: float, from -60.0 to 0.0
- `ratio`: int, from 0 to 11
- `knee`: int, from 0 to 5
- `mgain`: float, from 0.0 to 24.0
- `attack`: int, from 0 to 120
- `hold`: float, from 0.02 to 2000
- `release`: int, from 5 to 4000
- `mix`: int, from 0 to 100
- `keysource`: int, from 0 to 22
- `auto`: bool
- `filteron`: bool
- `filtertype`: int, from 0 to 8
- `filterfreq`: float, from 20 to 20000
- `on`: bool
- `mode`: str, one of ('comp', 'exp')
- `det`: str, one of ('peak', 'rms')
- `env`: str, one of ('lin', 'log')
- `threshold`: float, from -60.0 to 0.0
- `ratio`: int, from 0 to 11
- `knee`: int, from 0 to 5
- `mgain`: float, from 0.0 to 24.0
- `attack`: int, from 0 to 120
- `hold`: float, from 0.02 to 2000
- `release`: int, from 5 to 4000
- `mix`: int, from 0 to 100
- `keysource`: int, from 0 to 22
- `auto`: bool
- `filteron`: bool
- `filtertype`: int, from 0 to 8
- `filterfreq`: float, from 20 to 20000
`Insert`
- `on`: bool
- `sel`: int
- `on`: bool
- `sel`: int
`GEQ`
The following method names preceded by `slider_`
- `20`, `25`, `31_5`, `40`, `50`, `63`, `80`, `100`, `125`, `160`,
- `200`, `250`, `315`, `400`, `500`, `630`, `800`, `1k`, `1k25`, `1k6`, `2k`,
- `2k5`, `3k15`, `4k`, `5k`, `6k3`, `8k`, `10k`, `12k5`, `16k`, `20k`: float, from -15.0 to 15.0
- `20`, `25`, `31_5`, `40`, `50`, `63`, `80`, `100`, `125`, `160`,
- `200`, `250`, `315`, `400`, `500`, `630`, `800`, `1k`, `1k25`, `1k6`, `2k`,
- `2k5`, `3k15`, `4k`, `5k`, `6k3`, `8k`, `10k`, `12k5`, `16k`, `20k`: float, from -15.0 to 15.0
for example: `slider_20`, `slider_6k3` etc..
`EQ`
- `on`: bool
- `mode`: str, one of ('peq', 'geq', 'teq')
- `on`: bool
- `mode`: str, one of ('peq', 'geq', 'teq')
For the subclasses: `low`, `low2`, `lomid`, `himid`, `high2`, `high` the following properties are available:
- `type`: int, from 0 to 5
- `frequency`: float, from 20.0 to 20000.0
- `gain`: float, -15.0 to 15.0
- `quality`: float, from 0.3 to 10.0
- `type`: int, from 0 to 5
- `frequency`: float, from 20.0 to 20000.0
- `gain`: float, -15.0 to 15.0
- `quality`: float, from 0.3 to 10.0
for example: `eq.low2.type`
`Mix`
- `on`: bool
- `fader`: float, -inf, to 10.0
- `lr`: bool
- `on`: bool
- `fader`: float, -inf, to 10.0
- `lr`: bool
`Group`
- `dca`: int, from 0 to 15
- `mute`: int, from 0 to 15
- `dca`: int, from 0 to 15
- `mute`: int, from 0 to 15
`Automix`
- `group`: int, from 0 to 2
- `weight`: float, from -12.0 to 12.0
- `group`: int, from 0 to 2
- `weight`: float, from -12.0 to 12.0
### `DCA`
- `on`: bool
- `name`: str
- `color`: int, from 0 to 15
- `on`: bool
- `name`: str
- `color`: int, from 0 to 15
### `Config`
The following method names preceded by `chlink`
- `1_2`, `3_4`, `5_6`, `7_8`, `9_10`, `11_12`, `13_14`, `15_16`
- `1_2`, `3_4`, `5_6`, `7_8`, `9_10`, `11_12`, `13_14`, `15_16`
The following method names preceded by `buslink`
- `1_2`, `3_4`, `5_6`
- `1_2`, `3_4`, `5_6`
for example: `chlink1_2`, `buslink5_6` etc..
- `link_eq`: bool
- `link_dyn`: bool
- `link_fader_mute`: bool
- `amixenable`: bool
- `amixlock`: bool
- `link_eq`: bool
- `link_dyn`: bool
- `link_fader_mute`: bool
- `amixenable`: bool
- `amixlock`: bool
For the subclass `monitor` the following properties are available
- `level`: float, -inf to 10.0
- `source`: int, from 0 to 14
- `sourcetrim`: float, from -18.0 to 18.0
- `chmode`: bool
- `busmode`: bool
- `dim`: bool
- `dimgain`: float, from -40.0 to 0.0
- `mono`: bool
- `mute`: bool
- `dimfpl`: bool
- `level`: float, -inf to 10.0
- `source`: int, from 0 to 14
- `sourcetrim`: float, from -18.0 to 18.0
- `chmode`: bool
- `busmode`: bool
- `dim`: bool
- `dimgain`: float, from -40.0 to 0.0
- `mono`: bool
- `mute`: bool
- `dimfpl`: bool
for example: `config.monitor.chmode`
@@ -284,15 +284,21 @@ for example: `config.monitor.chmode`
tuple containing a class for each mute group
- `on`: bool, from 0 to 3
- `on`: bool, from 0 to 3
for example: `config.mute_group[0].on = True`
### `Send`
- `level`: float, -inf to 10.0
for example: `mixer.strip[10].send[3].level = -16.5`
### XAirRemote class (lower level)
Send an OSC command directly to the mixer
- `send(osc command, value)`
- `send(osc command, value)`
for example:
@@ -303,7 +309,7 @@ mixer.send("/bus/2/config/name", "somename")
Query the value of a command:
- `query(osc command)`
- `query(osc command)`
for example:

View File

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

View File

@@ -1,12 +1,19 @@
import obsws_python as obs
import xair_api
class Observer:
def __init__(self, mixer):
self._mixer = mixer
self._cl = obs.EventClient()
self._cl.callback.register(self.on_current_program_scene_changed)
self._client = obs.EventClient()
self._client.callback.register(self.on_current_program_scene_changed)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self._client.disconnect()
def on_current_program_scene_changed(self, data):
scene = data.scene_name
@@ -28,11 +35,9 @@ class Observer:
def main():
with xair_api.connect("MR18", ip="mixer.local") as mixer:
Observer(mixer)
while cmd := input("<Enter> to exit\n"):
if not cmd:
break
with Observer(mixer):
while _ := input("Press <Enter> to exit\n"):
pass
if __name__ == "__main__":

211
poetry.lock generated
View File

@@ -1,22 +1,5 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
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"
@@ -62,6 +45,28 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cachetools"
version = "5.3.2"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
files = [
{file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
{file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"},
]
[[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 = "click"
version = "8.1.3"
@@ -78,15 +83,56 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[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.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[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.13.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
@@ -128,18 +174,15 @@ files = [
[[package]]
name = "packaging"
version = "21.3"
version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pathspec"
version = "0.10.1"
@@ -153,28 +196,28 @@ files = [
[[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.2.0"
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.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
]
[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 (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
[[package]]
name = "pluggy"
version = "1.0.0"
version = "1.4.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.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
]
[package.extras]
@@ -182,52 +225,45 @@ 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 = "pyproject-api"
version = "1.6.1"
description = "API to interact with the python pyproject.toml based projects"
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 = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"},
{file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"},
]
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false
python-versions = ">=3.6.8"
files = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
[package.dependencies]
packaging = ">=23.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"]
[[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"
@@ -265,7 +301,54 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "tox"
version = "4.12.1"
description = "tox is a generic virtualenv management and test command line tool"
optional = false
python-versions = ">=3.8"
files = [
{file = "tox-4.12.1-py3-none-any.whl", hash = "sha256:c07ea797880a44f3c4f200ad88ad92b446b83079d4ccef89585df64cc574375c"},
{file = "tox-4.12.1.tar.gz", hash = "sha256:61aafbeff1bd8a5af84e54ef6e8402f53c6a6066d0782336171ddfbf5362122e"},
]
[package.dependencies]
cachetools = ">=5.3.2"
chardet = ">=5.2"
colorama = ">=0.4.6"
filelock = ">=3.13.1"
packaging = ">=23.2"
platformdirs = ">=4.1"
pluggy = ">=1.3"
pyproject-api = ">=1.6.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
virtualenv = ">=20.25"
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"]
testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=8.0.2)", "distlib (>=0.3.8)", "flaky (>=3.7)", "hatch-vcs (>=0.4)", "hatchling (>=1.21)", "psutil (>=5.9.7)", "pytest (>=7.4.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-xdist (>=3.5)", "re-assert (>=1.1)", "time-machine (>=2.13)", "wheel (>=0.42)"]
[[package]]
name = "virtualenv"
version = "20.25.0"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
{file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
]
[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)", "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)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "def96d1658f870a9820fef363ee6a04455f1d895e15a189ea4f39801f168552f"
content-hash = "43084819c12a97bdb6adbd7fd0b077292e8c4e78e4921fd8c0f14dd192a334ff"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "xair-api"
version = "2.2.3"
version = "2.3.1"
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"
tox = "^4.12.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -24,5 +25,19 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
obs = "scripts:ex_obs"
sends = "scripts:ex_sends"
xair = "scripts:test_xair"
x32 = "scripts:test_x32"
all = "scripts:test_all"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py310,py311,py312
[testenv]
allowlist_externals = poetry
commands =
poetry install -v
poetry run pytest tests/
"""

View File

@@ -1,10 +1,16 @@
import subprocess
import sys
from pathlib import Path
def ex_obs():
path = Path.cwd() / "examples" / "xair_obs" / "."
subprocess.run(["py", str(path)])
subprocess.run([sys.executable, str(path)])
def ex_sends():
path = Path.cwd() / "examples" / "sends" / "."
subprocess.run([sys.executable, str(path)])
def test_xair():
@@ -15,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

@@ -25,13 +25,13 @@ 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):

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,9 +1,10 @@
import abc
import logging
from . import kinds
from .errors import XAirRemoteError
from . import kinds, util
from .meta import bool_prop
from .util import _get_level_val, _set_level_val, lin_get, lin_set
logger = logging.getLogger(__name__)
class IConfig(abc.ABC):
@@ -11,6 +12,7 @@ class IConfig(abc.ABC):
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}")
@@ -34,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),
@@ -48,7 +50,7 @@ class Config(IConfig):
@property
def address(self) -> str:
return f"/config"
return "/config"
@property
def amixenable(self) -> bool:
@@ -91,13 +93,14 @@ class Config(IConfig):
return f"{root}/solo"
@property
@util.db_from
def level(self) -> float:
retval = self.getter("level")[0]
return _get_level_val(retval)
return self.getter("level")[0]
@level.setter
@util.db_to
def level(self, val: float):
_set_level_val(self, val)
self.setter("level", val)
@property
def source(self) -> int:
@@ -105,17 +108,19 @@ class Config(IConfig):
@source.setter
def source(self, val: int):
self.setter(f"source", val)
self.setter("source", val)
@property
def sourcetrim(self) -> float:
return round(lin_get(-18, 18, self.getter("sourcetrim")[0]), 1)
return round(util.lin_get(-18, 18, self.getter("sourcetrim")[0]), 1)
@sourcetrim.setter
def sourcetrim(self, val: float):
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
self.setter("sourcetrim", lin_set(-18, 18, val))
self.logger.warning(
f"sourcetrim got {val}, expected value in range -18.0 to 18.0"
)
self.setter("sourcetrim", util.lin_set(-18, 18, val))
@property
def chmode(self) -> bool:
@@ -135,13 +140,15 @@ class Config(IConfig):
@property
def dimgain(self) -> int:
return int(lin_get(-40, 0, self.getter("dimatt")[0]))
return int(util.lin_get(-40, 0, self.getter("dimatt")[0]))
@dimgain.setter
def dimgain(self, val: int):
if not -40 <= val <= 0:
raise XAirRemoteError("expected value in range -40 to 0")
self.setter("dimatt", lin_set(-40, 0, val))
self.logger.warning(
f"dimgain got {val}, expected value in range -40 to 0"
)
self.setter("dimatt", util.lin_set(-40, 0, val))
@property
def dim(self) -> bool:

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,4 +1,2 @@
class XAirRemoteError(Exception):
"""Base error class for XAIR Remote."""
pass

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}")

View File

@@ -1,18 +1,21 @@
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):
"""Abstract Base Class for buses"""
"""Abstract Base Class for lr"""
def __init__(self, remote, index: Optional[int] = None):
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}")
@@ -26,7 +29,7 @@ class ILR(abc.ABC):
class LR(ILR):
"""Concrete class for buses"""
"""Concrete class for lr"""
@classmethod
def make(cls, remote, index=None):
@@ -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,18 +1,21 @@
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, Config, Group, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IRtn(abc.ABC):
"""Abstract Base Class for aux"""
"""Abstract Base Class for rtn"""
def __init__(self, remote, index: Optional[int] = None):
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}")
@@ -26,13 +29,15 @@ class IRtn(abc.ABC):
class AuxRtn(IRtn):
"""Concrete class for aux"""
"""Concrete class for auxrtn"""
@classmethod
def make(cls, remote, index=None):
"""
Factory function for auxrtn
Creates a mixin of shared subclasses, sets them as class attributes.
Returns an AuxRtn class of a kind.
"""
AUXRTN_cls = type(
@@ -51,6 +56,10 @@ class AuxRtn(IRtn):
Group,
)
},
"send": tuple(
Send.make(cls, i, remote)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(),
},
)
@@ -62,13 +71,15 @@ class AuxRtn(IRtn):
class FxRtn(IRtn):
"""Concrete class for rtn"""
"""Concrete class for fxrtn"""
@classmethod
def make(cls, remote, index):
"""
Factory function for fxrtn
Creates a mixin of shared subclasses, sets them as class attributes.
Returns an FxRtn class of a kind.
"""
FXRTN_cls = type(
@@ -87,6 +98,10 @@ class FxRtn(IRtn):
Group,
)
},
"send": tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(),
},
)

View File

@@ -1,8 +1,7 @@
from typing import Union
from typing import Optional, Union
from .errors import XAirRemoteError
from . import util
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, /rtn/aux, /bus, /fxsend, /lr
@@ -56,13 +55,15 @@ class Preamp:
@property
def usbtrim(self) -> float:
return round(lin_get(-18, 18, self.getter("rtntrim")[0]), 1)
return round(util.lin_get(-18, 18, self.getter("rtntrim")[0]), 1)
@usbtrim.setter
def usbtrim(self, val: float):
if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0")
self.setter("rtntrim", lin_set(-18, 18, val))
self.logger.warning(
f"usbtrim got {val}, expected value in range -18.0 to 18.0"
)
self.setter("rtntrim", util.lin_set(-18, 18, val))
@property
def usbinput(self) -> bool:
@@ -90,13 +91,15 @@ class Preamp:
@property
def highpassfilter(self) -> int:
return int(log_get(20, 400, self.getter("hpf")[0]))
return int(util.log_get(20, 400, self.getter("hpf")[0]))
@highpassfilter.setter
def highpassfilter(self, val: int):
if not 20 <= val <= 400:
raise XAirRemoteError("expected value in range 20 to 400")
self.setter("hpf", log_set(20, 400, val))
self.logger.warning(
f"highpassfilter got {val}, expected value in range 20 to 400"
)
self.setter("hpf", util.log_set(20, 400, val))
class Gate:
@@ -122,59 +125,63 @@ 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
def threshold(self) -> float:
return round(lin_get(-80, 0, self.getter("thr")[0]), 1)
return round(util.lin_get(-80, 0, self.getter("thr")[0]), 1)
@threshold.setter
def threshold(self, val: float):
if not -80 <= val <= 0:
raise XAirRemoteError("expected value in range -80.0 to 0.0")
self.setter("thr", lin_set(-80, 0, val))
self.logger.warning(
f"threshold got {val}, expected value in range -80.0 to 0.0"
)
self.setter("thr", util.lin_set(-80, 0, val))
@property
def range(self) -> int:
return int(lin_get(3, 60, self.getter("range")[0]))
return int(util.lin_get(3, 60, self.getter("range")[0]))
@range.setter
def range(self, val: int):
if not 3 <= val <= 60:
raise XAirRemoteError("expected value in range 3 to 60")
self.setter("range", lin_set(3, 60, val))
self.logger.warning(f"range got {val}, expected value in range 3 to 60")
self.setter("range", util.lin_set(3, 60, val))
@property
def attack(self) -> int:
return int(lin_get(0, 120, self.getter("attack")[0]))
return int(util.lin_get(0, 120, self.getter("attack")[0]))
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.setter("attack", lin_set(0, 120, val))
self.logger.warning(f"attack got {val}, expected value in range 0 to 120")
self.setter("attack", util.lin_set(0, 120, val))
@property
def hold(self) -> Union[float, int]:
val = log_get(0.02, 2000, self.getter("hold")[0])
val = util.log_get(0.02, 2000, self.getter("hold")[0])
return round(val, 1) if val < 100 else int(val)
@hold.setter
def hold(self, val: float):
if not 0.02 <= val <= 2000:
raise XAirRemoteError("expected value in range 0.02 to 2000.0")
self.setter("hold", log_set(0.02, 2000, val))
self.logger.warning(
f"hold got {val}, expected value in range 0.02 to 2000.0"
)
self.setter("hold", util.log_set(0.02, 2000, val))
@property
def release(self) -> int:
return int(log_get(5, 4000, self.getter("release")[0]))
return int(util.log_get(5, 4000, self.getter("release")[0]))
@release.setter
def release(self, val: int):
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.setter("release", log_set(5, 4000, val))
self.logger.warning(f"release got {val}, expected value in range 5 to 4000")
self.setter("release", util.log_set(5, 4000, val))
@property
def keysource(self):
@@ -202,14 +209,16 @@ class Gate:
@property
def filterfreq(self) -> Union[float, int]:
retval = log_get(20, 20000, self.getter("filter/f")[0])
retval = util.log_get(20, 20000, self.getter("filter/f")[0])
return int(retval) if retval > 1000 else round(retval, 1)
@filterfreq.setter
def filterfreq(self, val: Union[float, int]):
if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20 to 20000")
self.setter("filter/f", log_set(20, 20000, val))
self.logger.warning(
f"filterfreq got {val}, expected value in range 20 to 20000"
)
self.setter("filter/f", util.log_set(20, 20000, val))
class Dyn:
@@ -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,18 +268,20 @@ 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
def threshold(self) -> float:
return round(lin_get(-60, 0, self.getter("thr")[0]), 1)
return round(util.lin_get(-60, 0, self.getter("thr")[0]), 1)
@threshold.setter
def threshold(self, val: float):
if not -60 <= val <= 0:
raise XAirRemoteError("expected value in range -60.0 to 0")
self.setter("thr", lin_set(-60, 0, val))
self.logger.warning(
f"threshold got {val}, expected value in range -60.0 to 0"
)
self.setter("thr", util.lin_set(-60, 0, val))
@property
def ratio(self) -> Union[float, int]:
@@ -283,64 +294,66 @@ class Dyn:
@property
def knee(self) -> int:
return int(lin_get(0, 5, self.getter("knee")[0]))
return int(util.lin_get(0, 5, self.getter("knee")[0]))
@knee.setter
def knee(self, val: int):
if not 0 <= val <= 5:
raise XAirRemoteError("expected value in range 0 to 5")
self.setter("knee", lin_set(0, 5, val))
self.logger.warning(f"knee got {val}, expected value in range 0 to 5")
self.setter("knee", util.lin_set(0, 5, val))
@property
def mgain(self) -> float:
return round(lin_get(0, 24, self.getter("mgain")[0]), 1)
return round(util.lin_get(0, 24, self.getter("mgain")[0]), 1)
@mgain.setter
def mgain(self, val: float):
if not 0 <= val <= 24:
raise XAirRemoteError("expected value in range 0.0 to 24.0")
self.setter("mgain", lin_set(0, 24, val))
self.logger.warning(f"mgain got {val}, expected value in range 0.0 to 24.0")
self.setter("mgain", util.lin_set(0, 24, val))
@property
def attack(self) -> int:
return int(lin_get(0, 120, self.getter("attack")[0]))
return int(util.lin_get(0, 120, self.getter("attack")[0]))
@attack.setter
def attack(self, val: int):
if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120")
self.setter("attack", lin_set(0, 120, val))
self.logger.warning(f"attack got {val}, expected value in range 0 to 120")
self.setter("attack", util.lin_set(0, 120, val))
@property
def hold(self) -> Union[float, int]:
val = log_get(0.02, 2000, self.getter("hold")[0])
val = util.log_get(0.02, 2000, self.getter("hold")[0])
return round(val, 1) if val < 100 else int(val)
@hold.setter
def hold(self, val: float):
if not 0.02 <= val <= 2000:
raise XAirRemoteError("expected value in range 0.02 to 2000.0")
self.setter("hold", log_set(0.02, 2000, val))
self.logger.warning(
f"hold got {val}, expected value in range 0.02 to 2000.0"
)
self.setter("hold", util.log_set(0.02, 2000, val))
@property
def release(self) -> int:
return int(log_get(5, 4000, self.getter("release")[0]))
return int(util.log_get(5, 4000, self.getter("release")[0]))
@release.setter
def release(self, val: int):
if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000")
self.setter("release", log_set(5, 4000, val))
self.logger.warning(f"release got {val}, expected value in range 5 to 4000")
self.setter("release", util.log_set(5, 4000, val))
@property
def mix(self) -> int:
return int(lin_get(0, 100, self.getter("mix")[0]))
return int(util.lin_get(0, 100, self.getter("mix")[0]))
@mix.setter
def mix(self, val: int):
if not 0 <= val <= 100:
raise XAirRemoteError("expected value in range 0 to 100")
self.setter("mix", lin_set(0, 100, val))
self.logger.warning(f"mix got {val}, expected value in range 0 to 100")
self.setter("mix", util.lin_set(0, 100, val))
@property
def keysource(self):
@@ -376,14 +389,16 @@ class Dyn:
@property
def filterfreq(self) -> Union[float, int]:
retval = log_get(20, 20000, self.getter("filter/f")[0])
retval = util.log_get(20, 20000, self.getter("filter/f")[0])
return int(retval) if retval > 1000 else round(retval, 1)
@filterfreq.setter
def filterfreq(self, val: Union[float, int]):
if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20 to 20000")
self.setter("filter/f", log_set(20, 20000, val))
self.logger.warning(
f"filterfreq got {val}, expected value in range 20 to 20000"
)
self.setter("filter/f", util.log_set(20, 20000, val))
class Insert:
@@ -462,15 +477,12 @@ 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:
def __init__(self, i, remote, index):
if index is None:
super(EQ.EQBand, self).__init__(remote)
else:
super(EQ.EQBand, self).__init__(remote, index)
super(EQ.EQBand, self).__init__(remote, index)
self.i = i
@property
@@ -484,39 +496,45 @@ class EQ:
@type.setter
def type(self, val: int):
self.setter(f"type", val)
self.setter("type", val)
@property
def frequency(self) -> float:
retval = log_get(20, 20000, self.getter("f")[0])
retval = util.log_get(20, 20000, self.getter("f")[0])
return round(retval, 1)
@frequency.setter
def frequency(self, val: float):
if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20.0 to 20000.0")
self.setter("f", log_set(20, 20000, val))
self.logger.warning(
f"frequency got {val}, expected value in range 20.0 to 20000.0"
)
self.setter("f", util.log_set(20, 20000, val))
@property
def gain(self) -> float:
return round(lin_get(-15, 15, self.getter("g")[0]), 1)
return round(util.lin_get(-15, 15, self.getter("g")[0]), 1)
@gain.setter
def gain(self, val: float):
if not -15 <= val <= 15:
raise XAirRemoteError("expected value in range -15.0 to 15.0")
self.setter("g", lin_set(-15, 15, val))
self.logger.warning(
f"gain got {val}, expected value in range -15.0 to 15.0"
)
self.setter("g", util.lin_set(-15, 15, val))
@property
def quality(self) -> float:
retval = log_get(0.3, 10, self.getter("q")[0])
retval = util.log_get(0.3, 10, self.getter("q")[0])
return round(retval, 1)
@quality.setter
def quality(self, val: float):
if not 0.3 <= val <= 10:
raise XAirRemoteError("expected value in range 0.3 to 10.0")
self.setter("q", log_set(0.3, 10, val))
self.logger.warning(
f"quality got {val}, expected value in range 0.3 to 10.0"
)
self.setter("q", util.log_set(0.3, 10, val))
class GEQ:
@@ -531,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",
]
@@ -561,13 +579,14 @@ class Mix:
self.setter("on", 1 if val else 0)
@property
@util.db_from
def fader(self) -> float:
retval = self.getter("fader")[0]
return _get_fader_val(retval)
return self.getter("fader")[0]
@fader.setter
@util.db_to
def fader(self, val: float):
_set_fader_val(self, val)
self.setter("fader", val)
@property
def lr(self) -> bool:
@@ -617,10 +636,38 @@ class Automix:
@property
def weight(self) -> float:
return round(lin_get(-12, 12, self.getter("weight")[0]), 1)
return round(util.lin_get(-12, 12, self.getter("weight")[0]), 1)
@weight.setter
def weight(self, val: float):
if not -12 <= val <= 12:
raise XAirRemoteError("expected value in range -12.0 to 12.0")
self.setter("weight", lin_set(-12, 12, val))
self.logger.warning(
f"weight got {val}, expected value in range -12.0 to 12.0"
)
self.setter("weight", util.lin_set(-12, 12, val))
class Send:
def __init__(self, i, remote, index: Optional[int] = None):
super(Send, self).__init__(remote, index)
self.i = i + 1
@classmethod
def make(cls, _cls, i, remote, index=None):
SEND_cls = type("Send", (cls, _cls), {})
return SEND_cls(i, remote, index)
@property
def address(self) -> str:
root = super(Send, self).address
return f"{root}/mix/{str(self.i).zfill(2)}"
@property
@util.db_from
def level(self) -> float:
return self.getter("level")[0]
@level.setter
@util.db_to
def level(self, val: float):
self.setter("level", val)

View File

@@ -1,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, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IStrip(abc.ABC):
@@ -11,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}")
@@ -56,6 +59,10 @@ class Strip(IStrip):
Automix,
)
},
"send": tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(),
},
)

View File

@@ -1,3 +1,4 @@
import functools
from math import exp, log
@@ -17,61 +18,48 @@ def log_set(min, max, val):
return log(val / min) / log(max / min)
def _get_fader_val(retval):
if retval >= 1:
return 10
elif retval >= 0.5:
return round((40 * retval) - 30, 1)
elif retval >= 0.25:
return round((80 * retval) - 50, 1)
elif retval >= 0.0625:
return round((160 * retval) - 70, 1)
elif retval >= 0:
return round((480 * retval) - 90, 1)
else:
return -90
def db_from(func):
"""fader|level converter for getters"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
retval = func(*args, **kwargs)
if retval >= 1:
return 10
elif retval >= 0.5:
return round((40 * retval) - 30, 1)
elif retval >= 0.25:
return round((80 * retval) - 50, 1)
elif retval >= 0.0625:
return round((160 * retval) - 70, 1)
elif retval >= 0:
return round((480 * retval) - 90, 1)
else:
return -90
return wrapper
def _set_fader_val(self, val):
if val >= 10:
self.setter("fader", 1)
elif val >= -10:
self.setter("fader", (val + 30) / 40)
elif val >= -30:
self.setter("fader", (val + 50) / 80)
elif val >= -60:
self.setter("fader", (val + 70) / 160)
elif val >= -90:
self.setter("fader", (val + 90) / 480)
else:
self.setter("fader", 0)
def db_to(func):
"""fader|level converter for setters"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
param, val = args
if val >= 10:
val = 1
elif val >= -10:
val = (val + 30) / 40
elif val >= -30:
val = (val + 50) / 80
elif val >= -60:
val = (val + 70) / 160
elif val >= -90:
val = (val + 90) / 480
else:
val = 0
def _get_level_val(retval):
if retval >= 1:
return 10
elif retval >= 0.5:
return round((40 * retval) - 30, 1)
elif retval >= 0.25:
return round((80 * retval) - 50, 1)
elif retval >= 0.0625:
return round((160 * retval) - 70, 1)
elif retval >= 0:
return round((480 * retval) - 90, 1)
else:
return -90
func(param, val, **kwargs)
def _set_level_val(self, val):
if val >= 10:
self.setter("level", 1)
elif val >= -10:
self.setter("level", (val + 30) / 40)
elif val >= -30:
self.setter("level", (val + 50) / 80)
elif val >= -60:
self.setter("level", (val + 70) / 160)
elif val >= -90:
self.setter("level", (val + 90) / 480)
else:
self.setter("level", 0)
return wrapper

View File

@@ -25,6 +25,8 @@ 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,8 +47,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 = []
@@ -57,6 +57,7 @@ class XAirRemote(abc.ABC):
self.xair_ip = kwargs["ip"] or self._ip_from_toml()
self.xair_port = kwargs["port"]
self._delay = kwargs["delay"]
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)
@@ -80,7 +81,7 @@ class XAirRemote(abc.ABC):
raise XAirRemoteError(
"Failed to setup OSC connection to mixer. Please check for correct ip address."
)
print(
self.logger.info(
f"Successfully connected to {self.info_response[2]} at {self.info_response[0]}."
)
@@ -174,9 +175,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)