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

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)