package renamed to xair-api

now packaged with poetry and added to pypi

using tomllib, requires python 3.11

readme, changelog updated to reflect changes

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

3
xair_api/__init__.py Normal file
View File

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

62
xair_api/bus.py Normal file
View File

@@ -0,0 +1,62 @@
import abc
from .errors import XAirRemoteError
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
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}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_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}"

211
xair_api/config.py Normal file
View File

@@ -0,0 +1,211 @@
import abc
from . import kinds
from .errors import XAirRemoteError
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}",
(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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(
"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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(
"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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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}",
(),
{
"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}

60
xair_api/dca.py Normal file
View File

@@ -0,0 +1,60 @@
import abc
from .errors import XAirRemoteError
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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("color is an int parameter")
self.setter("config/color", val)

4
xair_api/errors.py Normal file
View File

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

72
xair_api/fx.py Normal file
View File

@@ -0,0 +1,72 @@
import abc
from .errors import XAirRemoteError
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
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}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_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 XAirRemoteError("type is an integer parameter")
self.setter("type", val)

66
xair_api/kinds.py Normal file
View File

@@ -0,0 +1,66 @@
from dataclasses import dataclass
"""
# osc slightly different, interface would need adjusting to support this mixer.
@dataclass
class X32KindMap:
id_: str = "X32"
num_dca: int = 8
num_strip: int = 32
num_bus: int = 16
num_fx: int = 8
num_rtn: int = 6
"""
@dataclass
class KindMap:
def __str__(self) -> str:
return self.id_.capitalize()
@dataclass
class MR18KindMap(KindMap):
# note ch 17-18 defined as aux rtn
id_: str
num_dca: int = 4
num_strip: int = 16
num_bus: int = 6
num_fx: int = 4
num_rtn: int = 4
@dataclass
class XR16KindMap(KindMap):
id_: str
num_dca: int = 4
num_strip: int = 16
num_bus: int = 4
num_fx: int = 4
num_rtn: int = 4
@dataclass
class XR12KindMap(KindMap):
id_: str
num_dca: int = 4
num_strip: int = 12
num_bus: int = 2
num_fx: int = 4
num_rtn: int = 4
_kinds = {
"XR18": MR18KindMap(id_="XR18"),
"MR18": MR18KindMap(id_="MR18"),
"XR16": XR16KindMap(id_="XR16"),
"XR12": XR12KindMap(id_="XR12"),
}
def get(kind_id):
return _kinds[kind_id]
all = list(kind for kind in _kinds.values())

60
xair_api/lr.py Normal file
View File

@@ -0,0 +1,60 @@
import abc
from .errors import XAirRemoteError
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
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}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_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"

77
xair_api/meta.py Normal file
View File

@@ -0,0 +1,77 @@
from .errors import XAirRemoteError
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 XAirRemoteError(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 XAirRemoteError(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 XAirRemoteError(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 XAirRemoteError(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)

99
xair_api/rtn.py Normal file
View File

@@ -0,0 +1,99 @@
import abc
from typing import Optional
from .errors import XAirRemoteError
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
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}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_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
xair_api/shared.py Normal file
View File

@@ -0,0 +1,680 @@
from typing import Union
from .errors import XAirRemoteError
from .meta import geq_prop
from .util import _get_fader_val, _set_fader_val, lin_get, lin_set, log_get, log_set
"""
Classes shared by /ch, /rtn, /rt/aux, /bus, /fxsend, /lr
"""
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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(
"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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(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 XAirRemoteError(
"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 XAirRemoteError(
"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 XAirRemoteError(
"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 XAirRemoteError(
"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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(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 XAirRemoteError(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 XAirRemoteError(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 XAirRemoteError(
"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 XAirRemoteError("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 XAirRemoteError(
"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 XAirRemoteError(
"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 XAirRemoteError(
"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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError("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 XAirRemoteError(
"weight is a float parameter, expected value in range -12 to 12"
)
self.setter("weight", lin_set(-12, 12, val))

64
xair_api/strip.py Normal file
View File

@@ -0,0 +1,64 @@
import abc
from .errors import XAirRemoteError
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
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}",
(cls,),
{
**{
_cls.__name__.lower(): type(
f"{_cls.__name__}{remote.kind}", (_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
xair_api/util.py Normal file
View File

@@ -0,0 +1,77 @@
from math import exp, log
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)

150
xair_api/xair.py Normal file
View File

@@ -0,0 +1,150 @@
import abc
import threading
import time
from configparser import ConfigParser
from pathlib import Path
from typing import Optional, Self
import tomllib
from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_message_builder import OscMessageBuilder
from pythonosc.osc_server import BlockingOSCUDPServer
from . import kinds
from .bus import Bus
from .config import Config
from .dca import DCA
from .errors import XAirRemoteError
from .fx import FXReturn, FXSend
from .kinds import KindMap
from .lr import LR
from .rtn import Aux, Rtn
from .strip import Strip
class OSCClientServer(BlockingOSCUDPServer):
def __init__(self, address: str, dispatcher: Dispatcher):
super().__init__(("", 0), dispatcher)
self.xr_address = address
def send_message(self, address: str, value: str):
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 XAirRemote(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_toml()
self.xair_port = kwargs["port"] or self.XAIR_PORT
if not (self.xair_ip and self.xair_port):
raise XAirRemoteError("No valid ip or password detected")
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
def __enter__(self) -> Self:
self.worker = threading.Thread(target=self.run_server, daemon=True)
self.worker.start()
self.validate_connection()
return self
def _ip_from_toml(self) -> str:
filepath = Path.cwd() / "config.toml"
with open(filepath, "rb") as f:
conn = tomllib.load(f)
return conn["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: str, param: Optional[str] = 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: KindMap) -> XAirRemote:
"""
Creates a new MAIR remote class.
The returned class will subclass MAirRemote.
"""
def init(self, *args, **kwargs):
defaultkwargs = {"ip": None, "port": None}
kwargs = defaultkwargs | kwargs
XAirRemote.__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}",
(XAirRemote,),
{
"__init__": init,
},
)
_remotes = {kind.id_: _make_remote(kind) for kind in kinds.all}
def request_remote_obj(kind_id: str, *args, **kwargs) -> XAirRemote:
"""
Interface entry point. Wraps factory expression and handles errors
Returns a reference to an XAirRemote class of a kind
"""
MAIRREMOTE_cls = None
try:
MAIRREMOTE_cls = _remotes[kind_id]
except ValueError as e:
raise SystemExit(e)
return MAIRREMOTE_cls(*args, **kwargs)