mirror of
https://github.com/onyx-and-iris/xair-api-python.git
synced 2024-11-15 17:40:57 +00:00
initial commit
initial commit
This commit is contained in:
commit
bf9b72f31f
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
bin/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Rope
|
||||
.ropeproject
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
*.pot
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# config files
|
||||
*.ini
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Peter Dikant
|
||||
Copyright (c) 2022 Onyx and Iris
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
262
README.md
Normal file
262
README.md
Normal file
@ -0,0 +1,262 @@
|
||||
[![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)
|
||||
# Mair Remote
|
||||
|
||||
This package offers a python interface to the Midas MR18 digital rack mixer.
|
||||
All testing was done using a Midas MR18 but I've been informed that the software for XR18 is identical.
|
||||
Midas are not affiliated with/nor do they support this package in any way.
|
||||
|
||||
## Prerequisites
|
||||
- Python 3.9+
|
||||
|
||||
## Installation
|
||||
```
|
||||
git clone https://github.com/onyx-and-iris/mair-api-python
|
||||
cd mair-api-python
|
||||
```
|
||||
|
||||
Just the interface:
|
||||
```
|
||||
pip install .
|
||||
```
|
||||
|
||||
With development dependencies:
|
||||
```
|
||||
pip install -e .['development']
|
||||
```
|
||||
|
||||
## Usage
|
||||
### 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:
|
||||
```
|
||||
[connection]
|
||||
ip=<ip address>
|
||||
```
|
||||
Alternatively you may state it explicitly as an argument to mair.connect()
|
||||
|
||||
### Example 1
|
||||
```python
|
||||
import mair
|
||||
|
||||
def main():
|
||||
with mair.connect(kind_id, ip=ip) as mixer:
|
||||
mixer.strip[8].config.name = 'sm7b'
|
||||
mixer.strip[8].config.on = True
|
||||
print(f'strip 09 ({mixer.strip[8].config.name}) has been set to {mixer.strip[8].config.on}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
kind_id = 'MR18'
|
||||
ip = '<ip address>'
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
## API
|
||||
Currently the following devices are support:
|
||||
- `XR18`
|
||||
- `MR18`
|
||||
|
||||
However, this interface can be expanded upon to support other devices.
|
||||
|
||||
### MAirRemote (higher level)
|
||||
`mixer.lr`
|
||||
|
||||
A class representing Main LR channel
|
||||
|
||||
`mixer.strip`
|
||||
|
||||
A Strip tuple containing a class for each input strip channel
|
||||
|
||||
`mixer.bus`
|
||||
|
||||
A Bus tuple containing a class for each output bus channel
|
||||
|
||||
`mixer.dca`
|
||||
|
||||
A DCA tuple containing a class for each DCA group
|
||||
|
||||
`mixer.fxsend`
|
||||
|
||||
An FXSend tuple containing a class for each FX Send channel
|
||||
|
||||
`mixer.fxreturn`
|
||||
|
||||
An FXReturn tuple containing a class for each FX Return channel
|
||||
|
||||
`mixer.aux`
|
||||
|
||||
A class representing aux channel
|
||||
|
||||
`mixer.rtn`
|
||||
|
||||
An RTN tuple containing a class for each rtn channel
|
||||
|
||||
`mixer.config`
|
||||
|
||||
A class representing the main config settings
|
||||
|
||||
|
||||
### `LR`
|
||||
Contains the subclasses:
|
||||
(`Config`, `Dyn`, `Insert`, `EQ`, `Mix`)
|
||||
|
||||
### `Strip`
|
||||
Contains the subclasses:
|
||||
(`Config`, `Preamp`, `Gate`, `Dyn`, `Insert`, `GEQ`, `EQ`, `Mix`, `Group`, `Automix`)
|
||||
|
||||
### `Bus`
|
||||
Contains the subclasses:
|
||||
(`Config`, `Dyn`, `Insert`, `EQ`, `Mix`, `Group`)
|
||||
|
||||
### `FXSend`
|
||||
Contains the subclasses:
|
||||
(`Config`, `Mix`, `Group`)
|
||||
|
||||
### `Aux`
|
||||
Contains the subclasses:
|
||||
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`)
|
||||
|
||||
### `Rtn`
|
||||
Contains the subclasses:
|
||||
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`)
|
||||
|
||||
|
||||
### `Subclasses`
|
||||
For each subclass the corresponding properties are available.
|
||||
|
||||
`Config`
|
||||
- `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
|
||||
|
||||
`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
|
||||
|
||||
`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
|
||||
|
||||
`Insert`
|
||||
- `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
|
||||
|
||||
for example: `slider_20`, `slider_6k3` etc..
|
||||
|
||||
`EQ`
|
||||
- `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
|
||||
|
||||
for example: `eq.low2.type`
|
||||
|
||||
`Mix`
|
||||
- `on`: bool
|
||||
- `fader`: float, -inf, to 10.0
|
||||
- `lr`: bool
|
||||
|
||||
`Group`
|
||||
- `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
|
||||
|
||||
|
||||
### `DCA`
|
||||
- `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`
|
||||
|
||||
The following method names preceded by `buslink`
|
||||
- `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
|
||||
- `mute_group`: bool
|
||||
|
||||
For the subclass `monitor` the following properties are available
|
||||
- `level`: float, -inf to 10.0
|
||||
- `source`: int, from 0 to 14
|
||||
- `chmode` bool
|
||||
- `busmode` bool
|
||||
- `dim` bool
|
||||
- `mono` bool
|
||||
- `mute` bool
|
||||
- `dimfpl` bool
|
||||
|
||||
for example: `config.monitor.chmode`
|
||||
|
||||
|
||||
### `Tests`
|
||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/mair-api-python#installation)
|
||||
|
||||
To run the tests from tests directory:
|
||||
|
||||
WARNING: First save your settings and make sure your equipment is safe from damage.
|
||||
Run tests at your own risk.
|
||||
|
||||
`nosetests --r test -v`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
|
||||
|
||||
## Special Thanks
|
||||
|
||||
Peter Dikant for writing the base class
|
14
__main__.py
Normal file
14
__main__.py
Normal file
@ -0,0 +1,14 @@
|
||||
import mair
|
||||
from time import sleep
|
||||
|
||||
def main():
|
||||
with mair.connect(kind_id, ip=ip) as mixer:
|
||||
mixer.strip[8].config.name = 'sm7b'
|
||||
mixer.strip[8].config.on = True
|
||||
print(f'strip 09 ({mixer.strip[8].config.name}) has been set to {mixer.strip[8].config.on}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
kind_id = 'MR18'
|
||||
ip = '<ip address>'
|
||||
|
||||
main()
|
3
mair/__init__.py
Normal file
3
mair/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .mair import connect
|
||||
|
||||
_ALL__ = ['connect']
|
72
mair/bus.py
Normal file
72
mair/bus.py
Normal file
@ -0,0 +1,72 @@
|
||||
import abc
|
||||
from .errors import MAirRemoteError
|
||||
from .shared import (
|
||||
Config,
|
||||
Preamp,
|
||||
Gate,
|
||||
Dyn,
|
||||
Insert,
|
||||
EQ,
|
||||
GEQ,
|
||||
Mix,
|
||||
Group,
|
||||
Automix,
|
||||
)
|
||||
|
||||
|
||||
class IBus(abc.ABC):
|
||||
"""Abstract Base Class for buses"""
|
||||
|
||||
def __init__(self, remote, index: int):
|
||||
self._remote = remote
|
||||
self.index = index + 1
|
||||
|
||||
def getter(self, param: str):
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class Bus(IBus):
|
||||
"""Concrete class for buses"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
"""
|
||||
Factory function for buses
|
||||
|
||||
Creates a mixin of shared subclasses, sets them as class attributes.
|
||||
|
||||
Returns a Bus class of a kind.
|
||||
"""
|
||||
BUS_cls = type(
|
||||
f"Bus{remote.kind.id_}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
_cls.__name__.lower(): type(
|
||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
||||
)(remote, index)
|
||||
for _cls in (
|
||||
Config,
|
||||
Dyn,
|
||||
Insert,
|
||||
GEQ.make(),
|
||||
EQ.make_sixband(cls, remote, index),
|
||||
Mix,
|
||||
Group,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
return BUS_cls(remote, index)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/bus/{self.index}"
|
210
mair/config.py
Normal file
210
mair/config.py
Normal file
@ -0,0 +1,210 @@
|
||||
import abc
|
||||
from .errors import MAirRemoteError
|
||||
from . import kinds
|
||||
from .meta import bool_prop
|
||||
from .util import _get_level_val, _set_level_val, lin_get, lin_set
|
||||
|
||||
|
||||
class IConfig(abc.ABC):
|
||||
"""Abstract Base Class for config"""
|
||||
|
||||
def __init__(self, remote):
|
||||
self._remote = remote
|
||||
|
||||
def getter(self, param: str):
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class Config(IConfig):
|
||||
"""Concrete class for config"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote):
|
||||
"""
|
||||
Factory function for Config
|
||||
|
||||
Returns a Config class of a kind.
|
||||
"""
|
||||
LINKS_cls = _make_links_mixins[remote.kind.id_]
|
||||
MONITOR_cls = type(f"ConfigMonitor", (Config.Monitor, cls), {})
|
||||
CONFIG_cls = type(
|
||||
f"Config{remote.kind.id_}",
|
||||
(cls, LINKS_cls),
|
||||
{"monitor": MONITOR_cls(remote)},
|
||||
)
|
||||
return CONFIG_cls(remote)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/config"
|
||||
|
||||
@property
|
||||
def amixenable(self) -> bool:
|
||||
return self.getter("mute")[0] == 1
|
||||
|
||||
@amixenable.setter
|
||||
def amixenable(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("amixenable is a bool parameter")
|
||||
self.setter("amixenable", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def amixlock(self) -> bool:
|
||||
return self.getter("amixlock")[0] == 1
|
||||
|
||||
@amixlock.setter
|
||||
def amixlock(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("amixlock is a bool parameter")
|
||||
self.setter("amixlock", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mute_group(self) -> bool:
|
||||
return self.getter("mute")[0] == 1
|
||||
|
||||
@mute_group.setter
|
||||
def mute_group(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("mute_group is a bool parameter")
|
||||
self.setter("mute", 1 if val else 0)
|
||||
|
||||
class Monitor:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Config.Monitor, self).address
|
||||
return f"{root}/solo"
|
||||
|
||||
@property
|
||||
def level(self) -> float:
|
||||
retval = self.getter("level")[0]
|
||||
return _get_level_val(retval)
|
||||
|
||||
@level.setter
|
||||
def level(self, val: float):
|
||||
_set_level_val(self, val)
|
||||
|
||||
@property
|
||||
def source(self) -> int:
|
||||
return int(self.getter("source")[0])
|
||||
|
||||
@source.setter
|
||||
def source(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("source is an int parameter")
|
||||
self.setter(f"source", val)
|
||||
|
||||
@property
|
||||
def sourcetrim(self) -> float:
|
||||
return round(lin_get(-18, 18, self.getter("sourcetrim")[0]), 1)
|
||||
|
||||
@sourcetrim.setter
|
||||
def sourcetrim(self, val: float):
|
||||
if not isinstance(val, float):
|
||||
raise MAirRemoteError(
|
||||
"sourcetrim is a float parameter, expected value in range -18 to 18"
|
||||
)
|
||||
self.setter("sourcetrim", lin_set(-18, 18, val))
|
||||
|
||||
@property
|
||||
def chmode(self) -> bool:
|
||||
return self.getter("chmode")[0] == 1
|
||||
|
||||
@chmode.setter
|
||||
def chmode(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("chmode is a bool parameter")
|
||||
self.setter("chmode", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def busmode(self) -> bool:
|
||||
return self.getter("busmode")[0] == 1
|
||||
|
||||
@busmode.setter
|
||||
def busmode(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("busmode is a bool parameter")
|
||||
self.setter("busmode", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def dimgain(self) -> int:
|
||||
return int(lin_get(-40, 0, self.getter("dimatt")[0]))
|
||||
|
||||
@dimgain.setter
|
||||
def dimgain(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"dimgain is an int parameter, expected value in range -40 to 0"
|
||||
)
|
||||
self.setter("dimatt", lin_set(-40, 0, val))
|
||||
|
||||
@property
|
||||
def dim(self) -> bool:
|
||||
return self.getter("dim")[0] == 1
|
||||
|
||||
@dim.setter
|
||||
def dim(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("dim is a bool parameter")
|
||||
self.setter("dim", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mono(self) -> bool:
|
||||
return self.getter("mono")[0] == 1
|
||||
|
||||
@mono.setter
|
||||
def mono(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("mono is a bool parameter")
|
||||
self.setter("mono", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mute(self) -> bool:
|
||||
return self.getter("mute")[0] == 1
|
||||
|
||||
@mute.setter
|
||||
def mute(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("mute is a bool parameter")
|
||||
self.setter("mute", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def dimfpl(self) -> bool:
|
||||
return self.getter("dimfpl")[0] == 1
|
||||
|
||||
@dimfpl.setter
|
||||
def dimfpl(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("dimfpl is a bool parameter")
|
||||
self.setter("dimfpl", 1 if val else 0)
|
||||
|
||||
|
||||
def _make_links_mixin(kind):
|
||||
"""Creates a links mixin"""
|
||||
return type(
|
||||
f"Links{kind.id_}",
|
||||
(),
|
||||
{
|
||||
"link_eq": bool_prop("linkcfg/eq"),
|
||||
"link_dyn": bool_prop("linkcfg/dyn"),
|
||||
"link_fader_mute": bool_prop("linkcfg/fdrmute"),
|
||||
**{
|
||||
f"chlink{i}_{i+1}": bool_prop(f"chlink/{i}-{i+1}")
|
||||
for i in range(1, kind.num_strip, 2)
|
||||
},
|
||||
**{
|
||||
f"buslink{i}_{i+1}": bool_prop(f"buslink/{i}-{i+1}")
|
||||
for i in range(1, kind.num_bus, 2)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
_make_links_mixins = {kind.id_: _make_links_mixin(kind) for kind in kinds.all}
|
59
mair/dca.py
Normal file
59
mair/dca.py
Normal file
@ -0,0 +1,59 @@
|
||||
import abc
|
||||
from .errors import MAirRemoteError
|
||||
|
||||
|
||||
class IDCA(abc.ABC):
|
||||
"""Abstract Base Class for DCA groups"""
|
||||
|
||||
def __init__(self, remote, index: int):
|
||||
self._remote = remote
|
||||
self.index = index + 1
|
||||
|
||||
def getter(self, param: str) -> tuple:
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class DCA(IDCA):
|
||||
"""Concrete class for DCA groups"""
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/dca/{self.index}"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on")[0] == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("on is a boolean parameter")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.getter("config/name")[0]
|
||||
|
||||
@name.setter
|
||||
def name(self, val: str):
|
||||
if not isinstance(val, str):
|
||||
raise MAirRemoteError("name is a str parameter")
|
||||
self.setter("config/name")[0]
|
||||
|
||||
@property
|
||||
def color(self) -> int:
|
||||
return self.getter("config/color")[0]
|
||||
|
||||
@color.setter
|
||||
def color(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("color is an int parameter")
|
||||
self.setter("config/color", val)
|
4
mair/errors.py
Normal file
4
mair/errors.py
Normal file
@ -0,0 +1,4 @@
|
||||
class MAirRemoteError(Exception):
|
||||
"""Base error class for MAIR Remote."""
|
||||
|
||||
pass
|
82
mair/fx.py
Normal file
82
mair/fx.py
Normal file
@ -0,0 +1,82 @@
|
||||
import abc
|
||||
from .errors import MAirRemoteError
|
||||
from .shared import (
|
||||
Config,
|
||||
Preamp,
|
||||
Gate,
|
||||
Dyn,
|
||||
Insert,
|
||||
EQ,
|
||||
GEQ,
|
||||
Mix,
|
||||
Group,
|
||||
Automix,
|
||||
)
|
||||
|
||||
|
||||
class IFX(abc.ABC):
|
||||
"""Abstract Base Class for fxs"""
|
||||
|
||||
def __init__(self, remote, index: int):
|
||||
self._remote = remote
|
||||
self.index = index + 1
|
||||
|
||||
def getter(self, param: str):
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class FXSend(IFX):
|
||||
"""Concrete class for fxsend"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
"""
|
||||
Factory function for FXSend
|
||||
|
||||
Creates a mixin of shared subclasses, sets them as class attributes.
|
||||
|
||||
Returns an FXSend class of a kind.
|
||||
"""
|
||||
FXSEND_cls = type(
|
||||
f"FXSend{remote.kind.id_}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
_cls.__name__.lower(): type(
|
||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
||||
)(remote, index)
|
||||
for _cls in (Config, Mix, Group)
|
||||
}
|
||||
},
|
||||
)
|
||||
return FXSEND_cls(remote, index)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/fxsend/{self.index}"
|
||||
|
||||
|
||||
class FXReturn(IFX):
|
||||
"""Concrete class for fxreturn"""
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/fx/{self.index}"
|
||||
|
||||
@property
|
||||
def type(self) -> int:
|
||||
return self.getter("type")[0]
|
||||
|
||||
@type.setter
|
||||
def type(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("type is an integer parameter")
|
||||
self.setter("type", val)
|
19
mair/kinds.py
Normal file
19
mair/kinds.py
Normal file
@ -0,0 +1,19 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MR18KindMap:
|
||||
id_: str = "MR18"
|
||||
num_dca: int = 4
|
||||
num_strip: int = 16
|
||||
num_bus: int = 6
|
||||
num_fx: int = 4
|
||||
num_rtn: int = 4
|
||||
|
||||
|
||||
_kinds = {
|
||||
"XR18": MR18KindMap(),
|
||||
"MR18": MR18KindMap(),
|
||||
}
|
||||
|
||||
all = list(kind for kind in _kinds.values())
|
70
mair/lr.py
Normal file
70
mair/lr.py
Normal file
@ -0,0 +1,70 @@
|
||||
import abc
|
||||
from .errors import MAirRemoteError
|
||||
from .shared import (
|
||||
Config,
|
||||
Preamp,
|
||||
Gate,
|
||||
Dyn,
|
||||
Insert,
|
||||
EQ,
|
||||
GEQ,
|
||||
Mix,
|
||||
Group,
|
||||
Automix,
|
||||
)
|
||||
|
||||
|
||||
class ILR(abc.ABC):
|
||||
"""Abstract Base Class for buses"""
|
||||
|
||||
def __init__(self, remote):
|
||||
self._remote = remote
|
||||
|
||||
def getter(self, param: str):
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class LR(ILR):
|
||||
"""Concrete class for buses"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote):
|
||||
"""
|
||||
Factory function for LR
|
||||
|
||||
Creates a mixin of shared subclasses, sets them as class attributes.
|
||||
|
||||
Returns an LR class of a kind.
|
||||
"""
|
||||
LR_cls = type(
|
||||
f"LR{remote.kind.id_}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
_cls.__name__.lower(): type(
|
||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
||||
)(remote)
|
||||
for _cls in (
|
||||
Config,
|
||||
Dyn,
|
||||
Insert,
|
||||
GEQ.make(),
|
||||
EQ.make_sixband(cls, remote),
|
||||
Mix,
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
return LR_cls(remote)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/lr"
|
138
mair/mair.py
Normal file
138
mair/mair.py
Normal file
@ -0,0 +1,138 @@
|
||||
import abc
|
||||
import time
|
||||
import threading
|
||||
from pythonosc.dispatcher import Dispatcher
|
||||
from pythonosc.osc_server import BlockingOSCUDPServer
|
||||
from pythonosc.osc_message_builder import OscMessageBuilder
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from . import kinds
|
||||
from .lr import LR
|
||||
from .strip import Strip
|
||||
from .bus import Bus
|
||||
from .dca import DCA
|
||||
from .fx import FXSend, FXReturn
|
||||
from .config import Config
|
||||
from .rtn import Aux, Rtn
|
||||
|
||||
|
||||
class OSCClientServer(BlockingOSCUDPServer):
|
||||
def __init__(self, address, dispatcher):
|
||||
super().__init__(("", 0), dispatcher)
|
||||
self.xr_address = address
|
||||
|
||||
def send_message(self, address, value):
|
||||
builder = OscMessageBuilder(address=address)
|
||||
if value is None:
|
||||
values = list()
|
||||
elif isinstance(value, list):
|
||||
values = value
|
||||
else:
|
||||
values = [value]
|
||||
for val in values:
|
||||
builder.add_arg(val)
|
||||
msg = builder.build()
|
||||
self.socket.sendto(msg.dgram, self.xr_address)
|
||||
|
||||
|
||||
class MAirRemote(abc.ABC):
|
||||
"""
|
||||
Handles the communication with the M-Air mixer via the OSC protocol
|
||||
"""
|
||||
|
||||
_CONNECT_TIMEOUT = 0.5
|
||||
_WAIT_TIME = 0.025
|
||||
_REFRESH_TIMEOUT = 5
|
||||
|
||||
XAIR_PORT = 10024
|
||||
|
||||
info_response = []
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
dispatcher = Dispatcher()
|
||||
dispatcher.set_default_handler(self.msg_handler)
|
||||
self.xair_ip = kwargs["ip"] or self._ip_from_ini()
|
||||
self.server = OSCClientServer((self.xair_ip, self.XAIR_PORT), dispatcher)
|
||||
|
||||
def __enter__(self):
|
||||
self.worker = threading.Thread(target=self.run_server)
|
||||
self.worker.daemon = True
|
||||
self.worker.start()
|
||||
self.validate_connection()
|
||||
return self
|
||||
|
||||
def _ip_from_ini(self):
|
||||
ini_path = Path.cwd() / "config.ini"
|
||||
parser = ConfigParser()
|
||||
if not parser.read(ini_path):
|
||||
print("Could not read config file")
|
||||
return parser["connection"].get("ip")
|
||||
|
||||
def validate_connection(self):
|
||||
self.send("/xinfo")
|
||||
time.sleep(self._CONNECT_TIMEOUT)
|
||||
if len(self.info_response) > 0:
|
||||
print(f"Successfully connected to {self.info_response[2]}.")
|
||||
else:
|
||||
print(
|
||||
"Error: Failed to setup OSC connection to mixer. Please check for correct ip address."
|
||||
)
|
||||
|
||||
def run_server(self):
|
||||
self.server.serve_forever()
|
||||
|
||||
def msg_handler(self, addr, *data):
|
||||
self.info_response = data[:]
|
||||
|
||||
def send(self, address, param=None):
|
||||
self.server.send_message(address, param)
|
||||
time.sleep(self._WAIT_TIME)
|
||||
|
||||
def _query(self, address):
|
||||
self.send(address)
|
||||
time.sleep(self._WAIT_TIME)
|
||||
return self.info_response
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_tr):
|
||||
self.server.shutdown()
|
||||
|
||||
|
||||
def _make_remote(kind: kinds.MR18KindMap) -> MAirRemote:
|
||||
"""
|
||||
Creates a new MAIR remote class.
|
||||
|
||||
The returned class will subclass MAirRemote.
|
||||
"""
|
||||
|
||||
def init(self, *args, **kwargs):
|
||||
defaultkwargs = {"ip": None}
|
||||
kwargs = defaultkwargs | kwargs
|
||||
MAirRemote.__init__(self, *args, **kwargs)
|
||||
self.kind = kind
|
||||
self.lr = LR.make(self)
|
||||
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
|
||||
self.bus = tuple(Bus.make(self, i) for i in range(kind.num_bus))
|
||||
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
|
||||
self.fxsend = tuple(FXSend.make(self, i) for i in range(kind.num_fx))
|
||||
self.fxreturn = tuple(FXReturn(self, i) for i in range(kind.num_fx))
|
||||
self.config = Config.make(self)
|
||||
self.aux = Aux.make(self)
|
||||
self.rtn = tuple(Rtn.make(self, i) for i in range(kind.num_rtn))
|
||||
|
||||
return type(
|
||||
f"MAirRemote{kind.id_}",
|
||||
(MAirRemote,),
|
||||
{
|
||||
"__init__": init,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
_remotes = {kind.id_: _make_remote(kind) for kind in kinds.all}
|
||||
|
||||
|
||||
def connect(kind_id: str, *args, **kwargs):
|
||||
MAIRREMOTE_cls = _remotes[kind_id]
|
||||
return MAIRREMOTE_cls(*args, **kwargs)
|
77
mair/meta.py
Normal file
77
mair/meta.py
Normal file
@ -0,0 +1,77 @@
|
||||
from .errors import MAirRemoteError
|
||||
from .util import lin_get, lin_set
|
||||
|
||||
|
||||
def bool_prop(param):
|
||||
"""A boolean property object."""
|
||||
|
||||
def fget(self):
|
||||
return self.getter(param)[0] == 1
|
||||
|
||||
def fset(self, val):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError(f"{param} is a boolean parameter")
|
||||
self.setter(param, 1 if val else 0)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def string_prop(param):
|
||||
"""A string property object"""
|
||||
|
||||
def fget(self):
|
||||
return self.getter(param)[0]
|
||||
|
||||
def fset(self, val):
|
||||
if not isinstance(val, str):
|
||||
raise MAirRemoteError(f"{param} is a string parameter")
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def int_prop(param):
|
||||
"""An integer property object"""
|
||||
|
||||
def fget(self):
|
||||
return int(self.getter(param)[0])
|
||||
|
||||
def fset(self, val):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(f"{param} is an integer parameter")
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def float_prop(param):
|
||||
"""A float property object"""
|
||||
|
||||
def fget(self):
|
||||
return round(self.getter(param)[0], 1)
|
||||
|
||||
def fset(self, val):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(f"{param} is a float parameter")
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
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:
|
||||
return round(lin_get(-15, 15, self.getter(param)[0]), 1)
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, lin_set(-15, 15, val))
|
||||
|
||||
return property(fget, fset)
|
109
mair/rtn.py
Normal file
109
mair/rtn.py
Normal file
@ -0,0 +1,109 @@
|
||||
import abc
|
||||
from typing import Optional
|
||||
from .errors import MAirRemoteError
|
||||
from .shared import (
|
||||
Config,
|
||||
Preamp,
|
||||
Gate,
|
||||
Dyn,
|
||||
Insert,
|
||||
EQ,
|
||||
GEQ,
|
||||
Mix,
|
||||
Group,
|
||||
Automix,
|
||||
)
|
||||
|
||||
|
||||
class IRtn(abc.ABC):
|
||||
"""Abstract Base Class for aux"""
|
||||
|
||||
def __init__(self, remote, index: Optional[int] = None):
|
||||
self._remote = remote
|
||||
if index is not None:
|
||||
self.index = index + 1
|
||||
|
||||
def getter(self, param: str):
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class Aux(IRtn):
|
||||
"""Concrete class for aux"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote):
|
||||
"""
|
||||
Factory function for aux
|
||||
|
||||
Creates a mixin of shared subclasses, sets them as class attributes.
|
||||
|
||||
Returns an Aux class of a kind.
|
||||
"""
|
||||
AUX_cls = type(
|
||||
f"Aux{remote.kind.id_}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
_cls.__name__.lower(): type(
|
||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
||||
)(remote)
|
||||
for _cls in (
|
||||
Config,
|
||||
Preamp,
|
||||
EQ.make_fourband(cls, remote),
|
||||
Mix,
|
||||
Group,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
return AUX_cls(remote)
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return "/rtn/aux"
|
||||
|
||||
|
||||
class Rtn(IRtn):
|
||||
"""Concrete class for rtn"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
"""
|
||||
Factory function for rtn
|
||||
|
||||
Creates a mixin of shared subclasses, sets them as class attributes.
|
||||
|
||||
Returns an Rtn class of a kind.
|
||||
"""
|
||||
RTN_cls = type(
|
||||
f"Rtn{remote.kind.id_}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
_cls.__name__.lower(): type(
|
||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
||||
)(remote, index)
|
||||
for _cls in (
|
||||
Config,
|
||||
Preamp,
|
||||
EQ.make_fourband(cls, remote, index),
|
||||
Mix,
|
||||
Group,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
return RTN_cls(remote, index)
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return f"/rtn/{self.index}"
|
680
mair/shared.py
Normal file
680
mair/shared.py
Normal file
@ -0,0 +1,680 @@
|
||||
from typing import Union
|
||||
|
||||
from .errors import MAirRemoteError
|
||||
from .util import lin_get, lin_set, log_get, log_set, _get_fader_val, _set_fader_val
|
||||
from .meta import geq_prop
|
||||
|
||||
"""
|
||||
Classes shared by /ch, /rtn, /rt/aux, /bus, /fxsend, /lr
|
||||
"""
|
||||
|
||||
|
||||
class Config:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Config, self).address
|
||||
return f"{root}/config"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.getter("name")[0]
|
||||
|
||||
@name.setter
|
||||
def name(self, val: str):
|
||||
if not isinstance(val, str):
|
||||
raise MAirRemoteError("name is a string parameter")
|
||||
self.setter("name", val)
|
||||
|
||||
@property
|
||||
def color(self) -> int:
|
||||
return self.getter("color")[0]
|
||||
|
||||
@color.setter
|
||||
def color(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("color is an int parameter")
|
||||
self.setter("color", val)
|
||||
|
||||
@property
|
||||
def inputsource(self) -> int:
|
||||
return self.getter("insrc")[0]
|
||||
|
||||
@inputsource.setter
|
||||
def inputsource(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("inputsource is an int parameter")
|
||||
self.setter("insrc", val)
|
||||
|
||||
@property
|
||||
def usbreturn(self) -> int:
|
||||
return self.getter("rtnsrc")[0]
|
||||
|
||||
@usbreturn.setter
|
||||
def usbreturn(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("usbreturn is an int parameter")
|
||||
self.setter("rtnsrc", val)
|
||||
|
||||
|
||||
class Preamp:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Preamp, self).address
|
||||
return f"{root}/preamp"
|
||||
|
||||
@property
|
||||
def usbtrim(self) -> float:
|
||||
return round(lin_get(-18, 18, self.getter("rtntrim")[0]), 1)
|
||||
|
||||
@usbtrim.setter
|
||||
def usbtrim(self, val: float):
|
||||
if not isinstance(val, float):
|
||||
raise MAirRemoteError(
|
||||
"usbtrim is a float parameter, expected value in range -18 to 18"
|
||||
)
|
||||
self.setter("rtntrim", lin_set(-18, 18, val))
|
||||
|
||||
@property
|
||||
def usbinput(self) -> bool:
|
||||
return self.getter("rtnsw")[0] == 1
|
||||
|
||||
@usbinput.setter
|
||||
def usbinput(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("rtnsw is a bool parameter")
|
||||
self.setter("rtnsw", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def invert(self) -> bool:
|
||||
return self.getter("invert")[0] == 1
|
||||
|
||||
@invert.setter
|
||||
def invert(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("invert is a bool parameter")
|
||||
self.setter("invert", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def highpasson(self) -> bool:
|
||||
return self.getter("hpon")[0] == 1
|
||||
|
||||
@highpasson.setter
|
||||
def highpasson(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("hpon is a bool parameter")
|
||||
self.setter("hpon", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def highpassfilter(self) -> int:
|
||||
return int(log_get(20, 400, self.getter("hpf")[0]))
|
||||
|
||||
@highpassfilter.setter
|
||||
def highpassfilter(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("highpassfilter is an int parameter")
|
||||
self.setter("hpf", log_set(20, 400, val))
|
||||
|
||||
|
||||
class Gate:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Gate, self).address
|
||||
return f"{root}/gate"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on")[0] == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("on is a boolean parameter")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
opts = ("gate", "exp2", "exp3", "exp4", "duck")
|
||||
return opts[self.getter("mode")[0]]
|
||||
|
||||
@mode.setter
|
||||
def mode(self, val: str):
|
||||
opts = ("gate", "exp2", "exp3", "exp4", "duck")
|
||||
if not isinstance(val, str) and val not in opts:
|
||||
raise MAirRemoteError(f"mode is a string parameter, 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)
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
if not isinstance(val, float):
|
||||
raise MAirRemoteError(
|
||||
"threshold is a float parameter, expected value in range -80 to 0"
|
||||
)
|
||||
self.setter("thr", lin_set(-80, 0, val))
|
||||
|
||||
@property
|
||||
def range(self) -> int:
|
||||
return int(lin_get(3, 60, self.getter("range")[0]))
|
||||
|
||||
@range.setter
|
||||
def range(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"range is an int parameter, expected value in range 3 to 60"
|
||||
)
|
||||
self.setter("range", lin_set(3, 60, val))
|
||||
|
||||
@property
|
||||
def attack(self) -> int:
|
||||
return int(lin_get(0, 120, self.getter("attack")[0]))
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"attack is an int parameter, expected value in range 0 to 120"
|
||||
)
|
||||
self.setter("attack", lin_set(0, 120, val))
|
||||
|
||||
@property
|
||||
def hold(self) -> Union[float, int]:
|
||||
val = 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):
|
||||
self.setter("hold", log_set(0.02, 2000, val))
|
||||
|
||||
@property
|
||||
def release(self) -> int:
|
||||
return int(log_get(5, 4000, self.getter("release")[0]))
|
||||
|
||||
@release.setter
|
||||
def release(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"release is an int parameter, expected value in range 5 to 4000"
|
||||
)
|
||||
self.setter("release", log_set(5, 4000, val))
|
||||
|
||||
@property
|
||||
def keysource(self):
|
||||
return self.getter("keysrc")[0]
|
||||
|
||||
@keysource.setter
|
||||
def keysource(self, val):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("keysource is an int parameter")
|
||||
self.setter("keysrc", val)
|
||||
|
||||
@property
|
||||
def filteron(self):
|
||||
return self.getter("filter/on")[0] == 1
|
||||
|
||||
@filteron.setter
|
||||
def filteron(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("filteron is a boolean parameter")
|
||||
self.setter("filter/on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def filtertype(self) -> int:
|
||||
return int(self.getter("filter/type")[0])
|
||||
|
||||
@filtertype.setter
|
||||
def filtertype(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("filtertype is an int parameter")
|
||||
self.setter("filter/type", val)
|
||||
|
||||
@property
|
||||
def filterfreq(self) -> Union[float, int]:
|
||||
retval = 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]):
|
||||
self.setter("filter/f", log_set(20, 20000, val))
|
||||
|
||||
|
||||
class Dyn:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Dyn, self).address
|
||||
return f"{root}/dyn"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on")[0] == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("on is a boolean parameter")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
opts = ("comp", "exp")
|
||||
return opts[self.getter("mode")[0]]
|
||||
|
||||
@mode.setter
|
||||
def mode(self, val: str):
|
||||
opts = ("comp", "exp")
|
||||
if not isinstance(val, str) and val not in opts:
|
||||
raise MAirRemoteError(f"mode is a string parameter, expected one of {opts}")
|
||||
self.setter("mode", opts.index(val))
|
||||
|
||||
@property
|
||||
def det(self) -> str:
|
||||
opts = ("peak", "rms")
|
||||
return opts[self.getter("det")[0]]
|
||||
|
||||
@det.setter
|
||||
def det(self, val: str):
|
||||
opts = ("peak", "rms")
|
||||
if not isinstance(val, str) and val not in opts:
|
||||
raise MAirRemoteError(f"det is a string parameter, expected one of {opts}")
|
||||
self.setter("det", opts.index(val))
|
||||
|
||||
@property
|
||||
def env(self) -> str:
|
||||
opts = ("lin", "log")
|
||||
return opts[self.getter("env")[0]]
|
||||
|
||||
@env.setter
|
||||
def env(self, val: str):
|
||||
opts = ("lin", "log")
|
||||
if not isinstance(val, str) and val not in opts:
|
||||
raise MAirRemoteError(f"env is a string parameter, 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)
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
if not isinstance(val, float):
|
||||
raise MAirRemoteError(
|
||||
"threshold is a float parameter, expected value in range -80 to 0"
|
||||
)
|
||||
self.setter("thr", lin_set(-60, 0, val))
|
||||
|
||||
@property
|
||||
def ratio(self) -> Union[float, int]:
|
||||
opts = (1.1, 1.3, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10, 20, 100)
|
||||
return opts[self.getter("ratio")[0]]
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("ratio is an int parameter")
|
||||
self.setter("ratio", val)
|
||||
|
||||
@property
|
||||
def knee(self) -> int:
|
||||
return int(lin_get(0, 5, self.getter("knee")[0]))
|
||||
|
||||
@knee.setter
|
||||
def knee(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"knee is an int parameter, expected value in range 0 to 5"
|
||||
)
|
||||
self.setter("knee", lin_set(0, 5, val))
|
||||
|
||||
@property
|
||||
def mgain(self) -> float:
|
||||
return round(lin_get(0, 24, self.getter("mgain")[0]), 1)
|
||||
|
||||
@mgain.setter
|
||||
def mgain(self, val: float):
|
||||
self.setter("mgain", lin_set(0, 24, val))
|
||||
|
||||
@property
|
||||
def attack(self) -> int:
|
||||
return int(lin_get(0, 120, self.getter("attack")[0]))
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: int):
|
||||
self.setter("attack", lin_set(0, 120, val))
|
||||
|
||||
@property
|
||||
def hold(self) -> Union[float, int]:
|
||||
val = 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):
|
||||
self.setter("hold", log_set(0.02, 2000, val))
|
||||
|
||||
@property
|
||||
def release(self) -> int:
|
||||
return int(log_get(5, 4000, self.getter("release")[0]))
|
||||
|
||||
@release.setter
|
||||
def release(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"release is an int parameter, expected value in range 5 to 4000"
|
||||
)
|
||||
self.setter("release", log_set(5, 4000, val))
|
||||
|
||||
@property
|
||||
def mix(self) -> int:
|
||||
return int(lin_get(0, 100, self.getter("mix")[0]))
|
||||
|
||||
@mix.setter
|
||||
def mix(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError(
|
||||
"mix is an int parameter, expected value in range 0 to 5"
|
||||
)
|
||||
self.setter("mix", lin_set(0, 100, val))
|
||||
|
||||
@property
|
||||
def keysource(self):
|
||||
return self.getter("keysrc")[0]
|
||||
|
||||
@keysource.setter
|
||||
def keysource(self, val):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("keysource is an int parameter")
|
||||
self.setter("keysrc", val)
|
||||
|
||||
@property
|
||||
def auto(self) -> bool:
|
||||
return self.getter("auto")[0] == 1
|
||||
|
||||
@auto.setter
|
||||
def auto(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("auto is a boolean parameter")
|
||||
self.setter("auto", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def filteron(self):
|
||||
return self.getter("filter/on")[0] == 1
|
||||
|
||||
@filteron.setter
|
||||
def filteron(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("filteron is a boolean parameter")
|
||||
self.setter("filter/on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def filtertype(self) -> int:
|
||||
return int(self.getter("filter/type")[0])
|
||||
|
||||
@filtertype.setter
|
||||
def filtertype(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("filtertype is an int parameter")
|
||||
self.setter("filter/type", val)
|
||||
|
||||
@property
|
||||
def filterfreq(self) -> Union[float, int]:
|
||||
retval = 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]):
|
||||
self.setter("filter/f", log_set(20, 20000, val))
|
||||
|
||||
|
||||
class Insert:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Insert, self).address
|
||||
return f"{root}/insert"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on")[0] == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("on is a boolean parameter")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def sel(self) -> int:
|
||||
return self.getter("sel")[0]
|
||||
|
||||
@sel.setter
|
||||
def sel(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("sel is an int parameter")
|
||||
self.setter("sel", val)
|
||||
|
||||
|
||||
class EQ:
|
||||
@classmethod
|
||||
def make_fourband(cls, _cls, remote, index=None):
|
||||
EQBand_cls = type("EQBand", (EQ.EQBand, _cls), {})
|
||||
return type(
|
||||
"EQ",
|
||||
(cls,),
|
||||
{
|
||||
"low": EQBand_cls(1, remote, index),
|
||||
"lomid": EQBand_cls(2, remote, index),
|
||||
"himid": EQBand_cls(3, remote, index),
|
||||
"high": EQBand_cls(4, remote, index),
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_sixband(cls, _cls, remote, index=None):
|
||||
EQBand_cls = type("EQBand", (EQ.EQBand, _cls), {})
|
||||
return type(
|
||||
"EQ",
|
||||
(cls,),
|
||||
{
|
||||
"low": EQBand_cls(1, remote, index),
|
||||
"low2": EQBand_cls(2, remote, index),
|
||||
"lomid": EQBand_cls(3, remote, index),
|
||||
"himid": EQBand_cls(4, remote, index),
|
||||
"high2": EQBand_cls(5, remote, index),
|
||||
"high": EQBand_cls(6, remote, index),
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(EQ, self).address
|
||||
return f"{root}/eq"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on")[0] == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("on is a boolean parameter")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
opts = ("peq", "geq", "teq")
|
||||
return opts[self.getter("mode")[0]]
|
||||
|
||||
@mode.setter
|
||||
def mode(self, val: str):
|
||||
opts = ("peq", "geq", "teq")
|
||||
if not isinstance(val, str) and val not in opts:
|
||||
raise MAirRemoteError(f"mode is a string parameter, 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)
|
||||
self.i = i
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(EQ.EQBand, self).address
|
||||
return f"{root}/eq/{self.i}"
|
||||
|
||||
@property
|
||||
def type(self) -> int:
|
||||
return int(self.getter("type")[0])
|
||||
|
||||
@type.setter
|
||||
def type(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("type is an int parameter")
|
||||
self.setter(f"type", val)
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
retval = log_get(20, 20000, self.getter("f")[0])
|
||||
return round(retval, 1)
|
||||
|
||||
@frequency.setter
|
||||
def frequency(self, val: float):
|
||||
self.setter("f", log_set(20, 20000, val))
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
return round(lin_get(-15, 15, self.getter("g")[0]), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("g", lin_set(-15, 15, val))
|
||||
|
||||
@property
|
||||
def quality(self) -> float:
|
||||
retval = log_get(0.3, 10, self.getter("q")[0])
|
||||
return round(retval, 1)
|
||||
|
||||
@quality.setter
|
||||
def quality(self, val: float):
|
||||
self.setter("q", log_set(0.3, 10, val))
|
||||
|
||||
|
||||
class GEQ:
|
||||
@classmethod
|
||||
def make(cls):
|
||||
# fmt: off
|
||||
return type(
|
||||
"GEQ",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
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",
|
||||
"1k25", "1k6", "2k", "2k5", "3k15", "4k", "5k", "6k3", "8k",
|
||||
"10k", "12k5", "16k", "20k",
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(GEQ, self).address
|
||||
return f"{root}/geq"
|
||||
|
||||
|
||||
class Mix:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Mix, self).address
|
||||
return f"{root}/mix"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on")[0] == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("on is a boolean parameter")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def fader(self) -> float:
|
||||
retval = self.getter("fader")[0]
|
||||
return _get_fader_val(retval)
|
||||
|
||||
@fader.setter
|
||||
def fader(self, val: float):
|
||||
_set_fader_val(self, val)
|
||||
|
||||
@property
|
||||
def lr(self) -> bool:
|
||||
return self.getter("lr")[0] == 1
|
||||
|
||||
@lr.setter
|
||||
def lr(self, val: bool):
|
||||
if not isinstance(val, bool):
|
||||
raise MAirRemoteError("lr is a boolean parameter")
|
||||
self.setter("lr", 1 if val else 0)
|
||||
|
||||
|
||||
class Group:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Group, self).address
|
||||
return f"{root}/grp"
|
||||
|
||||
@property
|
||||
def dca(self) -> int:
|
||||
return self.getter("dca")[0]
|
||||
|
||||
@dca.setter
|
||||
def dca(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("dca is an int parameter")
|
||||
self.setter("dca", val)
|
||||
|
||||
@property
|
||||
def mute(self) -> int:
|
||||
return self.getter("mute")[0]
|
||||
|
||||
@mute.setter
|
||||
def mute(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("mute is an int parameter")
|
||||
self.setter("mute", val)
|
||||
|
||||
|
||||
class Automix:
|
||||
@property
|
||||
def address(self) -> str:
|
||||
root = super(Automix, self).address
|
||||
return f"{root}/automix"
|
||||
|
||||
@property
|
||||
def group(self) -> int:
|
||||
return self.getter("group")[0]
|
||||
|
||||
@group.setter
|
||||
def group(self, val: int):
|
||||
if not isinstance(val, int):
|
||||
raise MAirRemoteError("group is an int parameter")
|
||||
self.setter("group", val)
|
||||
|
||||
@property
|
||||
def weight(self) -> float:
|
||||
return round(lin_get(-12, 12, self.getter("weight")[0]), 1)
|
||||
|
||||
@weight.setter
|
||||
def weight(self, val: float):
|
||||
if not isinstance(val, float):
|
||||
raise MAirRemoteError(
|
||||
"weight is a float parameter, expected value in range -12 to 12"
|
||||
)
|
||||
self.setter("weight", lin_set(-12, 12, val))
|
74
mair/strip.py
Normal file
74
mair/strip.py
Normal file
@ -0,0 +1,74 @@
|
||||
import abc
|
||||
from .errors import MAirRemoteError
|
||||
from .shared import (
|
||||
Config,
|
||||
Preamp,
|
||||
Gate,
|
||||
Dyn,
|
||||
Insert,
|
||||
EQ,
|
||||
GEQ,
|
||||
Mix,
|
||||
Group,
|
||||
Automix,
|
||||
)
|
||||
|
||||
|
||||
class IStrip(abc.ABC):
|
||||
"""Abstract Base Class for strips"""
|
||||
|
||||
def __init__(self, remote, index: int):
|
||||
self._remote = remote
|
||||
self.index = index + 1
|
||||
|
||||
def getter(self, param: str) -> tuple:
|
||||
self._remote.send(f"{self.address}/{param}")
|
||||
return self._remote.info_response
|
||||
|
||||
def setter(self, param: str, val: int):
|
||||
self._remote.send(f"{self.address}/{param}", val)
|
||||
|
||||
@abc.abstractmethod
|
||||
def address(self):
|
||||
pass
|
||||
|
||||
|
||||
class Strip(IStrip):
|
||||
"""Concrete class for strips"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
"""
|
||||
Factory function for strips
|
||||
|
||||
Creates a mixin of shared subclasses, sets them as class attributes.
|
||||
|
||||
Returns a Strip class of a kind.
|
||||
"""
|
||||
STRIP_cls = type(
|
||||
f"Strip{remote.kind.id_}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
_cls.__name__.lower(): type(
|
||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
||||
)(remote, index)
|
||||
for _cls in (
|
||||
Config,
|
||||
Preamp,
|
||||
Gate,
|
||||
Dyn,
|
||||
Insert,
|
||||
EQ.make_fourband(cls, remote, index),
|
||||
Mix,
|
||||
Group,
|
||||
Automix,
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
return STRIP_cls(remote, index)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
return f"/ch/{str(self.index).zfill(2)}"
|
77
mair/util.py
Normal file
77
mair/util.py
Normal file
@ -0,0 +1,77 @@
|
||||
from math import log, exp
|
||||
|
||||
|
||||
def lin_get(min, max, val):
|
||||
return min + (max - min) * val
|
||||
|
||||
|
||||
def lin_set(min, max, val):
|
||||
return (val - min) / (max - min)
|
||||
|
||||
|
||||
def log_get(min, max, val):
|
||||
return min * exp(log(max / min) * val)
|
||||
|
||||
|
||||
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 _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 _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
|
||||
|
||||
|
||||
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)
|
18
setup.py
Normal file
18
setup.py
Normal file
@ -0,0 +1,18 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='mair_remote',
|
||||
version='0.1',
|
||||
description='MAIR Remote Python API',
|
||||
packages=['mair'],
|
||||
install_requires=[
|
||||
'python-osc'
|
||||
],
|
||||
extras_require={
|
||||
'development': [
|
||||
'nose',
|
||||
'randomize',
|
||||
'parameterized'
|
||||
]
|
||||
}
|
||||
)
|
17
tests/__init__.py
Normal file
17
tests/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
import mair
|
||||
from mair import kinds
|
||||
import threading
|
||||
|
||||
_kind = 'MR18'
|
||||
|
||||
mars = {kind.id_: mair.connect(_kind) for kind in kinds.all}
|
||||
tests = mars[_kind]
|
||||
|
||||
def setup_package():
|
||||
tests.worker = threading.Thread(target = tests.run_server)
|
||||
tests.worker.daemon = True
|
||||
tests.worker.start()
|
||||
tests.validate_connection()
|
||||
|
||||
def teardown_package():
|
||||
tests.server.shutdown()
|
332
tests/shared_tests.py
Normal file
332
tests/shared_tests.py
Normal file
@ -0,0 +1,332 @@
|
||||
from nose.tools import assert_equal, nottest
|
||||
from parameterized import parameterized, parameterized_class
|
||||
|
||||
import unittest
|
||||
from tests import tests
|
||||
|
||||
"""
|
||||
Not every subclass is tested for every superclass to avoid redundancy.
|
||||
LR: mix, config, insert, geq
|
||||
Strip: mix, preamp, config, gate, automix
|
||||
Bus: config, dyn, eq
|
||||
FXSend: group
|
||||
"""
|
||||
|
||||
""" LR TESTS """
|
||||
#@nottest
|
||||
class TestSetAndGetLRMixHigher(unittest.TestCase):
|
||||
""" Mix """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'lr')
|
||||
self.target = getattr(self.target, 'mix')
|
||||
|
||||
@parameterized.expand([
|
||||
('on', True), ('on', False)
|
||||
])
|
||||
def test_it_sets_and_gets_lr_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('fader', -80.6), ('fader', -67.0)
|
||||
])
|
||||
def test_it_sets_and_gets_lr_float_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
class TestSetAndGetLRConfigHigher(unittest.TestCase):
|
||||
""" Config """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'lr')
|
||||
self.target = getattr(self.target, 'config')
|
||||
|
||||
@parameterized.expand([
|
||||
('name', 'test0'), ('name', 'test1')
|
||||
])
|
||||
def test_it_sets_and_gets_lr_string_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
class TestSetAndGetLRInsertHigher(unittest.TestCase):
|
||||
""" Insert """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'lr')
|
||||
self.target = getattr(self.target, 'insert')
|
||||
|
||||
@parameterized.expand([
|
||||
('on', True), ('on', False)
|
||||
])
|
||||
def test_it_sets_and_gets_lr_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('sel', 0), ('sel', 4)
|
||||
])
|
||||
def test_it_sets_and_gets_lr_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
class TestSetAndGetLRGEQHigher(unittest.TestCase):
|
||||
""" GEQ """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'lr')
|
||||
self.target = getattr(self.target, 'geq')
|
||||
|
||||
@parameterized.expand([
|
||||
('slider_20', -13.5), ('slider_20', 5.5), ('slider_6k3', -8.5), ('slider_6k3', 8.5)
|
||||
])
|
||||
def test_it_sets_and_gets_lr_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
|
||||
""" STRIP TESTS """
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 15 }
|
||||
])
|
||||
class TestSetAndGetStripMixHigher(unittest.TestCase):
|
||||
""" Mix """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'strip')
|
||||
self.target = getattr(self.target[self.i], 'mix')
|
||||
|
||||
@parameterized.expand([
|
||||
('on', True), ('on', False), ('lr', True), ('lr', False)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 8 }
|
||||
])
|
||||
class TestSetAndGetStripPreampHigher(unittest.TestCase):
|
||||
""" Preamp """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'strip')
|
||||
self.target = getattr(self.target[self.i], 'preamp')
|
||||
|
||||
@parameterized.expand([
|
||||
('highpasson', True), ('highpasson', False), ('usbinput', True), ('usbinput', False)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('highpassfilter', 20), ('highpassfilter', 399)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('usbtrim', -16.5), ('usbtrim', 5.5)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_float_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 3 }
|
||||
])
|
||||
class TestSetAndGetStripConfigHigher(unittest.TestCase):
|
||||
""" Config """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'strip')
|
||||
self.target = getattr(self.target[self.i], 'config')
|
||||
|
||||
@parameterized.expand([
|
||||
('inputsource', 0), ('inputsource', 18), ('usbreturn', 3), ('usbreturn', 12)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized_class([
|
||||
{ 'i': 12 }
|
||||
])
|
||||
class TestSetAndGetStripGateHigher(unittest.TestCase):
|
||||
""" Gate """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'strip')
|
||||
self.target = getattr(self.target[self.i], 'gate')
|
||||
|
||||
@parameterized.expand([
|
||||
('on', True), ('on', False), ('invert', True), ('invert', False),
|
||||
('filteron', True), ('filteron', False)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('range', 11), ('range', 48), ('attack', 5), ('attack', 110),
|
||||
('release', 360), ('release', 2505), ('filtertype', 0), ('filtertype', 8)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('mode', 'exp2'), ('mode', 'duck')
|
||||
])
|
||||
def test_it_sets_and_gets_strip_string_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('threshold', -80.0), ('threshold', 0.0), ('hold', 355), ('hold', 63.2),
|
||||
('filterfreq', 37.2), ('filterfreq', 12765)
|
||||
])
|
||||
def test_it_sets_and_gets_strip_float_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 6 }
|
||||
])
|
||||
class TestSetAndGetStripAutomixHigher(unittest.TestCase):
|
||||
""" Automix """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'strip')
|
||||
self.target = getattr(self.target[self.i], 'automix')
|
||||
|
||||
@parameterized.expand([
|
||||
('group', 0), ('group', 2)
|
||||
])
|
||||
def test_it_sets_and_gets_fxsend_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('weight', -10.5), ('weight', 3.5)
|
||||
])
|
||||
def test_it_sets_and_gets_fxsend_float_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
|
||||
""" BUS TESTS """
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 1 }
|
||||
])
|
||||
class TestSetAndGetBusConfigHigher(unittest.TestCase):
|
||||
""" Config """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'bus')
|
||||
self.target = getattr(self.target[self.i], 'config')
|
||||
|
||||
@parameterized.expand([
|
||||
('color', 0), ('color', 15)
|
||||
])
|
||||
def test_it_sets_and_gets_bus_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 2 }
|
||||
])
|
||||
class TestSetAndGetBusDynHigher(unittest.TestCase):
|
||||
""" Dyn """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'bus')
|
||||
self.target = getattr(self.target[self.i], 'dyn')
|
||||
|
||||
@parameterized.expand([
|
||||
('on', True), ('on', False)
|
||||
])
|
||||
def test_it_sets_and_gets_bus_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('mode', 'comp'), ('mode', 'exp'), ('env', 'lin'), ('env', 'log'),
|
||||
('det', 'peak'), ('det', 'rms')
|
||||
])
|
||||
def test_it_sets_and_gets_bus_string_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 0 }
|
||||
])
|
||||
class TestSetAndGetBusEQHigher(unittest.TestCase):
|
||||
""" EQ """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'bus')
|
||||
self.target = getattr(self.target[self.i], 'eq')
|
||||
|
||||
@parameterized.expand([
|
||||
('on', True), ('on', False)
|
||||
])
|
||||
def test_it_sets_and_gets_bus_bool_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
self.assertTrue(isinstance(retval, bool))
|
||||
assert_equal(retval, val)
|
||||
|
||||
@parameterized.expand([
|
||||
('mode', 'peq'), ('mode', 'geq'), ('mode', 'teq')
|
||||
])
|
||||
def test_it_sets_and_gets_bus_string_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
||||
|
||||
|
||||
""" FXSEND TESTS """
|
||||
#@nottest
|
||||
@parameterized_class([
|
||||
{ 'i': 1 }
|
||||
])
|
||||
class TestSetAndGetFXSendGroupHigher(unittest.TestCase):
|
||||
""" Group """
|
||||
def setUp(self):
|
||||
self.target = getattr(tests, 'fxsend')
|
||||
self.target = getattr(self.target[self.i], 'group')
|
||||
|
||||
@parameterized.expand([
|
||||
('dca', 0), ('dca', 12), ('mute', 3), ('mute', 8)
|
||||
])
|
||||
def test_it_sets_and_gets_fxsend_int_params(self, param, val):
|
||||
setattr(self.target, param, val)
|
||||
retval = getattr(self.target, param)
|
||||
assert_equal(retval, val)
|
Loading…
Reference in New Issue
Block a user