initial commit

initial commit
This commit is contained in:
onyx-and-iris 2022-04-05 20:05:55 +01:00
commit bf9b72f31f
21 changed files with 2392 additions and 0 deletions

53
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View File

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

72
mair/bus.py Normal file
View 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
View 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
View 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
View File

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

82
mair/fx.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)